Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 69 additions & 12 deletions src/tools/error-lookup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
/**
* Error lookup tool — diagnose any Aztec error by message, code, or hex signature.
*
* Enhanced: when the static catalog + dynamic parsers produce no matches,
* falls back to semantic search via DocsGPT for broader documentation context.
* Enhanced: when the static catalog + dynamic parsers produce no STRONG
* matches, falls back to semantic search via DocsGPT for broader
* documentation context. Weak fuzzy hints (word-overlap, score < 70)
* no longer suppress the semantic fallback — they would shadow the
* better answer with a misleading top hit (e.g. "note already nullified"
* matching "Contract already initialized" with a Jaccard score of 54).
*/

import { lookupError } from "../utils/error-lookup.js";
Expand All @@ -12,6 +16,21 @@ import type { DocsGPTClient } from "../backends/docsgpt-client.js";
import type { SemanticSearchResult } from "../backends/docsgpt-client.js";
import { checkVersionGate, formatMismatchMessage } from "../utils/version-check.js";

/**
* Minimum catalog-match score that counts as "strong enough to short-
* circuit the semantic fallback." Aligned with the score system in
* ``utils/error-lookup.ts``:
* - 100 exact-code / hex-signature
* - 95 exact-pattern
* - 70-80 substring
* - 50-65 word-overlap (Jaccard)
*
* Threshold of 70 keeps every "real" match type and excludes only the
* Jaccard fuzzy band, which is exactly the noise floor we want to fall
* through past.
*/
const STRONG_MATCH_THRESHOLD = 70;

export type SemanticHealth =
| "ok" // semantic returned results
| "no_results" // semantic ran cleanly, returned empty
Expand Down Expand Up @@ -49,10 +68,31 @@ export async function lookupAztecError(

const result = lookupError(query, { category, maxResults });

const totalMatches = result.catalogMatches.length + result.codeMatches.length;
const hasStrongCatalogMatch = result.catalogMatches.some(
(m) => m.score >= STRONG_MATCH_THRESHOLD
);
const hasCodeMatch = result.codeMatches.length > 0;
const hasAnyCatalogMatch = result.catalogMatches.length > 0;

// When the caller passed an explicit ``category`` filter and the
// catalog produced any in-category match (even a weak one), keep
// the pre-PR short-circuit: falling through to a category-agnostic
// semantic search would surface out-of-scope docs and confuse the
// user who explicitly narrowed the request. The semantic backend
// doesn't honor the same category taxonomy, so respecting the
// filter means trusting the catalog at face value.
const hasCategoryFilteredHit = !!category && hasAnyCatalogMatch;

const hasStrongMatch =
hasStrongCatalogMatch || hasCodeMatch || hasCategoryFilteredHit;

// Static catalog hit: return immediately, semantic call not needed.
if (totalMatches > 0) {
// Strong static hit: return immediately, semantic call not needed.
// Weak fuzzy hits (word-overlap only, no category filter) fall
// through to the semantic path below — they remain in
// ``result.catalogMatches`` so the formatter can still render them
// as low-confidence hints, but they no longer suppress the
// semantic-fallback signal that produces the actually-useful answer.
if (hasStrongMatch) {
return {
success: true,
result,
Expand All @@ -61,16 +101,31 @@ export async function lookupAztecError(
};
}

// No static match. Try semantic fallback if a client exists.
const weakHintsCount = result.catalogMatches.length;

// Below the strong-match threshold (or zero matches). Try semantic
// fallback if a client exists; otherwise return the weak hints
// (if any) with a "skipped" health.
if (!docsgptClient) {
return {
success: true,
result,
semanticHealth: "skipped",
message: `No matches found for "${query}". Try a different error message, code, or hex signature.`,
message:
weakHintsCount > 0
? `No strong match for "${query}" — only ${weakHintsCount} low-confidence fuzzy hint(s) (word-overlap). Set API_KEY to enable semantic-documentation fallback (get a free key by running /mcp-key in the Aztec/Noir Discord: https://discord.gg/xMud5StFyA). Or try a different error message, code, or hex signature.`
: `No matches found for "${query}". Try a different error message, code, or hex signature.`,
};
}

// ``preface`` describes the static-catalog state; the semantic-result
// branch appends what semantic produced. Keeps phrasing accurate when
// weak fuzzy hints exist alongside the semantic results.
const preface =
weakHintsCount > 0
? `No strong static match for "${query}" — ${weakHintsCount} low-confidence fuzzy hint(s) shown below.`
: `No exact error match found for "${query}".`;

// Version gate before invoking semantic. Mirrors aztec_search_docs.
if (!allowVersionMismatch) {
const gate = await checkVersionGate(docsgptClient);
Expand All @@ -81,7 +136,7 @@ export async function lookupAztecError(
semanticHealth: "version_mismatch",
versionMismatch: { localVersion: gate.localVersion, corpusVersion: gate.corpusVersion },
message:
`No exact error match found for "${query}", and the semantic fallback was blocked by a version mismatch.\n\n` +
`${preface} The semantic fallback was blocked by a version mismatch.\n\n` +
formatMismatchMessage(gate.localVersion, gate.corpusVersion),
};
}
Expand All @@ -99,15 +154,18 @@ export async function lookupAztecError(
result,
semanticResults,
semanticHealth: "ok",
message: `No exact error match found for "${query}". Showing relevant documentation.`,
message: `${preface} Showing relevant documentation.`,
};
}

return {
success: true,
result,
semanticHealth: "no_results",
message: `No matches found for "${query}". Try a different error message, code, or hex signature.`,
message:
weakHintsCount > 0
? `${preface} Semantic search also returned no relevant documentation.`
: `No matches found for "${query}". Try a different error message, code, or hex signature.`,
};
} catch (err) {
// Sanitize: don't echo the raw upstream error string to the user.
Expand All @@ -133,8 +191,7 @@ export async function lookupAztecError(
success: true,
result,
semanticHealth: "failed",
message:
`No exact error match found for "${query}", and ${userFacing}.`,
message: `${preface} The semantic fallback was unavailable: ${userFacing}.`,
};
}
}
86 changes: 64 additions & 22 deletions src/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,60 @@ export function formatErrorLookupResult(result: ErrorLookupToolResult): string {

const { catalogMatches, codeMatches } = result.result;

if (catalogMatches.length > 0) {
lines.push("## Known Errors");
// When semantic results exist AND every catalog match is below the
// strong-match threshold, the catalog hits are low-confidence cues
// that shouldn't visually dominate the response. Render semantic
// first under "## Related Documentation", and the catalog after
// under "## Lower-Confidence Catalog Hints" so the LLM consumer
// doesn't anchor on a misleading top hit (e.g. "note already
// nullified" matching "Contract already initialized" with score 54).
const semanticHasResults =
!!result.semanticResults && result.semanticResults.length > 0;
const catalogIsWeakOnly =
catalogMatches.length > 0 &&
catalogMatches.every((m) => m.score < 70);
const renderSemanticFirst = semanticHasResults && catalogIsWeakOnly;

function renderSemantic() {
if (!result.semanticResults || result.semanticResults.length === 0) return;
lines.push("## Related Documentation");
lines.push("");
for (const match of result.semanticResults) {
if (match.title) {
lines.push(`**${match.title}**`);
}
if (match.source) {
lines.push(`Source: ${match.source}`);
}
lines.push("");
lines.push(match.text);
lines.push("");
lines.push("---");
lines.push("");
}
}

function renderCatalog() {
if (catalogMatches.length === 0) return;
lines.push(
catalogIsWeakOnly
? "## Lower-Confidence Catalog Hints"
: "## Known Errors"
);
if (catalogIsWeakOnly) {
// Only point at "documentation results above" when there
// actually is a semantic section above (semantic ran AND
// returned hits, AND we reordered to render it first). In
// every other weak-only state — no client, version mismatch,
// backend failed, semantic returned empty — there's no docs
// section to point at, so use neutral copy that names the
// weakness without implying a better answer is below.
lines.push(
renderSemanticFirst
? "_These are word-overlap fuzzy matches, not direct hits — the documentation results above are likely more authoritative._"
: "_These are word-overlap fuzzy matches, not direct hits. Treat as low-confidence cues only._"
);
}
lines.push("");

for (const m of catalogMatches) {
Expand All @@ -178,10 +230,10 @@ export function formatErrorLookupResult(result: ErrorLookupToolResult): string {
}
}

if (codeMatches.length > 0) {
function renderCode() {
if (codeMatches.length === 0) return;
lines.push("## Related Code References");
lines.push("");

for (const match of codeMatches) {
lines.push(`**${match.file}:${match.line}**`);
lines.push("```");
Expand All @@ -191,24 +243,14 @@ export function formatErrorLookupResult(result: ErrorLookupToolResult): string {
}
}

// Semantic fallback results from DocsGPT
if (result.semanticResults && result.semanticResults.length > 0) {
lines.push("## Related Documentation");
lines.push("");

for (const match of result.semanticResults) {
if (match.title) {
lines.push(`**${match.title}**`);
}
if (match.source) {
lines.push(`Source: ${match.source}`);
}
lines.push("");
lines.push(match.text);
lines.push("");
lines.push("---");
lines.push("");
}
if (renderSemanticFirst) {
renderSemantic();
renderCatalog();
renderCode();
} else {
renderCatalog();
renderCode();
renderSemantic();
}

if (
Expand Down
Loading
Loading