|
1 | 1 | import { createOpenRouter } from '@openrouter/ai-sdk-provider'; |
2 | | -import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage } from 'ai'; |
| 2 | +import { convertToModelMessages, streamText, tool, type UIMessage } from 'ai'; |
3 | 3 | import { z } from 'zod'; |
4 | 4 | import { source } from '@/lib/source'; |
5 | 5 | import { Document, type DocumentData } from 'flexsearch'; |
@@ -56,61 +56,72 @@ async function chunkedAll<O>(promises: Promise<O>[]): Promise<O[]> { |
56 | 56 | return out; |
57 | 57 | } |
58 | 58 |
|
| 59 | +async function runSearch(query: string, limit = 8): Promise<CustomDocument[]> { |
| 60 | + const search = await searchServer; |
| 61 | + const results = await search.searchAsync(query, { limit, merge: true, enrich: true }); |
| 62 | + return (results as any[]) |
| 63 | + .flatMap((r) => r.result ?? []) |
| 64 | + .map((d) => ({ |
| 65 | + ...d.doc, |
| 66 | + content: d.doc?.content?.slice(0, 1200) ?? '', |
| 67 | + })) |
| 68 | + .filter((d) => d.url); |
| 69 | +} |
| 70 | + |
59 | 71 | const openrouter = createOpenRouter({ |
60 | 72 | apiKey: process.env.OPENROUTER_API_KEY, |
61 | 73 | }); |
62 | 74 |
|
63 | 75 | const systemPrompt = [ |
64 | 76 | 'You are a helpful technical assistant for Next Commerce developer documentation.', |
65 | | - 'Your job is to answer developer questions clearly and concisely based on the documentation.', |
66 | | - 'Always use the `search` tool first to find relevant documentation before answering.', |
67 | | - 'Base your answers on the search results. Cite sources as markdown links using the document `url` field.', |
| 77 | + 'Your job is to answer developer questions clearly and concisely based on the documentation provided in context.', |
| 78 | + 'Base your answers on the documentation context below. Cite sources as markdown links using the document URL.', |
68 | 79 | 'When writing code examples, use the language shown in the documentation.', |
69 | | - 'If the search results do not contain enough information to answer the question, say so honestly.', |
| 80 | + 'If the documentation context does not contain enough information to answer the question, say so honestly.', |
70 | 81 | 'Do not make up API endpoints, parameters, or behaviours that are not in the documentation.', |
71 | 82 | 'Keep answers focused and practical. Avoid lengthy preambles — get straight to the answer.', |
72 | 83 | ].join('\n'); |
73 | 84 |
|
74 | 85 | export async function POST(req: Request) { |
75 | 86 | const reqJson: { messages?: UIMessage[] } = await req.json(); |
| 87 | + const messages = reqJson.messages ?? []; |
| 88 | + |
| 89 | + // Extract latest user question and search server-side — don't rely on model to call tools |
| 90 | + const lastUserText = messages |
| 91 | + .filter((m) => m.role === 'user') |
| 92 | + .at(-1) |
| 93 | + ?.parts?.filter((p: any) => p.type === 'text') |
| 94 | + .map((p: any) => p.text as string) |
| 95 | + .join(' ') ?? ''; |
| 96 | + |
| 97 | + const docs = lastUserText ? await runSearch(lastUserText) : []; |
| 98 | + |
| 99 | + const contextBlock = |
| 100 | + docs.length > 0 |
| 101 | + ? 'Relevant documentation:\n\n' + |
| 102 | + docs.map((d) => `### [${d.title}](${d.url})\n${d.description ? d.description + '\n' : ''}${d.content}`).join('\n\n---\n\n') |
| 103 | + : 'No relevant documentation found.'; |
76 | 104 |
|
77 | 105 | const result = streamText({ |
78 | 106 | model: openrouter.chat(process.env.OPENROUTER_MODEL ?? 'anthropic/claude-3.5-sonnet'), |
79 | | - stopWhen: stepCountIs(5), |
80 | | - tools: { |
81 | | - search: searchTool, |
82 | | - }, |
83 | 107 | messages: [ |
84 | | - { role: 'system', content: systemPrompt }, |
85 | | - ...(await convertToModelMessages(reqJson.messages ?? [])), |
| 108 | + { role: 'system', content: `${systemPrompt}\n\n${contextBlock}` }, |
| 109 | + ...(await convertToModelMessages(messages)), |
86 | 110 | ], |
87 | | - prepareStep: ({ stepNumber }) => ({ |
88 | | - toolChoice: stepNumber === 0 ? 'required' : 'auto', |
89 | | - }), |
90 | 111 | }); |
91 | 112 |
|
92 | 113 | return result.toUIMessageStreamResponse(); |
93 | 114 | } |
94 | 115 |
|
| 116 | +// Keep tool definition so the UI type import still works |
95 | 117 | const searchTool = tool({ |
96 | 118 | description: 'Search the docs content and return raw JSON results.', |
97 | 119 | inputSchema: z.object({ |
98 | 120 | query: z.string(), |
99 | | - limit: z.number().int().min(1).max(20).default(5), |
| 121 | + limit: z.number().int().min(1).max(20).default(8), |
100 | 122 | }), |
101 | 123 | async execute({ query, limit }) { |
102 | | - const search = await searchServer; |
103 | | - const results = await search.searchAsync(query, { limit, merge: true, enrich: true }); |
104 | | - // Truncate content to avoid flooding the context window |
105 | | - return results.map((r: any) => ({ |
106 | | - ...r, |
107 | | - result: r.result?.map((doc: any) => ({ |
108 | | - ...doc, |
109 | | - doc: doc.doc |
110 | | - ? { ...doc.doc, content: doc.doc.content?.slice(0, 1500) } |
111 | | - : doc.doc, |
112 | | - })), |
113 | | - })); |
| 124 | + return runSearch(query, limit); |
114 | 125 | }, |
115 | 126 | }); |
116 | 127 |
|
|
0 commit comments