|
|
import { describe, it, expect, beforeEach } from "bun:test"; |
|
|
import { OpenAIService } from "../src/openai-service"; |
|
|
import type { |
|
|
ChatCompletionRequest, |
|
|
ToolDefinition, |
|
|
ToolCall, |
|
|
} from "../src/types"; |
|
|
|
|
|
describe("OpenAIService with Tools", () => { |
|
|
let openAIService: OpenAIService; |
|
|
|
|
|
beforeEach(() => { |
|
|
openAIService = new OpenAIService(); |
|
|
}); |
|
|
|
|
|
const sampleTools: ToolDefinition[] = [ |
|
|
{ |
|
|
type: "function", |
|
|
function: { |
|
|
name: "get_current_time", |
|
|
description: "Get the current time", |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
type: "function", |
|
|
function: { |
|
|
name: "calculate", |
|
|
description: "Perform mathematical calculations", |
|
|
parameters: { |
|
|
type: "object", |
|
|
properties: { |
|
|
expression: { |
|
|
type: "string", |
|
|
description: "Mathematical expression to evaluate", |
|
|
}, |
|
|
}, |
|
|
required: ["expression"], |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
type: "function", |
|
|
function: { |
|
|
name: "get_weather", |
|
|
description: "Get weather information for a location", |
|
|
parameters: { |
|
|
type: "object", |
|
|
properties: { |
|
|
location: { |
|
|
type: "string", |
|
|
description: "The city and state, e.g. San Francisco, CA", |
|
|
}, |
|
|
}, |
|
|
required: ["location"], |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
]; |
|
|
|
|
|
describe("validateRequest with tools", () => { |
|
|
it("should validate requests with valid tools", () => { |
|
|
const request = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "What's the weather like?" }], |
|
|
tools: sampleTools, |
|
|
}; |
|
|
|
|
|
const validated = openAIService.validateRequest(request); |
|
|
expect(validated.tools).toEqual(sampleTools); |
|
|
}); |
|
|
|
|
|
it("should reject requests with invalid tools", () => { |
|
|
const request = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "Hello" }], |
|
|
tools: [ |
|
|
{ |
|
|
type: "invalid", |
|
|
function: { name: "test" }, |
|
|
}, |
|
|
], |
|
|
}; |
|
|
|
|
|
expect(() => openAIService.validateRequest(request)).toThrow( |
|
|
"Invalid tools" |
|
|
); |
|
|
}); |
|
|
|
|
|
it("should validate tool messages", () => { |
|
|
const request = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [ |
|
|
{ role: "user", content: "What time is it?" }, |
|
|
{ |
|
|
role: "assistant", |
|
|
content: null, |
|
|
tool_calls: [ |
|
|
{ |
|
|
id: "call_1", |
|
|
type: "function", |
|
|
function: { |
|
|
name: "get_current_time", |
|
|
arguments: "{}", |
|
|
}, |
|
|
}, |
|
|
], |
|
|
}, |
|
|
{ |
|
|
role: "tool", |
|
|
content: "2024-01-15T10:30:00Z", |
|
|
tool_call_id: "call_1", |
|
|
}, |
|
|
], |
|
|
}; |
|
|
|
|
|
const validated = openAIService.validateRequest(request); |
|
|
expect(validated.messages).toHaveLength(3); |
|
|
}); |
|
|
|
|
|
it("should reject tool messages without tool_call_id", () => { |
|
|
const request = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [ |
|
|
{ |
|
|
role: "tool", |
|
|
content: "Some result", |
|
|
}, |
|
|
], |
|
|
}; |
|
|
|
|
|
expect(() => openAIService.validateRequest(request)).toThrow( |
|
|
"Tool messages must have a tool_call_id" |
|
|
); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe("registerFunction", () => { |
|
|
it("should allow registering custom functions", () => { |
|
|
const customFunction = (args: { name: string }) => `Hello, ${args.name}!`; |
|
|
openAIService.registerFunction("greet", customFunction); |
|
|
|
|
|
|
|
|
expect(openAIService["availableFunctions"]["greet"]).toBe(customFunction); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe("executeToolCall", () => { |
|
|
it("should execute built-in functions", async () => { |
|
|
const toolCall = { |
|
|
id: "call_1", |
|
|
type: "function" as const, |
|
|
function: { |
|
|
name: "get_current_time", |
|
|
arguments: "{}", |
|
|
}, |
|
|
}; |
|
|
|
|
|
const result = await openAIService.executeToolCall(toolCall); |
|
|
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); |
|
|
}); |
|
|
|
|
|
it("should execute calculate function", async () => { |
|
|
const toolCall = { |
|
|
id: "call_1", |
|
|
type: "function" as const, |
|
|
function: { |
|
|
name: "calculate", |
|
|
arguments: '{"expression": "2 + 2"}', |
|
|
}, |
|
|
}; |
|
|
|
|
|
const result = await openAIService.executeToolCall(toolCall); |
|
|
const parsed = JSON.parse(result); |
|
|
expect(parsed.result).toBe(4); |
|
|
}); |
|
|
|
|
|
it("should execute weather function", async () => { |
|
|
const toolCall = { |
|
|
id: "call_1", |
|
|
type: "function" as const, |
|
|
function: { |
|
|
name: "get_weather", |
|
|
arguments: '{"location": "New York"}', |
|
|
}, |
|
|
}; |
|
|
|
|
|
const result = await openAIService.executeToolCall(toolCall); |
|
|
const parsed = JSON.parse(result); |
|
|
expect(parsed.location).toBe("New York"); |
|
|
expect(parsed.temperature).toBeTypeOf("number"); |
|
|
expect(parsed.condition).toBeTypeOf("string"); |
|
|
}); |
|
|
|
|
|
it("should handle function execution errors", async () => { |
|
|
const toolCall = { |
|
|
id: "call_1", |
|
|
type: "function" as const, |
|
|
function: { |
|
|
name: "calculate", |
|
|
arguments: '{"expression": "invalid expression"}', |
|
|
}, |
|
|
}; |
|
|
|
|
|
const result = await openAIService.executeToolCall(toolCall); |
|
|
const parsed = JSON.parse(result); |
|
|
expect(parsed.error).toBeDefined(); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe("createChatCompletion with tools", () => { |
|
|
it("should handle requests without tools normally", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "Hello, how are you?" }], |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async () => "I'm doing well, thank you!"; |
|
|
|
|
|
const response = await openAIService.createChatCompletion(request); |
|
|
|
|
|
expect(response.choices[0].message.role).toBe("assistant"); |
|
|
expect(response.choices[0].message.content).toBe( |
|
|
"I'm doing well, thank you!" |
|
|
); |
|
|
expect(response.choices[0].finish_reason).toBe("stop"); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
|
|
|
it("should detect and extract function calls from AI response", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "What time is it?" }], |
|
|
tools: [sampleTools[0]], |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async () => |
|
|
JSON.stringify({ |
|
|
tool_calls: [ |
|
|
{ |
|
|
id: "call_1", |
|
|
type: "function", |
|
|
function: { |
|
|
name: "get_current_time", |
|
|
arguments: "{}", |
|
|
}, |
|
|
}, |
|
|
], |
|
|
}); |
|
|
|
|
|
const response = await openAIService.createChatCompletion(request); |
|
|
|
|
|
expect(response.choices[0].message.role).toBe("assistant"); |
|
|
expect(response.choices[0].message.content).toBe(null); |
|
|
expect(response.choices[0].message.tool_calls).toHaveLength(1); |
|
|
expect(response.choices[0].message.tool_calls![0].function.name).toBe( |
|
|
"get_current_time" |
|
|
); |
|
|
expect(response.choices[0].finish_reason).toBe("tool_calls"); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
|
|
|
it("should handle tool_choice 'required'", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "Calculate 5 + 3" }], |
|
|
tools: [sampleTools[1]], |
|
|
tool_choice: "required", |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async (req) => { |
|
|
|
|
|
const systemMessage = req.messages.find((m) => m.role === "system"); |
|
|
expect(systemMessage?.content).toContain( |
|
|
"You MUST call at least one function" |
|
|
); |
|
|
|
|
|
return JSON.stringify({ |
|
|
tool_calls: [ |
|
|
{ |
|
|
id: "call_1", |
|
|
type: "function", |
|
|
function: { |
|
|
name: "calculate", |
|
|
arguments: '{"expression": "5 + 3"}', |
|
|
}, |
|
|
}, |
|
|
], |
|
|
}); |
|
|
}; |
|
|
|
|
|
const response = await openAIService.createChatCompletion(request); |
|
|
expect(response.choices[0].finish_reason).toBe("tool_calls"); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
|
|
|
it("should handle tool_choice 'none'", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "Hello" }], |
|
|
tools: sampleTools, |
|
|
tool_choice: "none", |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async () => |
|
|
"Hello! How can I help you today?"; |
|
|
|
|
|
const response = await openAIService.createChatCompletion(request); |
|
|
|
|
|
expect(response.choices[0].message.content).toBe( |
|
|
"Hello! How can I help you today?" |
|
|
); |
|
|
expect(response.choices[0].finish_reason).toBe("stop"); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe("createChatCompletionStream with tools", () => { |
|
|
it("should handle streaming with function calls", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "What time is it?" }], |
|
|
tools: sampleTools, |
|
|
stream: true, |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async () => |
|
|
JSON.stringify({ |
|
|
tool_calls: [ |
|
|
{ |
|
|
id: "call_1", |
|
|
type: "function", |
|
|
function: { |
|
|
name: "get_current_time", |
|
|
arguments: "{}", |
|
|
}, |
|
|
}, |
|
|
], |
|
|
}); |
|
|
|
|
|
const stream = await openAIService.createChatCompletionStream(request); |
|
|
const chunks: string[] = []; |
|
|
|
|
|
const reader = stream.getReader(); |
|
|
while (true) { |
|
|
const { done, value } = await reader.read(); |
|
|
if (done) break; |
|
|
|
|
|
if (value) { |
|
|
const text = new TextDecoder().decode(value); |
|
|
chunks.push(text); |
|
|
} |
|
|
} |
|
|
|
|
|
const fullResponse = chunks.join(""); |
|
|
expect(fullResponse).toContain("data:"); |
|
|
expect(fullResponse).toContain("[DONE]"); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
|
|
|
it("should handle streaming without tools", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "Hello!" }], |
|
|
stream: true, |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async () => "Hello! How can I help you?"; |
|
|
|
|
|
const stream = await openAIService.createChatCompletionStream(request); |
|
|
const chunks: string[] = []; |
|
|
|
|
|
const reader = stream.getReader(); |
|
|
let chunkCount = 0; |
|
|
while (true && chunkCount < 10) { |
|
|
|
|
|
const { done, value } = await reader.read(); |
|
|
if (done) break; |
|
|
|
|
|
if (value) { |
|
|
const text = new TextDecoder().decode(value); |
|
|
chunks.push(text); |
|
|
} |
|
|
chunkCount++; |
|
|
} |
|
|
|
|
|
const fullResponse = chunks.join(""); |
|
|
expect(fullResponse).toContain("data:"); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe("Advanced Tool Scenarios", () => { |
|
|
it("should handle tool_choice with specific function", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "Calculate something" }], |
|
|
tools: sampleTools, |
|
|
tool_choice: { |
|
|
type: "function", |
|
|
function: { name: "calculate" }, |
|
|
}, |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async () => "I'll calculate that for you."; |
|
|
|
|
|
const response = await openAIService.createChatCompletion(request); |
|
|
|
|
|
|
|
|
expect(response.choices[0].finish_reason).toBe("tool_calls"); |
|
|
expect(response.choices[0].message.tool_calls).toHaveLength(1); |
|
|
expect(response.choices[0].message.tool_calls![0].function.name).toBe( |
|
|
"calculate" |
|
|
); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
|
|
|
it("should handle empty response from Duck.ai gracefully", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "Test" }], |
|
|
tools: sampleTools, |
|
|
tool_choice: "required", |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async () => ""; |
|
|
|
|
|
const response = await openAIService.createChatCompletion(request); |
|
|
|
|
|
|
|
|
expect(response.choices[0].finish_reason).toBe("tool_calls"); |
|
|
expect(response.choices[0].message.tool_calls).toHaveLength(1); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
|
|
|
it("should handle conversation with multiple tool calls", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [ |
|
|
{ role: "user", content: "What time is it and what's 2+2?" }, |
|
|
{ |
|
|
role: "assistant", |
|
|
content: null, |
|
|
tool_calls: [ |
|
|
{ |
|
|
id: "call_1", |
|
|
type: "function", |
|
|
function: { name: "get_current_time", arguments: "{}" }, |
|
|
}, |
|
|
], |
|
|
}, |
|
|
{ |
|
|
role: "tool", |
|
|
content: "2024-01-15T10:30:00Z", |
|
|
tool_call_id: "call_1", |
|
|
}, |
|
|
], |
|
|
tools: sampleTools, |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async () => |
|
|
"The time is 2024-01-15T10:30:00Z. Now let me calculate 2+2."; |
|
|
|
|
|
const response = await openAIService.createChatCompletion(request); |
|
|
|
|
|
expect(response.choices[0].message.role).toBe("assistant"); |
|
|
expect(response.choices[0].message.content).toContain( |
|
|
"2024-01-15T10:30:00Z" |
|
|
); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
|
|
|
it("should handle custom registered functions", async () => { |
|
|
|
|
|
const customFunction = (args: { name: string }) => `Hello, ${args.name}!`; |
|
|
openAIService.registerFunction("greet", customFunction); |
|
|
|
|
|
const toolCall: ToolCall = { |
|
|
id: "call_1", |
|
|
type: "function", |
|
|
function: { |
|
|
name: "greet", |
|
|
arguments: '{"name": "Alice"}', |
|
|
}, |
|
|
}; |
|
|
|
|
|
const result = await openAIService.executeToolCall(toolCall); |
|
|
expect(result).toBe("Hello, Alice!"); |
|
|
}); |
|
|
|
|
|
it("should handle tool validation edge cases", () => { |
|
|
|
|
|
expect(() => { |
|
|
openAIService.validateRequest({ |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "test" }], |
|
|
tools: [], |
|
|
}); |
|
|
}).not.toThrow(); |
|
|
|
|
|
|
|
|
expect(() => { |
|
|
openAIService.validateRequest({ |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "test" }], |
|
|
tools: null, |
|
|
}); |
|
|
}).not.toThrow(); |
|
|
}); |
|
|
|
|
|
it("should handle malformed tool_calls in assistant messages", () => { |
|
|
const request = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [ |
|
|
{ |
|
|
role: "assistant", |
|
|
content: null, |
|
|
tool_calls: [ |
|
|
{ |
|
|
|
|
|
type: "function", |
|
|
}, |
|
|
], |
|
|
}, |
|
|
], |
|
|
}; |
|
|
|
|
|
|
|
|
expect(() => openAIService.validateRequest(request)).not.toThrow(); |
|
|
}); |
|
|
|
|
|
it("should handle concurrent tool executions", async () => { |
|
|
const slowFunction = async (args: any) => { |
|
|
await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
return `Slow result: ${args.input}`; |
|
|
}; |
|
|
|
|
|
openAIService.registerFunction("slow_func", slowFunction); |
|
|
|
|
|
const toolCalls = [ |
|
|
{ |
|
|
id: "call_1", |
|
|
type: "function" as const, |
|
|
function: { name: "slow_func", arguments: '{"input": "test1"}' }, |
|
|
}, |
|
|
{ |
|
|
id: "call_2", |
|
|
type: "function" as const, |
|
|
function: { name: "slow_func", arguments: '{"input": "test2"}' }, |
|
|
}, |
|
|
]; |
|
|
|
|
|
const results = await Promise.all( |
|
|
toolCalls.map((call) => openAIService.executeToolCall(call)) |
|
|
); |
|
|
|
|
|
expect(results).toHaveLength(2); |
|
|
expect(results[0]).toBe("Slow result: test1"); |
|
|
expect(results[1]).toBe("Slow result: test2"); |
|
|
}); |
|
|
|
|
|
|
|
|
it("should handle tool_choice with non-existent function gracefully", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "Test" }], |
|
|
tools: sampleTools, |
|
|
tool_choice: { |
|
|
type: "function", |
|
|
function: { name: "non_existent_function" }, |
|
|
}, |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async () => "I'll help you with that."; |
|
|
|
|
|
const response = await openAIService.createChatCompletion(request); |
|
|
|
|
|
|
|
|
expect(response.choices[0].finish_reason).toBe("tool_calls"); |
|
|
expect(response.choices[0].message.tool_calls).toHaveLength(1); |
|
|
expect(response.choices[0].message.tool_calls![0].function.name).toBe( |
|
|
"non_existent_function" |
|
|
); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
|
|
|
it("should handle complex tool arguments extraction", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [ |
|
|
{ role: "user", content: "Calculate the result of 15 * 8 + 42" }, |
|
|
], |
|
|
tools: [sampleTools[1]], |
|
|
tool_choice: "required", |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async () => ""; |
|
|
|
|
|
const response = await openAIService.createChatCompletion(request); |
|
|
|
|
|
expect(response.choices[0].finish_reason).toBe("tool_calls"); |
|
|
expect(response.choices[0].message.tool_calls).toHaveLength(1); |
|
|
expect(response.choices[0].message.tool_calls![0].function.name).toBe( |
|
|
"calculate" |
|
|
); |
|
|
|
|
|
|
|
|
const args = JSON.parse( |
|
|
response.choices[0].message.tool_calls![0].function.arguments |
|
|
); |
|
|
expect(args.expression).toBe("15 * 8"); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
|
|
|
it("should handle weather function with location extraction", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [ |
|
|
{ |
|
|
role: "user", |
|
|
content: "What's the weather like in San Francisco?", |
|
|
}, |
|
|
], |
|
|
tools: [sampleTools[2]], |
|
|
tool_choice: "required", |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async () => ""; |
|
|
|
|
|
const response = await openAIService.createChatCompletion(request); |
|
|
|
|
|
expect(response.choices[0].finish_reason).toBe("tool_calls"); |
|
|
expect(response.choices[0].message.tool_calls).toHaveLength(1); |
|
|
expect(response.choices[0].message.tool_calls![0].function.name).toBe( |
|
|
"get_weather" |
|
|
); |
|
|
|
|
|
|
|
|
const args = JSON.parse( |
|
|
response.choices[0].message.tool_calls![0].function.arguments |
|
|
); |
|
|
expect(args.location).toBe("San Francisco"); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
|
|
|
it("should handle mixed content with function calls", async () => { |
|
|
const request: ChatCompletionRequest = { |
|
|
model: "gpt-4o-mini", |
|
|
messages: [{ role: "user", content: "Hello! What time is it?" }], |
|
|
tools: sampleTools, |
|
|
}; |
|
|
|
|
|
|
|
|
const originalChat = openAIService["duckAI"].chat; |
|
|
openAIService["duckAI"].chat = async () => |
|
|
JSON.stringify({ |
|
|
message: "Hello! Let me check the time for you.", |
|
|
tool_calls: [ |
|
|
{ |
|
|
id: "call_1", |
|
|
type: "function", |
|
|
function: { |
|
|
name: "get_current_time", |
|
|
arguments: "{}", |
|
|
}, |
|
|
}, |
|
|
], |
|
|
}); |
|
|
|
|
|
const response = await openAIService.createChatCompletion(request); |
|
|
|
|
|
expect(response.choices[0].finish_reason).toBe("tool_calls"); |
|
|
expect(response.choices[0].message.tool_calls).toHaveLength(1); |
|
|
expect(response.choices[0].message.tool_calls![0].function.name).toBe( |
|
|
"get_current_time" |
|
|
); |
|
|
|
|
|
|
|
|
openAIService["duckAI"].chat = originalChat; |
|
|
}); |
|
|
|
|
|
it("should handle function execution with complex return types", async () => { |
|
|
|
|
|
const complexReturnFunction = (args: { type: string }) => { |
|
|
switch (args.type) { |
|
|
case "array": |
|
|
return [1, 2, 3, "four", { five: 5 }]; |
|
|
case "object": |
|
|
return { nested: { data: "value" }, array: [1, 2, 3] }; |
|
|
case "null": |
|
|
return null; |
|
|
case "boolean": |
|
|
return true; |
|
|
case "number": |
|
|
return 42.5; |
|
|
default: |
|
|
return "string result"; |
|
|
} |
|
|
}; |
|
|
|
|
|
openAIService.registerFunction("complex_return", complexReturnFunction); |
|
|
|
|
|
const testCases = [ |
|
|
{ type: "array", expected: [1, 2, 3, "four", { five: 5 }] }, |
|
|
{ |
|
|
type: "object", |
|
|
expected: { nested: { data: "value" }, array: [1, 2, 3] }, |
|
|
}, |
|
|
{ type: "null", expected: null }, |
|
|
{ type: "boolean", expected: true }, |
|
|
{ type: "number", expected: 42.5 }, |
|
|
{ type: "string", expected: "string result" }, |
|
|
]; |
|
|
|
|
|
for (const testCase of testCases) { |
|
|
const toolCall: ToolCall = { |
|
|
id: "call_1", |
|
|
type: "function", |
|
|
function: { |
|
|
name: "complex_return", |
|
|
arguments: JSON.stringify({ type: testCase.type }), |
|
|
}, |
|
|
}; |
|
|
|
|
|
const result = await openAIService.executeToolCall(toolCall); |
|
|
|
|
|
|
|
|
if (testCase.type === "string") { |
|
|
expect(result).toBe(testCase.expected as string); |
|
|
} else { |
|
|
const parsed = JSON.parse(result); |
|
|
expect(parsed).toEqual(testCase.expected); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|