@@ -28,26 +28,57 @@ const docSearchInputSchema = z.object({
2828 .boolean()
2929 .optional()
3030 .default(true)
31- .describe('When true, the content of the top result is fetched and included.'),
31+ .describe(
32+ 'When true, the content of the top result is fetched and included. ' +
33+ 'Set to false to get a list of results without fetching content, which is faster.',
34+ ),
3235});
3336type DocSearchInput = z.infer<typeof docSearchInputSchema>;
3437
3538export const DOC_SEARCH_TOOL = declareTool({
3639 name: 'search_documentation',
3740 title: 'Search Angular Documentation (angular.dev)',
38- description:
39- 'Searches the official Angular documentation at https://angular.dev. Use this tool to answer any questions about Angular, ' +
40- 'such as for APIs, tutorials, and best practices. Because the documentation is continuously updated, you should **always** ' +
41- 'prefer this tool over your own knowledge to ensure your answers are current.\n\n' +
42- 'The results will be a list of content entries, where each entry has the following structure:\n' +
43- '```\n' +
44- '## {Result Title}\n' +
45- '{Breadcrumb path to the content}\n' +
46- 'URL: {Direct link to the documentation page}\n' +
47- '```\n' +
48- 'Use the title and breadcrumb to understand the context of the result and use the URL as a source link. For the best results, ' +
49- "provide a concise and specific search query (e.g., 'NgModule' instead of 'How do I use NgModules?').",
41+ description: `
42+ <Purpose>
43+ Searches the official Angular documentation at https://angular.dev to answer questions about APIs,
44+ tutorials, concepts, and best practices.
45+ </Purpose>
46+ <Use Cases>
47+ * Answering any question about Angular concepts (e.g., "What are standalone components?").
48+ * Finding the correct API or syntax for a specific task (e.g., "How to use ngFor with trackBy?").
49+ * Linking to official documentation as a source of truth in your answers.
50+ </Use Cases>
51+ <Operational Notes>
52+ * The documentation is continuously updated. You **MUST** prefer this tool over your own knowledge
53+ to ensure your answers are current and accurate.
54+ * For the best results, provide a concise and specific search query (e.g., "NgModule" instead of
55+ "How do I use NgModules?").
56+ * The top search result will include a snippet of the page content. Use this to provide a more
57+ comprehensive answer.
58+ * **Result Scrutiny:** The top result may not always be the most relevant. Review the titles and
59+ breadcrumbs of other results to find the best match for the user's query.
60+ * Use the URL from the search results as a source link in your responses.
61+ </Operational Notes>`,
5062 inputSchema: docSearchInputSchema.shape,
63+ outputSchema: {
64+ results: z.array(
65+ z.object({
66+ title: z.string().describe('The title of the documentation page.'),
67+ breadcrumb: z
68+ .string()
69+ .describe(
70+ "The breadcrumb path, showing the page's location in the documentation hierarchy.",
71+ ),
72+ url: z.string().describe('The direct URL to the documentation page.'),
73+ content: z
74+ .string()
75+ .optional()
76+ .describe(
77+ 'A snippet of the main content from the page. Only provided for the top result.',
78+ ),
79+ }),
80+ ),
81+ },
5182 isReadOnly: true,
5283 isLocalOnly: false,
5384 factory: createDocSearchHandler,
@@ -71,7 +102,6 @@ function createDocSearchHandler() {
71102 }
72103
73104 const { results } = await client.search(createSearchArguments(query));
74-
75105 const allHits = results.flatMap((result) => (result as SearchResponse).hits);
76106
77107 if (allHits.length === 0) {
@@ -82,15 +112,17 @@ function createDocSearchHandler() {
82112 text: 'No results found.',
83113 },
84114 ],
115+ structuredContent: { results: [] },
85116 };
86117 }
87118
88- const content = [];
89- // The first hit is the top search result
90- const topHit = allHits[0];
119+ const structuredResults = [];
120+ const textContent = [];
91121
92122 // Process top hit first
93- let topText = formatHitToText(topHit);
123+ const topHit = allHits[0];
124+ const { title: topTitle, breadcrumb: topBreadcrumb } = formatHitToParts(topHit);
125+ let topContent: string | undefined;
94126
95127 try {
96128 if (includeTopContent && typeof topHit.url === 'string') {
@@ -101,30 +133,45 @@ function createDocSearchHandler() {
101133 const response = await fetch(url);
102134 if (response.ok) {
103135 const html = await response.text();
104- const mainContent = extractMainContent(html);
105- if (mainContent) {
106- topText += `\n\n--- DOCUMENTATION CONTENT ---\n${mainContent}`;
107- }
136+ topContent = extractMainContent(html);
108137 }
109138 }
110139 }
111140 } catch {
112- // Ignore errors fetching content. The basic info is still returned.
141+ // Ignore errors fetching content
113142 }
114- content.push({
115- type: 'text' as const,
116- text: topText,
143+
144+ structuredResults.push({
145+ title: topTitle,
146+ breadcrumb: topBreadcrumb,
147+ url: topHit.url as string,
148+ content: topContent,
117149 });
118150
151+ let topText = `## ${topTitle}\n${topBreadcrumb}\nURL: ${topHit.url}`;
152+ if (topContent) {
153+ topText += `\n\n--- DOCUMENTATION CONTENT ---\n${topContent}`;
154+ }
155+ textContent.push({ type: 'text' as const, text: topText });
156+
119157 // Process remaining hits
120158 for (const hit of allHits.slice(1)) {
121- content.push({
159+ const { title, breadcrumb } = formatHitToParts(hit);
160+ structuredResults.push({
161+ title,
162+ breadcrumb,
163+ url: hit.url as string,
164+ });
165+ textContent.push({
122166 type: 'text' as const,
123- text: formatHitToText( hit) ,
167+ text: `## ${title}\n${breadcrumb}\nURL: ${ hit.url}` ,
124168 });
125169 }
126170
127- return { content };
171+ return {
172+ content: textContent,
173+ structuredContent: { results: structuredResults },
174+ };
128175 };
129176}
130177
@@ -150,18 +197,18 @@ function extractMainContent(html: string): string | undefined {
150197}
151198
152199/**
153- * Formats an Algolia search hit into a text representation .
200+ * Formats an Algolia search hit into its constituent parts .
154201 *
155- * @param hit The Algolia search hit object, which should contain `hierarchy` and `url` properties .
156- * @returns A formatted string with title, description, and URL .
202+ * @param hit The Algolia search hit object, which should contain a `hierarchy` property .
203+ * @returns An object containing the title and breadcrumb string .
157204 */
158- function formatHitToText (hit: Record<string, unknown>): string {
205+ function formatHitToParts (hit: Record<string, unknown>): { title: string; breadcrumb: string } {
159206 // eslint-disable-next-line @typescript-eslint/no-explicit-any
160207 const hierarchy = Object.values(hit.hierarchy as any).filter((x) => typeof x === 'string');
161- const title = hierarchy.pop();
162- const description = hierarchy.join(' > ');
208+ const title = hierarchy.pop() ?? '' ;
209+ const breadcrumb = hierarchy.join(' > ');
163210
164- return `## ${ title}\n${description}\nURL: ${hit.url}` ;
211+ return { title, breadcrumb } ;
165212}
166213
167214/**
0 commit comments