Skip to content

Commit 64672fe

Browse files
committed
chore: refactor ai/lm tools prior to adding new tools
Signed-off-by: Gordon Smith <GordonJSmith@gmail.com>
1 parent ff7c23a commit 64672fe

File tree

11 files changed

+218
-55
lines changed

11 files changed

+218
-55
lines changed

.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/tools.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ let eclLMTools: ECLLMTools;
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 {

src/ecl/lm/tools/findLogicalFiles.ts

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,75 @@
11
import * as vscode from "vscode";
2-
import { isPlatformConnected, sessionManager } from "../../../hpccplatform/session";
2+
import { isPlatformConnected } from "../../../hpccplatform/session";
33
import { reporter } from "../../../telemetry";
44
import localize from "../../../util/localize";
5+
import { logToolEvent, requireConnectedSession, throwIfCancellationRequested } from "../utils/index";
56

67
export interface IFindLogicalFilesParameters {
78
pattern: string;
89
}
910

1011
export class FindLogicalFilesTool implements vscode.LanguageModelTool<IFindLogicalFilesParameters> {
1112
async invoke(options: vscode.LanguageModelToolInvocationOptions<IFindLogicalFilesParameters>, token: vscode.CancellationToken) {
12-
if (!isPlatformConnected()) {
13-
throw new vscode.LanguageModelError(localize("HPCC Platform not connected"), { cause: "not_connected" });
14-
}
1513
reporter?.sendTelemetryEvent("lmTool.invoke", { tool: "findLogicalFiles" });
1614
const params = options.input;
17-
return sessionManager.session?.findLogicalFiles(params.pattern).then((files) => {
18-
const parts: Array<vscode.LanguageModelTextPart | vscode.LanguageModelPromptTsxPart> = [];
19-
parts.push(new vscode.LanguageModelTextPart(localize("Found {0} files matching \"{1}\":", files.length.toString(), params.pattern)));
20-
for (const file of files) {
21-
parts.push(new vscode.LanguageModelPromptTsxPart({ kind: "hpccFile", ...file }));
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+
}
2250
}
51+
52+
logToolEvent("findLogicalFiles", "invoke success", {
53+
pattern,
54+
fileCount: files.length,
55+
});
56+
2357
return new vscode.LanguageModelToolResult(parts);
24-
});
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+
}
2563
}
2664

2765
async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<IFindLogicalFilesParameters>, _token: vscode.CancellationToken) {
2866
const connected = isPlatformConnected();
67+
const pattern = typeof options.input.pattern === "string" ? options.input.pattern.trim() : "";
68+
2969
return {
30-
invocationMessage: connected ? localize("Searching workspace for \"{0}\"", options.input.pattern) : localize("Cannot search: HPCC Platform not connected"),
70+
invocationMessage: connected
71+
? localize("Searching HPCC Platform for \"{0}\"", pattern || localize("(empty pattern)"))
72+
: localize("Cannot search: HPCC Platform not connected"),
3173
confirmationMessages: connected ? undefined : {
3274
title: localize("HPCC Platform not connected"),
3375
message: new vscode.MarkdownString(localize("This tool requires an active HPCC connection.")),

src/ecl/lm/tools/syntaxCheck.ts

Lines changed: 86 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,113 @@
11
import * as vscode from "vscode";
22
import * as os from "os";
3-
import { sessionManager } from "../../../hpccplatform/session";
3+
import { isPlatformConnected } from "../../../hpccplatform/session";
44
import { reporter } from "../../../telemetry";
55
import localize from "../../../util/localize";
6+
import { logToolEvent, requireConnectedSession, throwIfCancellationRequested } from "../utils/index";
67

78
export interface ISyntaxCheckParameters {
89
ecl: string;
910
}
1011

1112
export class SyntaxCheckTool implements vscode.LanguageModelTool<ISyntaxCheckParameters> {
12-
async invoke(options: vscode.LanguageModelToolInvocationOptions<ISyntaxCheckParameters>, _token: vscode.CancellationToken) {
13+
async invoke(options: vscode.LanguageModelToolInvocationOptions<ISyntaxCheckParameters>, token: vscode.CancellationToken) {
1314
reporter?.sendTelemetryEvent("lmTool.invoke", { tool: "syntaxCheck" });
1415
const params = options.input;
15-
if (typeof params.ecl === "string") {
16-
const tmpFileName = `ecl_syntax_check_${Date.now()}.ecl`;
17-
const tmpUri = vscode.Uri.joinPath(vscode.Uri.file(os.tmpdir()), tmpFileName);
18-
try {
19-
const eclContent = new TextEncoder().encode(params.ecl);
20-
await vscode.workspace.fs.writeFile(tmpUri, eclContent);
21-
const result = await sessionManager.session?.checkSyntax(tmpUri);
22-
return new vscode.LanguageModelToolResult([
23-
new vscode.LanguageModelTextPart(result ? localize("ECL syntax is valid.") : localize("ECL syntax is invalid."))
24-
]);
25-
} catch (error) {
26-
return new vscode.LanguageModelToolResult([
27-
new vscode.LanguageModelTextPart(`${localize("Error checking syntax:")}: ${error}`)
28-
]);
29-
} finally {
30-
try {
31-
await vscode.workspace.fs.delete(tmpUri);
32-
} catch (e) {
33-
// ignore
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));
3465
}
3566
}
36-
} else {
37-
return new vscode.LanguageModelToolResult([
38-
new vscode.LanguageModelTextPart(localize("Invalid input: ECL code must be a string."))
39-
]);
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+
}
4090
}
4191
}
4292

4393
async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<ISyntaxCheckParameters>, _token: vscode.CancellationToken) {
44-
const confirmationMessages = {
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 ? {
4598
title: localize("Check ECL Syntax"),
4699
message: new vscode.MarkdownString(
47-
localize("Check the syntax of ECL code?") + (options.input.ecl !== undefined ? ` ${localize("in ECL code")} ${options.input.ecl}` : "")
100+
localize("Check the syntax of ECL code?") + eclPreview
48101
),
102+
} : {
103+
title: localize("HPCC Platform not connected"),
104+
message: new vscode.MarkdownString(localize("This tool requires an active HPCC connection.")),
49105
};
50106

51107
return {
52-
invocationMessage: localize("Checking ECL syntax"),
108+
invocationMessage: connected
109+
? localize("Checking ECL syntax")
110+
: localize("Cannot check syntax: HPCC Platform not connected"),
53111
confirmationMessages,
54112
};
55113
}

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";

src/ecl/lm/utils/logger.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as vscode from "vscode";
2+
3+
let outputChannel: vscode.OutputChannel | undefined;
4+
5+
function channel(): vscode.OutputChannel {
6+
if (!outputChannel) {
7+
outputChannel = vscode.window.createOutputChannel("ECL LM Tools", { log: true });
8+
}
9+
return outputChannel;
10+
}
11+
12+
export function logToolEvent(tool: string, message: string, details: Record<string, unknown> = {}): void {
13+
const timestamp = new Date().toISOString();
14+
let serialized = "";
15+
if (details && Object.keys(details).length > 0) {
16+
try {
17+
serialized = ` ${JSON.stringify(details)}`;
18+
} catch {
19+
serialized = " {\"error\":\"Unable to serialize details\"}";
20+
}
21+
}
22+
channel().appendLine(`[${timestamp}] [${tool}] ${message}${serialized}`);
23+
}

src/ecl/lm/utils/session.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as vscode from "vscode";
2+
import { isPlatformConnected, sessionManager } from "../../../hpccplatform/session";
3+
import localize from "../../../util/localize";
4+
5+
export function requireConnectedSession(): NonNullable<typeof sessionManager.session> {
6+
if (!isPlatformConnected()) {
7+
throw new vscode.LanguageModelError(localize("HPCC Platform not connected"), { cause: "not_connected" });
8+
}
9+
10+
const session = sessionManager.session;
11+
if (!session) {
12+
throw new vscode.LanguageModelError(localize("No active session configuration"), { cause: "not_connected" });
13+
}
14+
15+
return session;
16+
}
17+
18+
export async function createServiceOptions(session?: NonNullable<typeof sessionManager.session>) {
19+
const activeSession = session ?? requireConnectedSession();
20+
return activeSession.options();
21+
}
22+
23+
export function throwIfCancellationRequested(token: vscode.CancellationToken): void {
24+
if (token.isCancellationRequested) {
25+
throw new vscode.LanguageModelError(localize("Operation cancelled"), { cause: "cancelled" });
26+
}
27+
}

0 commit comments

Comments
 (0)