Skip to content

Commit 6ad6056

Browse files
committed
feat: add RAG documentaion tool
Similar to the @ECL chat participant, but without having to rememver to type @ECL Signed-off-by: Gordon Smith <GordonJSmith@gmail.com>
1 parent dc69c4b commit 6ad6056

File tree

4 files changed

+143
-0
lines changed

4 files changed

+143
-0
lines changed

ecl-sample/ProgGuide/GenData.ecl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
//
44
IMPORT Std;
55

6+
#option('generateLogicalGraph', true);
7+
68
//This code generates a nested child dataset containing
79
// 1,000,000 Person (parent) records and all their associated Accounts (children)
810
// by starting with 1000 first names and 1000 last names

package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,34 @@
349349
}
350350
}
351351
}
352+
},
353+
{
354+
"name": "ecl-extension-eclDocsLookup",
355+
"tags": [
356+
"documentation",
357+
"help",
358+
"reference",
359+
"ecl",
360+
"language",
361+
"ecl-extension"
362+
],
363+
"toolReferenceName": "eclDocsLookup",
364+
"displayName": "ECL Documentation Lookup",
365+
"modelDescription": "Search ECL documentation using AI-powered retrieval (RAG). Use this tool when user asks about ECL language features, functions, syntax, standard libraries, or needs help understanding ECL concepts. Returns relevant documentation pages from the ECL Language Reference, Standard Library Reference, and Programmer's Guide.",
366+
"canBeReferencedInPrompt": true,
367+
"icon": "$(book)",
368+
"inputSchema": {
369+
"type": "object",
370+
"properties": {
371+
"query": {
372+
"type": "string",
373+
"description": "The documentation search query. Can be a keyword, function name, concept, or question about ECL."
374+
}
375+
},
376+
"required": [
377+
"query"
378+
]
379+
}
352380
}
353381
],
354382
"languages": [

src/ecl/lm/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { GetWorkunitECLTool } from "./tools/getWorkunitECL";
55
import { GetWorkunitMetricsTool } from "./tools/getWorkunitMetrics";
66
import { FindLogicalFilesTool } from "./tools/findLogicalFiles";
77
import { SyntaxCheckTool } from "./tools/syntaxCheck";
8+
import { ECLDocsLookupTool } from "./tools/eclDocsLookup";
89

910
let eclLMTools: ECLLMTools;
1011

@@ -18,6 +19,7 @@ export class ECLLMTools {
1819

1920
ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-findLogicalFiles", new FindLogicalFilesTool()));
2021
ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-syntaxCheck", new SyntaxCheckTool()));
22+
ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-eclDocsLookup", new ECLDocsLookupTool(ctx)));
2123
}
2224

2325
static attach(ctx: vscode.ExtensionContext): ECLLMTools {

src/ecl/lm/tools/eclDocsLookup.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as vscode from "vscode";
2+
import { fetchContext, fetchIndexes, Hit } from "../../docs";
3+
import { reporter } from "../../../telemetry";
4+
import localize from "../../../util/localize";
5+
import { logToolEvent, throwIfCancellationRequested } from "../utils/index";
6+
import { checkModelExists } from "../utils/model";
7+
8+
export interface IECLDocsLookupParameters {
9+
query: string;
10+
}
11+
12+
export class ECLDocsLookupTool implements vscode.LanguageModelTool<IECLDocsLookupParameters> {
13+
private modelPath: Promise<vscode.Uri>;
14+
private docsPath: vscode.Uri;
15+
16+
constructor(ctx: vscode.ExtensionContext) {
17+
this.modelPath = checkModelExists(ctx);
18+
this.docsPath = vscode.Uri.joinPath(ctx.extensionUri, "dist", "docs.vecdb");
19+
}
20+
21+
async invoke(options: vscode.LanguageModelToolInvocationOptions<IECLDocsLookupParameters>, token: vscode.CancellationToken) {
22+
reporter?.sendTelemetryEvent("lmTool.invoke", { tool: "eclDocsLookup" });
23+
const params = options.input;
24+
25+
if (typeof params.query !== "string" || params.query.trim().length === 0) {
26+
throw new vscode.LanguageModelError(localize("Query is required for ECL documentation lookup"), { cause: "invalid_parameters" });
27+
}
28+
29+
logToolEvent("eclDocsLookup", "invoke start", { queryLength: params.query.length });
30+
31+
try {
32+
throwIfCancellationRequested(token);
33+
34+
// Fetch relevant documentation using RAG (Retrieval-Augmented Generation)
35+
const hits = await fetchContext(params.query, this.modelPath, this.docsPath);
36+
37+
throwIfCancellationRequested(token);
38+
39+
const parts: vscode.LanguageModelTextPart[] = [];
40+
41+
if (hits.length === 0) {
42+
// Fall back to suggesting web links from the indexes
43+
const indexHits = await fetchIndexes();
44+
parts.push(new vscode.LanguageModelTextPart(
45+
localize("No specific documentation found for query: {0}. Suggesting general ECL documentation sources:", params.query)
46+
));
47+
48+
const suggestedLinks = indexHits
49+
.map((hit, idx) => `${idx + 1}. ${hit.label}: ${hit.url}`)
50+
.join("\n");
51+
52+
parts.push(new vscode.LanguageModelTextPart(
53+
`${localize("Available Documentation:")}\n${suggestedLinks}`
54+
));
55+
56+
logToolEvent("eclDocsLookup", "invoke no hits", {
57+
query: params.query,
58+
indexCount: indexHits.length
59+
});
60+
} else {
61+
// Create summary with URLs first - this ensures the LM shows them to users
62+
const urlList = hits.map((hit, idx) => `${idx + 1}. ${hit.label}: ${hit.url}`).join("\n");
63+
64+
parts.push(new vscode.LanguageModelTextPart(
65+
`IMPORTANT: Always include these documentation URLs in your response to the user:\n\n${urlList}\n\n`
66+
));
67+
68+
// Format each documentation hit with its content
69+
const formattedHits = hits.map((hit, idx) => {
70+
const header = `## ${idx + 1}. ${hit.label}`;
71+
const content = hit.content ? `\n${hit.content}` : "";
72+
const error = hit.error ? `\n**Error:** ${hit.error}\n` : "";
73+
return `${header}${content}${error}`;
74+
}).join("\n\n---\n\n");
75+
76+
parts.push(new vscode.LanguageModelTextPart(formattedHits));
77+
78+
logToolEvent("eclDocsLookup", "invoke success", {
79+
query: params.query,
80+
hitCount: hits.length,
81+
hits: hits.map(h => ({ label: h.label, url: h.url }))
82+
});
83+
}
84+
85+
return new vscode.LanguageModelToolResult(parts);
86+
} catch (error) {
87+
const message = error instanceof Error ? error.message : String(error);
88+
logToolEvent("eclDocsLookup", "invoke failed", { error: message, query: params.query });
89+
throw new vscode.LanguageModelError(
90+
localize("Error looking up ECL documentation: {0}", message),
91+
{ cause: error }
92+
);
93+
}
94+
}
95+
96+
async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<IECLDocsLookupParameters>, _token: vscode.CancellationToken) {
97+
const queryPreview = options.input.query
98+
? `\n\nQuery: "${options.input.query.slice(0, 100)}${options.input.query.length > 100 ? "…" : ""}"`
99+
: "";
100+
101+
return {
102+
invocationMessage: localize("Looking up ECL documentation for: {0}", options.input.query || ""),
103+
confirmationMessages: {
104+
title: localize("Lookup ECL Documentation"),
105+
message: new vscode.MarkdownString(
106+
localize("Search ECL documentation using AI-powered retrieval?") + queryPreview
107+
),
108+
},
109+
};
110+
}
111+
}

0 commit comments

Comments
 (0)