init
This commit is contained in:
commit
0964819cdc
34 files changed
+7912
No files matched your search
+171
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Query expansion, on the local model.
|
||||
*
|
||||
* A developer's first query is often a description rather than a name — "how
|
||||
* does a room decide it is occupied" — and that phrasing shares few tokens with
|
||||
* the code that answers it. Asking a small local model for a handful of
|
||||
* alternative phrasings, embedding each, and fusing the rankings costs one local
|
||||
* generation and a few local embeddings, and is free in money.
|
||||
*
|
||||
* ## Every failure is a fallback, never an error
|
||||
*
|
||||
* This runs on the critical path of a search, against a model on somebody else's
|
||||
* box. It can be slow, wedged, or absent. None of those may fail the search, so
|
||||
* the contract is narrow: **the returned list always contains the original
|
||||
* query**, and anything that goes wrong simply returns it alone. The caller can
|
||||
* treat expansion as a bonus that is either there or not.
|
||||
*
|
||||
* The internal timeout is deliberately separate from the caller's signal: the
|
||||
* caller cancelling means stop everything, while the model being slow just means
|
||||
* search with what we have.
|
||||
*/
|
||||
|
||||
/** Attempts per request. Expansion is a bonus; one more try is enough. */
|
||||
const MAX_ATTEMPTS = 2
|
||||
|
||||
/** Characters of query allowed through. Longer is a paste, not a question. */
|
||||
const MAX_QUERY_CHARS = 500
|
||||
|
||||
/** A variant this long is a paragraph, not a search phrase. */
|
||||
const MAX_VARIANT_CHARS = 200
|
||||
|
||||
/** Variants shorter than this match everything and rank nothing. */
|
||||
const MIN_VARIANT_CHARS = 3
|
||||
|
||||
/**
|
||||
* Openers that mark a line as the model talking rather than a phrasing.
|
||||
*
|
||||
* A small model asked for a list sometimes answers with a sentence about the
|
||||
* request — "I cannot help with that." A line like that is not a weak variant,
|
||||
* it is not a variant at all: embedding it would search the codebase for the
|
||||
* model's apology and rank whatever matched.
|
||||
*
|
||||
* `i\b` and the refusal check below are the load-bearing ones. A question-shaped
|
||||
* variant ("how does a room decide it is occupied") is entirely legitimate, so
|
||||
* the test is first-person and refusal vocabulary, not sentence punctuation.
|
||||
*/
|
||||
const COMMENTARY = /^(?:i\b|i'm\b|i am\b|sorry\b|unfortunately\b|note\b|explanation\b|here\b|sure\b|of course\b|certainly\b|these\b|those\b|the following\b|as an?\b|phrasings?\b|query\b)/i
|
||||
|
||||
/** Refusal vocabulary, wherever it sits in the line. */
|
||||
const REFUSAL = /\b(?:cannot help|can't help|don't have|do not have|not able to|unable to|no way to)\b/i
|
||||
|
||||
const PROMPT = `You turn a developer's search query into alternative phrasings that would match source code.
|
||||
|
||||
Rules:
|
||||
- One phrasing per line. No numbering, no bullets, no commentary, no blank lines.
|
||||
- Use the vocabulary the code itself would use: identifiers, domain nouns, verb phrases.
|
||||
- Vary the angle: a likely function name, a likely filename fragment, a description of the behaviour.
|
||||
- Do not repeat the original query.
|
||||
|
||||
Query: {{QUERY}}
|
||||
|
||||
Phrasings:`
|
||||
|
||||
/**
|
||||
* Reduce one model line to a usable phrase, or nothing.
|
||||
*
|
||||
* Models are reliably inclined to number a list, wrap it in quotes, or add a
|
||||
* closing remark. None of that is worth an error path, so it is all stripped.
|
||||
*/
|
||||
export function cleanVariant(line) {
|
||||
let text = String(line ?? '')
|
||||
.replace(/^\s*(?:[-*•]|\d+[.)])\s*/, '')
|
||||
.trim()
|
||||
// A trailing aside ("this should find the handler") is common; drop it.
|
||||
text = text.split(/\s+[—–-]{1,2}\s+/)[0].trim()
|
||||
if (text.length >= 2 && ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'")))) {
|
||||
text = text.slice(1, -1).trim()
|
||||
}
|
||||
if (text.length < MIN_VARIANT_CHARS || text.length > MAX_VARIANT_CHARS) return undefined
|
||||
if (COMMENTARY.test(text) || REFUSAL.test(text)) return undefined
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a model response into alternative queries, best effort.
|
||||
*
|
||||
* @param {string} body raw model output
|
||||
* @param {string} query the original, excluded from the results
|
||||
* @param {number} count most variants to keep
|
||||
* @returns {string[]} deduplicated variants, in the order the model gave them
|
||||
*/
|
||||
export function parseVariants(body, query, count) {
|
||||
const seen = new Set([String(query).trim().toLowerCase()])
|
||||
const out = []
|
||||
for (const line of String(body ?? '').split('\n')) {
|
||||
if (out.length >= count) break
|
||||
const variant = cleanVariant(line)
|
||||
if (variant === undefined) continue
|
||||
const key = variant.toLowerCase()
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
out.push(variant)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an expander bound to one local model.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {string} options.apiBase endpoint, no trailing slash
|
||||
* @param {string} options.model model id
|
||||
* @param {number} [options.timeoutMs] budget for the whole expansion
|
||||
* @param {number} [options.count] most variants to ask for
|
||||
* @param {typeof fetch} [options.fetchImpl] injection point for tests
|
||||
*/
|
||||
export function createExpander(options) {
|
||||
const { apiBase, model, timeoutMs = 8_000, count = 4, fetchImpl = fetch } = options
|
||||
const base = String(apiBase).replace(/\/+$/, '')
|
||||
|
||||
/** One request, bounded by its own deadline. */
|
||||
async function request(query, budget, signal) {
|
||||
// A non-finite budget would make AbortSignal.timeout throw, and the caller's
|
||||
// catch would turn that into a silent fallback — a misconfiguration wearing
|
||||
// the disguise of a slow model. Clamp instead.
|
||||
const deadline = AbortSignal.timeout(Number.isFinite(budget) && budget > 0 ? budget : 8_000)
|
||||
const combined = signal === undefined ? deadline : AbortSignal.any([signal, deadline])
|
||||
const response = await fetchImpl(`${base}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
prompt: PROMPT.replace('{{QUERY}}', query),
|
||||
stream: false,
|
||||
// A handful of short lines; a model allowed to ramble will.
|
||||
options: { num_predict: 120, temperature: 0.4 },
|
||||
}),
|
||||
signal: combined,
|
||||
})
|
||||
if (!response.ok) throw new Error(`${base} responded ${response.status}`)
|
||||
const payload = await response.json()
|
||||
return typeof payload?.response === 'string' ? payload.response : ''
|
||||
}
|
||||
|
||||
return {
|
||||
model,
|
||||
/**
|
||||
* Alternative phrasings for a query.
|
||||
*
|
||||
* @param {string} query the original
|
||||
* @param {AbortSignal} [signal] caller cancellation
|
||||
* @returns {Promise<string[]>} the original first, then whatever else worked
|
||||
*/
|
||||
async expand(query, signal) {
|
||||
const original = String(query ?? '').trim()
|
||||
if (original === '' || original.length > MAX_QUERY_CHARS) return [original]
|
||||
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
|
||||
if (signal?.aborted) return [original]
|
||||
try {
|
||||
const body = await request(original, timeoutMs, signal)
|
||||
return [original, ...parseVariants(body, original, count)]
|
||||
} catch {
|
||||
// Slow, wedged, or unreachable all mean the same thing here: search
|
||||
// with the query we were given rather than failing the call.
|
||||
if (signal?.aborted) return [original]
|
||||
}
|
||||
}
|
||||
return [original]
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user