@@ -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