99import { z } from "zod" ;
1010
1111import { openaiClient } from "../utils/client" ;
12+ import { CreateTodoSchema , ExecuteTodoSchema } from "./toolTypes" ;
1213
1314export type Message =
1415 | ChatCompletionSystemMessageParam
@@ -48,11 +49,71 @@ export const LLMChatOutputSchema = z.object({
4849export type LLMChatInput = z . infer < typeof LLMChatInputSchema > ;
4950export type LLMChatOutput = z . infer < typeof LLMChatOutputSchema > ;
5051
51- // System prompt for structured output
52- const structuredOutputPrompt = `You are a helpful assistant that can create and execute todos.
53- Respond with valid JSON in one of these formats:
54- - For general questions: {"type": "text", "content": "Your response"}
55- - For actions: {"type": "function_call", "function_name": "createTodo or executeTodoWorkflow", "function_arguments": {...}}` ;
52+ // Convert Zod schema to OpenAI function parameter schema
53+ function zodToJsonSchema ( schema : z . ZodType ) : any {
54+ if ( schema instanceof z . ZodObject ) {
55+ const shape = schema . _def . shape ( ) ;
56+ const properties : Record < string , any > = { } ;
57+ const required : string [ ] = [ ] ;
58+
59+ Object . entries ( shape ) . forEach ( ( [ key , value ] ) => {
60+ const zodField = value as z . ZodType ;
61+ properties [ key ] = zodToJsonSchema ( zodField ) ;
62+
63+ if ( ! zodField . isOptional ( ) ) {
64+ required . push ( key ) ;
65+ }
66+ } ) ;
67+
68+ return {
69+ type : "object" ,
70+ properties,
71+ ...( required . length > 0 ? { required } : { } ) ,
72+ } ;
73+ } else if ( schema instanceof z . ZodString ) {
74+ return { type : "string" } ;
75+ } else if ( schema instanceof z . ZodNumber ) {
76+ return { type : "number" } ;
77+ } else if ( schema instanceof z . ZodBoolean ) {
78+ return { type : "boolean" } ;
79+ } else if ( schema instanceof z . ZodArray ) {
80+ return {
81+ type : "array" ,
82+ items : zodToJsonSchema ( schema . _def . type ) ,
83+ } ;
84+ } else if ( schema instanceof z . ZodOptional ) {
85+ return zodToJsonSchema ( schema . _def . innerType ) ;
86+ } else {
87+ return { type : "string" } ; // Default fallback
88+ }
89+ }
90+
91+ // Define the available functions
92+ const availableFunctions = [
93+ {
94+ schema : CreateTodoSchema ,
95+ name : "createTodo" ,
96+ description : CreateTodoSchema . description || "Creates a new todo item"
97+ } ,
98+ {
99+ schema : ExecuteTodoSchema ,
100+ name : "executeTodoWorkflow" ,
101+ description : ExecuteTodoSchema . description || "Executes a todo item"
102+ }
103+ ] ;
104+
105+ // Convert to OpenAI function format
106+ const functionsForOpenAI = availableFunctions . map ( fn => ( {
107+ name : fn . name ,
108+ description : fn . description ,
109+ parameters : zodToJsonSchema ( fn . schema ) ,
110+ } ) ) ;
111+
112+ // Base system message
113+ const baseSystemMessage = `You are a helpful assistant that can create and execute todos.
114+ When a user asks to "do something" or "complete a task", you should:
115+ 1. First create the todo using createTodo
116+ 2. Then execute it using executeTodoWorkflow with the todoId from the previous step` ;
56117
57118export const llmChat = async ( {
58119 systemContent = "" ,
@@ -62,52 +123,57 @@ export const llmChat = async ({
62123 try {
63124 const openai = openaiClient ( { } ) ;
64125
65- // Combine system prompts if provided
126+ // Combine system messages
66127 const finalSystemContent = systemContent
67- ? `${ structuredOutputPrompt } \n\n${ systemContent } `
68- : structuredOutputPrompt ;
128+ ? `${ baseSystemMessage } \n\n${ systemContent } `
129+ : baseSystemMessage ;
69130
70- // Set up response format for JSON
71- const responseFormat = {
72- type : "json_object" as const
73- } ;
74-
75- // Chat parameters
131+ // Chat parameters with tools
76132 const chatParams : ChatCompletionCreateParamsNonStreaming = {
77133 messages : [ { role : "system" , content : finalSystemContent } , ...messages ] ,
78134 model,
79- response_format : responseFormat ,
135+ tools : functionsForOpenAI . map ( fn => ( { type : "function" , function : fn } ) ) ,
136+ tool_choice : "auto" ,
80137 } ;
81138
82139 log . debug ( "OpenAI chat completion params" , { chatParams } ) ;
83140
84141 const completion = await openai . chat . completions . create ( chatParams ) ;
85142 const message = completion . choices [ 0 ] . message ;
86-
87- // Parse structured data from JSON response
88- let structuredData ;
89- if ( message . content ) {
90- try {
91- const parsedData = JSON . parse ( message . content ) ;
92- // Validate against our schema
93- const validationResult = ResponseSchema . safeParse ( parsedData ) ;
94- if ( validationResult . success ) {
95- structuredData = validationResult . data ;
96- log . debug ( "Structured response validated" , { structuredData } ) ;
97- } else {
98- log . error ( "Invalid JSON structure" , {
99- errors : validationResult . error . errors ,
100- content : message . content
143+
144+ // Ensure we have a string content or default to empty string
145+ const messageContent = message . content || "" ;
146+
147+ // Parse function call if available
148+ let structuredData : StructuredResponse | undefined ;
149+ if ( message . tool_calls && message . tool_calls . length > 0 ) {
150+ const toolCall = message . tool_calls [ 0 ] ;
151+ if ( toolCall . type === "function" ) {
152+ try {
153+ const functionArguments = JSON . parse ( toolCall . function . arguments ) ;
154+ structuredData = {
155+ type : "function_call" as const ,
156+ function_name : toolCall . function . name ,
157+ function_arguments : functionArguments ,
158+ } ;
159+ log . debug ( "Function call detected" , { structuredData } ) ;
160+ } catch ( error ) {
161+ log . error ( "Failed to parse function arguments" , {
162+ arguments : toolCall . function . arguments
101163 } ) ;
102164 }
103- } catch ( error ) {
104- log . error ( "Failed to parse JSON response" , { content : message . content } ) ;
105165 }
166+ } else if ( messageContent ) {
167+ // Handle regular text response
168+ structuredData = {
169+ type : "text" as const ,
170+ content : messageContent ,
171+ } ;
106172 }
107173
108174 return {
109175 role : "assistant" ,
110- content : message . content ,
176+ content : messageContent ,
111177 structured_data : structuredData ,
112178 } ;
113179 } catch ( error ) {
0 commit comments