Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

181 lines
8.3 KiB
Go

// Package llm — prompt tiers and structured-output schemas.
//
// The search-assist passes (expand / rerank / verify) need different
// prompting depending on the model behind the active provider. A
// small local GGUF model needs verbose, rule-heavy, example-laden
// instructions; a hosted frontier model reasons well with light
// direction and is measurably *hurt* by over-constraining prompts.
//
// Rather than carry a prompt set per provider, prompts are keyed by a
// capability *tier* (PromptProfile): two sets to maintain, not four.
// The svc layer maps a provider to its tier via ProfileForProvider.
package llm
// PromptProfile selects a prompt tier.
type PromptProfile int
const (
// ProfileSmall is the verbose, rule-heavy tier — tuned for small
// local GGUF models and small Ollama coder models, which need
// explicit rules and blocklists to behave.
ProfileSmall PromptProfile = iota
// ProfileFrontier is the terse tier — tuned for hosted frontier
// models (Anthropic, OpenAI) that reason well with light direction
// and lose quality when over-constrained.
ProfileFrontier
)
// ProfileForProvider maps a provider's Name() to its prompt tier.
// "local" and "ollama" run small models → ProfileSmall; the hosted
// providers and the CLI providers ("claudecli", "codex" — which shell
// out to Claude / Codex) run frontier models → ProfileFrontier.
// Unknown names fall back to ProfileSmall (the safe, more-explicit
// tier).
func ProfileForProvider(name string) PromptProfile {
switch name {
case "anthropic", "openai", "azure", "claudecli", "codex", "copilot", "cursor", "opencode", "gemini", "bedrock", "deepseek":
return ProfileFrontier
default:
return ProfileSmall
}
}
// --- Expand -----------------------------------------------------------------
const expandSystemSmall = `You expand a code-search query into a small set of CONCRETE identifier-style terms a programmer would actually grep for. ` +
`Output strict JSON: {"terms":["<term1>","<term2>",...]}. ` +
`Include 2 to 5 terms. Each term MUST be a single word with no spaces and no punctuation other than underscores. ` +
`
RULES:
1. Prefer DOMAIN-SPECIFIC terms over generic English. ` +
`GOOD examples: bcrypt, argon2, scrypt, sha256, hmac, jwt, oauth, pbkdf2, kdf, salt. ` +
`BAD examples (NEVER emit): function, library, algorithm, code, system, data, service, value, info, content, thing, stuff, name, general, common, logic, process, handler, flow, action, helper, util, utility. ` +
`
2. Prefer terms that are likely SYMBOL names in a codebase (camelCase / snake_case / PascalCase fragments), library or protocol names, well-known acronyms. ` +
`
3. Do NOT echo the original query words. ` +
`
4. If the query has no obvious domain-specific neighbours, emit FEWER terms (or an empty array) — quality over quantity.`
const expandSystemFrontier = `Expand a code-search query into 2-5 concrete identifier-style terms a programmer would grep for: library and protocol names, well-known acronyms, camelCase / snake_case symbol fragments. ` +
`Skip generic English nouns (function, data, handler, ...). Do not echo the query words. Fewer strong terms beat many weak ones — an empty list is fine when there are no good neighbours. ` +
`Output JSON: {"terms":["<term>",...]}.`
// ExpandSystemPrompt returns the system prompt for the query-expansion
// pass at the given tier.
func ExpandSystemPrompt(p PromptProfile) string {
if p == ProfileFrontier {
return expandSystemFrontier
}
return expandSystemSmall
}
// --- Rerank -----------------------------------------------------------------
const rerankSystemSmall = `You rerank code-search results by relevance to a natural-language task. ` +
`Given a query and a list of candidate symbols (id | name | optional signature), output strict JSON: {"order":["id1","id2",...]} ` +
`with the most relevant candidates first. ` +
`Use ONLY the provided ids verbatim. Do not invent ids. You may drop ids that are clearly unrelated.`
const rerankSystemFrontier = `Reorder the candidate symbols by relevance to the query, most relevant first. ` +
`Use the provided ids verbatim; drop ones that are clearly unrelated. Output JSON: {"order":["<id>",...]}.`
// RerankSystemPrompt returns the system prompt for the rerank pass at
// the given tier.
func RerankSystemPrompt(p PromptProfile) string {
if p == ProfileFrontier {
return rerankSystemFrontier
}
return rerankSystemSmall
}
// --- Verify -----------------------------------------------------------------
const verifySystemSmall = `You filter code-search candidates by reading their BODY, SIGNATURE, and CALLERS, and keeping every one whose code is genuinely about the user's query. ` +
`Each candidate is presented as:
<id> | <name> | <signature>
body:
<code body, truncated>
callers:
- <caller_name> | <caller_signature>
- ...
---
Output strict JSON: {"keep":["id1","id2",...]} listing EVERY id whose code is meaningfully related to the query, in your preferred order (most relevant first).
RULES (follow exactly):
1. Evaluate EACH candidate INDEPENDENTLY. Multiple candidates can be valid matches — keep them all.
2. A name that contains a query word is not enough by itself — read what the code DOES.
3. Cross-reference the CALLERS and the SIGNATURE's parameter types against the query DOMAIN. If a function hashes data but is only called from a "publishDiagnostics" or "renderLog" path with a non-password parameter type, it is NOT about hashing passwords — DROP it.
4. Be GENEROUS, not restrictive: if a candidate's body AND callers AND signature are all plausibly about the query, KEEP it. The user wants signal, not a single "best" pick.
5. Drop a candidate when its body, signature, or callers reveal the operation is on the wrong KIND of data for the query.
6. Returning {"keep":[]} is valid ONLY when NO candidate is genuinely about the query.
7. Use ONLY the provided ids verbatim. Never invent or modify an id.`
const verifySystemFrontier = `Filter code-search candidates: keep every one whose body, signature, and callers show it genuinely concerns the query; drop the rest. ` +
`Judge by what the code DOES and the data domain its callers imply — not by name overlap with the query. Keep all genuine matches, not just the single best. ` +
`An empty result {"keep":[]} is valid and correct when nothing genuinely matches. Use the provided ids verbatim. Output JSON: {"keep":["<id>",...]}.`
// VerifySystemPrompt returns the system prompt for the body-grounded
// verification pass at the given tier.
func VerifySystemPrompt(p PromptProfile) string {
if p == ProfileFrontier {
return verifySystemFrontier
}
return verifySystemSmall
}
// --- Structured-output schemas ----------------------------------------------
// JSONSchemaFor returns a provider-agnostic JSON Schema (as a
// marshalable map) describing the response shape for a JSONShape. The
// HTTP providers feed it to their native structured-output mechanism
// (Anthropic forced-tool input_schema, OpenAI json_schema, Ollama
// format); the local provider ignores it and uses a GBNF grammar
// instead. Returns nil for ShapeFreeform.
//
// tools is consulted only for ShapeToolCall, where the "tool" field is
// constrained to an enum of the tool names.
func JSONSchemaFor(shape JSONShape, tools []ToolSpec) map[string]any {
stringArray := map[string]any{
"type": "array",
"items": map[string]any{"type": "string"},
}
switch shape {
case ShapeExpandTerms:
return listSchema("terms", stringArray)
case ShapeRerankOrder:
return listSchema("order", stringArray)
case ShapeVerifyKeep:
return listSchema("keep", stringArray)
case ShapeToolCall:
names := make([]any, len(tools))
for i, t := range tools {
names[i] = t.Name
}
return map[string]any{
"type": "object",
"properties": map[string]any{
"tool": map[string]any{"type": "string", "enum": names},
"args": map[string]any{"type": "object"},
},
"required": []any{"tool", "args"},
"additionalProperties": false,
}
default:
return nil
}
}
// listSchema builds the schema for a single-key object whose value is
// a JSON array — the shape shared by expand / rerank / verify.
func listSchema(key string, arraySchema map[string]any) map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]any{key: arraySchema},
"required": []any{key},
"additionalProperties": false,
}
}