diff --git a/lib/agents/generalAgent/__tests__/getGeneralAgent.test.ts b/lib/agents/generalAgent/__tests__/getGeneralAgent.test.ts index 0b0ce5c3..d29ed8c5 100644 --- a/lib/agents/generalAgent/__tests__/getGeneralAgent.test.ts +++ b/lib/agents/generalAgent/__tests__/getGeneralAgent.test.ts @@ -2,6 +2,17 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { ChatRequestBody } from "@/lib/chat/validateChatRequest"; import { ToolLoopAgent, stepCountIs } from "ai"; +// Import after mocks +import getGeneralAgent from "../getGeneralAgent"; +import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails"; +import { selectAccountInfo } from "@/lib/supabase/account_info/selectAccountInfo"; +import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails"; +import { getKnowledgeBaseText } from "@/lib/files/getKnowledgeBaseText"; +import { setupToolsForRequest } from "@/lib/chat/setupToolsForRequest"; +import { getSystemPrompt } from "@/lib/prompts/getSystemPrompt"; +import { extractImageUrlsFromMessages } from "@/lib/messages/extractImageUrlsFromMessages"; +import { buildSystemPromptWithImages } from "@/lib/chat/buildSystemPromptWithImages"; + // Mock all external dependencies vi.mock("@/lib/supabase/account_emails/selectAccountEmails", () => ({ default: vi.fn(), @@ -35,17 +46,6 @@ vi.mock("@/lib/chat/buildSystemPromptWithImages", () => ({ buildSystemPromptWithImages: vi.fn(), })); -// Import after mocks -import getGeneralAgent from "../getGeneralAgent"; -import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails"; -import { selectAccountInfo } from "@/lib/supabase/account_info/selectAccountInfo"; -import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails"; -import { getKnowledgeBaseText } from "@/lib/files/getKnowledgeBaseText"; -import { setupToolsForRequest } from "@/lib/chat/setupToolsForRequest"; -import { getSystemPrompt } from "@/lib/prompts/getSystemPrompt"; -import { extractImageUrlsFromMessages } from "@/lib/messages/extractImageUrlsFromMessages"; -import { buildSystemPromptWithImages } from "@/lib/chat/buildSystemPromptWithImages"; - const mockSelectAccountEmails = vi.mocked(selectAccountEmails); const mockSelectAccountInfo = vi.mocked(selectAccountInfo); const mockGetAccountWithDetails = vi.mocked(getAccountWithDetails); @@ -465,5 +465,52 @@ describe("getGeneralAgent", () => { expect(result.stopWhen).toBeDefined(); expect(typeof result.stopWhen).toBe("function"); }); + + it("creates ToolLoopAgent with providerOptions for thinking/reasoning", async () => { + const body: ChatRequestBody = { + accountId: "account-123", + orgId: null, + messages: [{ id: "1", role: "user", content: "Hello" }], + }; + + const result = await getGeneralAgent(body); + + // providerOptions should be baked into the agent constructor (stored in settings) + const settings = (result.agent as any).settings; + expect(settings.providerOptions).toBeDefined(); + expect(settings.providerOptions.anthropic).toEqual( + expect.objectContaining({ + thinking: { type: "enabled", budgetTokens: 12000 }, + }), + ); + expect(settings.providerOptions.google).toEqual( + expect.objectContaining({ + thinkingConfig: expect.objectContaining({ + thinkingBudget: 8192, + includeThoughts: true, + }), + }), + ); + expect(settings.providerOptions.openai).toEqual( + expect.objectContaining({ + reasoningEffort: "medium", + reasoningSummary: "detailed", + }), + ); + }); + + it("creates ToolLoopAgent with prepareStep function", async () => { + const body: ChatRequestBody = { + accountId: "account-123", + orgId: null, + messages: [{ id: "1", role: "user", content: "Hello" }], + }; + + const result = await getGeneralAgent(body); + + // prepareStep should be baked into the agent constructor (stored in settings) + const settings = (result.agent as any).settings; + expect(settings.prepareStep).toBeInstanceOf(Function); + }); }); }); diff --git a/lib/agents/generalAgent/getGeneralAgent.ts b/lib/agents/generalAgent/getGeneralAgent.ts index e5794c20..7c2c9407 100644 --- a/lib/agents/generalAgent/getGeneralAgent.ts +++ b/lib/agents/generalAgent/getGeneralAgent.ts @@ -1,4 +1,7 @@ import { stepCountIs, ToolLoopAgent } from "ai"; +import { AnthropicProviderOptions } from "@ai-sdk/anthropic"; +import { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google"; +import { OpenAIResponsesProviderOptions } from "@ai-sdk/openai"; import { DEFAULT_MODEL } from "@/lib/const"; import { RoutingDecision } from "@/lib/chat/types"; import { extractImageUrlsFromMessages } from "@/lib/messages/extractImageUrlsFromMessages"; @@ -10,6 +13,7 @@ import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmai import { selectAccountInfo } from "@/lib/supabase/account_info/selectAccountInfo"; import { getKnowledgeBaseText } from "@/lib/files/getKnowledgeBaseText"; import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails"; +import getPrepareStepResult from "@/lib/chat/toolChains/getPrepareStepResult"; /** * Gets the general agent for the chat @@ -55,6 +59,26 @@ export default async function getGeneralAgent(body: ChatRequestBody): Promise { + const next = getPrepareStepResult(options); + if (next) return { ...options, ...next }; + return options; + }, + providerOptions: { + anthropic: { + thinking: { type: "enabled", budgetTokens: 12000 }, + } satisfies AnthropicProviderOptions, + google: { + thinkingConfig: { + thinkingBudget: 8192, + includeThoughts: true, + }, + } satisfies GoogleGenerativeAIProviderOptions, + openai: { + reasoningEffort: "medium", + reasoningSummary: "detailed", + } satisfies OpenAIResponsesProviderOptions, + }, }); return { diff --git a/lib/chat/__tests__/handleChatGenerate.test.ts b/lib/chat/__tests__/handleChatGenerate.test.ts index 5449c4ec..05e219f5 100644 --- a/lib/chat/__tests__/handleChatGenerate.test.ts +++ b/lib/chat/__tests__/handleChatGenerate.test.ts @@ -1,6 +1,13 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { NextResponse } from "next/server"; +import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; +import { validateOverrideAccountId } from "@/lib/accounts/validateOverrideAccountId"; +import { setupChatRequest } from "@/lib/chat/setupChatRequest"; +import { saveChatCompletion } from "@/lib/chat/saveChatCompletion"; +import { setupConversation } from "@/lib/chat/setupConversation"; +import { handleChatGenerate } from "../handleChatGenerate"; + // Mock all dependencies before importing the module under test vi.mock("@/lib/auth/getApiKeyAccountId", () => ({ getApiKeyAccountId: vi.fn(), @@ -26,10 +33,6 @@ vi.mock("@/lib/chat/setupChatRequest", () => ({ setupChatRequest: vi.fn(), })); -vi.mock("ai", () => ({ - generateText: vi.fn(), -})); - vi.mock("@/lib/chat/saveChatCompletion", () => ({ saveChatCompletion: vi.fn(), })); @@ -58,30 +61,32 @@ vi.mock("@/lib/chat/setupConversation", () => ({ setupConversation: vi.fn(), })); -import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; -import { validateOverrideAccountId } from "@/lib/accounts/validateOverrideAccountId"; -import { setupChatRequest } from "@/lib/chat/setupChatRequest"; -import { generateText } from "ai"; -import { saveChatCompletion } from "@/lib/chat/saveChatCompletion"; -import { generateUUID } from "@/lib/uuid/generateUUID"; -import { createNewRoom } from "@/lib/chat/createNewRoom"; -import { setupConversation } from "@/lib/chat/setupConversation"; -import { handleChatGenerate } from "../handleChatGenerate"; - const mockGetApiKeyAccountId = vi.mocked(getApiKeyAccountId); const mockValidateOverrideAccountId = vi.mocked(validateOverrideAccountId); const mockSetupChatRequest = vi.mocked(setupChatRequest); -const mockGenerateText = vi.mocked(generateText); const mockSaveChatCompletion = vi.mocked(saveChatCompletion); -const mockGenerateUUID = vi.mocked(generateUUID); -const mockCreateNewRoom = vi.mocked(createNewRoom); const mockSetupConversation = vi.mocked(setupConversation); +// Helper to create a mock agent with .generate() +/** + * + * @param generateResult + */ +function createMockAgent(generateResult: Record) { + return { + generate: vi.fn().mockResolvedValue(generateResult), + stream: vi.fn(), + tools: {}, + }; +} + // Helper to create mock NextRequest -function createMockRequest( - body: unknown, - headers: Record = {}, -): Request { +/** + * + * @param body + * @param headers + */ +function createMockRequest(body: unknown, headers: Record = {}): Request { return { json: () => Promise.resolve(body), headers: { @@ -109,10 +114,7 @@ describe("handleChatGenerate", () => { it("returns 400 error when neither messages nor prompt is provided", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); - const request = createMockRequest( - { roomId: "room-123" }, - { "x-api-key": "test-key" }, - ); + const request = createMockRequest({ roomId: "room-123" }, { "x-api-key": "test-key" }); const result = await handleChatGenerate(request as any); @@ -135,22 +137,10 @@ describe("handleChatGenerate", () => { }); describe("text generation", () => { - it("returns generated text for valid requests", async () => { + it("returns generated text using agent.generate() for valid requests", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); - const mockChatConfig = { - model: "gpt-4", - instructions: "You are a helpful assistant", - system: "You are a helpful assistant", - messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, - }; - - mockSetupChatRequest.mockResolvedValue(mockChatConfig as any); - - mockGenerateText.mockResolvedValue({ + const mockAgent = createMockAgent({ text: "Hello! How can I help you?", reasoningText: undefined, sources: [], @@ -161,15 +151,18 @@ describe("handleChatGenerate", () => { headers: {}, body: null, }, + }); + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + messages: [], } as any); - const request = createMockRequest( - { prompt: "Hello, world!" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello, world!" }, { "x-api-key": "valid-key" }); const result = await handleChatGenerate(request as any); + expect(mockAgent.generate).toHaveBeenCalled(); expect(result.status).toBe(200); const json = await result.json(); expect(json.text).toBe("Hello! How can I help you?"); @@ -180,28 +173,20 @@ describe("handleChatGenerate", () => { it("uses messages array when provided", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); - mockSetupChatRequest.mockResolvedValue({ - model: "gpt-4", - instructions: "test", - system: "test", - messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, - } as any); - - mockGenerateText.mockResolvedValue({ + const mockAgent = createMockAgent({ text: "Response", finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, response: { messages: [], headers: {}, body: null }, + }); + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + messages: [], } as any); const messages = [{ role: "user", content: "Hello" }]; - const request = createMockRequest( - { messages }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ messages }, { "x-api-key": "valid-key" }); await handleChatGenerate(request as any); @@ -220,21 +205,16 @@ describe("handleChatGenerate", () => { memoryId: "memory-id", }); - mockSetupChatRequest.mockResolvedValue({ - model: "claude-3-opus", - instructions: "test", - system: "test", - messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, - } as any); - - mockGenerateText.mockResolvedValue({ + const mockAgent = createMockAgent({ text: "Response", finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, response: { messages: [], headers: {}, body: null }, + }); + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + messages: [], } as any); const request = createMockRequest( @@ -263,29 +243,21 @@ describe("handleChatGenerate", () => { it("includes reasoningText when present", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); - mockSetupChatRequest.mockResolvedValue({ - model: "gpt-4", - instructions: "test", - system: "test", - messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, - } as any); - - mockGenerateText.mockResolvedValue({ + const mockAgent = createMockAgent({ text: "Response", reasoningText: "Let me think about this...", sources: [{ url: "https://example.com" }], finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, response: { messages: [], headers: {}, body: null }, + }); + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + messages: [], } as any); - const request = createMockRequest( - { prompt: "Hello" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); const result = await handleChatGenerate(request as any); @@ -301,10 +273,7 @@ describe("handleChatGenerate", () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); mockSetupChatRequest.mockRejectedValue(new Error("Setup failed")); - const request = createMockRequest( - { prompt: "Hello" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); const result = await handleChatGenerate(request as any); @@ -314,25 +283,21 @@ describe("handleChatGenerate", () => { expect(json.status).toBe("error"); }); - it("returns 500 error when generateText fails", async () => { + it("returns 500 error when agent.generate() fails", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); + const mockAgent = { + generate: vi.fn().mockRejectedValue(new Error("Generation failed")), + stream: vi.fn(), + tools: {}, + }; + mockSetupChatRequest.mockResolvedValue({ - model: "gpt-4", - instructions: "test", - system: "test", + agent: mockAgent, messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, } as any); - mockGenerateText.mockRejectedValue(new Error("Generation failed")); - - const request = createMockRequest( - { prompt: "Hello" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); const result = await handleChatGenerate(request as any); @@ -350,21 +315,16 @@ describe("handleChatGenerate", () => { accountId: "target-account-456", }); - mockSetupChatRequest.mockResolvedValue({ - model: "gpt-4", - instructions: "test", - system: "test", - messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, - } as any); - - mockGenerateText.mockResolvedValue({ + const mockAgent = createMockAgent({ text: "Response", finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, response: { messages: [], headers: {}, body: null }, + }); + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + messages: [], } as any); const request = createMockRequest( @@ -390,21 +350,16 @@ describe("handleChatGenerate", () => { memoryId: "memory-id", }); - mockSetupChatRequest.mockResolvedValue({ - model: "gpt-4", - instructions: "test", - system: "test", - messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, - } as any); - - mockGenerateText.mockResolvedValue({ + const mockAgent = createMockAgent({ text: "Hello! How can I help you?", finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, response: { messages: [], headers: {}, body: null }, + }); + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + messages: [], } as any); mockSaveChatCompletion.mockResolvedValue(null); @@ -429,29 +384,21 @@ describe("handleChatGenerate", () => { memoryId: "memory-id", }); - mockSetupChatRequest.mockResolvedValue({ - model: "gpt-4", - instructions: "test", - system: "test", - messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, - } as any); - - mockGenerateText.mockResolvedValue({ + const mockAgent = createMockAgent({ text: "Response", finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, response: { messages: [], headers: {}, body: null }, + }); + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + messages: [], } as any); mockSaveChatCompletion.mockResolvedValue(null); - const request = createMockRequest( - { prompt: "Hello" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); await handleChatGenerate(request as any); @@ -469,21 +416,16 @@ describe("handleChatGenerate", () => { memoryId: "memory-id", }); - mockSetupChatRequest.mockResolvedValue({ - model: "gpt-4", - instructions: "test", - system: "test", - messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, - } as any); - - mockGenerateText.mockResolvedValue({ + const mockAgent = createMockAgent({ text: "Response", finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, response: { messages: [], headers: {}, body: null }, + }); + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + messages: [], } as any); mockSaveChatCompletion.mockResolvedValue(null); @@ -507,29 +449,21 @@ describe("handleChatGenerate", () => { memoryId: "memory-id", }); - mockSetupChatRequest.mockResolvedValue({ - model: "gpt-4", - instructions: "test", - system: "test", - messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, - } as any); - - mockGenerateText.mockResolvedValue({ + const mockAgent = createMockAgent({ text: "Response", finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, response: { messages: [], headers: {}, body: null }, + }); + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + messages: [], } as any); mockSaveChatCompletion.mockResolvedValue(null); - const request = createMockRequest( - { prompt: "Hello" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); const result = await handleChatGenerate(request as any); @@ -545,21 +479,16 @@ describe("handleChatGenerate", () => { memoryId: "memory-id", }); - mockSetupChatRequest.mockResolvedValue({ - model: "gpt-4", - instructions: "test", - system: "test", - messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, - } as any); - - mockGenerateText.mockResolvedValue({ + const mockAgent = createMockAgent({ text: "This is the assistant response text", finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, response: { messages: [], headers: {}, body: null }, + }); + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + messages: [], } as any); mockSaveChatCompletion.mockResolvedValue(null); @@ -584,21 +513,16 @@ describe("handleChatGenerate", () => { memoryId: "memory-id", }); - mockSetupChatRequest.mockResolvedValue({ - model: "gpt-4", - instructions: "test", - system: "test", - messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, - } as any); - - mockGenerateText.mockResolvedValue({ + const mockAgent = createMockAgent({ text: "Response", finishReason: "stop", usage: { promptTokens: 10, completionTokens: 20 }, response: { messages: [], headers: {}, body: null }, + }); + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + messages: [], } as any); mockSaveChatCompletion.mockRejectedValue(new Error("Database error")); diff --git a/lib/chat/__tests__/handleChatStream.test.ts b/lib/chat/__tests__/handleChatStream.test.ts index b29127dc..b98655e1 100644 --- a/lib/chat/__tests__/handleChatStream.test.ts +++ b/lib/chat/__tests__/handleChatStream.test.ts @@ -1,6 +1,13 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { NextResponse } from "next/server"; +import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; +import { validateOverrideAccountId } from "@/lib/accounts/validateOverrideAccountId"; +import { setupChatRequest } from "@/lib/chat/setupChatRequest"; +import { setupConversation } from "@/lib/chat/setupConversation"; +import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; +import { handleChatStream } from "../handleChatStream"; + // Mock all dependencies before importing the module under test vi.mock("@/lib/auth/getApiKeyAccountId", () => ({ getApiKeyAccountId: vi.fn(), @@ -23,18 +30,20 @@ vi.mock("@/lib/organizations/validateOrganizationAccess", () => ({ })); vi.mock("@/lib/chat/setupConversation", () => ({ - setupConversation: vi.fn().mockResolvedValue({ roomId: "mock-room-id", memoryId: "mock-memory-id" }), + setupConversation: vi + .fn() + .mockResolvedValue({ roomId: "mock-room-id", memoryId: "mock-memory-id" }), })); vi.mock("@/lib/chat/validateMessages", () => ({ - validateMessages: vi.fn((messages) => ({ + validateMessages: vi.fn(messages => ({ lastMessage: messages[messages.length - 1] || { id: "mock-id", role: "user", parts: [] }, validMessages: messages, })), })); vi.mock("@/lib/messages/convertToUiMessages", () => ({ - default: vi.fn((messages) => messages), + default: vi.fn(messages => messages), })); vi.mock("@/lib/chat/setupChatRequest", () => ({ @@ -50,13 +59,6 @@ vi.mock("ai", () => ({ createUIMessageStreamResponse: vi.fn(), })); -import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; -import { validateOverrideAccountId } from "@/lib/accounts/validateOverrideAccountId"; -import { setupChatRequest } from "@/lib/chat/setupChatRequest"; -import { setupConversation } from "@/lib/chat/setupConversation"; -import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; -import { handleChatStream } from "../handleChatStream"; - const mockGetApiKeyAccountId = vi.mocked(getApiKeyAccountId); const mockValidateOverrideAccountId = vi.mocked(validateOverrideAccountId); const mockSetupConversation = vi.mocked(setupConversation); @@ -65,10 +67,12 @@ const mockCreateUIMessageStream = vi.mocked(createUIMessageStream); const mockCreateUIMessageStreamResponse = vi.mocked(createUIMessageStreamResponse); // Helper to create mock NextRequest -function createMockRequest( - body: unknown, - headers: Record = {}, -): Request { +/** + * + * @param body + * @param headers + */ +function createMockRequest(body: unknown, headers: Record = {}): Request { return { json: () => Promise.resolve(body), headers: { @@ -97,10 +101,7 @@ describe("handleChatStream", () => { it("returns 400 error when neither messages nor prompt is provided", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); - const request = createMockRequest( - { roomId: "room-123" }, - { "x-api-key": "test-key" }, - ); + const request = createMockRequest({ roomId: "room-123" }, { "x-api-key": "test-key" }); const result = await handleChatStream(request as any); @@ -136,13 +137,7 @@ describe("handleChatStream", () => { mockSetupChatRequest.mockResolvedValue({ agent: mockAgent, - model: "gpt-4", - instructions: "You are a helpful assistant", - system: "You are a helpful assistant", messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, } as any); const mockStream = new ReadableStream(); @@ -151,10 +146,7 @@ describe("handleChatStream", () => { const mockResponse = new Response(mockStream); mockCreateUIMessageStreamResponse.mockReturnValue(mockResponse); - const request = createMockRequest( - { prompt: "Hello, world!" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello, world!" }, { "x-api-key": "valid-key" }); const result = await handleChatStream(request as any); @@ -165,7 +157,8 @@ describe("handleChatStream", () => { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS, PATCH", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With, x-api-key", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-Requested-With, x-api-key", }, }); expect(result).toBe(mockResponse); @@ -184,13 +177,7 @@ describe("handleChatStream", () => { mockSetupChatRequest.mockResolvedValue({ agent: mockAgent, - model: "gpt-4", - instructions: "You are a helpful assistant", - system: "You are a helpful assistant", messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, } as any); const mockStream = new ReadableStream(); @@ -198,10 +185,7 @@ describe("handleChatStream", () => { mockCreateUIMessageStreamResponse.mockReturnValue(new Response(mockStream)); const messages = [{ role: "user", content: "Hello" }]; - const request = createMockRequest( - { messages }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ messages }, { "x-api-key": "valid-key" }); await handleChatStream(request as any); @@ -226,13 +210,7 @@ describe("handleChatStream", () => { mockSetupChatRequest.mockResolvedValue({ agent: mockAgent, - model: "claude-3-opus", - instructions: "You are a helpful assistant", - system: "You are a helpful assistant", messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, } as any); const mockStream = new ReadableStream(); @@ -268,10 +246,7 @@ describe("handleChatStream", () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); mockSetupChatRequest.mockRejectedValue(new Error("Setup failed")); - const request = createMockRequest( - { prompt: "Hello" }, - { "x-api-key": "valid-key" }, - ); + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); const result = await handleChatStream(request as any); @@ -299,13 +274,7 @@ describe("handleChatStream", () => { mockSetupChatRequest.mockResolvedValue({ agent: mockAgent, - model: "gpt-4", - instructions: "test", - system: "test", messages: [], - experimental_generateMessageId: vi.fn(), - tools: {}, - providerOptions: {}, } as any); const mockStream = new ReadableStream(); diff --git a/lib/chat/__tests__/integration/chatEndToEnd.test.ts b/lib/chat/__tests__/integration/chatEndToEnd.test.ts index c4af728f..6e075cc9 100644 --- a/lib/chat/__tests__/integration/chatEndToEnd.test.ts +++ b/lib/chat/__tests__/integration/chatEndToEnd.test.ts @@ -370,7 +370,7 @@ describe("Chat Integration Tests", () => { expect(mockGetKnowledgeBaseText).not.toHaveBeenCalled(); }); - it("returns ChatConfig with agent and tools", async () => { + it("returns ChatConfig with agent and messages", async () => { const body = { accountId: "account-123", messages: [{ id: "msg-1", role: "user", content: "Hello" }], @@ -379,22 +379,8 @@ describe("Chat Integration Tests", () => { const result = await setupChatRequest(body as any); expect(result).toHaveProperty("agent"); - expect(result).toHaveProperty("model"); - expect(result).toHaveProperty("instructions"); - expect(result).toHaveProperty("tools"); - expect(result).toHaveProperty("system"); - }); - - it("uses provided model override", async () => { - const body = { - accountId: "account-123", - messages: [{ id: "msg-1", role: "user", content: "Hello" }], - model: "claude-3-opus", - }; - - const result = await setupChatRequest(body as any); - - expect(result.model).toBe("claude-3-opus"); + expect(result).toHaveProperty("messages"); + expect(Object.keys(result)).toEqual(["agent", "messages"]); }); it("fetches account details for system prompt context", async () => { @@ -653,7 +639,7 @@ describe("Chat Integration Tests", () => { const chatConfig = await setupChatRequest(validationResult as any); expect(chatConfig.agent).toBeDefined(); - expect(chatConfig.model).toBeDefined(); + expect(chatConfig.messages).toBeDefined(); }); it("validates messages-based requests through full pipeline", async () => { @@ -716,7 +702,7 @@ describe("Chat Integration Tests", () => { // 4. Handle credits await handleChatCredits({ usage: { promptTokens: 100, completionTokens: 50 }, - model: chatConfig.model, + model: (body as any).model || "openai/gpt-5-mini", accountId: (body as any).accountId, }); diff --git a/lib/chat/__tests__/setupChatRequest.test.ts b/lib/chat/__tests__/setupChatRequest.test.ts index 96ca7195..bd2ac4e1 100644 --- a/lib/chat/__tests__/setupChatRequest.test.ts +++ b/lib/chat/__tests__/setupChatRequest.test.ts @@ -1,49 +1,51 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { ChatRequestBody } from "../validateChatRequest"; +import { setupChatRequest } from "../setupChatRequest"; +import getGeneralAgent from "@/lib/agents/generalAgent/getGeneralAgent"; +import { convertToModelMessages } from "ai"; + // Mock dependencies vi.mock("@/lib/agents/generalAgent/getGeneralAgent", () => ({ default: vi.fn(), })); -vi.mock("ai", async (importOriginal) => { +vi.mock("ai", async importOriginal => { const actual = (await importOriginal()) as Record; return { ...actual, - convertToModelMessages: vi.fn((messages) => messages), + convertToModelMessages: vi.fn(messages => messages), stepCountIs: actual.stepCountIs, ToolLoopAgent: actual.ToolLoopAgent, }; }); -import { setupChatRequest } from "../setupChatRequest"; -import getGeneralAgent from "@/lib/agents/generalAgent/getGeneralAgent"; -import { convertToModelMessages } from "ai"; - const mockGetGeneralAgent = vi.mocked(getGeneralAgent); const mockConvertToModelMessages = vi.mocked(convertToModelMessages); describe("setupChatRequest", () => { + const mockAgent = { + model: "claude-sonnet-4-20250514", + tools: {}, + instructions: "You are a helpful assistant", + stopWhen: undefined, + }; + const mockRoutingDecision = { model: "claude-sonnet-4-20250514", instructions: "You are a helpful assistant", - agent: { - model: "claude-sonnet-4-20250514", - tools: {}, - instructions: "You are a helpful assistant", - stopWhen: undefined, - }, + agent: mockAgent, stopWhen: undefined, }; beforeEach(() => { vi.clearAllMocks(); mockGetGeneralAgent.mockResolvedValue(mockRoutingDecision as any); - mockConvertToModelMessages.mockImplementation((messages) => messages as any); + mockConvertToModelMessages.mockImplementation(messages => messages as any); }); describe("basic functionality", () => { - it("returns a ChatConfig object with all required properties", async () => { + it("returns a ChatConfig with only agent and messages", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, @@ -52,11 +54,27 @@ describe("setupChatRequest", () => { const result = await setupChatRequest(body); - expect(result).toHaveProperty("system"); + expect(result).toHaveProperty("agent"); expect(result).toHaveProperty("messages"); - expect(result).toHaveProperty("experimental_generateMessageId"); - expect(result).toHaveProperty("tools"); - expect(result).toHaveProperty("providerOptions"); + expect(Object.keys(result)).toEqual(["agent", "messages"]); + }); + + it("does not include system, tools, model, instructions, or experimental fields", async () => { + const body: ChatRequestBody = { + accountId: "account-123", + orgId: null, + messages: [{ id: "1", role: "user", content: "Hello" }], + }; + + const result = await setupChatRequest(body); + + expect(result).not.toHaveProperty("system"); + expect(result).not.toHaveProperty("tools"); + expect(result).not.toHaveProperty("model"); + expect(result).not.toHaveProperty("instructions"); + expect(result).not.toHaveProperty("experimental_generateMessageId"); + expect(result).not.toHaveProperty("providerOptions"); + expect(result).not.toHaveProperty("prepareStep"); }); it("calls getGeneralAgent with the body", async () => { @@ -71,7 +89,7 @@ describe("setupChatRequest", () => { expect(mockGetGeneralAgent).toHaveBeenCalledWith(body); }); - it("uses instructions from routing decision as system prompt", async () => { + it("returns the agent from the routing decision", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, @@ -80,7 +98,7 @@ describe("setupChatRequest", () => { const result = await setupChatRequest(body); - expect(result.system).toBe(mockRoutingDecision.instructions); + expect(result.agent).toBe(mockAgent); }); }); @@ -100,7 +118,16 @@ describe("setupChatRequest", () => { expect(mockConvertToModelMessages).toHaveBeenCalledWith(messages, expect.any(Object)); }); - it("passes tools to convertToModelMessages", async () => { + it("passes agent tools to convertToModelMessages", async () => { + const mockTools = { tool1: {}, tool2: {} }; + mockGetGeneralAgent.mockResolvedValue({ + ...mockRoutingDecision, + agent: { + ...mockAgent, + tools: mockTools, + }, + } as any); + const body: ChatRequestBody = { accountId: "account-123", orgId: null, @@ -112,7 +139,7 @@ describe("setupChatRequest", () => { expect(mockConvertToModelMessages).toHaveBeenCalledWith( expect.any(Array), expect.objectContaining({ - tools: expect.any(Object), + tools: mockTools, ignoreIncompleteToolCalls: true, }), ); @@ -137,192 +164,4 @@ describe("setupChatRequest", () => { expect(result.messages.length).toBeLessThanOrEqual(55); }); }); - - describe("experimental_generateMessageId", () => { - it("provides a function that generates unique UUIDs", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await setupChatRequest(body); - - const id1 = result.experimental_generateMessageId(); - const id2 = result.experimental_generateMessageId(); - - expect(typeof id1).toBe("string"); - expect(typeof id2).toBe("string"); - expect(id1).not.toBe(id2); - // UUID format validation - expect(id1).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); - }); - }); - - describe("provider options", () => { - it("includes anthropic thinking configuration", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await setupChatRequest(body); - - expect(result.providerOptions).toHaveProperty("anthropic"); - expect(result.providerOptions?.anthropic).toHaveProperty("thinking"); - }); - - it("includes google thinkingConfig", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await setupChatRequest(body); - - expect(result.providerOptions).toHaveProperty("google"); - expect(result.providerOptions?.google).toHaveProperty("thinkingConfig"); - }); - - it("includes openai reasoning configuration", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await setupChatRequest(body); - - expect(result.providerOptions).toHaveProperty("openai"); - expect(result.providerOptions?.openai).toHaveProperty("reasoningEffort"); - }); - }); - - describe("routing decision properties", () => { - it("spreads routing decision properties into result", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await setupChatRequest(body); - - expect(result.model).toBe(mockRoutingDecision.model); - expect(result.instructions).toBe(mockRoutingDecision.instructions); - expect(result.agent).toBeDefined(); - }); - - it("includes tools from routing decision agent", async () => { - const mockTools = { tool1: {}, tool2: {} }; - mockGetGeneralAgent.mockResolvedValue({ - ...mockRoutingDecision, - agent: { - ...mockRoutingDecision.agent, - tools: mockTools, - }, - } as any); - - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await setupChatRequest(body); - - expect(result.tools).toEqual(mockTools); - }); - }); - - describe("prepareStep function", () => { - it("includes a prepareStep function", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await setupChatRequest(body); - - expect(result.prepareStep).toBeInstanceOf(Function); - }); - - it("prepareStep returns options when no tool chain matches", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await setupChatRequest(body); - - const mockOptions = { steps: [], stepNumber: 0, model: "test-model", messages: [] }; - const prepareResult = result.prepareStep!(mockOptions as any); - - // Should return the original options when no tool chain matches - expect(prepareResult).toEqual(mockOptions); - }); - - it("prepareStep returns next tool when tool chain is triggered", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await setupChatRequest(body); - - // Simulate create_new_artist tool being executed - const mockOptions = { - steps: [ - { - toolResults: [ - { toolCallId: "call-1", toolName: "create_new_artist", output: { type: "json", value: {} } }, - ], - }, - ], - stepNumber: 1, - model: "test-model", - messages: [], - }; - const prepareResult = result.prepareStep!(mockOptions as any); - - // Should return next tool in the create_new_artist chain (get_spotify_search) - expect(prepareResult).toHaveProperty("toolChoice"); - expect((prepareResult as any).toolChoice.toolName).toBe("get_spotify_search"); - }); - - it("prepareStep merges tool chain result with original options", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await setupChatRequest(body); - - // Simulate create_new_artist tool being executed - const mockOptions = { - steps: [ - { - toolResults: [ - { toolCallId: "call-1", toolName: "create_new_artist", output: { type: "json", value: {} } }, - ], - }, - ], - stepNumber: 1, - model: "original-model", - messages: [{ role: "user", content: "original" }], - }; - const prepareResult = result.prepareStep!(mockOptions as any); - - // Should merge original options with tool chain result - expect(prepareResult).toHaveProperty("stepNumber", 1); - expect(prepareResult).toHaveProperty("model", "original-model"); - expect(prepareResult).toHaveProperty("toolChoice"); - }); - }); }); diff --git a/lib/chat/__tests__/types.test.ts b/lib/chat/__tests__/types.test.ts index 0cb89a5e..f4755355 100644 --- a/lib/chat/__tests__/types.test.ts +++ b/lib/chat/__tests__/types.test.ts @@ -34,109 +34,31 @@ describe("Chat Types", () => { }); describe("ChatConfig", () => { - it("should extend RoutingDecision", () => { + it("should have required agent property", () => { const agent = new ToolLoopAgent({ model: "gpt-4" }); const config: ChatConfig = { - model: "gpt-4", - instructions: "Test instructions", - agent, - system: "You are a helpful assistant", - messages: [], - experimental_generateMessageId: () => "test-id", - tools: {}, - }; - expect(config.model).toBe("gpt-4"); - expect(config.system).toBe("You are a helpful assistant"); - }); - - it("should have required system property", () => { - const agent = new ToolLoopAgent({ model: "gpt-4" }); - const config: ChatConfig = { - model: "gpt-4", - instructions: "Test instructions", agent, - system: "System prompt", messages: [], - experimental_generateMessageId: () => "test-id", - tools: {}, }; - expect(config.system).toBe("System prompt"); + expect(config.agent).toBe(agent); }); it("should have required messages array", () => { const agent = new ToolLoopAgent({ model: "gpt-4" }); const config: ChatConfig = { - model: "gpt-4", - instructions: "Test instructions", agent, - system: "System prompt", messages: [{ role: "user", content: "Hello" }], - experimental_generateMessageId: () => "test-id", - tools: {}, }; expect(config.messages).toHaveLength(1); }); - it("should have required experimental_generateMessageId function", () => { - const agent = new ToolLoopAgent({ model: "gpt-4" }); - const config: ChatConfig = { - model: "gpt-4", - instructions: "Test instructions", - agent, - system: "System prompt", - messages: [], - experimental_generateMessageId: () => "generated-id-123", - tools: {}, - }; - expect(config.experimental_generateMessageId()).toBe("generated-id-123"); - }); - - it("should have optional experimental_download function", () => { + it("should only contain agent and messages (YAGNI)", () => { const agent = new ToolLoopAgent({ model: "gpt-4" }); const config: ChatConfig = { - model: "gpt-4", - instructions: "Test instructions", - agent, - system: "System prompt", - messages: [], - experimental_generateMessageId: () => "test-id", - tools: {}, - experimental_download: async () => [], - }; - expect(config.experimental_download).toBeDefined(); - }); - - it("should have optional prepareStep function", () => { - const agent = new ToolLoopAgent({ model: "gpt-4" }); - const config: ChatConfig = { - model: "gpt-4", - instructions: "Test instructions", - agent, - system: "System prompt", - messages: [], - experimental_generateMessageId: () => "test-id", - tools: {}, - prepareStep: (options) => options, - }; - expect(config.prepareStep).toBeDefined(); - }); - - it("should have optional providerOptions property", () => { - const agent = new ToolLoopAgent({ model: "gpt-4" }); - const config: ChatConfig = { - model: "gpt-4", - instructions: "Test instructions", agent, - system: "System prompt", messages: [], - experimental_generateMessageId: () => "test-id", - tools: {}, - providerOptions: { - anthropic: { thinking: { type: "enabled", budgetTokens: 12000 } }, - }, }; - expect(config.providerOptions).toBeDefined(); - expect(config.providerOptions?.anthropic).toBeDefined(); + expect(Object.keys(config)).toEqual(["agent", "messages"]); }); }); }); diff --git a/lib/chat/handleChatGenerate.ts b/lib/chat/handleChatGenerate.ts index ab3d678e..1f0a4a97 100644 --- a/lib/chat/handleChatGenerate.ts +++ b/lib/chat/handleChatGenerate.ts @@ -1,5 +1,4 @@ import { NextRequest, NextResponse } from "next/server"; -import { generateText } from "ai"; import { validateChatRequest } from "./validateChatRequest"; import { setupChatRequest } from "./setupChatRequest"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; @@ -27,8 +26,9 @@ export async function handleChatGenerate(request: NextRequest): Promise { const decision = await getGeneralAgent(body); - const system = decision.instructions; - const tools = decision.agent.tools; - const convertedMessages = convertToModelMessages(body.messages, { - tools, + tools: decision.agent.tools, ignoreIncompleteToolCalls: true, }).slice(-MAX_MESSAGES); - const config: ChatConfig = { - ...decision, - system, + return { + agent: decision.agent, messages: convertedMessages, - experimental_generateMessageId: generateUUID, - tools, - prepareStep: (options) => { - const next = getPrepareStepResult(options); - if (next) { - return { ...options, ...next }; - } - return options; - }, - providerOptions: { - anthropic: { - thinking: { type: "enabled", budgetTokens: 12000 }, - } satisfies AnthropicProviderOptions, - google: { - thinkingConfig: { - thinkingBudget: 8192, - includeThoughts: true, - }, - } satisfies GoogleGenerativeAIProviderOptions, - openai: { - reasoningEffort: "medium", - reasoningSummary: "detailed", - } satisfies OpenAIResponsesProviderOptions, - }, }; - - return config; } diff --git a/lib/chat/types.ts b/lib/chat/types.ts index 2fe93bfd..83c9d976 100644 --- a/lib/chat/types.ts +++ b/lib/chat/types.ts @@ -1,14 +1,5 @@ -import { AnthropicProviderOptions } from "@ai-sdk/anthropic"; -import { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google"; -import { OpenAIResponsesProviderOptions } from "@ai-sdk/openai"; import { VercelToolCollection } from "@composio/vercel"; -import { - type ModelMessage, - type ToolSet, - type StopCondition, - type PrepareStepFunction, - type ToolLoopAgent, -} from "ai"; +import { type ModelMessage, type ToolSet, type StopCondition, type ToolLoopAgent } from "ai"; export interface RoutingDecision { model: string; @@ -17,18 +8,7 @@ export interface RoutingDecision { stopWhen?: StopCondition> | StopCondition>[] | undefined; } -export interface ChatConfig extends RoutingDecision { - system: string; +export interface ChatConfig { + agent: ToolLoopAgent; messages: ModelMessage[]; - experimental_generateMessageId: () => string; - experimental_download?: ( - files: Array<{ url: URL; isUrlSupportedByModel: boolean }> - ) => Promise>; - tools: ToolSet; - prepareStep?: PrepareStepFunction; - providerOptions?: { - anthropic?: AnthropicProviderOptions; - google?: GoogleGenerativeAIProviderOptions; - openai?: OpenAIResponsesProviderOptions; - }; } diff --git a/lib/evals/callChatFunctionsWithResult.ts b/lib/evals/callChatFunctionsWithResult.ts index 207e2b62..16330158 100644 --- a/lib/evals/callChatFunctionsWithResult.ts +++ b/lib/evals/callChatFunctionsWithResult.ts @@ -1,7 +1,6 @@ import { UIMessage } from "ai"; import { DEFAULT_MODEL, EVAL_ACCOUNT_ID, EVAL_ACCESS_TOKEN } from "@/lib/consts"; import { setupChatRequest } from "@/lib/chat/setupChatRequest"; -import { generateText } from "ai"; import { ChatRequestBody } from "@/lib/chat/validateChatRequest"; /** @@ -35,8 +34,8 @@ export async function callChatFunctionsWithResult(input: string) { excludeTools: [], // Don't exclude any tools - we want to test tool usage }; - const chatConfig = await setupChatRequest(body); - const result = await generateText(chatConfig); + const { agent, messages: convertedMessages } = await setupChatRequest(body); + const result = await agent.generate({ messages: convertedMessages }); // Collect tool calls from ALL steps, not just the last one const allToolCalls =