openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
271 lines (270 loc) • 10.8 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs";
import { a as shouldLogVerbose, r as logVerbose } from "./globals-DyT306Z_.mjs";
import { n as resolveConcurrency } from "./resolve-BqzK3oW6.mjs";
import { t as finalizeInboundContext } from "./inbound-context-jAt0CAb-.mjs";
import { a as resolveAttachmentKind } from "./attachments.normalize-BQ4LOjcC.mjs";
import { i as selectAttachments } from "./attachments--JUC76Hk.mjs";
import { t as buildMediaUnderstandingRegistry } from "./provider-registry-Q7DQ9cll.mjs";
import { n as normalizeMediaAttachments, r as resolveMediaAttachmentLocalRoots, t as createMediaAttachmentCache } from "./runner.attachments-BftO5Pw6.mjs";
import { n as runCapability } from "./runner-B2CPSR_W.mjs";
import { n as sendTranscriptEcho } from "./echo-transcript-p3KDEJNK.mjs";
import { t as resolveFileExtractionLimits } from "./file-extraction-limits-BQqx3SLJ.mjs";
import { a as renderMediaAttachmentDisposition, i as applyAttachmentMarkerBudget, t as extractFileContext } from "./file-context-CNgNP-v3.mjs";
import pMap from "p-map";
//#region packages/media-understanding-common/src/format.ts
const sectionByKind = {
"audio.transcription": {
title: "Audio",
label: "Transcript"
},
"image.description": {
title: "Image",
label: "Description"
},
"video.description": {
title: "Video",
label: "Description"
}
};
/** Formats media-understanding outputs into the chat body sent back to the model. */
function formatMediaUnderstandingBody(params) {
const outputs = params.outputs.filter((output) => output.text.trim());
if (outputs.length === 0) return params.body ?? "";
const userText = params.body?.trim() || void 0;
const sections = [];
if (userText && outputs.length > 1) sections.push(`User text:\n${userText}`);
const counts = /* @__PURE__ */ new Map();
for (const output of outputs) counts.set(output.kind, (counts.get(output.kind) ?? 0) + 1);
const seen = /* @__PURE__ */ new Map();
for (const output of outputs) {
const count = counts.get(output.kind) ?? 1;
const next = (seen.get(output.kind) ?? 0) + 1;
seen.set(output.kind, next);
const suffix = count > 1 ? ` ${next}/${count}` : "";
const section = sectionByKind[output.kind];
const userSection = outputs.length === 1 && userText ? `User text:\n${userText}\n` : "";
sections.push(`[${section.title}${suffix}]\n${userSection}${section.label}:\n${output.text}`);
}
return sections.join("\n\n").trim();
}
/** Formats one or more audio transcript outputs for legacy transcript-only callers. */
function formatAudioTranscripts(outputs) {
if (outputs.length === 1) {
const [output] = outputs;
if (output) return output.text;
throw new Error("expected single audio transcript to be defined");
}
return outputs.map((output, index) => `Audio ${index + 1}:\n${output.text}`).join("\n\n");
}
//#endregion
//#region src/media-understanding/apply-capability.ts
async function runMediaCapability(params) {
try {
return await runCapability(params);
} catch (err) {
if (shouldLogVerbose()) logVerbose(`Media understanding task failed: ${String(err)}`);
const selection = selectAttachments({
capability: params.capability,
attachments: params.media,
policy: params.config?.attachments
});
return {
outputs: [],
decision: {
capability: params.capability,
outcome: "failed",
attachments: [],
attachmentDispositions: Object.fromEntries([...selection.selected.map(({ index }) => [index, { kind: "failed" }]), ...selection.droppedAttachmentIndexes.map((index) => [index, { kind: "not-selected" }])]),
...params.capability === "image" ? { nativeVisionActive: false } : {}
}
};
}
}
//#endregion
//#region src/media-understanding/apply.ts
const CAPABILITY_ORDER = [
"image",
"audio",
"video"
];
const AUDIO_ONLY_CAPABILITY_ORDER = ["audio"];
const EMPTY_VOICE_NOTE_PLACEHOLDER = "[Voice note could not be transcribed because the audio attachment was too small]";
function appendFileBlocks(body, suffix) {
if (suffix === void 0) return body ?? "";
const base = typeof body === "string" ? body.trim() : "";
if (!base) return suffix;
return `${base}\n\n${suffix}`.trim();
}
const SELF_SERVE_CONTEXT_FIELDS = [
"Body",
"BodyForAgent",
"agentText"
];
function enableLocalPathSelfServe(upgrades, contexts, stagedPaths) {
for (const context of contexts) for (const upgrade of upgrades) {
const stagedPath = stagedPaths?.get(upgrade.attachmentIndex);
if (stagedPaths && !stagedPath) continue;
const selfServe = upgrade.render(stagedPath);
if (!selfServe) continue;
for (const field of SELF_SERVE_CONTEXT_FIELDS) {
const value = context[field];
if (typeof value === "string") context[field] = value.replace(upgrade.fallback, selfServe);
}
}
}
function renderMediaAttachmentMarkers(params) {
const handledIndexes = new Set(params.outputs.map((output) => output.attachmentIndex));
const decisions = new Map(params.decisions.map((decision) => [decision.capability, decision]));
return params.attachments.flatMap((attachment) => {
const capability = resolveAttachmentKind(attachment);
if (capability !== "image" && capability !== "audio" && capability !== "video") return [];
if (capability === "image" && params.deliveredImageIndexes?.has(attachment.index)) return [];
const decision = decisions.get(capability);
if (!decision || handledIndexes.has(attachment.index)) return [];
const disposition = decision.attachmentDispositions?.[attachment.index];
if (capability === "image" && decision.nativeVisionActive !== false && disposition?.kind !== "failed") return [];
const text = disposition ? renderMediaAttachmentDisposition(capability, disposition) : null;
return text ? [{
text,
consumesMarkerBudget: true
}] : [];
});
}
async function applyMediaUnderstanding(params) {
const { ctx, cfg } = params;
const originalUserText = [
ctx.CommandBody,
ctx.RawBody,
ctx.Body
].map((value) => normalizeOptionalString(value)).find(Boolean);
const attachments = normalizeMediaAttachments(ctx);
let builtProviderRegistry;
let providerRegistryError;
const providerRegistry = () => {
if (providerRegistryError) throw providerRegistryError.error;
try {
return builtProviderRegistry ??= buildMediaUnderstandingRegistry(params.providers, cfg);
} catch (error) {
providerRegistryError = { error };
throw error;
}
};
const cache = createMediaAttachmentCache(attachments, {
localPathRoots: resolveMediaAttachmentLocalRoots({
cfg,
ctx,
workspaceDir: params.workspaceDir
}),
includeDefaultLocalPathRoots: false,
ssrfPolicy: cfg.tools?.web?.fetch?.ssrfPolicy,
workspaceDir: params.workspaceDir
});
try {
const results = await pMap(params.processingMode === "files-only" ? [] : params.processingMode === "audio-only" || params.processingMode === "audio-and-files" ? AUDIO_ONLY_CAPABILITY_ORDER : CAPABILITY_ORDER, async (capability) => await runMediaCapability({
capability,
cfg,
ctx,
attachments: cache,
media: attachments,
agentId: params.agentId,
agentDir: params.agentDir,
workspaceDir: params.workspaceDir,
providerRegistry,
config: cfg.tools?.media?.[capability],
activeModel: params.activeModel
}), {
concurrency: resolveConcurrency(cfg),
stopOnError: false
});
if (providerRegistryError) throw providerRegistryError.error;
const outputs = [];
const decisions = [];
const audioAttachmentIndexes = /* @__PURE__ */ new Set();
for (const entry of results) {
decisions.push(entry.decision);
if (entry.decision.capability !== "audio") {
for (const output of entry.outputs) outputs.push(output);
continue;
}
const audioOutputsByAttachmentIndex = /* @__PURE__ */ new Map();
for (const output of entry.outputs) {
audioOutputsByAttachmentIndex.set(output.attachmentIndex, output);
audioAttachmentIndexes.add(output.attachmentIndex);
}
for (const attachment of entry.decision.attachments) {
const output = audioOutputsByAttachmentIndex.get(attachment.attachmentIndex);
if (output) outputs.push(output);
else if (attachment.attempts.some((attempt) => attempt.reason?.trim().startsWith("tooSmall"))) outputs.push({
kind: "audio.transcription",
attachmentIndex: attachment.attachmentIndex,
text: EMPTY_VOICE_NOTE_PLACEHOLDER,
provider: "openclaw",
model: "synthetic-empty-audio"
});
}
}
if (decisions.length > 0) ctx.MediaUnderstandingDecisions = [...ctx.MediaUnderstandingDecisions ?? [], ...decisions];
if (outputs.length > 0) {
const audioOutputs = outputs.filter((output) => output.kind === "audio.transcription");
if (audioOutputs.length > 0) {
const transcript = formatAudioTranscripts(audioOutputs);
ctx.Transcript = transcript;
if (originalUserText) {
ctx.CommandBody = originalUserText;
ctx.RawBody = originalUserText;
} else {
ctx.CommandBody = transcript;
ctx.RawBody = transcript;
}
const audioCfg = cfg.tools?.media?.audio;
if (audioCfg?.echoTranscript && transcript) await sendTranscriptEcho({
ctx,
cfg,
transcript,
format: audioCfg.echoFormat ?? "📝 \"{transcript}\""
});
} else if (originalUserText) {
ctx.CommandBody = originalUserText;
ctx.RawBody = originalUserText;
}
ctx.MediaUnderstanding = [...ctx.MediaUnderstanding ?? [], ...outputs];
}
const fileContext = params.processingMode === "audio-only" ? {
blocks: [],
images: [],
localPathSelfServeUpgrades: []
} : await extractFileContext({
attachments,
cache,
cfg,
limits: resolveFileExtractionLimits(cfg),
skipAttachmentIndexes: audioAttachmentIndexes.size > 0 ? audioAttachmentIndexes : void 0,
selfServePathsEnabled: params.selfServeLocalPaths === true
});
const mediaMarkers = renderMediaAttachmentMarkers({
attachments,
decisions,
outputs,
deliveredImageIndexes: params.deliveredImageIndexes
});
const contextBlocks = applyAttachmentMarkerBudget([...fileContext.blocks, ...mediaMarkers]);
if (outputs.length > 0 || contextBlocks.length > 0) {
const fileSuffix = contextBlocks.length > 0 ? contextBlocks.join("\n\n").trim() : void 0;
const enrich = (body) => appendFileBlocks(formatMediaUnderstandingBody({
body,
outputs
}), fileSuffix);
ctx.agentText = enrich(ctx.agentText ?? ctx.BodyForAgent ?? ctx.Body);
ctx.Body = enrich(ctx.Body);
finalizeInboundContext(ctx, { forceBodyForCommands: true });
}
return {
extractedFileImages: fileContext.images,
...fileContext.localPathSelfServeUpgrades.length > 0 ? { enableLocalPathSelfServe: (contexts, stagedPaths) => enableLocalPathSelfServe(fileContext.localPathSelfServeUpgrades, contexts, stagedPaths) } : {}
};
} finally {
await cache.cleanup();
}
}
//#endregion
export { applyMediaUnderstanding as t };