Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/app/api/chat/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<AgentSummary[] | null>(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,
Expand Down
13 changes: 12 additions & 1 deletion src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
} from "./shared.chat";
import {
rememberAgentAction,
rememberAvailableAgentsAction,
rememberMcpServerCustomizationsAction,
} from "./actions";
import { getSession } from "auth/server";
Expand Down Expand Up @@ -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,
);
Expand Down
122 changes: 122 additions & 0 deletions src/lib/ai/prompts.test.ts
Original file line number Diff line number Diff line change
@@ -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("<available_agents>");
expect(prompt).toContain("</available_agents>");
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("<available_agents>");
expect(prompt).not.toContain("</available_agents>");
// 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("<available_agents>");
expect(prompt).not.toContain("</available_agents>");
});

it("should not include available agents section when list is undefined", () => {
const prompt = buildUserSystemPrompt(
undefined,
undefined,
undefined,
undefined,
);

expect(prompt).not.toContain("<available_agents>");
expect(prompt).not.toContain("</available_agents>");
});

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**:");
});
});
});
22 changes: 21 additions & 1 deletion src/lib/ai/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -52,6 +52,7 @@ export const buildUserSystemPrompt = (
user?: User,
userPreferences?: UserPreferences,
agent?: Agent,
availableAgents?: AgentSummary[],
) => {
const assistantName =
agent?.name || userPreferences?.botName || "better-chatbot";
Expand Down Expand Up @@ -99,6 +100,25 @@ You can assist with:
- Adapting communication to user preferences and context
</general_capabilities>`;

// 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 += `

<available_agents>
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.
</available_agents>`;
}

// Communication preferences
const displayName = userPreferences?.displayName || user?.name;
const hasStyleExample = userPreferences?.responseStyleExample;
Expand Down
1 change: 1 addition & 0 deletions src/lib/cache/cache-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
};
Loading