Skip to content

Commit 819e38d

Browse files
authored
chore: refactor ai/lm tools prior to adding new tools (#476)
* chore: refactor ai/lm tools prior to adding new tools Signed-off-by: Gordon Smith <GordonJSmith@gmail.com> * chore: refactor ai/lm tools prior to adding new tools Signed-off-by: Gordon Smith <GordonJSmith@gmail.com> --------- Signed-off-by: Gordon Smith <GordonJSmith@gmail.com>
1 parent d687780 commit 819e38d

13 files changed

Lines changed: 274 additions & 111 deletions

File tree

.vscode/settings.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
},
1212
"editor.insertSpaces": true,
1313
"ecl.debugLogging": true,
14-
"ecl.launchConfiguration": "not found",
14+
"ecl.launchConfiguration": "no selection",
1515
"ecl.targetCluster": {
1616
"localhost": "thor",
17-
"not found": "thor"
17+
"not found": "thor",
18+
"no selection": "thor"
1819
},
1920
"svg.preview.background": "dark-transparent"
2021
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159
"contributes": {
160160
"languageModelTools": [
161161
{
162-
"name": "ecl-extension_syntaxCheck",
162+
"name": "ecl-extension-syntaxCheck",
163163
"tags": [
164164
"editors",
165165
"syntax check",
@@ -182,7 +182,7 @@
182182
}
183183
},
184184
{
185-
"name": "ecl-extension_findLogicalFiles",
185+
"name": "ecl-extension-findLogicalFiles",
186186
"tags": [
187187
"logical files",
188188
"search",

src/ecl/lm/constants.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,8 @@ export const SAMPLE_COLLECTION_URL = `https://cdn.jsdelivr.net/gh/${OWNER}/${REP
1212

1313
export const MODEL_VENDOR: string = "copilot";
1414

15-
enum LANGUAGE_MODEL_ID {
16-
GPT_3 = "gpt-3.5-turbo",
17-
GPT_4 = "gpt-4",
18-
GPT_4o = "gpt-4o"
19-
}
20-
21-
export const MODEL_SELECTOR: vscode.LanguageModelChatSelector = { vendor: MODEL_VENDOR, family: LANGUAGE_MODEL_ID.GPT_4o };
15+
// Only constrain the vendor so VS Code can honor the user's currently selected reasoning model.
16+
export const MODEL_SELECTOR: vscode.LanguageModelChatSelector = { vendor: MODEL_VENDOR };
2217

2318
export const FETCH_ISSUE_DETAIL_CMD = "Fetch Issue Details Command";
2419

src/ecl/lm/findLogicalFiles.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/ecl/lm/syntaxCheck.ts

Lines changed: 0 additions & 56 deletions
This file was deleted.

src/ecl/lm/tools.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import * as vscode from "vscode";
2-
import { FindLogicalFilesTool } from "./findLogicalFiles";
3-
import { SyntaxCheckTool } from "./syntaxCheck";
2+
import { FindLogicalFilesTool } from "./tools/findLogicalFiles";
3+
import { SyntaxCheckTool } from "./tools/syntaxCheck";
44

55
let eclLMTools: ECLLMTools;
66

77
export class ECLLMTools {
88

99
protected constructor(ctx: vscode.ExtensionContext) {
10-
ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension_syntaxCheck", new SyntaxCheckTool()));
11-
ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension_findLogicalFiles", new FindLogicalFilesTool()));
10+
ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-syntaxCheck", new SyntaxCheckTool()));
11+
ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-findLogicalFiles", new FindLogicalFilesTool()));
1212
}
1313

1414
static attach(ctx: vscode.ExtensionContext): ECLLMTools {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import * as vscode from "vscode";
2+
import { isPlatformConnected } from "../../../hpccplatform/session";
3+
import { reporter } from "../../../telemetry";
4+
import localize from "../../../util/localize";
5+
import { logToolEvent, requireConnectedSession, throwIfCancellationRequested } from "../utils/index";
6+
7+
export interface IFindLogicalFilesParameters {
8+
pattern: string;
9+
}
10+
11+
export class FindLogicalFilesTool implements vscode.LanguageModelTool<IFindLogicalFilesParameters> {
12+
async invoke(options: vscode.LanguageModelToolInvocationOptions<IFindLogicalFilesParameters>, token: vscode.CancellationToken) {
13+
reporter?.sendTelemetryEvent("lmTool.invoke", { tool: "findLogicalFiles" });
14+
const params = options.input;
15+
const pattern = typeof params.pattern === "string" ? params.pattern.trim() : "";
16+
if (pattern.length === 0) {
17+
throw new vscode.LanguageModelError(localize("Search pattern is required"), { cause: "invalid_parameters" });
18+
}
19+
20+
logToolEvent("findLogicalFiles", "invoke start", { pattern });
21+
22+
try {
23+
const session = requireConnectedSession();
24+
25+
throwIfCancellationRequested(token);
26+
27+
const files = await session.findLogicalFiles(pattern);
28+
29+
throwIfCancellationRequested(token);
30+
31+
const parts: vscode.LanguageModelTextPart[] = [];
32+
33+
if (files.length === 0) {
34+
parts.push(new vscode.LanguageModelTextPart(localize("No logical files match \"{0}\".", pattern)));
35+
} else {
36+
parts.push(new vscode.LanguageModelTextPart(localize("Found {0} logical file(s) matching \"{1}\".", files.length.toString(), pattern)));
37+
38+
const list = files.map(file => {
39+
const name = file.Name || localize("Unnamed file");
40+
const owner = file.Owner || localize("unknown owner");
41+
const description = file.Description ? ` — ${file.Description}` : "";
42+
return `- ${name} (${owner})${description}`;
43+
}).join("\n");
44+
45+
parts.push(new vscode.LanguageModelTextPart(list));
46+
47+
for (const file of files) {
48+
parts.push(new vscode.LanguageModelTextPart(JSON.stringify(file, null, 2)));
49+
}
50+
}
51+
52+
logToolEvent("findLogicalFiles", "invoke success", {
53+
pattern,
54+
fileCount: files.length,
55+
});
56+
57+
return new vscode.LanguageModelToolResult(parts);
58+
} catch (error) {
59+
const message = error instanceof Error ? error.message : String(error);
60+
logToolEvent("findLogicalFiles", "invoke failed", { pattern, error: message });
61+
throw new vscode.LanguageModelError(localize("Failed to search logical files: {0}", message), { cause: error });
62+
}
63+
}
64+
65+
async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<IFindLogicalFilesParameters>, _token: vscode.CancellationToken) {
66+
const connected = isPlatformConnected();
67+
const pattern = typeof options.input.pattern === "string" ? options.input.pattern.trim() : "";
68+
69+
return {
70+
invocationMessage: connected
71+
? localize("Searching HPCC Platform for \"{0}\"", pattern || localize("(empty pattern)"))
72+
: localize("Cannot search: HPCC Platform not connected"),
73+
confirmationMessages: connected ? undefined : {
74+
title: localize("HPCC Platform not connected"),
75+
message: new vscode.MarkdownString(localize("This tool requires an active HPCC connection.")),
76+
}
77+
};
78+
}
79+
}

src/ecl/lm/tools/syntaxCheck.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import * as vscode from "vscode";
2+
import * as os from "os";
3+
import { isPlatformConnected } from "../../../hpccplatform/session";
4+
import { reporter } from "../../../telemetry";
5+
import localize from "../../../util/localize";
6+
import { logToolEvent, requireConnectedSession, throwIfCancellationRequested } from "../utils/index";
7+
8+
export interface ISyntaxCheckParameters {
9+
ecl: string;
10+
}
11+
12+
export class SyntaxCheckTool implements vscode.LanguageModelTool<ISyntaxCheckParameters> {
13+
async invoke(options: vscode.LanguageModelToolInvocationOptions<ISyntaxCheckParameters>, token: vscode.CancellationToken) {
14+
reporter?.sendTelemetryEvent("lmTool.invoke", { tool: "syntaxCheck" });
15+
const params = options.input;
16+
if (typeof params.ecl !== "string" || params.ecl.trim().length === 0) {
17+
throw new vscode.LanguageModelError(localize("ECL code is required"), { cause: "invalid_parameters" });
18+
}
19+
20+
logToolEvent("syntaxCheck", "invoke start", { inputLength: params.ecl.length });
21+
22+
const session = requireConnectedSession();
23+
const tmpFileName = `ecl_syntax_check_${Date.now()}.ecl`;
24+
const tmpUri = vscode.Uri.joinPath(vscode.Uri.file(os.tmpdir()), tmpFileName);
25+
26+
try {
27+
const eclContent = new TextEncoder().encode(params.ecl);
28+
await vscode.workspace.fs.writeFile(tmpUri, eclContent);
29+
30+
throwIfCancellationRequested(token);
31+
32+
const result = await session.checkSyntax(tmpUri);
33+
34+
throwIfCancellationRequested(token);
35+
36+
const errors = result?.errors ?? [];
37+
const checked = result?.checked ?? [];
38+
const issueCount = errors.length;
39+
const checkedCount = checked.length;
40+
41+
const parts: vscode.LanguageModelTextPart[] = [];
42+
43+
if (issueCount === 0) {
44+
parts.push(new vscode.LanguageModelTextPart(localize("No syntax errors found. Checked {0} file(s).", checkedCount.toString())));
45+
} else {
46+
parts.push(new vscode.LanguageModelTextPart(localize("Detected {0} syntax issue(s) across {1} file(s).", issueCount.toString(), Math.max(checkedCount, 1).toString())));
47+
48+
const formatted = errors
49+
.map((error: any, idx: number) => {
50+
const filePath = typeof error.filePath === "string" && error.filePath.length > 0 ? error.filePath : checked[0] || tmpUri.fsPath;
51+
const line = typeof error.line === "number" ? error.line : undefined;
52+
const column = typeof error.col === "number" ? error.col : undefined;
53+
const severity = typeof error.severity === "string" ? error.severity : "error";
54+
const code = error.code ? `[${error.code}] ` : "";
55+
const location = [filePath, line, column]
56+
.filter(value => value !== undefined && value !== "")
57+
.join(":");
58+
const message = error.msg || error.message || localize("Unknown syntax issue");
59+
return `${idx + 1}. ${location} ${severity.toUpperCase()} ${code}${message}`;
60+
})
61+
.join("\n");
62+
63+
if (formatted) {
64+
parts.push(new vscode.LanguageModelTextPart(formatted));
65+
}
66+
}
67+
68+
if (checkedCount > 0) {
69+
const checkedFiles = checked.join("\n");
70+
parts.push(new vscode.LanguageModelTextPart(`${localize("Files checked:")}\n${checkedFiles}`));
71+
}
72+
73+
logToolEvent("syntaxCheck", "invoke success", {
74+
issueCount,
75+
checkedCount,
76+
filesChecked: checked,
77+
});
78+
79+
return new vscode.LanguageModelToolResult(parts);
80+
} catch (error) {
81+
const message = error instanceof Error ? error.message : String(error);
82+
logToolEvent("syntaxCheck", "invoke failed", { error: message });
83+
throw new vscode.LanguageModelError(localize("Error checking syntax: {0}", message), { cause: error });
84+
} finally {
85+
try {
86+
await vscode.workspace.fs.delete(tmpUri);
87+
} catch {
88+
// ignore
89+
}
90+
}
91+
}
92+
93+
async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<ISyntaxCheckParameters>, _token: vscode.CancellationToken) {
94+
const connected = isPlatformConnected();
95+
const eclPreview = options.input.ecl ? `\n\n${options.input.ecl.slice(0, 200)}${options.input.ecl.length > 200 ? "…" : ""}` : "";
96+
97+
const confirmationMessages = connected ? {
98+
title: localize("Check ECL Syntax"),
99+
message: new vscode.MarkdownString(
100+
localize("Check the syntax of ECL code?") + eclPreview
101+
),
102+
} : {
103+
title: localize("HPCC Platform not connected"),
104+
message: new vscode.MarkdownString(localize("This tool requires an active HPCC connection.")),
105+
};
106+
107+
return {
108+
invocationMessage: connected
109+
? localize("Checking ECL syntax")
110+
: localize("Cannot check syntax: HPCC Platform not connected"),
111+
confirmationMessages,
112+
};
113+
}
114+
}

src/ecl/lm/utils/chatResponse.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export interface PromptProps extends BasePromptElementProps {
77
}
88

99
export async function getChatResponse<T extends PromptElementCtor<P, any>, P extends PromptProps>(prompt: T, promptProps: P, token: vscode.CancellationToken): Promise<Thenable<vscode.LanguageModelChatResponse>> {
10-
const models = await vscode.lm.selectChatModels({ family: MODEL_SELECTOR.family, vendor: MODEL_SELECTOR.vendor });
10+
const models = await vscode.lm.selectChatModels({ vendor: MODEL_SELECTOR.vendor });
1111
if (models.length) {
1212
const { messages } = await renderPrompt(prompt, promptProps, { modelMaxPromptTokens: models[0].maxInputTokens }, models[0] as any);
1313
return await models[0].sendRequest(messages, {}, token);

src/ecl/lm/utils/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
export * from "./chatResponse";
1+
export * from "./chatResponse";
2+
export * from "./session";
3+
export * from "./logger";

0 commit comments

Comments
 (0)