From 06b6a68b9b67db99a9f4a5b8d02dcafc627573f1 Mon Sep 17 00:00:00 2001 From: geoffsee <> Date: Fri, 4 Jul 2025 08:56:11 -0400 Subject: [PATCH] Enable tool-based message generation in `chat-stream-provider` and add `BasicValueTool` and `WeatherTool`. Updated dependencies to latest versions in `bun.lock`. Modified development script in `package.json` to include watch mode. --- bun.lock | 18 +- packages/ai/src/chat-sdk/chat-sdk.ts | 1 + .../ai/src/providers/chat-stream-provider.ts | 269 +++++++++++++++++- packages/ai/src/tools/basic.ts | 41 +++ packages/server/package.json | 2 +- 5 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 packages/ai/src/tools/basic.ts diff --git a/bun.lock b/bun.lock index 7cd3872..56aa0a7 100644 --- a/bun.lock +++ b/bun.lock @@ -66,7 +66,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-icons": "^5.4.0", - "react-streaming": "^0.3.44", + "react-streaming": "^0.4.2", "react-textarea-autosize": "^8.5.5", "shiki": "^1.24.0", "typescript": "^5.7.2", @@ -148,7 +148,7 @@ "bun": "^1.2.17", "bun-sqlite-key-value": "^1.13.1", "chokidar": "^4.0.1", - "dotenv": "^16.5.0", + "dotenv": "^17.0.0", "itty-router": "^5.0.18", "jsdom": "^24.0.0", "mobx": "^6.13.5", @@ -177,7 +177,7 @@ "@vitest/ui": "^3.1.4", "bun-sqlite-key-value": "^1.13.1", "chokidar": "^4.0.1", - "dotenv": "^16.5.0", + "dotenv": "^17.0.0", "itty-router": "^5.0.18", "jsdom": "^24.0.0", "mobx": "^6.13.5", @@ -994,7 +994,7 @@ "dompurify": ["dompurify@3.2.5", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ=="], - "dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="], + "dotenv": ["dotenv@17.0.1", "", {}, "sha512-GLjkduuAL7IMJg/ZnOPm9AnWKJ82mSE2tzXLaJ/6hD6DhwGfZaXG77oB8qbReyiczNxnbxQKyh0OE5mXq0bAHA=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -1566,7 +1566,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-streaming": ["react-streaming@0.3.50", "", { "dependencies": { "@brillout/import": "^0.2.3", "@brillout/json-serializer": "^0.5.1", "@brillout/picocolors": "^1.0.11", "isbot-fast": "1.2.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-BRjWzY83dtnjgFUCYM0H2J3iRuYruE++/AidXmjxRclTsz1O/zBdUVgjAO2uKb05y/zybskcKjmK/5xyMc1MSg=="], + "react-streaming": ["react-streaming@0.4.2", "", { "dependencies": { "@brillout/import": "^0.2.3", "@brillout/json-serializer": "^0.5.1", "@brillout/picocolors": "^1.0.11", "isbot-fast": "1.2.0" }, "peerDependencies": { "react": ">=19", "react-dom": ">=19" } }, "sha512-b192E9E0TnE9wWdc8uZg00MMY36btQmFV25cDOGcWQwl5qF8vzNcQPVJwWkljLOec5qIJWumH9cvEvAL1JlAGg=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], @@ -2146,6 +2146,10 @@ "@open-gsio/client/vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="], + "@open-gsio/coordinators/@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + + "@open-gsio/scripts/@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + "@open-gsio/server/vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="], "@open-gsio/services/vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="], @@ -2450,6 +2454,10 @@ "@open-gsio/client/vite/rollup": ["rollup@4.44.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.0", "@rollup/rollup-android-arm64": "4.44.0", "@rollup/rollup-darwin-arm64": "4.44.0", "@rollup/rollup-darwin-x64": "4.44.0", "@rollup/rollup-freebsd-arm64": "4.44.0", "@rollup/rollup-freebsd-x64": "4.44.0", "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", "@rollup/rollup-linux-arm-musleabihf": "4.44.0", "@rollup/rollup-linux-arm64-gnu": "4.44.0", "@rollup/rollup-linux-arm64-musl": "4.44.0", "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", "@rollup/rollup-linux-riscv64-gnu": "4.44.0", "@rollup/rollup-linux-riscv64-musl": "4.44.0", "@rollup/rollup-linux-s390x-gnu": "4.44.0", "@rollup/rollup-linux-x64-gnu": "4.44.0", "@rollup/rollup-linux-x64-musl": "4.44.0", "@rollup/rollup-win32-arm64-msvc": "4.44.0", "@rollup/rollup-win32-ia32-msvc": "4.44.0", "@rollup/rollup-win32-x64-msvc": "4.44.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA=="], + "@open-gsio/coordinators/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + + "@open-gsio/scripts/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + "@open-gsio/server/vite/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], "@open-gsio/server/vite/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], diff --git a/packages/ai/src/chat-sdk/chat-sdk.ts b/packages/ai/src/chat-sdk/chat-sdk.ts index 52fc1df..b23aa9e 100644 --- a/packages/ai/src/chat-sdk/chat-sdk.ts +++ b/packages/ai/src/chat-sdk/chat-sdk.ts @@ -2,6 +2,7 @@ import { Schema } from '@open-gsio/schema'; import type { Instance } from 'mobx-state-tree'; import { OpenAI } from 'openai'; +import type Message from '../../../schema/src/models/Message.ts'; import { AssistantSdk } from '../assistant-sdk'; import { ProviderRepository } from '../providers/_ProviderRepository.ts'; import type { diff --git a/packages/ai/src/providers/chat-stream-provider.ts b/packages/ai/src/providers/chat-stream-provider.ts index 2f9b8f1..7589498 100644 --- a/packages/ai/src/providers/chat-stream-provider.ts +++ b/packages/ai/src/providers/chat-stream-provider.ts @@ -1,6 +1,7 @@ import { OpenAI } from 'openai'; import ChatSdk from '../chat-sdk/chat-sdk.ts'; +import { BasicValueTool, WeatherTool } from '../tools/basic.ts'; import type { GenericEnv } from '../types'; export interface CommonProviderParams { @@ -35,12 +36,270 @@ export abstract class BaseChatProvider implements ChatStreamProvider { }); const client = this.getOpenAIClient(param); - const streamParams = this.getStreamParams(param, safeMessages); - const stream = await client.chat.completions.create(streamParams); - for await (const chunk of stream as unknown as AsyncIterable) { - const shouldBreak = await this.processChunk(chunk, dataCallback); - if (shouldBreak) break; + // const tools = [WeatherTool]; + const tools = [ + { + type: 'function', + function: { + name: 'getCurrentTemperature', + description: 'Get the current temperature for a specific location', + parameters: { + type: 'object', + properties: { + location: { + type: 'string', + description: 'The city and state, e.g., San Francisco, CA', + }, + unit: { + type: 'string', + enum: ['Celsius', 'Fahrenheit'], + description: "The temperature unit to use. Infer this from the user's location.", + }, + }, + required: ['location', 'unit'], + }, + }, + }, + ]; + + const getCurrentTemp = (location: string) => { + return '20C'; + }; + + const callFunction = async (name, args) => { + if (name === 'getCurrentTemperature') { + return getCurrentTemp(args.location); + } + }; + + // Main conversation loop - handle tool calls properly + let conversationComplete = false; + let toolCallIterations = 0; + const maxToolCallIterations = 5; // Prevent infinite loops + let toolsExecuted = false; // Track if we've executed tools + + while (!conversationComplete && toolCallIterations < maxToolCallIterations) { + const streamParams = this.getStreamParams(param, safeMessages); + // Only provide tools on the first call, after that force text response + const currentTools = toolsExecuted ? undefined : tools; + const stream = await client.chat.completions.create({ ...streamParams, tools: currentTools }); + + let assistantMessage = ''; + const toolCalls: any[] = []; + + for await (const chunk of stream as unknown as AsyncIterable) { + console.log('chunk', chunk); + + // Handle tool calls + if (chunk.choices[0]?.delta?.tool_calls) { + const deltaToolCalls = chunk.choices[0].delta.tool_calls; + + for (const deltaToolCall of deltaToolCalls) { + if (deltaToolCall.index !== undefined) { + // Initialize or get existing tool call + if (!toolCalls[deltaToolCall.index]) { + toolCalls[deltaToolCall.index] = { + id: deltaToolCall.id || '', + type: deltaToolCall.type || 'function', + function: { + name: deltaToolCall.function?.name || '', + arguments: deltaToolCall.function?.arguments || '', + }, + }; + } else { + // Append to existing tool call + if (deltaToolCall.function?.arguments) { + toolCalls[deltaToolCall.index].function.arguments += + deltaToolCall.function.arguments; + } + if (deltaToolCall.function?.name) { + toolCalls[deltaToolCall.index].function.name += deltaToolCall.function.name; + } + if (deltaToolCall.id) { + toolCalls[deltaToolCall.index].id += deltaToolCall.id; + } + } + } + } + } + + // Handle regular content + if (chunk.choices[0]?.delta?.content) { + assistantMessage += chunk.choices[0].delta.content; + } + + // Check if stream is finished + if (chunk.choices[0]?.finish_reason) { + if (chunk.choices[0].finish_reason === 'tool_calls' && toolCalls.length > 0) { + // Increment tool call iterations counter + toolCallIterations++; + console.log(`Tool call iteration ${toolCallIterations}/${maxToolCallIterations}`); + + // Execute tool calls and add results to conversation + console.log('Executing tool calls:', toolCalls); + + // Send feedback to user about tool invocation + dataCallback({ + type: 'chat', + data: { + choices: [ + { + delta: { + content: `\n\n🔧 Invoking ${toolCalls.length} tool${toolCalls.length > 1 ? 's' : ''}...\n`, + }, + }, + ], + }, + }); + + // Add assistant message with tool calls to conversation + safeMessages.push({ + role: 'assistant', + content: assistantMessage || null, + tool_calls: toolCalls, + }); + + // Execute each tool call and add results + for (const toolCall of toolCalls) { + if (toolCall.type === 'function') { + const name = toolCall.function.name; + console.log(`Calling function: ${name}`); + + // Send feedback about specific tool being called + dataCallback({ + type: 'chat', + data: { + choices: [ + { + delta: { + content: `📞 Calling ${name}...`, + }, + }, + ], + }, + }); + + try { + const args = JSON.parse(toolCall.function.arguments); + console.log(`Function arguments:`, args); + + const result = await callFunction(name, args); + console.log(`Function result:`, result); + + // Send feedback about tool completion + dataCallback({ + type: 'chat', + data: { + choices: [ + { + delta: { + content: ` ✅\n`, + }, + }, + ], + }, + }); + + // Add tool result to conversation + safeMessages.push({ + role: 'tool', + tool_call_id: toolCall.id, + content: result?.toString() || '', + }); + } catch (error) { + console.error(`Error executing tool ${name}:`, error); + + // Send feedback about tool error + dataCallback({ + type: 'chat', + data: { + choices: [ + { + delta: { + content: ` ❌ Error\n`, + }, + }, + ], + }, + }); + + safeMessages.push({ + role: 'tool', + tool_call_id: toolCall.id, + content: `Error: ${error.message}`, + }); + } + } + } + + // Mark that tools have been executed to prevent repeated calls + toolsExecuted = true; + + // Send feedback that tool execution is complete + dataCallback({ + type: 'chat', + data: { + choices: [ + { + delta: { + content: `\n🎯 Tool execution complete. Generating response...\n\n`, + }, + }, + ], + }, + }); + + // Continue conversation with tool results + break; + } else { + // Regular completion - send final response + conversationComplete = true; + } + } + + // Process chunk normally for non-tool-call responses + if (!chunk.choices[0]?.delta?.tool_calls) { + console.log('after-tool-call-chunk', chunk); + const shouldBreak = await this.processChunk(chunk, dataCallback); + if (shouldBreak) { + conversationComplete = true; + break; + } + } + } + } + + // Handle case where we hit maximum tool call iterations + if (toolCallIterations >= maxToolCallIterations && !conversationComplete) { + console.log('Maximum tool call iterations reached, forcing completion'); + + // Send a message indicating we've hit the limit and provide available information + dataCallback({ + type: 'chat', + data: { + choices: [ + { + delta: { + content: + '\n\n⚠️ Maximum tool execution limit reached. Based on the available information, I can provide the following response:\n\n', + }, + }, + ], + }, + }); + + // Make one final call without tools to get a response based on the tool results + const finalStreamParams = this.getStreamParams(param, safeMessages); + const finalStream = await client.chat.completions.create({ + ...finalStreamParams, + tools: undefined, // Remove tools to force a text response + }); + + for await (const chunk of finalStream as unknown as AsyncIterable) { + const shouldBreak = await this.processChunk(chunk, dataCallback); + if (shouldBreak) break; + } } } } diff --git a/packages/ai/src/tools/basic.ts b/packages/ai/src/tools/basic.ts new file mode 100644 index 0000000..632d38a --- /dev/null +++ b/packages/ai/src/tools/basic.ts @@ -0,0 +1,41 @@ +// tools/basicValue.ts +export interface BasicValueResult { + value: string; +} + +export const BasicValueTool = { + name: 'basicValue', + type: 'function', + description: 'Returns a basic value (timestamp-based) for testing', + parameters: { + type: 'object', + properties: {}, + required: [], + }, + function: async (): Promise => { + // generate something obviously basic + const basic = `tool-called-${Date.now()}`; + console.log('[BasicValueTool] returning:', basic); + return { value: basic }; + }, +}; +export const WeatherTool = { + name: 'get_weather', + type: 'function', + description: 'Get current temperature for a given location.', + parameters: { + type: 'object', + properties: { + location: { + type: 'string', + description: 'City and country e.g. Bogotá, Colombia', + }, + }, + required: ['location'], + additionalProperties: false, + }, + function: async (params: { location: string }) => { + console.log('[WeatherTool] Getting weather for:', params.location); + return { temperature: '25°C' }; + }, +}; diff --git a/packages/server/package.json b/packages/server/package.json index 8a7a0ea..1a788a4 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -2,7 +2,7 @@ "name": "@open-gsio/server", "type": "module", "scripts": { - "dev": "bun src/server/server.ts", + "dev": "bun --watch src/server/server.ts", "build": "bun ./src/server/build.ts" }, "devDependencies": {