1- import { sql } from 'drizzle-orm'
21import { type NextRequest , NextResponse } from 'next/server'
32import { z } from 'zod'
3+ import { searchDocumentation } from '@/lib/copilot/service'
44import { createLogger } from '@/lib/logs/console-logger'
5- import { generateEmbeddings } from '@/app/api/knowledge/utils'
6- import { db } from '@/db'
7- import { docsEmbeddings } from '@/db/schema'
85
9- const logger = createLogger ( 'DocsSearch ' )
6+ const logger = createLogger ( 'DocsSearchAPI ' )
107
11- const DocsSearchSchema = z . object ( {
8+ const SearchSchema = z . object ( {
129 query : z . string ( ) . min ( 1 , 'Query is required' ) ,
13- topK : z . number ( ) . min ( 1 ) . max ( 10 ) . default ( 5 ) ,
10+ topK : z . number ( ) . min ( 1 ) . max ( 20 ) . default ( 5 ) ,
1411} )
1512
16- /**
17- * Generate embedding for search query
18- */
19- async function generateSearchEmbedding ( query : string ) : Promise < number [ ] > {
20- try {
21- const embeddings = await generateEmbeddings ( [ query ] )
22- return embeddings [ 0 ] || [ ]
23- } catch ( error ) {
24- logger . error ( 'Failed to generate search embedding:' , error )
25- throw new Error ( 'Failed to generate search embedding' )
26- }
27- }
28-
29- /**
30- * Search docs embeddings using vector similarity
31- */
32- async function searchDocs ( queryEmbedding : number [ ] , topK : number ) {
33- try {
34- const results = await db
35- . select ( {
36- chunkId : docsEmbeddings . chunkId ,
37- chunkText : docsEmbeddings . chunkText ,
38- sourceDocument : docsEmbeddings . sourceDocument ,
39- sourceLink : docsEmbeddings . sourceLink ,
40- headerText : docsEmbeddings . headerText ,
41- headerLevel : docsEmbeddings . headerLevel ,
42- similarity : sql < number > `1 - (${ docsEmbeddings . embedding } <=> ${ JSON . stringify ( queryEmbedding ) } ::vector)` ,
43- } )
44- . from ( docsEmbeddings )
45- . orderBy ( sql `${ docsEmbeddings . embedding } <=> ${ JSON . stringify ( queryEmbedding ) } ::vector` )
46- . limit ( topK )
47-
48- return results
49- } catch ( error ) {
50- logger . error ( 'Failed to search docs:' , error )
51- throw new Error ( 'Failed to search docs' )
52- }
53- }
54-
5513/**
5614 * POST /api/docs/search
57- * Search Sim Studio documentation using vector similarity
15+ * Search documentation for copilot tools
5816 */
5917export async function POST ( req : NextRequest ) {
6018 const requestId = crypto . randomUUID ( )
6119
6220 try {
6321 const body = await req . json ( )
64- const { query, topK } = DocsSearchSchema . parse ( body )
65-
66- logger . info ( `[${ requestId } ] 🔍 DOCS SEARCH TOOL CALLED - Query: "${ query } "` , { topK } )
67-
68- // Step 1: Generate embedding for the query
69- logger . info ( `[${ requestId } ] Generating query embedding...` )
70- const queryEmbedding = await generateSearchEmbedding ( query )
71-
72- if ( queryEmbedding . length === 0 ) {
73- return NextResponse . json ( { error : 'Failed to generate query embedding' } , { status : 500 } )
74- }
75-
76- // Step 2: Search for relevant docs chunks
77- logger . info ( `[${ requestId } ] Searching docs for top ${ topK } chunks...` )
78- const chunks = await searchDocs ( queryEmbedding , topK )
79-
80- if ( chunks . length === 0 ) {
81- return NextResponse . json ( {
82- success : true ,
83- response : "I couldn't find any relevant documentation for that query." ,
84- sources : [ ] ,
85- metadata : {
86- requestId,
87- chunksFound : 0 ,
88- query,
89- } ,
90- } )
91- }
92-
93- // Step 3: Format the response with context and sources
94- const context = chunks
95- . map ( ( chunk , index ) => {
96- const headerText =
97- typeof chunk . headerText === 'string'
98- ? chunk . headerText
99- : String ( chunk . headerText || 'Untitled Section' )
100- const sourceDocument =
101- typeof chunk . sourceDocument === 'string'
102- ? chunk . sourceDocument
103- : String ( chunk . sourceDocument || 'Unknown Document' )
104- const sourceLink =
105- typeof chunk . sourceLink === 'string' ? chunk . sourceLink : String ( chunk . sourceLink || '#' )
106- const chunkText =
107- typeof chunk . chunkText === 'string' ? chunk . chunkText : String ( chunk . chunkText || '' )
22+ const { query, topK } = SearchSchema . parse ( body )
10823
109- return `[${ index + 1 } ] ${ headerText }
110- Document: ${ sourceDocument }
111- URL: ${ sourceLink }
112- Content: ${ chunkText } `
113- } )
114- . join ( '\n\n' )
24+ logger . info ( `[${ requestId } ] Documentation search request: "${ query } "` , { topK } )
11525
116- // Step 4: Format sources for response
117- const sources = chunks . map ( ( chunk , index ) => ( {
118- id : index + 1 ,
119- title : chunk . headerText ,
120- document : chunk . sourceDocument ,
121- link : chunk . sourceLink ,
122- similarity : Math . round ( chunk . similarity * 100 ) / 100 ,
123- } ) )
26+ const results = await searchDocumentation ( query , { topK } )
12427
125- logger . info ( `[${ requestId } ] Found ${ chunks . length } relevant chunks` )
28+ logger . info ( `[${ requestId } ] Found ${ results . length } documentation results` , { query } )
12629
12730 return NextResponse . json ( {
12831 success : true ,
129- response : context ,
130- sources,
32+ results,
33+ query,
34+ totalResults : results . length ,
13135 metadata : {
13236 requestId,
133- chunksFound : chunks . length ,
13437 query,
135- topSimilarity : sources [ 0 ] ?. similarity ,
38+ topK ,
13639 } ,
13740 } )
13841 } catch ( error ) {
@@ -143,7 +46,13 @@ Content: ${chunkText}`
14346 )
14447 }
14548
146- logger . error ( `[${ requestId } ] Docs search error:` , error )
147- return NextResponse . json ( { error : 'Internal server error' } , { status : 500 } )
49+ logger . error ( `[${ requestId } ] Documentation search error:` , error )
50+ return NextResponse . json (
51+ {
52+ error : 'Failed to search documentation' ,
53+ details : error instanceof Error ? error . message : 'Unknown error'
54+ } ,
55+ { status : 500 }
56+ )
14857 }
14958}
0 commit comments