feat: per-agent tools toggle and better python scratchpad
Add ToolSelector for enabling sandboxed python per agent, drop unsafe filesystem/bash tools, bundle fetchUrl with web search, and return Monty's last expression value so agents need not print.
This commit is contained in:
@@ -50,6 +50,10 @@ const searchConfig = ref({
|
|||||||
rerank: props.agent?.config?.search?.rerank ?? false,
|
rerank: props.agent?.config?.search?.rerank ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toolsConfig = ref({
|
||||||
|
python: props.agent?.config?.tools?.python ?? false,
|
||||||
|
});
|
||||||
|
|
||||||
watch(() => props.agent?.config?.search, (val) => {
|
watch(() => props.agent?.config?.search, (val) => {
|
||||||
searchConfig.value = {
|
searchConfig.value = {
|
||||||
enabled: val?.enabled ?? false,
|
enabled: val?.enabled ?? false,
|
||||||
@@ -58,21 +62,45 @@ watch(() => props.agent?.config?.search, (val) => {
|
|||||||
};
|
};
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
const saveSearchConfig = async () => {
|
watch(() => props.agent?.config?.tools, (val) => {
|
||||||
|
toolsConfig.value = {
|
||||||
|
python: val?.python ?? false,
|
||||||
|
};
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
const saveAgentConfig = async (partial: {
|
||||||
|
search?: typeof searchConfig.value;
|
||||||
|
tools?: typeof toolsConfig.value;
|
||||||
|
}) => {
|
||||||
if (!props.agent) return;
|
if (!props.agent) return;
|
||||||
const currentConfig = props.agent.config ?? {};
|
const currentConfig = props.agent.config ?? {};
|
||||||
const newConfig = {
|
const newConfig = {
|
||||||
...currentConfig,
|
...currentConfig,
|
||||||
search: {
|
...(partial.search !== undefined ? {
|
||||||
enabled: searchConfig.value.enabled,
|
search: {
|
||||||
maxResults: searchConfig.value.maxResults,
|
enabled: partial.search.enabled,
|
||||||
rerank: searchConfig.value.rerank,
|
maxResults: partial.search.maxResults,
|
||||||
},
|
rerank: partial.search.rerank,
|
||||||
|
},
|
||||||
|
} : {}),
|
||||||
|
...(partial.tools !== undefined ? {
|
||||||
|
tools: {
|
||||||
|
python: partial.tools.python,
|
||||||
|
},
|
||||||
|
} : {}),
|
||||||
};
|
};
|
||||||
patchAgentLocally(props.agent.id, { config: newConfig });
|
patchAgentLocally(props.agent.id, { config: newConfig });
|
||||||
updateAgent(props.agent.id, { config: newConfig });
|
updateAgent(props.agent.id, { config: newConfig });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveSearchConfig = async () => {
|
||||||
|
await saveAgentConfig({ search: searchConfig.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveToolsConfig = async () => {
|
||||||
|
await saveAgentConfig({ tools: toolsConfig.value });
|
||||||
|
};
|
||||||
|
|
||||||
const isUploading = computed(() => files.value.some((f) => f.status === 'uploading'));
|
const isUploading = computed(() => files.value.some((f) => f.status === 'uploading'));
|
||||||
const hasFailedUploads = computed(() => files.value.some((f) => f.status === 'error'));
|
const hasFailedUploads = computed(() => files.value.some((f) => f.status === 'error'));
|
||||||
|
|
||||||
@@ -386,6 +414,8 @@ onUnmounted(() => {
|
|||||||
@update:enabled="(v: boolean) => { searchConfig.enabled = v; saveSearchConfig() }"
|
@update:enabled="(v: boolean) => { searchConfig.enabled = v; saveSearchConfig() }"
|
||||||
@update:max-results="(v: number) => { searchConfig.maxResults = v; saveSearchConfig() }"
|
@update:max-results="(v: number) => { searchConfig.maxResults = v; saveSearchConfig() }"
|
||||||
@update:rerank="(v: boolean) => { searchConfig.rerank = v; saveSearchConfig() }" />
|
@update:rerank="(v: boolean) => { searchConfig.rerank = v; saveSearchConfig() }" />
|
||||||
|
<ToolSelector v-if="selectedModel?.capabilities.includes('tools')" :python="toolsConfig.python"
|
||||||
|
@update:python="(v: boolean) => { toolsConfig.python = v; saveToolsConfig() }" />
|
||||||
<FileSelector ref="fileSelectorRef" :selected-model="selectedModel" v-model="files" />
|
<FileSelector ref="fileSelectorRef" :selected-model="selectedModel" v-model="files" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="allowManualRole" class="flex items-center">
|
<div v-if="allowManualRole" class="flex items-center">
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const openSettings = () => {
|
|||||||
</span>
|
</span>
|
||||||
<span class="text-xs leading-snug"
|
<span class="text-xs leading-snug"
|
||||||
:class="enabled ? 'text-[var(--text-secondary)]' : 'text-[var(--text-dim)]'">
|
:class="enabled ? 'text-[var(--text-secondary)]' : 'text-[var(--text-dim)]'">
|
||||||
Search the web automatically when needed
|
Search the web and fetch URLs when needed
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
python: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:python': [value: boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const anyEnabled = computed(() => props.python);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dropdown dropdownClass="text-sm" placement="top">
|
||||||
|
<template #default="{ toggle, setRef }">
|
||||||
|
<button :ref="setRef" @click="toggle"
|
||||||
|
class="flex items-center justify-center h-8.5 w-8.5 @hover:bg-[var(--color-hover)] rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||||
|
<span class="pointer-events-none i-mynaui-archive text-5 transition-colors duration-200"
|
||||||
|
:class="anyEnabled ? 'text-[var(--color-accent)]' : 'text-[var(--text-secondary)]'"></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #dropdown>
|
||||||
|
<div class="flex flex-col gap-1 p-1 min-w-48">
|
||||||
|
<label
|
||||||
|
class="flex items-start gap-3 rounded-xl px-3 py-2.5 text-left cursor-pointer transition-colors duration-150 @hover:bg-[var(--color-hover)]"
|
||||||
|
:class="python ? 'bg-[var(--color-active)]' : ''">
|
||||||
|
<input type="checkbox" :checked="python"
|
||||||
|
@change="emit('update:python', ($event.target as HTMLInputElement).checked)"
|
||||||
|
class="mt-0.5 w-4 h-4 rounded border-[var(--color-border)] bg-transparent text-[var(--color-accent)] focus:ring-[var(--color-accent)] focus:ring-offset-0 cursor-pointer shrink-0" />
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<span class="text-sm font-medium"
|
||||||
|
:class="python ? 'text-[var(--text-primary)]' : 'text-[var(--text-secondary)]'">
|
||||||
|
Python
|
||||||
|
</span>
|
||||||
|
<span class="text-xs leading-snug"
|
||||||
|
:class="python ? 'text-[var(--text-secondary)]' : 'text-[var(--text-dim)]'">
|
||||||
|
Run code in a sandbox
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</template>
|
||||||
+4
-1
@@ -112,7 +112,10 @@ export const agents = pgTable('agents', {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
rerank: boolean;
|
rerank: boolean;
|
||||||
maxResults: number;
|
maxResults: number;
|
||||||
}
|
};
|
||||||
|
tools?: {
|
||||||
|
python?: boolean;
|
||||||
|
};
|
||||||
}>().default({}),
|
}>().default({}),
|
||||||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||||||
}, (table) => [
|
}, (table) => [
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
rerank: z.boolean(),
|
rerank: z.boolean(),
|
||||||
maxResults: z.number().min(1).max(50),
|
maxResults: z.number().min(1).max(50),
|
||||||
}).optional(),
|
}).optional(),
|
||||||
|
tools: z.object({
|
||||||
|
python: z.boolean().optional(),
|
||||||
|
}).optional(),
|
||||||
}).optional(),
|
}).optional(),
|
||||||
})
|
})
|
||||||
.safeParse(body),
|
.safeParse(body),
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { db } from "~~/server/lib/db";
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import { type MessageEntity } from '~/composables/useChat';
|
import { type MessageEntity } from '~/composables/useChat';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { glob } from 'glob';
|
|
||||||
import { isLoopFinished, type ModelMessage, rerank, type RerankingModel, streamText, type StreamTextTransform, type Tool, tool } from "ai";
|
import { isLoopFinished, type ModelMessage, rerank, type RerankingModel, streamText, type StreamTextTransform, type Tool, tool } from "ai";
|
||||||
import { generations, messageParts, messages, toolCalls, ToolCallType } from "~~/drizzle/schema";
|
import { generations, messageParts, messages, toolCalls, ToolCallType } from "~~/drizzle/schema";
|
||||||
import { topicEvents } from "~~/server/utils/events";
|
import { topicEvents } from "~~/server/utils/events";
|
||||||
@@ -332,6 +331,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
agentmessage as MessageEntity,
|
agentmessage as MessageEntity,
|
||||||
{ gateway: gateway.gateway, model, parameters: args },
|
{ gateway: gateway.gateway, model, parameters: args },
|
||||||
searchParam,
|
searchParam,
|
||||||
|
{
|
||||||
|
python: topic.agent.config?.tools?.python ?? false,
|
||||||
|
},
|
||||||
agentmessage.generationId!,
|
agentmessage.generationId!,
|
||||||
userId,
|
userId,
|
||||||
topicId,
|
topicId,
|
||||||
@@ -363,24 +365,54 @@ const formatToolCallId = (nativeId: string) => {
|
|||||||
return `veridian__tool-${nativeId.slice(0, 16)}-${nanoid()}`;
|
return `veridian__tool-${nativeId.slice(0, 16)}-${nanoid()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const evalPython = async (code: string) => {
|
const formatPythonValue = (value: unknown): string => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
let stdout = ''
|
return JSON.stringify(value, (_, v) => typeof v === 'bigint' ? v.toString() : v);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const evalPython = async (code: string): Promise<string> => {
|
||||||
|
try {
|
||||||
|
let stdout = '';
|
||||||
const printCallback = (_: string, text: string) => {
|
const printCallback = (_: string, text: string) => {
|
||||||
stdout += text
|
stdout += text;
|
||||||
|
};
|
||||||
|
const m = new Monty(code);
|
||||||
|
const result = m.run({
|
||||||
|
printCallback,
|
||||||
|
limits: {
|
||||||
|
maxDurationSecs: 10,
|
||||||
|
maxMemory: 32 * 1024 * 1024,
|
||||||
|
maxRecursionDepth: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const expressionOutput = formatPythonValue(result);
|
||||||
|
if (stdout && expressionOutput) {
|
||||||
|
return stdout.endsWith('\n')
|
||||||
|
? `${stdout}${expressionOutput}`
|
||||||
|
: `${stdout}\n${expressionOutput}`;
|
||||||
}
|
}
|
||||||
const m = new Monty(code)
|
return stdout || expressionOutput || '';
|
||||||
await m.run({ printCallback })
|
|
||||||
return stdout
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof MontySyntaxError) {
|
if (error instanceof MontySyntaxError) {
|
||||||
console.log('Syntax error:', error.message)
|
return `SyntaxError: ${error.message}`;
|
||||||
} else if (error instanceof MontyRuntimeError) {
|
|
||||||
console.log('Runtime error:', error.message)
|
|
||||||
console.log('Traceback:', error.traceback())
|
|
||||||
} else if (error instanceof MontyTypingError) {
|
|
||||||
console.log('Type error:', error.displayDiagnostics())
|
|
||||||
}
|
}
|
||||||
|
if (error instanceof MontyRuntimeError) {
|
||||||
|
return error.display('traceback') || `RuntimeError: ${error.message}`;
|
||||||
|
}
|
||||||
|
if (error instanceof MontyTypingError) {
|
||||||
|
return error.display('concise') || `TypeError: ${error.message}`;
|
||||||
|
}
|
||||||
|
return error instanceof Error ? error.message : 'Python execution failed';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -438,165 +470,47 @@ export const searchTheWeb = (config: SearchTheWebConfig) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: obviously come up with a better way for the user to define their own tools
|
const fetchUrlTool = tool({
|
||||||
const { listDirectoryTool, globTool, readFileTool, readFilesTool, fetchUrlTool, pythonTool, bashTool } = {
|
description: 'Fetches the content of a URL',
|
||||||
listDirectoryTool: tool({
|
inputSchema: z.object({
|
||||||
description: 'Lists the contents of a directory',
|
url: z.string(),
|
||||||
inputSchema: z.object({
|
}),
|
||||||
path: z.string(),
|
outputSchema: z.object({
|
||||||
}),
|
content: z.string(),
|
||||||
outputSchema: z.object({
|
}),
|
||||||
files: z.array(z.object({ name: z.string(), type: z.string() })),
|
execute: async ({ url }) => {
|
||||||
}),
|
const response = await fetch('https://api.firecrawl.dev/v2/scrape', {
|
||||||
execute: async ({ path }) => {
|
method: 'POST',
|
||||||
const rawFiles = await fs.readdir(path, { withFileTypes: true });
|
headers: {
|
||||||
const files = rawFiles.map((file) => ({
|
'Content-Type': 'application/json',
|
||||||
name: file.name,
|
'Authorization': 'Bearer ' + process.env.FIRECRAWL_API_KEY,
|
||||||
type: file.isFile() ? 'file' : 'directory',
|
},
|
||||||
}));
|
body: JSON.stringify({
|
||||||
|
url,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const content = await response.json();
|
||||||
|
return {
|
||||||
|
content: content.data.markdown,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
const pythonTool = tool({
|
||||||
files,
|
description: 'Executes Python in a sandbox. The value of the last expression is returned automatically (no print needed). stdout from print() is also included.',
|
||||||
};
|
inputSchema: z.object({
|
||||||
},
|
code: z.string().describe('Python code to run. Prefer a final expression over print(), e.g. `2 + 2` returns `4`.'),
|
||||||
}),
|
}),
|
||||||
globTool: tool({
|
outputSchema: z.object({
|
||||||
description: 'Lists files matching a glob pattern',
|
output: z.string(),
|
||||||
inputSchema: z.object({
|
|
||||||
pattern: z.string(),
|
|
||||||
}),
|
|
||||||
outputSchema: z.object({
|
|
||||||
files: z.array(z.object({ path: z.string(), type: z.string() })),
|
|
||||||
}),
|
|
||||||
execute: async ({ pattern }) => {
|
|
||||||
const rawFiles = await glob(pattern, { withFileTypes: true });
|
|
||||||
const files = rawFiles.map((file) => ({
|
|
||||||
path: file.parentPath + '/' + file.name,
|
|
||||||
type: file.isFile() ? 'file' : 'directory',
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
files,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
readFileTool: tool({
|
execute: async ({ code }) => {
|
||||||
description: 'Reads the contents of a file',
|
const output = await evalPython(code);
|
||||||
inputSchema: z.object({
|
return {
|
||||||
path: z.string(),
|
output,
|
||||||
}),
|
};
|
||||||
outputSchema: z.object({
|
},
|
||||||
path: z.string(),
|
});
|
||||||
content: z.string(),
|
|
||||||
}),
|
|
||||||
execute: async ({ path }) => {
|
|
||||||
const file = await fs.readFile(path);
|
|
||||||
return {
|
|
||||||
path,
|
|
||||||
content: file.toString(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
readFilesTool: tool({
|
|
||||||
description: 'Reads the contents of multiple files',
|
|
||||||
inputSchema: z.object({
|
|
||||||
paths: z.array(z.string()).describe('a list of file paths to read'),
|
|
||||||
}),
|
|
||||||
outputSchema: z.object({
|
|
||||||
files: z.array(
|
|
||||||
z.object({
|
|
||||||
path: z.string(),
|
|
||||||
content: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
execute: async ({ paths }) => {
|
|
||||||
const files = await Promise.all(
|
|
||||||
paths.map(async (path) => {
|
|
||||||
const file = await fs.readFile(path);
|
|
||||||
return {
|
|
||||||
path: path,
|
|
||||||
content: file.toString(),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
files,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
fetchUrlTool: tool({
|
|
||||||
description: 'Fetches the content of a URL',
|
|
||||||
inputSchema: z.object({
|
|
||||||
url: z.string(),
|
|
||||||
}),
|
|
||||||
outputSchema: z.object({
|
|
||||||
content: z.string(),
|
|
||||||
}),
|
|
||||||
execute: async ({ url }) => {
|
|
||||||
const response = await fetch('https://api.firecrawl.dev/v2/scrape', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer ' + process.env.FIRECRAWL_API_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
url,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const content = await response.json();
|
|
||||||
console.log(content);
|
|
||||||
return {
|
|
||||||
content: content.data.markdown,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
pythonTool: tool({
|
|
||||||
description: 'Executes a Python code snippet',
|
|
||||||
inputSchema: z.object({
|
|
||||||
code: z.string(),
|
|
||||||
}),
|
|
||||||
outputSchema: z.object({
|
|
||||||
output: z.string(),
|
|
||||||
}),
|
|
||||||
execute: async ({ code }) => {
|
|
||||||
const output = await evalPython(code);
|
|
||||||
return {
|
|
||||||
output,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
bashTool: tool({
|
|
||||||
description: 'Executes a Bash command',
|
|
||||||
inputSchema: z.object({
|
|
||||||
code: z.string(),
|
|
||||||
}),
|
|
||||||
outputSchema: z.object({
|
|
||||||
output: z.string(),
|
|
||||||
}),
|
|
||||||
execute: async ({ code }) => {
|
|
||||||
const { exec } = await import('child_process');
|
|
||||||
const { promisify } = await import('util');
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
async function runCommand(command: string) {
|
|
||||||
const { stdout, stderr } = await execAsync(command);
|
|
||||||
if (stderr) {
|
|
||||||
console.error(`Error: ${stderr}`);
|
|
||||||
return stderr;
|
|
||||||
} else {
|
|
||||||
return stdout;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const output = await runCommand(code);
|
|
||||||
return {
|
|
||||||
output,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateResponse(
|
async function generateResponse(
|
||||||
message: MessageEntity,
|
message: MessageEntity,
|
||||||
@@ -608,6 +522,9 @@ async function generateResponse(
|
|||||||
search: false | {
|
search: false | {
|
||||||
config: SearchTheWebConfig,
|
config: SearchTheWebConfig,
|
||||||
},
|
},
|
||||||
|
enabledTools: {
|
||||||
|
python?: boolean;
|
||||||
|
},
|
||||||
generationId: string,
|
generationId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
topicId: string,
|
topicId: string,
|
||||||
@@ -625,31 +542,11 @@ async function generateResponse(
|
|||||||
const activeToolCalls = new Set<string>();
|
const activeToolCalls = new Set<string>();
|
||||||
const nativeToDbToolCallId = new Map<string, string>();
|
const nativeToDbToolCallId = new Map<string, string>();
|
||||||
|
|
||||||
// TODO: somehow let the user turn on and off tools
|
const tools: Record<string, Tool> = {};
|
||||||
const tools: Record<string, Tool> = {
|
|
||||||
// writeFile: tool({
|
if (enabledTools.python) {
|
||||||
// inputSchema: z.object({
|
tools.python = pythonTool;
|
||||||
// path: z.string(),
|
}
|
||||||
// content: z.string(),
|
|
||||||
// }),
|
|
||||||
// outputSchema: z.object({
|
|
||||||
// success: z.boolean(),
|
|
||||||
// }),
|
|
||||||
// execute: async ({ path, content }) => {
|
|
||||||
// await fs.writeFile(path, content);
|
|
||||||
// return {
|
|
||||||
// success: true,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
listDirectory: listDirectoryTool,
|
|
||||||
glob: globTool,
|
|
||||||
readFile: readFileTool,
|
|
||||||
readFiles: readFilesTool,
|
|
||||||
fetchUrl: fetchUrlTool,
|
|
||||||
python: pythonTool,
|
|
||||||
bash: bashTool,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
tools.search = tool({
|
tools.search = tool({
|
||||||
@@ -669,6 +566,7 @@ async function generateResponse(
|
|||||||
return await searchTheWeb(search.config)(query);
|
return await searchTheWeb(search.config)(query);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
tools.fetchUrl = fetchUrlTool;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = streamText({
|
const response = streamText({
|
||||||
@@ -685,7 +583,7 @@ async function generateResponse(
|
|||||||
},
|
},
|
||||||
experimental_transform: streamTransoforms,
|
experimental_transform: streamTransoforms,
|
||||||
stopWhen: isLoopFinished(),
|
stopWhen: isLoopFinished(),
|
||||||
tools: model.model.capabilities.includes('tools') ? tools : undefined,
|
tools: model.model.capabilities.includes('tools') && Object.keys(tools).length > 0 ? tools : undefined,
|
||||||
onError: async (error: any) => {
|
onError: async (error: any) => {
|
||||||
// TODO: the docs say "The stream processing will pause until the callback promise is resolved." Suggesting that this error might not be fatal?
|
// TODO: the docs say "The stream processing will pause until the callback promise is resolved." Suggesting that this error might not be fatal?
|
||||||
console.error('generation error', error);
|
console.error('generation error', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user