refactor: migrate agent system from XML tool calling to OpenAI SDK with native function calling
- Replace custom XML-based tool parsing (XMLToolExecutor) with OpenAI SDK's native tool_calls via `openai` npm package (dangerouslyAllowBrowser) - Consolidate 4 LLM providers (OpenAI, Claude, Gemini, ClaudeOpenRouter) into a single OpenAI SDK-based LLMProvider compatible with any OpenAI-style API (OpenAI, OpenRouter, Ollama, vLLM) - Move agentic tool execution loop from ChatBox into AgentCore - Update Message type to support tool roles, tool_calls, and tool_call_id - Update system prompt to remove XML formatting instructions (~45% smaller) - Remove AttemptCompletionTool (replaced by stop_reason detection), ThinkTool, and ThinkingTool - Polish tool descriptions for OpenAI function calling schema compliance - Normalize base URLs by stripping /chat/completions suffix
This commit is contained in:
@@ -1,39 +1,8 @@
|
||||
import React, { memo, useState } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { extractXMLFromString } from '../../util/xmlUtil';
|
||||
|
||||
interface ToolXMLExpanderProps {
|
||||
toolName: string;
|
||||
xmlContent: string;
|
||||
}
|
||||
|
||||
const ToolXMLExpander: React.FC<ToolXMLExpanderProps> = ({ toolName, xmlContent }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="tool-xml-expander">
|
||||
<div
|
||||
className="tool-xml-expander-header"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<span className="tool-xml-expander-arrow">
|
||||
{isExpanded ? '▼' : '▶'}
|
||||
</span>
|
||||
<span className="tool-xml-expander-title">
|
||||
🔧 Tool: {toolName}
|
||||
</span>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div className="tool-xml-expander-content">
|
||||
{xmlContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface AssistantMessageProps {
|
||||
content: string;
|
||||
@@ -62,95 +31,34 @@ const CodeComponent = memo(({ inline, className, children, ...props }: any) => {
|
||||
});
|
||||
|
||||
const AssistantMessage: React.FC<AssistantMessageProps> = ({ content, isStreaming, onAbort }) => {
|
||||
// Function to process content and replace XML blocks with expanders
|
||||
const processContentWithXMLExpanders = (text: string) => {
|
||||
const xmlBlocks = extractXMLFromString(text);
|
||||
|
||||
if (xmlBlocks.length === 0) {
|
||||
// No XML blocks found, return content as-is
|
||||
return text;
|
||||
}
|
||||
|
||||
let processedContent = text;
|
||||
const expanders: React.ReactElement[] = [];
|
||||
let expanderIndex = 0;
|
||||
|
||||
// Replace each XML block with a placeholder
|
||||
xmlBlocks.forEach((xmlBlock) => {
|
||||
const toolNameMatch = xmlBlock.match(/<([a-zA-Z_][a-zA-Z0-9_-]*)/);
|
||||
const toolName = toolNameMatch ? toolNameMatch[1] : 'unknown_tool';
|
||||
|
||||
const placeholder = `__XML_EXPANDER_${expanderIndex}__`;
|
||||
processedContent = processedContent.replace(xmlBlock, placeholder);
|
||||
|
||||
expanders[expanderIndex] = (
|
||||
<ToolXMLExpander
|
||||
key={`xml-expander-${expanderIndex}`}
|
||||
toolName={toolName}
|
||||
xmlContent={xmlBlock}
|
||||
/>
|
||||
);
|
||||
|
||||
expanderIndex++;
|
||||
});
|
||||
|
||||
// Split content by placeholders and interleave with expanders
|
||||
const parts = processedContent.split(/__XML_EXPANDER_\d+__/);
|
||||
const result: (string | React.ReactElement)[] = [];
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (parts[i]) {
|
||||
result.push(parts[i]);
|
||||
}
|
||||
if (i < expanders.length) {
|
||||
result.push(expanders[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Handle special abort link for streaming messages
|
||||
const renderContent = () => {
|
||||
// Handle special abort link for streaming messages
|
||||
if (isStreaming && onAbort && content.includes('click here to abort')) {
|
||||
// Check if content has the processing wave HTML
|
||||
const hasProcessingWave = content.includes('<span class="processing-wave">Processing...</span>');
|
||||
|
||||
|
||||
if (hasProcessingWave) {
|
||||
// Parse the content to handle both the wave animation and abort link
|
||||
const parts = content.split('click here to abort');
|
||||
const beforeAbort = parts[0];
|
||||
const afterAbort = parts[1];
|
||||
|
||||
// Replace the HTML span with JSX
|
||||
const processedBefore = beforeAbort.replace(
|
||||
const beforeAbort = parts[0].replace(
|
||||
'<span class="processing-wave">Processing...</span>',
|
||||
''
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span className="processing-wave">Processing...</span>
|
||||
{processedBefore}
|
||||
<button
|
||||
onClick={onAbort}
|
||||
className="abort-link"
|
||||
>
|
||||
{beforeAbort}
|
||||
<button onClick={onAbort} className="abort-link">
|
||||
click here to abort
|
||||
</button>
|
||||
{afterAbort}
|
||||
{parts[1]}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
// Original logic for non-wave processing messages
|
||||
const parts = content.split('click here to abort');
|
||||
return (
|
||||
<span>
|
||||
{parts[0]}
|
||||
<button
|
||||
onClick={onAbort}
|
||||
className="abort-link"
|
||||
>
|
||||
<button onClick={onAbort} className="abort-link">
|
||||
click here to abort
|
||||
</button>
|
||||
{parts[1]}
|
||||
@@ -159,34 +67,6 @@ const AssistantMessage: React.FC<AssistantMessageProps> = ({ content, isStreamin
|
||||
}
|
||||
}
|
||||
|
||||
const processedContent = processContentWithXMLExpanders(content);
|
||||
|
||||
// If we have mixed content (text + React elements), render them separately
|
||||
if (Array.isArray(processedContent)) {
|
||||
return (
|
||||
<div>
|
||||
{processedContent.map((item, index) => {
|
||||
if (typeof item === 'string') {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={`text-${index}`}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code: CodeComponent,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
} else {
|
||||
return item; // React element (expander)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Plain text content, render with markdown
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
@@ -194,7 +74,7 @@ const AssistantMessage: React.FC<AssistantMessageProps> = ({ content, isStreamin
|
||||
code: CodeComponent,
|
||||
}}
|
||||
>
|
||||
{processedContent as string}
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
@@ -208,4 +88,4 @@ const AssistantMessage: React.FC<AssistantMessageProps> = ({ content, isStreamin
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AssistantMessage);
|
||||
export default memo(AssistantMessage);
|
||||
|
||||
Reference in New Issue
Block a user