enh: image tool response

This commit is contained in:
Timothy Jaeryang Baek 2025-04-02 23:46:39 -07:00
parent 561b2c0b1a
commit faa68fcdaa
4 changed files with 67 additions and 9 deletions

View File

@ -1201,13 +1201,15 @@ async def process_chat_response(
) )
tool_result = None tool_result = None
tool_result_files = None
for result in results: for result in results:
if tool_call_id == result.get("tool_call_id", ""): if tool_call_id == result.get("tool_call_id", ""):
tool_result = result.get("content", None) tool_result = result.get("content", None)
tool_result_files = result.get("files", None)
break break
if tool_result: if tool_result:
tool_calls_display_content = f'{tool_calls_display_content}\n<details type="tool_calls" done="true" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}" result="{html.escape(json.dumps(tool_result))}">\n<summary>Tool Executed</summary>\n</details>' tool_calls_display_content = f'{tool_calls_display_content}\n<details type="tool_calls" done="true" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}" result="{html.escape(json.dumps(tool_result))}" files="{html.escape(json.dumps(tool_result_files)) if tool_result_files else ""}">\n<summary>Tool Executed</summary>\n</details>\n'
else: else:
tool_calls_display_content = f'{tool_calls_display_content}\n<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}">\n<summary>Executing...</summary>\n</details>' tool_calls_display_content = f'{tool_calls_display_content}\n<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}">\n<summary>Executing...</summary>\n</details>'
@ -1901,6 +1903,13 @@ async def process_chat_response(
except Exception as e: except Exception as e:
tool_result = str(e) tool_result = str(e)
tool_result_files = []
if isinstance(tool_result, list):
for item in tool_result:
if item.startswith("data:"):
tool_result_files.append(item)
tool_result.remove(item)
if isinstance(tool_result, dict) or isinstance( if isinstance(tool_result, dict) or isinstance(
tool_result, list tool_result, list
): ):
@ -1910,6 +1919,11 @@ async def process_chat_response(
{ {
"tool_call_id": tool_call_id, "tool_call_id": tool_call_id,
"content": tool_result, "content": tool_result,
**(
{"files": tool_result_files}
if tool_result_files
else {}
),
} }
) )

View File

@ -48,7 +48,8 @@
splitStream, splitStream,
sleep, sleep,
removeDetails, removeDetails,
getPromptVariables getPromptVariables,
processDetails
} from '$lib/utils'; } from '$lib/utils';
import { generateChatCompletion } from '$lib/apis/ollama'; import { generateChatCompletion } from '$lib/apis/ollama';
@ -1514,7 +1515,7 @@
: undefined, : undefined,
...createMessagesList(_history, responseMessageId).map((message) => ({ ...createMessagesList(_history, responseMessageId).map((message) => ({
...message, ...message,
content: removeDetails(message.content, ['reasoning', 'code_interpreter']) content: processDetails(message.content)
})) }))
].filter((message) => message); ].filter((message) => message);

View File

@ -36,6 +36,7 @@
import Spinner from './Spinner.svelte'; import Spinner from './Spinner.svelte';
import CodeBlock from '../chat/Messages/CodeBlock.svelte'; import CodeBlock from '../chat/Messages/CodeBlock.svelte';
import Markdown from '../chat/Messages/Markdown.svelte'; import Markdown from '../chat/Messages/Markdown.svelte';
import Image from './Image.svelte';
export let open = false; export let open = false;
@ -53,9 +54,17 @@
export let disabled = false; export let disabled = false;
export let hide = false; export let hide = false;
function formatJSONString(obj) { function parseJSONString(str) {
try { try {
const parsed = JSON.parse(JSON.parse(obj)); return parseJSONString(JSON.parse(str));
} catch (e) {
return str;
}
}
function formatJSONString(str) {
try {
const parsed = parseJSONString(str);
// If parsed is an object/array, then it's valid JSON // If parsed is an object/array, then it's valid JSON
if (typeof parsed === 'object') { if (typeof parsed === 'object') {
return JSON.stringify(parsed, null, 2); return JSON.stringify(parsed, null, 2);
@ -65,7 +74,7 @@
} }
} catch (e) { } catch (e) {
// Not valid JSON, return as-is // Not valid JSON, return as-is
return obj; return str;
} }
} }
</script> </script>
@ -120,14 +129,14 @@
{#if attributes?.done === 'true'} {#if attributes?.done === 'true'}
<Markdown <Markdown
id={`tool-calls-${attributes?.id}`} id={`tool-calls-${attributes?.id}`}
content={$i18n.t('View Result from `{{NAME}}`', { content={$i18n.t('View Result from **{{NAME}}**', {
NAME: attributes.name NAME: attributes.name
})} })}
/> />
{:else} {:else}
<Markdown <Markdown
id={`tool-calls-${attributes?.id}`} id={`tool-calls-${attributes?.id}-executing`}
content={$i18n.t('Executing `{{NAME}}`...', { content={$i18n.t('Executing **{{NAME}}**...', {
NAME: attributes.name NAME: attributes.name
})} })}
/> />
@ -194,6 +203,7 @@
{#if attributes?.type === 'tool_calls'} {#if attributes?.type === 'tool_calls'}
{@const args = decode(attributes?.arguments)} {@const args = decode(attributes?.arguments)}
{@const result = decode(attributes?.result ?? '')} {@const result = decode(attributes?.result ?? '')}
{@const files = parseJSONString(decode(attributes?.files ?? ''))}
{#if attributes?.done === 'true'} {#if attributes?.done === 'true'}
<Markdown <Markdown
@ -203,6 +213,14 @@
> ${formatJSONString(result)} > ${formatJSONString(result)}
> \`\`\``} > \`\`\``}
/> />
{#if typeof files === 'object'}
{#each files ?? [] as file, idx}
{#if file.startsWith('data:image/')}
<Image id={`tool-calls-${attributes?.id}-result-${idx}`} src={file} alt="Image" />
{/if}
{/each}
{/if}
{:else} {:else}
<Markdown <Markdown
id={`tool-calls-${attributes?.id}-result`} id={`tool-calls-${attributes?.id}-result`}

View File

@ -683,6 +683,31 @@ export const removeDetails = (content, types) => {
return content; return content;
}; };
export const processDetails = (content) => {
content = removeDetails(content, ['reasoning', 'code_interpreter']);
// This regex matches <details> tags with type="tool_calls" and captures their attributes to convert them to <tool_calls> tags
const detailsRegex = /<details\s+type="tool_calls"([^>]*)>([\s\S]*?)<\/details>/gis;
const matches = content.match(detailsRegex);
if (matches) {
for (const match of matches) {
const attributesRegex = /(\w+)="([^"]*)"/g;
const attributes = {};
let attributeMatch;
while ((attributeMatch = attributesRegex.exec(match)) !== null) {
attributes[attributeMatch[1]] = attributeMatch[2];
}
content = content.replace(
match,
`<tool_calls name="${attributes.name}" result="${attributes.result}"/>`
);
}
}
return content;
};
// This regular expression matches code blocks marked by triple backticks // This regular expression matches code blocks marked by triple backticks
const codeBlockRegex = /```[\s\S]*?```/g; const codeBlockRegex = /```[\s\S]*?```/g;