changes
This commit is contained in:
+19
-14
@@ -46,11 +46,12 @@ const tools: Tool[] = [
|
||||
|
||||
// --- Tool Implementations ---
|
||||
|
||||
const availableTools: Record<string, Function> = {
|
||||
const availableTools: Record<string, (args: Record<string, unknown>) => string> = {
|
||||
get_current_time: () => {
|
||||
return new Date().toLocaleTimeString();
|
||||
},
|
||||
calculate: ({ expression }: { expression: string }) => {
|
||||
calculate: (args) => {
|
||||
const expression = args.expression as string;
|
||||
try {
|
||||
// Safety: simplistic eval for demo purposes.
|
||||
// In production, use a math parser library like mathjs.
|
||||
@@ -68,17 +69,19 @@ export async function chat(model: string, messages: ChatMessage[]) {
|
||||
let response;
|
||||
try {
|
||||
response = await ollama.chat({
|
||||
model: model,
|
||||
messages: messages,
|
||||
tools: tools,
|
||||
model,
|
||||
messages,
|
||||
tools,
|
||||
});
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
// Fallback: If model doesn't support tools, retry without them
|
||||
if (e.message?.includes('does not support tools')) {
|
||||
const errorMessage = e instanceof Error ? e.message : '';
|
||||
if (errorMessage.includes('does not support tools')) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Model ${model} does not support tools. Falling back to standard chat.`);
|
||||
response = await ollama.chat({
|
||||
model: model,
|
||||
messages: messages,
|
||||
model,
|
||||
messages,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
@@ -101,6 +104,7 @@ export async function chat(model: string, messages: ChatMessage[]) {
|
||||
const functionToCall = availableTools[functionName];
|
||||
|
||||
if (functionToCall) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`🤖 Tool Call: ${functionName}`, tool.function.arguments);
|
||||
const functionArgs = tool.function.arguments;
|
||||
const functionResponse = functionToCall(functionArgs);
|
||||
@@ -115,9 +119,9 @@ export async function chat(model: string, messages: ChatMessage[]) {
|
||||
|
||||
// 3. Send the tool results back to the model to get the final answer
|
||||
response = await ollama.chat({
|
||||
model: model,
|
||||
messages: messages,
|
||||
tools: tools,
|
||||
model,
|
||||
messages,
|
||||
tools,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -125,11 +129,12 @@ export async function chat(model: string, messages: ChatMessage[]) {
|
||||
success: true,
|
||||
message: response.message,
|
||||
};
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Chat error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Failed to generate response',
|
||||
error: error instanceof Error ? error.message : 'Failed to generate response',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+9
-10
@@ -144,21 +144,20 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Execute each tool and collect results
|
||||
for (const toolCall of toolCalls) {
|
||||
controller.enqueue(encoder.encode(`**Using tool: ${toolCall.name}**\n`));
|
||||
// Use structured markers for frontend parsing
|
||||
const toolOutput = await executeTool(toolCall.name, toolCall.arguments);
|
||||
const toolResult = toolOutput.success
|
||||
? toolOutput.result || ''
|
||||
: `Error: ${toolOutput.error}`;
|
||||
|
||||
const result = await executeTool(toolCall.name, toolCall.arguments);
|
||||
|
||||
// Send tool result to stream
|
||||
if (result.success) {
|
||||
controller.enqueue(encoder.encode(`\`\`\`\n${result.result}\n\`\`\`\n\n`));
|
||||
} else {
|
||||
controller.enqueue(encoder.encode(`Error: ${result.error}\n\n`));
|
||||
}
|
||||
// Format: <!--TOOL_START:name:args-->result<!--TOOL_END-->
|
||||
const toolMarker = `<!--TOOL_START:${toolCall.name}:${JSON.stringify(toolCall.arguments)}-->${toolResult}<!--TOOL_END-->\n\n`;
|
||||
controller.enqueue(encoder.encode(toolMarker));
|
||||
|
||||
// Add tool result to messages for next iteration
|
||||
workingMessages.push({
|
||||
role: 'tool',
|
||||
content: result.success ? result.result || '' : `Error: ${result.error}`,
|
||||
content: toolResult,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -528,6 +528,7 @@ export default function ChatLayout() {
|
||||
};
|
||||
|
||||
const handleRemoveChat = async (chatId: string) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (!window.confirm('Are you sure you want to delete this chat?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.markdown {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@@ -14,19 +13,20 @@
|
||||
.codeBlock {
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
animation: textFadeIn 0.3s ease-out forwards;
|
||||
animation: text-fade-in 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes textFadeIn {
|
||||
@keyframes text-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { ToolCallDisplay } from './ToolCallDisplay';
|
||||
import classes from './MarkdownMessage.module.css';
|
||||
// Import KaTeX CSS for LaTeX rendering
|
||||
import 'katex/dist/katex.min.css';
|
||||
@@ -27,6 +28,71 @@ interface MarkdownMessageProps {
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
interface ParsedToolCall {
|
||||
toolName: string;
|
||||
args: Record<string, unknown>;
|
||||
result: string;
|
||||
}
|
||||
|
||||
interface ContentSegment {
|
||||
type: 'text' | 'tool';
|
||||
content?: string;
|
||||
toolCall?: ParsedToolCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse content to extract tool calls and regular text segments
|
||||
*/
|
||||
function parseContentWithToolCalls(content: string): ContentSegment[] {
|
||||
const segments: ContentSegment[] = [];
|
||||
const toolPattern = /<!--TOOL_START:(\w+):(\{.*?\})-->([\s\S]*?)<!--TOOL_END-->/g;
|
||||
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = toolPattern.exec(content)) !== null) {
|
||||
// Add text before this tool call
|
||||
if (match.index > lastIndex) {
|
||||
const textBefore = content.slice(lastIndex, match.index).trim();
|
||||
if (textBefore) {
|
||||
segments.push({ type: 'text', content: textBefore });
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the tool call
|
||||
const toolName = match[1];
|
||||
let args: Record<string, unknown> = {};
|
||||
try {
|
||||
args = JSON.parse(match[2]);
|
||||
} catch {
|
||||
// Invalid JSON, use empty args
|
||||
}
|
||||
const result = match[3].trim();
|
||||
|
||||
segments.push({
|
||||
type: 'tool',
|
||||
toolCall: { toolName, args, result },
|
||||
});
|
||||
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
// Add remaining text after last tool call
|
||||
if (lastIndex < content.length) {
|
||||
const remainingText = content.slice(lastIndex).trim();
|
||||
if (remainingText) {
|
||||
segments.push({ type: 'text', content: remainingText });
|
||||
}
|
||||
}
|
||||
|
||||
// If no tool calls found, return the whole content as text
|
||||
if (segments.length === 0 && content.trim()) {
|
||||
segments.push({ type: 'text', content });
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that tracks content changes and returns content with fade-in markers for new text
|
||||
*/
|
||||
@@ -90,11 +156,15 @@ function escapeHtml(text: string): string {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export function MarkdownMessage({ content, isStreaming = false }: MarkdownMessageProps) {
|
||||
const processedContent = useStreamingContent(content, isStreaming);
|
||||
/**
|
||||
* Strip tool call markers from content for streaming display
|
||||
*/
|
||||
function stripToolMarkers(content: string): string {
|
||||
return content.replace(/<!--TOOL_START:\w+:\{.*?\}-->/g, '').replace(/<!--TOOL_END-->/g, '');
|
||||
}
|
||||
|
||||
function MarkdownContent({ content }: { content: string }) {
|
||||
return (
|
||||
<div className={classes.markdown}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeRaw, rehypeKatex]}
|
||||
@@ -247,8 +317,41 @@ export function MarkdownMessage({ content, isStreaming = false }: MarkdownMessag
|
||||
span: ({ className, children }) => <span className={className}>{children}</span>,
|
||||
}}
|
||||
>
|
||||
{processedContent}
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
|
||||
export function MarkdownMessage({ content, isStreaming = false }: MarkdownMessageProps) {
|
||||
const processedContent = useStreamingContent(content, isStreaming);
|
||||
|
||||
// During streaming, just show the raw content (with tool markers stripped)
|
||||
if (isStreaming) {
|
||||
return (
|
||||
<div className={classes.markdown}>
|
||||
<MarkdownContent content={stripToolMarkers(processedContent)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// When not streaming, parse and render tool calls
|
||||
const segments = parseContentWithToolCalls(content);
|
||||
|
||||
return (
|
||||
<div className={classes.markdown}>
|
||||
{segments.map((segment, index) => {
|
||||
if (segment.type === 'tool' && segment.toolCall) {
|
||||
return (
|
||||
<ToolCallDisplay
|
||||
key={`tool-${index}`}
|
||||
toolName={segment.toolCall.toolName}
|
||||
args={segment.toolCall.args}
|
||||
result={segment.toolCall.result}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <MarkdownContent key={`text-${index}`} content={segment.content || ''} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
.container {
|
||||
background-color: var(--mantine-color-default-hover);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.header:hover {
|
||||
background-color: var(--mantine-color-default-hover);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 var(--mantine-spacing-xs) var(--mantine-spacing-xs);
|
||||
border-top: 1px solid var(--mantine-color-default-border);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.code {
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { IconChevronDown, IconChevronRight, IconTool } from '@tabler/icons-react';
|
||||
import { ActionIcon, Code, Collapse, Group, Paper, Text, useMantineTheme } from '@mantine/core';
|
||||
import { useThemeContext } from '@/components/DynamicThemeProvider';
|
||||
import classes from './ToolCallDisplay.module.css';
|
||||
|
||||
interface ToolCallDisplayProps {
|
||||
toolName: string;
|
||||
args: Record<string, unknown>;
|
||||
result: string;
|
||||
}
|
||||
|
||||
// Friendly tool names
|
||||
const toolDisplayNames: Record<string, string> = {
|
||||
calculator: 'Calculator',
|
||||
get_current_datetime: 'Date/Time',
|
||||
fetch_url: 'Fetch URL',
|
||||
web_search: 'Web Search',
|
||||
execute_code: 'Code Execution',
|
||||
read_file: 'Read File',
|
||||
write_file: 'Write File',
|
||||
get_weather: 'Weather',
|
||||
generate_image: 'Image Generation',
|
||||
};
|
||||
|
||||
export function ToolCallDisplay({ toolName, args, result }: ToolCallDisplayProps) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const { primaryColor } = useThemeContext();
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const displayName = toolDisplayNames[toolName] || toolName;
|
||||
const isError = result.startsWith('Error:');
|
||||
|
||||
// Format args for display
|
||||
const argsDisplay = Object.entries(args)
|
||||
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
|
||||
.join(', ');
|
||||
|
||||
return (
|
||||
<Paper className={classes.container} withBorder radius="sm" p={0} my="xs">
|
||||
<Group
|
||||
className={classes.header}
|
||||
onClick={() => setOpened(!opened)}
|
||||
gap="xs"
|
||||
wrap="nowrap"
|
||||
p="xs"
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<ActionIcon variant="subtle" color={primaryColor} size="xs">
|
||||
{opened ? <IconChevronDown size={14} /> : <IconChevronRight size={14} />}
|
||||
</ActionIcon>
|
||||
<IconTool size={16} color={theme.colors[primaryColor][6]} />
|
||||
<Text size="sm" fw={500} c={primaryColor}>
|
||||
{displayName}
|
||||
</Text>
|
||||
{!opened && argsDisplay && (
|
||||
<Text size="xs" c="dimmed" truncate style={{ flex: 1 }}>
|
||||
{argsDisplay}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Collapse in={opened}>
|
||||
<div className={classes.content}>
|
||||
{Object.keys(args).length > 0 && (
|
||||
<div className={classes.section}>
|
||||
<Text size="xs" c="dimmed" mb={4}>
|
||||
Arguments:
|
||||
</Text>
|
||||
<Code block className={classes.code}>
|
||||
{JSON.stringify(args, null, 2)}
|
||||
</Code>
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.section}>
|
||||
<Text size="xs" c="dimmed" mb={4}>
|
||||
Result:
|
||||
</Text>
|
||||
<Code block className={classes.code} c={isError ? 'red' : undefined}>
|
||||
{result}
|
||||
</Code>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -35,19 +35,22 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { deleteModel, getInstalledModels, pullModel, type OllamaModel } from '@/app/actions/ollama';
|
||||
|
||||
const POPULAR_MODELS = [
|
||||
'llama3.2',
|
||||
'llama3.1',
|
||||
'mistral',
|
||||
'gemma2',
|
||||
'qwen2.5',
|
||||
'phi3.5',
|
||||
'neural-chat',
|
||||
'starling-lm',
|
||||
'codellama',
|
||||
'deepseek-coder',
|
||||
'llava',
|
||||
];
|
||||
/*
|
||||
* Popular models list - reserved for future autocomplete feature
|
||||
* const POPULAR_MODELS = [
|
||||
* 'llama3.2',
|
||||
* 'llama3.1',
|
||||
* 'mistral',
|
||||
* 'gemma2',
|
||||
* 'qwen2.5',
|
||||
* 'phi3.5',
|
||||
* 'neural-chat',
|
||||
* 'starling-lm',
|
||||
* 'codellama',
|
||||
* 'deepseek-coder',
|
||||
* 'llava',
|
||||
* ];
|
||||
*/
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@@ -98,8 +101,6 @@ export function SettingsModal({
|
||||
},
|
||||
});
|
||||
|
||||
const [value, setValue] = useState<string | null>(null);
|
||||
|
||||
// Filter installed models based on search
|
||||
const options = models
|
||||
.filter((item) => item.name.toLowerCase().includes(search.toLowerCase().trim()))
|
||||
@@ -129,6 +130,7 @@ export function SettingsModal({
|
||||
setUser(null);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
@@ -139,6 +141,7 @@ export function SettingsModal({
|
||||
const list = await getInstalledModels();
|
||||
setModels(list);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoadingModels(false);
|
||||
@@ -146,7 +149,9 @@ export function SettingsModal({
|
||||
};
|
||||
|
||||
const handlePullModel = async () => {
|
||||
if (!newModelName) return;
|
||||
if (!newModelName) {
|
||||
return;
|
||||
}
|
||||
setPullingModel(newModelName);
|
||||
try {
|
||||
const result = await pullModel(newModelName);
|
||||
@@ -157,6 +162,7 @@ export function SettingsModal({
|
||||
setError(result.message);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
} finally {
|
||||
setPullingModel(null);
|
||||
@@ -164,11 +170,15 @@ export function SettingsModal({
|
||||
};
|
||||
|
||||
const handleDeleteModel = async (name: string) => {
|
||||
if (!confirm(`Are you sure you want to delete ${name}?`)) return;
|
||||
// eslint-disable-next-line no-alert
|
||||
if (!confirm(`Are you sure you want to delete ${name}?`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteModel(name);
|
||||
await fetchModels();
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
@@ -195,8 +205,8 @@ export function SettingsModal({
|
||||
await fetchUser();
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -220,6 +230,7 @@ export function SettingsModal({
|
||||
body: JSON.stringify({ accentColor: color }),
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to save accent color:', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export const calculatorTool: Tool = {
|
||||
// Safe math evaluation using Function constructor with limited scope
|
||||
function safeEvaluate(expression: string): number {
|
||||
// Replace common math notation
|
||||
let expr = expression
|
||||
const expr = expression
|
||||
.replace(/\^/g, '**') // Exponents
|
||||
.replace(/sqrt/g, 'Math.sqrt')
|
||||
.replace(/sin/g, 'Math.sin')
|
||||
|
||||
@@ -44,9 +44,9 @@ export const codeExecutionHandler: ToolHandler = async (args): Promise<ToolResul
|
||||
const logs: string[] = [];
|
||||
const mockConsole = {
|
||||
log: (...args: unknown[]) => logs.push(args.map(String).join(' ')),
|
||||
error: (...args: unknown[]) => logs.push('[ERROR] ' + args.map(String).join(' ')),
|
||||
warn: (...args: unknown[]) => logs.push('[WARN] ' + args.map(String).join(' ')),
|
||||
info: (...args: unknown[]) => logs.push('[INFO] ' + args.map(String).join(' ')),
|
||||
error: (...args: unknown[]) => logs.push(`[ERROR] ${args.map(String).join(' ')}`),
|
||||
warn: (...args: unknown[]) => logs.push(`[WARN] ${args.map(String).join(' ')}`),
|
||||
info: (...args: unknown[]) => logs.push(`[INFO] ${args.map(String).join(' ')}`),
|
||||
};
|
||||
|
||||
// Create a sandboxed context with limited globals
|
||||
@@ -87,7 +87,6 @@ export const codeExecutionHandler: ToolHandler = async (args): Promise<ToolResul
|
||||
return fn.call(sandbox);
|
||||
};
|
||||
|
||||
let result: unknown;
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Execution timed out')), timeoutMs);
|
||||
});
|
||||
@@ -100,15 +99,15 @@ export const codeExecutionHandler: ToolHandler = async (args): Promise<ToolResul
|
||||
}
|
||||
});
|
||||
|
||||
result = await Promise.race([executionPromise, timeoutPromise]);
|
||||
const result = await Promise.race([executionPromise, timeoutPromise]);
|
||||
|
||||
// Format output
|
||||
let output = '';
|
||||
if (logs.length > 0) {
|
||||
output += 'Console output:\n' + logs.join('\n') + '\n\n';
|
||||
output += `Console output:\n${logs.join('\n')}\n\n`;
|
||||
}
|
||||
if (result !== undefined) {
|
||||
output += 'Result: ' + JSON.stringify(result, null, 2);
|
||||
output += `Result: ${JSON.stringify(result, null, 2)}`;
|
||||
} else if (logs.length === 0) {
|
||||
output = 'Code executed successfully (no output)';
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const dateTimeHandler: ToolHandler = async (args): Promise<ToolResult> =>
|
||||
|
||||
switch (format) {
|
||||
case 'iso':
|
||||
result = now.toLocaleString('sv-SE', { timeZone: timezone }).replace(' ', 'T') + 'Z';
|
||||
result = `${now.toLocaleString('sv-SE', { timeZone: timezone }).replace(' ', 'T')}Z`;
|
||||
break;
|
||||
case 'date_only':
|
||||
result = now.toLocaleDateString('en-US', {
|
||||
|
||||
@@ -13,11 +13,11 @@ export const imageGenerationTool: Tool = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'generate_image',
|
||||
description:
|
||||
'Generate an image based on a text description. Creates images using AI image generation. ' +
|
||||
(IMAGE_API_URL
|
||||
description: `Generate an image based on a text description. Creates images using AI image generation. ${
|
||||
IMAGE_API_URL
|
||||
? 'Image generation is available.'
|
||||
: 'NOTE: Image generation is not configured. Set IMAGE_GENERATION_API_URL environment variable.'),
|
||||
: 'NOTE: Image generation is not configured. Set IMAGE_GENERATION_API_URL environment variable.'
|
||||
}`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import { webSearchHandler, webSearchTool } from './web-search';
|
||||
export type { ToolHandler, ToolResult } from './types';
|
||||
|
||||
// Registry of tool handlers
|
||||
const toolHandlers: Map<string, ToolHandler> = new Map();
|
||||
const toolHandlers = new Map<string, ToolHandler>();
|
||||
|
||||
/**
|
||||
* Register a tool handler
|
||||
|
||||
@@ -106,7 +106,7 @@ export const urlFetchHandler: ToolHandler = async (args): Promise<ToolResult> =>
|
||||
|
||||
// Truncate if needed
|
||||
if (text.length > maxLength) {
|
||||
text = text.substring(0, maxLength) + '\n\n[Content truncated...]';
|
||||
text = `${text.substring(0, maxLength)}\n\n[Content truncated...]`;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user