scai 0.1.163 → 0.1.164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db/fileIndex.js +128 -126
- package/package.json +1 -1
package/dist/db/fileIndex.js
CHANGED
|
@@ -100,54 +100,143 @@ export function queryFiles(safeQuery, limit = 10) {
|
|
|
100
100
|
// - Uses LLM aggressively
|
|
101
101
|
// - Optimizes for precision
|
|
102
102
|
// --------------------------------------------------
|
|
103
|
-
export async function semanticSearchFiles(originalQuery,
|
|
103
|
+
export async function semanticSearchFiles(originalQuery, _query, // ignored now – LLM owns query construction
|
|
104
|
+
topK = 5) {
|
|
104
105
|
const db = getDbForRepo();
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
// --------------------------------------------------
|
|
107
|
+
// 1. LLM → primary FTS query (always)
|
|
108
|
+
// --------------------------------------------------
|
|
109
|
+
const primaryFtsQuery = await generatePrimaryFtsQuery(originalQuery);
|
|
110
|
+
logInputOutput("semanticSearchFiles LLM primary query", "output", {
|
|
111
|
+
originalQuery,
|
|
112
|
+
ftsQuery: primaryFtsQuery,
|
|
113
|
+
});
|
|
114
|
+
// --------------------------------------------------
|
|
115
|
+
// 2. Run primary FTS once
|
|
116
|
+
// --------------------------------------------------
|
|
110
117
|
const primaryResults = db
|
|
111
118
|
.prepare(sqlTemplates.searchFilesTemplate)
|
|
112
|
-
.all(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
count: primaryResults.length,
|
|
116
|
-
});
|
|
117
|
-
if (primaryResults.length === 0) {
|
|
118
|
-
// No FTS at all → blind expansion
|
|
119
|
-
return semanticFallbackSearch(originalQuery, topK);
|
|
119
|
+
.all(primaryFtsQuery, RELATED_FILES_LIMIT);
|
|
120
|
+
if (primaryResults.length > 0) {
|
|
121
|
+
return rankAndMap(new Map(primaryResults.map(r => [r.id, r])), topK);
|
|
120
122
|
}
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
const
|
|
125
|
-
logInputOutput("semanticSearchFiles
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Not relevant → model-guided expansion
|
|
135
|
-
// -----------------------------
|
|
136
|
-
for (const term of relevance.suggestedTerms) {
|
|
137
|
-
const safeTerm = sanitizeQueryForFts(term);
|
|
123
|
+
// --------------------------------------------------
|
|
124
|
+
// 3. Fallback: LLM → 2–3 subqueries (ONLY if zero results)
|
|
125
|
+
// --------------------------------------------------
|
|
126
|
+
const subQueries = await generateFallbackFtsQueries(originalQuery, primaryFtsQuery);
|
|
127
|
+
logInputOutput("semanticSearchFiles LLM fallback queries", "output", {
|
|
128
|
+
originalQuery,
|
|
129
|
+
primaryFtsQuery,
|
|
130
|
+
subQueries,
|
|
131
|
+
});
|
|
132
|
+
// --------------------------------------------------
|
|
133
|
+
// 4. Execute fallback queries sequentially
|
|
134
|
+
// --------------------------------------------------
|
|
135
|
+
for (const subQuery of subQueries) {
|
|
138
136
|
const rows = db
|
|
139
137
|
.prepare(sqlTemplates.searchFilesTemplate)
|
|
140
|
-
.all(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
.all(subQuery, RELATED_FILES_LIMIT);
|
|
139
|
+
if (rows.length > 0) {
|
|
140
|
+
return rankAndMap(new Map(rows.map(r => [r.id, r])), topK);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// --------------------------------------------------
|
|
144
|
+
// 5. Hard stop
|
|
145
|
+
// --------------------------------------------------
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
async function generatePrimaryFtsQuery(userQuery) {
|
|
149
|
+
const prompt = `
|
|
150
|
+
You are generating a SQLite FTS query for searching a source code repository.
|
|
151
|
+
|
|
152
|
+
Input (natural language):
|
|
153
|
+
"${userQuery}"
|
|
154
|
+
|
|
155
|
+
Task:
|
|
156
|
+
- Produce ONE concise FTS query
|
|
157
|
+
- Focus on filenames, symbols, module names, domain nouns
|
|
158
|
+
- Prefer literal identifiers likely to exist in code
|
|
159
|
+
- NO sentences
|
|
160
|
+
- NO stopwords
|
|
161
|
+
- NO explanations
|
|
162
|
+
- NO wildcards unless absolutely necessary
|
|
163
|
+
- Use OR between terms
|
|
164
|
+
- **MAX 10 terms only** — be selective and concise
|
|
165
|
+
|
|
166
|
+
Output JSON ONLY:
|
|
167
|
+
{
|
|
168
|
+
"ftsQuery": "term1 OR term2 OR term3"
|
|
169
|
+
}
|
|
170
|
+
`.trim();
|
|
171
|
+
try {
|
|
172
|
+
const response = await generate({ content: prompt, query: "" });
|
|
173
|
+
const cleaned = await cleanupModule.run({
|
|
174
|
+
query: userQuery,
|
|
175
|
+
content: response.data,
|
|
176
|
+
});
|
|
177
|
+
if (cleaned.data &&
|
|
178
|
+
typeof cleaned.data === "object" &&
|
|
179
|
+
"ftsQuery" in cleaned.data &&
|
|
180
|
+
typeof cleaned.data.ftsQuery === "string") {
|
|
181
|
+
return cleaned.data.ftsQuery;
|
|
144
182
|
}
|
|
145
183
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
primaryResults.forEach(r => seen.set(r.id, r));
|
|
184
|
+
catch (err) {
|
|
185
|
+
log(`⚠️ [semanticSearchFiles] Failed to generate primary FTS query: ${String(err)}`);
|
|
149
186
|
}
|
|
150
|
-
|
|
187
|
+
// Absolute safety fallback — never explode
|
|
188
|
+
return sanitizeQueryForFts(userQuery);
|
|
189
|
+
}
|
|
190
|
+
async function generateFallbackFtsQueries(userQuery, failedQuery) {
|
|
191
|
+
const prompt = `
|
|
192
|
+
You are generating fallback SQLite FTS queries for a source code search.
|
|
193
|
+
|
|
194
|
+
Original user query:
|
|
195
|
+
"${userQuery}"
|
|
196
|
+
|
|
197
|
+
Primary FTS query returned ZERO results:
|
|
198
|
+
"${failedQuery}"
|
|
199
|
+
|
|
200
|
+
Task:
|
|
201
|
+
- Generate 2–3 independent FTS queries (MAX 3)
|
|
202
|
+
- Each query should be concise: no more than 10 OR-joined search terms
|
|
203
|
+
- Focus on filenames, symbols, module names
|
|
204
|
+
- Avoid natural-language sentences
|
|
205
|
+
- Avoid recursion or refinement loops
|
|
206
|
+
- Use OR between terms
|
|
207
|
+
|
|
208
|
+
Output JSON ONLY:
|
|
209
|
+
{
|
|
210
|
+
"subQueries": [
|
|
211
|
+
"query1",
|
|
212
|
+
"query2",
|
|
213
|
+
"query3"
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
`.trim();
|
|
217
|
+
try {
|
|
218
|
+
const response = await generate({ content: prompt, query: "" });
|
|
219
|
+
const cleaned = await cleanupModule.run({
|
|
220
|
+
query: userQuery,
|
|
221
|
+
content: response.data,
|
|
222
|
+
});
|
|
223
|
+
if (cleaned.data &&
|
|
224
|
+
typeof cleaned.data === "object" &&
|
|
225
|
+
Array.isArray(cleaned.data.subQueries)) {
|
|
226
|
+
return cleaned.data.subQueries
|
|
227
|
+
.filter((q) => typeof q === "string")
|
|
228
|
+
.slice(0, 3) // cap to 3 queries
|
|
229
|
+
.map((q) => q
|
|
230
|
+
.split(' OR ')
|
|
231
|
+
.map(term => sanitizeQueryForFts(term)) // sanitize each term individually
|
|
232
|
+
.slice(0, 10) // cap terms per query
|
|
233
|
+
.join(' OR '));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
log(`⚠️ [semanticSearchFiles] Failed to generate fallback queries: ${String(err)}`);
|
|
238
|
+
}
|
|
239
|
+
return [];
|
|
151
240
|
}
|
|
152
241
|
// --------------------------------------------------
|
|
153
242
|
// PLANNER SEARCH (fileSearchModule, discovery)
|
|
@@ -209,71 +298,6 @@ function rankAndMap(seen, topK) {
|
|
|
209
298
|
bm25Score: r.bm25Score,
|
|
210
299
|
}));
|
|
211
300
|
}
|
|
212
|
-
/**
|
|
213
|
-
* Ask the model whether the initial FTS hits are relevant to the query.
|
|
214
|
-
* If not, suggest new search terms for another FTS pass.
|
|
215
|
-
*/
|
|
216
|
-
async function checkFtsRelevanceWithModel(query, ftsResults) {
|
|
217
|
-
if (ftsResults.length === 0) {
|
|
218
|
-
return { relevant: false, suggestedTerms: [] };
|
|
219
|
-
}
|
|
220
|
-
const prompt = `
|
|
221
|
-
You are assisting a code search system that uses full-text search (FTS)
|
|
222
|
-
over source code.
|
|
223
|
-
|
|
224
|
-
Query (natural language):
|
|
225
|
-
"${query}"
|
|
226
|
-
|
|
227
|
-
Initial FTS results (filenames and summaries):
|
|
228
|
-
${JSON.stringify(ftsResults)}
|
|
229
|
-
|
|
230
|
-
Task:
|
|
231
|
-
1. Decide whether these results are relevant to the query.
|
|
232
|
-
2. If they are NOT relevant, suggest alternative search terms.
|
|
233
|
-
|
|
234
|
-
IMPORTANT RULES FOR SUGGESTED TERMS:
|
|
235
|
-
- Terms MUST be likely to appear literally in source code.
|
|
236
|
-
- Prefer: filenames, module names, function names, variables, symbols, config keys.
|
|
237
|
-
- Use short identifiers (1–3 words max).
|
|
238
|
-
- Avoid natural-language phrases or explanations.
|
|
239
|
-
- Avoid conceptual or architectural descriptions.
|
|
240
|
-
- Examples of GOOD terms: "api", "router", "frontend", "backend", "client", "server", "routes", "config.ts"
|
|
241
|
-
- Examples of BAD terms: "frontend backend separation", "code architecture", "business logic"
|
|
242
|
-
|
|
243
|
-
Output format:
|
|
244
|
-
- If relevant:
|
|
245
|
-
{ "relevant": true, "suggestedTerms": [] }
|
|
246
|
-
- If not relevant:
|
|
247
|
-
{ "relevant": false, "suggestedTerms": ["term1", "term2", "term3"] }
|
|
248
|
-
|
|
249
|
-
Return ONLY valid JSON.
|
|
250
|
-
`.trim();
|
|
251
|
-
try {
|
|
252
|
-
const response = await generate({ content: prompt, query: "" });
|
|
253
|
-
const cleaned = await cleanupModule.run({
|
|
254
|
-
query,
|
|
255
|
-
content: response.data,
|
|
256
|
-
});
|
|
257
|
-
const data = cleaned.data;
|
|
258
|
-
// Type guard: ensure it's an object with correct properties
|
|
259
|
-
if (data &&
|
|
260
|
-
typeof data === "object" &&
|
|
261
|
-
"relevant" in data &&
|
|
262
|
-
"suggestedTerms" in data &&
|
|
263
|
-
typeof data.relevant === "boolean" &&
|
|
264
|
-
Array.isArray(data.suggestedTerms)) {
|
|
265
|
-
const relevant = data.relevant;
|
|
266
|
-
const suggestedTerms = data.suggestedTerms.filter((t) => typeof t === "string");
|
|
267
|
-
return { relevant, suggestedTerms };
|
|
268
|
-
}
|
|
269
|
-
return { relevant: false, suggestedTerms: [] };
|
|
270
|
-
}
|
|
271
|
-
catch (err) {
|
|
272
|
-
log(`⚠️ [searchFiles] Failed to check FTS relevance: ${String(err)}`);
|
|
273
|
-
return { relevant: false, suggestedTerms: [] };
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
;
|
|
277
301
|
async function expandQueryWithModel(query) {
|
|
278
302
|
const prompt = `
|
|
279
303
|
You are assisting a code search system.
|
|
@@ -305,25 +329,3 @@ Question:
|
|
|
305
329
|
return [];
|
|
306
330
|
}
|
|
307
331
|
}
|
|
308
|
-
async function semanticFallbackSearch(query, topK) {
|
|
309
|
-
const db = getDbForRepo();
|
|
310
|
-
const seen = new Map();
|
|
311
|
-
const llmTerms = await expandQueryWithModel(query);
|
|
312
|
-
logInputOutput("semanticSearchFiles blind expansion", "output", {
|
|
313
|
-
query,
|
|
314
|
-
suggestedTerms: llmTerms,
|
|
315
|
-
});
|
|
316
|
-
for (const term of llmTerms) {
|
|
317
|
-
const safeTerm = sanitizeQueryForFts(term);
|
|
318
|
-
const rows = db
|
|
319
|
-
.prepare(sqlTemplates.searchFilesTemplate)
|
|
320
|
-
.all(safeTerm, RELATED_FILES_LIMIT);
|
|
321
|
-
for (const row of rows) {
|
|
322
|
-
if (!seen.has(row.id))
|
|
323
|
-
seen.set(row.id, row);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
if (seen.size === 0)
|
|
327
|
-
return [];
|
|
328
|
-
return rankAndMap(seen, topK);
|
|
329
|
-
}
|