mirror of https://github.com/kortix-ai/suna.git
Merge pull request #1768 from kubet/fix/show-attached-files-in-tools
fix: show attached files in tools
This commit is contained in:
commit
ed912fcebf
|
@ -53,26 +53,52 @@ export function renderAttachments(attachments: string[], fileViewerHandler?: (fi
|
||||||
// Render Markdown content while preserving XML tags that should be displayed as tool calls
|
// Render Markdown content while preserving XML tags that should be displayed as tool calls
|
||||||
function preprocessTextOnlyTools(content: string): string {
|
function preprocessTextOnlyTools(content: string): string {
|
||||||
console.log('🔍 preprocessTextOnlyTools called with:', typeof content, content);
|
console.log('🔍 preprocessTextOnlyTools called with:', typeof content, content);
|
||||||
|
|
||||||
if (!content || typeof content !== 'string') {
|
if (!content || typeof content !== 'string') {
|
||||||
console.warn('❌ preprocessTextOnlyTools: Invalid content type:', typeof content, content);
|
console.warn('❌ preprocessTextOnlyTools: Invalid content type:', typeof content, content);
|
||||||
return content || '';
|
return content || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle new function calls format for text-only tools - extract text parameter content
|
// For ask/complete tools, we need to preserve them if they have attachments
|
||||||
// Complete XML format
|
// Only strip them if they don't have attachments parameter
|
||||||
content = content.replace(/<function_calls>\s*<invoke name="ask">\s*<parameter name="text">([\s\S]*?)<\/parameter>[\s\S]*?<\/invoke>\s*<\/function_calls>/gi, '$1');
|
|
||||||
content = content.replace(/<function_calls>\s*<invoke name="complete">\s*<parameter name="text">([\s\S]*?)<\/parameter>[\s\S]*?<\/invoke>\s*<\/function_calls>/gi, '$1');
|
// Handle new function calls format - only strip if no attachments
|
||||||
|
content = content.replace(/<function_calls>\s*<invoke name="ask">\s*<parameter name="text">([\s\S]*?)<\/parameter>\s*<\/invoke>\s*<\/function_calls>/gi, (match) => {
|
||||||
|
if (match.includes('<parameter name="attachments"')) return match;
|
||||||
|
return match.replace(/<function_calls>\s*<invoke name="ask">\s*<parameter name="text">([\s\S]*?)<\/parameter>\s*<\/invoke>\s*<\/function_calls>/gi, '$1');
|
||||||
|
});
|
||||||
|
|
||||||
|
content = content.replace(/<function_calls>\s*<invoke name="complete">\s*<parameter name="text">([\s\S]*?)<\/parameter>\s*<\/invoke>\s*<\/function_calls>/gi, (match) => {
|
||||||
|
if (match.includes('<parameter name="attachments"')) return match;
|
||||||
|
return match.replace(/<function_calls>\s*<invoke name="complete">\s*<parameter name="text">([\s\S]*?)<\/parameter>\s*<\/invoke>\s*<\/function_calls>/gi, '$1');
|
||||||
|
});
|
||||||
|
|
||||||
content = content.replace(/<function_calls>\s*<invoke name="present_presentation">[\s\S]*?<parameter name="text">([\s\S]*?)<\/parameter>[\s\S]*?<\/invoke>\s*<\/function_calls>/gi, '$1');
|
content = content.replace(/<function_calls>\s*<invoke name="present_presentation">[\s\S]*?<parameter name="text">([\s\S]*?)<\/parameter>[\s\S]*?<\/invoke>\s*<\/function_calls>/gi, '$1');
|
||||||
|
|
||||||
// Handle streaming/partial XML for message tools - extract text parameter content even if incomplete
|
// Handle streaming/partial XML for message tools - only strip if no attachments visible yet
|
||||||
content = content.replace(/<function_calls>\s*<invoke name="ask">\s*<parameter name="text">([\s\S]*?)$/gi, '$1');
|
content = content.replace(/<function_calls>\s*<invoke name="ask">\s*<parameter name="text">([\s\S]*?)$/gi, (match) => {
|
||||||
content = content.replace(/<function_calls>\s*<invoke name="complete">\s*<parameter name="text">([\s\S]*?)$/gi, '$1');
|
if (match.includes('<parameter name="attachments"')) return match;
|
||||||
|
return match.replace(/<function_calls>\s*<invoke name="ask">\s*<parameter name="text">([\s\S]*?)$/gi, '$1');
|
||||||
|
});
|
||||||
|
|
||||||
|
content = content.replace(/<function_calls>\s*<invoke name="complete">\s*<parameter name="text">([\s\S]*?)$/gi, (match) => {
|
||||||
|
if (match.includes('<parameter name="attachments"')) return match;
|
||||||
|
return match.replace(/<function_calls>\s*<invoke name="complete">\s*<parameter name="text">([\s\S]*?)$/gi, '$1');
|
||||||
|
});
|
||||||
|
|
||||||
content = content.replace(/<function_calls>\s*<invoke name="present_presentation">[\s\S]*?<parameter name="text">([\s\S]*?)$/gi, '$1');
|
content = content.replace(/<function_calls>\s*<invoke name="present_presentation">[\s\S]*?<parameter name="text">([\s\S]*?)$/gi, '$1');
|
||||||
|
|
||||||
// Also handle old format for backward compatibility
|
// Also handle old format - only strip if no attachments attribute
|
||||||
content = content.replace(/<ask[^>]*>([\s\S]*?)<\/ask>/gi, '$1');
|
content = content.replace(/<ask[^>]*>([\s\S]*?)<\/ask>/gi, (match) => {
|
||||||
content = content.replace(/<complete[^>]*>([\s\S]*?)<\/complete>/gi, '$1');
|
if (match.match(/<ask[^>]*attachments=/i)) return match;
|
||||||
|
return match.replace(/<ask[^>]*>([\s\S]*?)<\/ask>/gi, '$1');
|
||||||
|
});
|
||||||
|
|
||||||
|
content = content.replace(/<complete[^>]*>([\s\S]*?)<\/complete>/gi, (match) => {
|
||||||
|
if (match.match(/<complete[^>]*attachments=/i)) return match;
|
||||||
|
return match.replace(/<complete[^>]*>([\s\S]*?)<\/complete>/gi, '$1');
|
||||||
|
});
|
||||||
|
|
||||||
content = content.replace(/<present_presentation[^>]*>([\s\S]*?)<\/present_presentation>/gi, '$1');
|
content = content.replace(/<present_presentation[^>]*>([\s\S]*?)<\/present_presentation>/gi, '$1');
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
@ -88,7 +114,7 @@ export function renderMarkdownContent(
|
||||||
) {
|
) {
|
||||||
// Preprocess content to convert text-only tools to natural text
|
// Preprocess content to convert text-only tools to natural text
|
||||||
content = preprocessTextOnlyTools(content);
|
content = preprocessTextOnlyTools(content);
|
||||||
|
|
||||||
// If in debug mode, just display raw content in a pre tag
|
// If in debug mode, just display raw content in a pre tag
|
||||||
if (debugMode) {
|
if (debugMode) {
|
||||||
return (
|
return (
|
||||||
|
@ -139,7 +165,7 @@ export function renderMarkdownContent(
|
||||||
{renderAttachments(attachmentArray, fileViewerHandler, sandboxId, project)}
|
{renderAttachments(attachmentArray, fileViewerHandler, sandboxId, project)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Also render standalone attachments outside the message
|
// Also render standalone attachments outside the message
|
||||||
const standaloneAttachments = renderStandaloneAttachments(attachmentArray, fileViewerHandler, sandboxId, project);
|
const standaloneAttachments = renderStandaloneAttachments(attachmentArray, fileViewerHandler, sandboxId, project);
|
||||||
if (standaloneAttachments) {
|
if (standaloneAttachments) {
|
||||||
|
@ -165,7 +191,7 @@ export function renderMarkdownContent(
|
||||||
{renderAttachments(attachmentArray, fileViewerHandler, sandboxId, project)}
|
{renderAttachments(attachmentArray, fileViewerHandler, sandboxId, project)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Also render standalone attachments outside the message
|
// Also render standalone attachments outside the message
|
||||||
const standaloneAttachments = renderStandaloneAttachments(attachmentArray, fileViewerHandler, sandboxId, project);
|
const standaloneAttachments = renderStandaloneAttachments(attachmentArray, fileViewerHandler, sandboxId, project);
|
||||||
if (standaloneAttachments) {
|
if (standaloneAttachments) {
|
||||||
|
@ -268,7 +294,7 @@ export function renderMarkdownContent(
|
||||||
{renderAttachments(attachments, fileViewerHandler, sandboxId, project)}
|
{renderAttachments(attachments, fileViewerHandler, sandboxId, project)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Also render standalone attachments outside the message
|
// Also render standalone attachments outside the message
|
||||||
const standaloneAttachments = renderStandaloneAttachments(attachments, fileViewerHandler, sandboxId, project);
|
const standaloneAttachments = renderStandaloneAttachments(attachments, fileViewerHandler, sandboxId, project);
|
||||||
if (standaloneAttachments) {
|
if (standaloneAttachments) {
|
||||||
|
@ -296,7 +322,7 @@ export function renderMarkdownContent(
|
||||||
{renderAttachments(attachments, fileViewerHandler, sandboxId, project)}
|
{renderAttachments(attachments, fileViewerHandler, sandboxId, project)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Also render standalone attachments outside the message
|
// Also render standalone attachments outside the message
|
||||||
const standaloneAttachments = renderStandaloneAttachments(attachments, fileViewerHandler, sandboxId, project);
|
const standaloneAttachments = renderStandaloneAttachments(attachments, fileViewerHandler, sandboxId, project);
|
||||||
if (standaloneAttachments) {
|
if (standaloneAttachments) {
|
||||||
|
@ -671,13 +697,13 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
// Use merged groups instead of original grouped messages
|
// Use merged groups instead of original grouped messages
|
||||||
const finalGroupedMessages = mergedGroups;
|
const finalGroupedMessages = mergedGroups;
|
||||||
|
|
||||||
|
|
||||||
// Helper function to add streaming content to groups
|
// Helper function to add streaming content to groups
|
||||||
const appendStreamingContent = (content: string, isPlayback: boolean = false) => {
|
const appendStreamingContent = (content: string, isPlayback: boolean = false) => {
|
||||||
const messageId = isPlayback ? 'playbackStreamingText' : 'streamingTextContent';
|
const messageId = isPlayback ? 'playbackStreamingText' : 'streamingTextContent';
|
||||||
const metadata = isPlayback ? 'playbackStreamingText' : 'streamingTextContent';
|
const metadata = isPlayback ? 'playbackStreamingText' : 'streamingTextContent';
|
||||||
const keySuffix = isPlayback ? 'playback-streaming' : 'streaming';
|
const keySuffix = isPlayback ? 'playback-streaming' : 'streaming';
|
||||||
|
|
||||||
const lastGroup = finalGroupedMessages.at(-1);
|
const lastGroup = finalGroupedMessages.at(-1);
|
||||||
if (!lastGroup || lastGroup.type === 'user') {
|
if (!lastGroup || lastGroup.type === 'user') {
|
||||||
// Create new assistant group for streaming content
|
// Create new assistant group for streaming content
|
||||||
|
@ -770,7 +796,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
<div key={group.key} className="space-y-3">
|
<div key={group.key} className="space-y-3">
|
||||||
{/* All file attachments rendered outside message bubble */}
|
{/* All file attachments rendered outside message bubble */}
|
||||||
{renderStandaloneAttachments(attachments as string[], handleOpenFileViewer, sandboxId, project, true)}
|
{renderStandaloneAttachments(attachments as string[], handleOpenFileViewer, sandboxId, project, true)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<div className="flex max-w-[85%] rounded-3xl rounded-br-lg bg-card border px-4 py-3 break-words overflow-hidden">
|
<div className="flex max-w-[85%] rounded-3xl rounded-br-lg bg-card border px-4 py-3 break-words overflow-hidden">
|
||||||
<div className="space-y-3 min-w-0 flex-1">
|
<div className="space-y-3 min-w-0 flex-1">
|
||||||
|
@ -789,7 +815,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
// Get agent_id from the first assistant message in this group
|
// Get agent_id from the first assistant message in this group
|
||||||
const firstAssistantMsg = group.messages.find(m => m.type === 'assistant');
|
const firstAssistantMsg = group.messages.find(m => m.type === 'assistant');
|
||||||
const groupAgentId = firstAssistantMsg?.agent_id;
|
const groupAgentId = firstAssistantMsg?.agent_id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={group.key} ref={groupIndex === groupedMessages.length - 1 ? latestMessageRef : null}>
|
<div key={group.key} ref={groupIndex === groupedMessages.length - 1 ? latestMessageRef : null}>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
@ -903,7 +929,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
|
|
||||||
// Preprocess content first to remove text-only tool tags
|
// Preprocess content first to remove text-only tool tags
|
||||||
const textToRender = preprocessTextOnlyTools(streamingTextContent || '');
|
const textToRender = preprocessTextOnlyTools(streamingTextContent || '');
|
||||||
|
|
||||||
let detectedTag: string | null = null;
|
let detectedTag: string | null = null;
|
||||||
let tagStartIndex = -1;
|
let tagStartIndex = -1;
|
||||||
if (textToRender) {
|
if (textToRender) {
|
||||||
|
@ -927,11 +953,11 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
}
|
}
|
||||||
const textBeforeTag = detectedTag ? textToRender.substring(0, tagStartIndex) : textToRender;
|
const textBeforeTag = detectedTag ? textToRender.substring(0, tagStartIndex) : textToRender;
|
||||||
const showCursor =
|
const showCursor =
|
||||||
(streamHookStatus ===
|
(streamHookStatus ===
|
||||||
'streaming' ||
|
'streaming' ||
|
||||||
streamHookStatus ===
|
streamHookStatus ===
|
||||||
'connecting') &&
|
'connecting') &&
|
||||||
!detectedTag;
|
!detectedTag;
|
||||||
|
|
||||||
// Show minimal processing indicator when agent is active but no streaming text after preprocessing
|
// Show minimal processing indicator when agent is active but no streaming text after preprocessing
|
||||||
if (!textToRender && (streamHookStatus === 'streaming' || streamHookStatus === 'connecting')) {
|
if (!textToRender && (streamHookStatus === 'streaming' || streamHookStatus === 'connecting')) {
|
||||||
|
@ -946,8 +972,8 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StreamingText
|
<StreamingText
|
||||||
content={textBeforeTag}
|
content={textBeforeTag}
|
||||||
className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere"
|
className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -972,7 +998,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
{(() => {
|
{(() => {
|
||||||
// Preprocess content first to remove text-only tool tags
|
// Preprocess content first to remove text-only tool tags
|
||||||
const textToRender = preprocessTextOnlyTools(streamingText || '');
|
const textToRender = preprocessTextOnlyTools(streamingText || '');
|
||||||
|
|
||||||
let detectedTag: string | null = null;
|
let detectedTag: string | null = null;
|
||||||
let tagStartIndex = -1;
|
let tagStartIndex = -1;
|
||||||
if (textToRender) {
|
if (textToRender) {
|
||||||
|
@ -1009,8 +1035,8 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<StreamingText
|
<StreamingText
|
||||||
content={textBeforeTag}
|
content={textBeforeTag}
|
||||||
className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere"
|
className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue