mirror of
https://github.com/geoffsee/open-gsio.git
synced 2025-09-08 22:56:46 +00:00
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.
This commit is contained in:

committed by
Geoff Seemueller

parent
de968bcfbd
commit
06b6a68b9b
@@ -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 {
|
||||
|
@@ -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<any>) {
|
||||
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<any>) {
|
||||
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<any>) {
|
||||
const shouldBreak = await this.processChunk(chunk, dataCallback);
|
||||
if (shouldBreak) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
41
packages/ai/src/tools/basic.ts
Normal file
41
packages/ai/src/tools/basic.ts
Normal file
@@ -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<BasicValueResult> => {
|
||||
// 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' };
|
||||
},
|
||||
};
|
@@ -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": {
|
||||
|
Reference in New Issue
Block a user