mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 23:01:56 +00:00
Add image support in tool results across all providers
Tool results now use content blocks and can include both text and images. All providers (Anthropic, Google, OpenAI Completions, OpenAI Responses) correctly pass images from tool results to LLMs. - Update ToolResultMessage type to use content blocks - Add placeholder text for image-only tool results in Google/Anthropic - OpenAI providers send tool result + follow-up user message with images - Fix Anthropic JSON parsing for empty tool arguments - Add comprehensive tests for image-only and text+image tool results - Update README with tool result content blocks API
This commit is contained in:
parent
9dac37d836
commit
84dcab219b
37 changed files with 720 additions and 544 deletions
|
|
@ -191,7 +191,12 @@ export class ToolMessageDebugView extends LitElement {
|
|||
}
|
||||
|
||||
override render() {
|
||||
const output = this.pretty(this.result?.output);
|
||||
const textOutput =
|
||||
this.result?.content
|
||||
?.filter((c) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n") || "";
|
||||
const output = this.pretty(textOutput);
|
||||
const details = this.pretty(this.result?.details);
|
||||
|
||||
return html`
|
||||
|
|
@ -240,7 +245,7 @@ export class ToolMessage extends LitElement {
|
|||
? {
|
||||
role: "toolResult",
|
||||
isError: true,
|
||||
output: "",
|
||||
content: [],
|
||||
toolCallId: this.toolCall.id,
|
||||
toolName: this.toolCall.name,
|
||||
timestamp: Date.now(),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,17 @@ import type { ToolRenderer, ToolRenderResult } from "../types.js";
|
|||
import { ArtifactPill } from "./ArtifactPill.js";
|
||||
import type { ArtifactsPanel, ArtifactsParams } from "./artifacts.js";
|
||||
|
||||
// Helper to extract text from content blocks
|
||||
function getTextOutput(result: ToolResultMessage<any> | undefined): string {
|
||||
if (!result) return "";
|
||||
return (
|
||||
result.content
|
||||
?.filter((c) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n") || ""
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to determine language for syntax highlighting
|
||||
function getLanguageFromFilename(filename?: string): string {
|
||||
if (!filename) return "text";
|
||||
|
|
@ -109,8 +120,8 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
${isDiff ? diffContent : content ? html`<code-block .code=${content} language=${getLanguageFromFilename(filename)}></code-block>` : ""}
|
||||
${
|
||||
isHtml
|
||||
? html`<console-block .content=${result.output || i18n("An error occurred")} variant="error"></console-block>`
|
||||
: html`<div class="text-sm text-destructive">${result.output || i18n("An error occurred")}</div>`
|
||||
? html`<console-block .content=${getTextOutput(result) || i18n("An error occurred")} variant="error"></console-block>`
|
||||
: html`<div class="text-sm text-destructive">${getTextOutput(result) || i18n("An error occurred")}</div>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -124,7 +135,7 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
content: html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, FileCode2, headerText)}
|
||||
<div class="text-sm text-destructive">${result.output || i18n("An error occurred")}</div>
|
||||
<div class="text-sm text-destructive">${getTextOutput(result) || i18n("An error occurred")}</div>
|
||||
</div>
|
||||
`,
|
||||
isCustom: false,
|
||||
|
|
@ -141,7 +152,7 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
|
||||
// GET command: show code block with file content
|
||||
if (command === "get") {
|
||||
const fileContent = result.output || i18n("(no output)");
|
||||
const fileContent = getTextOutput(result) || i18n("(no output)");
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
|
|
@ -157,7 +168,7 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
|
||||
// LOGS command: show console block
|
||||
if (command === "logs") {
|
||||
const logs = result.output || i18n("(no output)");
|
||||
const logs = getTextOutput(result) || i18n("(no output)");
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
|
|
@ -175,7 +186,7 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
if (command === "create" || command === "rewrite") {
|
||||
const codeContent = content || "";
|
||||
const isHtml = filename?.endsWith(".html");
|
||||
const logs = result.output || "";
|
||||
const logs = getTextOutput(result) || "";
|
||||
|
||||
return {
|
||||
content: html`
|
||||
|
|
@ -193,7 +204,7 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
|
||||
if (command === "update") {
|
||||
const isHtml = filename?.endsWith(".html");
|
||||
const logs = result.output || "";
|
||||
const logs = getTextOutput(result) || "";
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ export class ArtifactsPanel extends LitElement {
|
|||
// Execute mutates our local store and returns a plain output
|
||||
execute: async (_toolCallId: string, args: Static<typeof artifactsParamsSchema>, _signal?: AbortSignal) => {
|
||||
const output = await this.executeCommand(args);
|
||||
return { output, details: undefined };
|
||||
return { content: [{ type: "text", text: output }], details: undefined };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ export function createExtractDocumentTool(): AgentTool<typeof extractDocumentSch
|
|||
}
|
||||
|
||||
return {
|
||||
output: attachment.extractedText,
|
||||
content: [{ type: "text" as const, text: attachment.extractedText }],
|
||||
details: {
|
||||
extractedText: attachment.extractedText,
|
||||
format,
|
||||
|
|
@ -210,7 +210,11 @@ export const extractDocumentRenderer: ToolRenderer<ExtractDocumentParams, Extrac
|
|||
? "Failed to extract document"
|
||||
: "Extracted text from document";
|
||||
|
||||
const output = result.output || "";
|
||||
const output =
|
||||
result.content
|
||||
?.filter((c) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n") || "";
|
||||
|
||||
return {
|
||||
content: html`
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ export function createJavaScriptReplTool(): AgentTool<typeof javascriptReplSchem
|
|||
contentBase64: base64,
|
||||
};
|
||||
});
|
||||
return { output: result.output, details: { files } };
|
||||
return { content: [{ type: "text", text: result.output }], details: { files } };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -210,7 +210,11 @@ export const javascriptReplRenderer: ToolRenderer<JavaScriptReplParams, JavaScri
|
|||
|
||||
// With result: show params + result
|
||||
if (result && params) {
|
||||
const output = result.output || "";
|
||||
const output =
|
||||
result.content
|
||||
?.filter((c) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n") || "";
|
||||
const files = result.details?.files || [];
|
||||
|
||||
const attachments: Attachment[] = files.map((f, i) => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@ export class BashRenderer implements ToolRenderer<BashParams, undefined> {
|
|||
|
||||
// With result: show command + output
|
||||
if (result && params?.command) {
|
||||
const output = result.output || "";
|
||||
const output =
|
||||
result.content
|
||||
?.filter((c) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n") || "";
|
||||
const combined = output ? `> ${params.command}\n\n${output}` : `> ${params.command}`;
|
||||
return {
|
||||
content: html`
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@ export class CalculateRenderer implements ToolRenderer<CalculateParams, undefine
|
|||
|
||||
// Full params + full result
|
||||
if (result && params?.expression) {
|
||||
const output = result.output || "";
|
||||
const output =
|
||||
result.content
|
||||
?.filter((c) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n") || "";
|
||||
|
||||
// Error: show expression in header, error below
|
||||
if (result.isError) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,11 @@ export class DefaultRenderer implements ToolRenderer {
|
|||
|
||||
// With result: show header + params + result
|
||||
if (result) {
|
||||
let outputJson = result.output || i18n("(no output)");
|
||||
let outputJson =
|
||||
result.content
|
||||
?.filter((c) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n") || i18n("(no output)");
|
||||
let outputLanguage = "text";
|
||||
|
||||
// Try to parse and pretty-print if it's valid JSON
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@ export class GetCurrentTimeRenderer implements ToolRenderer<GetCurrentTimeParams
|
|||
|
||||
// Full params + full result
|
||||
if (result && params) {
|
||||
const output = result.output || "";
|
||||
const output =
|
||||
result.content
|
||||
?.filter((c) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n") || "";
|
||||
const headerText = params.timezone
|
||||
? `${i18n("Getting current time in")} ${params.timezone}`
|
||||
: i18n("Getting current date and time");
|
||||
|
|
@ -43,7 +47,11 @@ export class GetCurrentTimeRenderer implements ToolRenderer<GetCurrentTimeParams
|
|||
|
||||
// Full result, no params
|
||||
if (result) {
|
||||
const output = result.output || "";
|
||||
const output =
|
||||
result.content
|
||||
?.filter((c) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n") || "";
|
||||
|
||||
// Error: show header, error below
|
||||
if (result.isError) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue