9
9
import { z } from "zod" ;
10
10
11
11
import { openaiClient } from "../utils/client" ;
12
+ import { CreateTodoSchema , ExecuteTodoSchema } from "./toolTypes" ;
12
13
13
14
export type Message =
14
15
| ChatCompletionSystemMessageParam
@@ -48,11 +49,71 @@ export const LLMChatOutputSchema = z.object({
48
49
export type LLMChatInput = z . infer < typeof LLMChatInputSchema > ;
49
50
export type LLMChatOutput = z . infer < typeof LLMChatOutputSchema > ;
50
51
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` ;
56
117
57
118
export const llmChat = async ( {
58
119
systemContent = "" ,
@@ -62,52 +123,57 @@ export const llmChat = async ({
62
123
try {
63
124
const openai = openaiClient ( { } ) ;
64
125
65
- // Combine system prompts if provided
126
+ // Combine system messages
66
127
const finalSystemContent = systemContent
67
- ? `${ structuredOutputPrompt } \n\n${ systemContent } `
68
- : structuredOutputPrompt ;
128
+ ? `${ baseSystemMessage } \n\n${ systemContent } `
129
+ : baseSystemMessage ;
69
130
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
76
132
const chatParams : ChatCompletionCreateParamsNonStreaming = {
77
133
messages : [ { role : "system" , content : finalSystemContent } , ...messages ] ,
78
134
model,
79
- response_format : responseFormat ,
135
+ tools : functionsForOpenAI . map ( fn => ( { type : "function" , function : fn } ) ) ,
136
+ tool_choice : "auto" ,
80
137
} ;
81
138
82
139
log . debug ( "OpenAI chat completion params" , { chatParams } ) ;
83
140
84
141
const completion = await openai . chat . completions . create ( chatParams ) ;
85
142
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
101
163
} ) ;
102
164
}
103
- } catch ( error ) {
104
- log . error ( "Failed to parse JSON response" , { content : message . content } ) ;
105
165
}
166
+ } else if ( messageContent ) {
167
+ // Handle regular text response
168
+ structuredData = {
169
+ type : "text" as const ,
170
+ content : messageContent ,
171
+ } ;
106
172
}
107
173
108
174
return {
109
175
role : "assistant" ,
110
- content : message . content ,
176
+ content : messageContent ,
111
177
structured_data : structuredData ,
112
178
} ;
113
179
} catch ( error ) {
0 commit comments