diff --git a/src/app/api/chat/actions.ts b/src/app/api/chat/actions.ts index c93356f3b..75b79ad60 100644 --- a/src/app/api/chat/actions.ts +++ b/src/app/api/chat/actions.ts @@ -33,7 +33,7 @@ import logger from "logger"; import { JSONSchema7 } from "json-schema"; import { ObjectJsonSchema7 } from "app-types/util"; import { jsonSchemaToZod } from "lib/json-schema-to-zod"; -import { Agent } from "app-types/agent"; +import { Agent, AgentSummary } from "app-types/agent"; export async function getUserId() { const session = await getSession(); @@ -226,6 +226,16 @@ export async function rememberAgentAction( return cachedAgent as Agent | undefined; } +export async function rememberAvailableAgentsAction(userId: string) { + const key = CacheKeys.availableAgents(userId); + let cachedAgents = await serverCache.get(key); + if (!cachedAgents) { + cachedAgents = await agentRepository.selectAgents(userId, ["all"], 50); + await serverCache.set(key, cachedAgents, 1000 * 60 * 5); // 5 minutes cache + } + return cachedAgents ?? []; +} + export async function exportChatAction({ threadId, expiresAt, diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 9f6e4e21c..cc358f80c 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -42,6 +42,7 @@ import { } from "./shared.chat"; import { rememberAgentAction, + rememberAvailableAgentsAction, rememberMcpServerCustomizationsAction, } from "./actions"; import { getSession } from "auth/server"; @@ -269,8 +270,18 @@ export async function POST(request: Request) { .map((v) => filterMcpServerCustomizations(MCP_TOOLS!, v)) .orElse({}); + // Fetch available agents only when no agent is selected + const availableAgents = !agent + ? await rememberAvailableAgentsAction(session.user.id) + : undefined; + const systemPrompt = mergeSystemPrompt( - buildUserSystemPrompt(session.user, userPreferences, agent), + buildUserSystemPrompt( + session.user, + userPreferences, + agent, + availableAgents, + ), buildMcpServerCustomizationsSystemPrompt(mcpServerCustomizations), !supportToolCall && buildToolCallUnsupportedModelSystemPrompt, ); diff --git a/src/lib/ai/prompts.test.ts b/src/lib/ai/prompts.test.ts new file mode 100644 index 000000000..fc2541a2f --- /dev/null +++ b/src/lib/ai/prompts.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, it } from "vitest"; +import { buildUserSystemPrompt } from "./prompts"; +import { AgentSummary, Agent } from "app-types/agent"; + +describe("buildUserSystemPrompt", () => { + const mockAvailableAgents: AgentSummary[] = [ + { + id: "agent-1", + name: "Code Reviewer", + description: "Reviews code for best practices", + userId: "user-1", + visibility: "private", + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "agent-2", + name: "Technical Writer", + description: "Writes documentation", + userId: "user-1", + visibility: "public", + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: "agent-3", + name: "Simple Agent", + // No description + userId: "user-1", + visibility: "private", + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; + + const mockSelectedAgent: Agent = { + id: "selected-agent", + name: "Selected Agent", + description: "The currently selected agent", + userId: "user-1", + visibility: "private", + createdAt: new Date(), + updatedAt: new Date(), + instructions: { + role: "Testing Expert", + systemPrompt: "You are a testing expert.", + }, + }; + + describe("available agents section", () => { + it("should include available agents when no agent is selected", () => { + const prompt = buildUserSystemPrompt( + undefined, + undefined, + undefined, + mockAvailableAgents, + ); + + expect(prompt).toContain(""); + expect(prompt).toContain(""); + expect(prompt).toContain( + "**Code Reviewer**: Reviews code for best practices", + ); + expect(prompt).toContain("**Technical Writer**: Writes documentation"); + expect(prompt).toContain("**Simple Agent**"); + expect(prompt).toContain("typing @ followed by the agent name"); + expect(prompt).toContain("from the tools menu"); + expect(prompt).toContain("agents menu in the sidebar"); + }); + + it("should not include available agents section when an agent is selected", () => { + const prompt = buildUserSystemPrompt( + undefined, + undefined, + mockSelectedAgent, + mockAvailableAgents, + ); + + expect(prompt).not.toContain(""); + expect(prompt).not.toContain(""); + // Should include the selected agent's instructions instead + expect(prompt).toContain("Selected Agent"); + expect(prompt).toContain("Testing Expert"); + }); + + it("should not include available agents section when list is empty", () => { + const prompt = buildUserSystemPrompt(undefined, undefined, undefined, []); + + expect(prompt).not.toContain(""); + expect(prompt).not.toContain(""); + }); + + it("should not include available agents section when list is undefined", () => { + const prompt = buildUserSystemPrompt( + undefined, + undefined, + undefined, + undefined, + ); + + expect(prompt).not.toContain(""); + expect(prompt).not.toContain(""); + }); + + it("should handle agents without descriptions", () => { + const prompt = buildUserSystemPrompt(undefined, undefined, undefined, [ + { + id: "no-desc", + name: "No Description Agent", + userId: "user-1", + visibility: "private", + createdAt: new Date(), + updatedAt: new Date(), + }, + ]); + + expect(prompt).toContain("**No Description Agent**"); + // Should not have a colon after the name when no description + expect(prompt).not.toContain("**No Description Agent**:"); + }); + }); +}); diff --git a/src/lib/ai/prompts.ts b/src/lib/ai/prompts.ts index 787c25b06..64e24ccbd 100644 --- a/src/lib/ai/prompts.ts +++ b/src/lib/ai/prompts.ts @@ -4,7 +4,7 @@ import { UserPreferences } from "app-types/user"; import { User } from "better-auth"; import { createMCPToolId } from "./mcp/mcp-tool-id"; import { format } from "date-fns"; -import { Agent } from "app-types/agent"; +import { Agent, AgentSummary } from "app-types/agent"; export const CREATE_THREAD_TITLE_PROMPT = ` You are a chat title generation expert. @@ -52,6 +52,7 @@ export const buildUserSystemPrompt = ( user?: User, userPreferences?: UserPreferences, agent?: Agent, + availableAgents?: AgentSummary[], ) => { const assistantName = agent?.name || userPreferences?.botName || "better-chatbot"; @@ -99,6 +100,25 @@ You can assist with: - Adapting communication to user preferences and context `; + // Available agents section (only when no agent is selected) + if (!agent && availableAgents && availableAgents.length > 0) { + const agentsList = availableAgents + .map((a) => { + const desc = a.description ? `: ${a.description}` : ""; + return `- **${a.name}**${desc}`; + }) + .join("\n"); + + prompt += ` + + +The user has access to the following specialized agents: +${agentsList} + +If the user's request would benefit from a specialized agent's expertise, you may suggest they select one. Users can select an agent by typing @ followed by the agent name, from the tools menu, or from the agents menu in the sidebar. Only suggest an agent if it's clearly relevant to what they're asking about. +`; + } + // Communication preferences const displayName = userPreferences?.displayName || user?.name; const hasStyleExample = userPreferences?.responseStyleExample; diff --git a/src/lib/cache/cache-keys.ts b/src/lib/cache/cache-keys.ts index 9e79f67d8..a12d06ea6 100644 --- a/src/lib/cache/cache-keys.ts +++ b/src/lib/cache/cache-keys.ts @@ -4,4 +4,5 @@ export const CacheKeys = { mcpServerCustomizations: (userId: string) => `mcp-server-customizations-${userId}`, agentInstructions: (agent: string) => `agent-instructions-${agent}`, + availableAgents: (userId: string) => `available-agents-${userId}`, };