Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GenAIScript task provider and CLI configuration in VSCode extension #709

Merged
merged 8 commits into from
Sep 13, 2024
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"promptdom",
"promptfoo",
"prompty",
"quoteify",
"stringifying",
"sysr",
"tabletojson",
Expand Down
18 changes: 18 additions & 0 deletions docs/src/content/docs/getting-started/running-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,28 @@ Use the [run](/genaiscript/reference/cli/run) command to execute a script from t
npx genaiscript run proofreader path/to/files*.md
```

## Tasks

The GenAIScript extension exposes each script as a [Task](https://code.visualstudio.com/docs/editor/tasks) automatically.

The task launches the [cli](/genaiscript/reference/cli) and runs the selected script and pass the path to the current opened editor.

- Open the command palette `Ctrl+Shift+P` and search "Tasks: Run Task"
- Select the `genaiscript` task provider
- Select the script you want to run

:::note

When running a script as a task, the result will not be visible in the GenAIScript.

:::

## Analyze results

By default, GenAIScript opens the output preview which shows a rendered view of the LLM output (assuming the LLM produces markdown).

The GenAIScript view provides an overview of the trace of the latest run.

You can also use the **Trace** to review the each transformation step of the script execution.

- Click on the GenAIScript status bar icon and select **Trace**
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/nodehost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
ResponseStatus,
} from "../../core/src/host"
import { AbortSignalOptions, TraceOptions } from "../../core/src/trace"
import { logVerbose, unique } from "../../core/src/util"
import { logVerbose, quoteify, unique } from "../../core/src/util"
import { parseModelIdentifier } from "../../core/src/models"
import {
AuthenticationToken,
Expand Down Expand Up @@ -300,7 +300,6 @@ export class NodeHost implements RuntimeHost {
if (command === "python" && process.platform !== "win32")
command = "python3"

const quoteify = (a: string) => (/\s/.test(a) ? `"${a}"` : a)
logVerbose(
`${cwd ? `${cwd}> ` : ""}${quoteify(command)} ${args.map(quoteify).join(" ")}`
)
pelikhan marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
19 changes: 18 additions & 1 deletion packages/core/src/annotations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { parseAnnotations } from "./annotations"
import assert from "assert/strict"

describe("annotations", () => {
test("error", () => {
test("github", () => {
const output = `
::error file=packages/core/src/github.ts,line=71,endLine=71,code=concatenation_override::The change on line 71 may lead to the original \`text\` content being overridden instead of appending the footer. Consider using \`text = appendGeneratedComment(script, info, text)\` to ensure the original text is preserved and the footer is appended. 😇

Expand All @@ -25,4 +25,21 @@ describe("annotations", () => {
"The change on line 71 may lead to the original `text` content being overridden instead of appending the footer. Consider using `text = appendGeneratedComment(script, info, text)` to ensure the original text is preserved and the footer is appended. 😇"
)
})

test("tsc", () => {
const output = `

src/annotations.ts:11:28 - error TS1005: ',' expected.
`

const diags = parseAnnotations(output)
// console.log(diags)
assert.strictEqual(diags.length, 1)
assert.strictEqual(diags[0].severity, "error")
assert.strictEqual(diags[0].filename, "src/annotations.ts")
assert.strictEqual(diags[0].range[0][0], 10)
assert.strictEqual(diags[0].range[1][0], 27)
assert.strictEqual(diags[0].code, "TS1005")
assert.strictEqual(diags[0].message, "',' expected.")
})
})
65 changes: 27 additions & 38 deletions packages/core/src/annotations.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-error-message
const GITHUB_ANNOTATIONS_RX =
/^\s*::(?<severity>notice|warning|error)\s*file=(?<file>[^,]+),\s*line=(?<line>\d+),\s*endLine=(?<endLine>\d+)\s*(,\s*code=(?<code>[^,:]+)?\s*)?::(?<message>.*)$/gim
// ##vso[task.logissue type=warning;sourcepath=consoleap
Expand All @@ -6,55 +7,43 @@ const GITHUB_ANNOTATIONS_RX =
const AZURE_DEVOPS_ANNOTATIONS_RX =
/^\s*##vso\[task.logissue\s+type=(?<severity>error|warning);sourcepath=(?<file>);linenumber=(?<line>\d+)(;code=(?<code>\d+);)?[^\]]*\](?<message>.*)$/gim

// https://code.visualstudio.com/docs/editor/tasks#_background-watching-tasks
const TYPESCRIPT_ANNOTATIONS_RX =
/^(?<file>[^:\s].*?):(?<line>\d+)(?::(?<endLine>\d+))?(?::\d+)?\s+-\s+(?<severity>error|warning)\s+(?<code>[^:]+)\s*:\s*(?<message>.*)$/gim

/**
* Matches ::(notice|warning|error) file=<filename>,line=<start line>::<message>
* Matches TypeScript, GitHub Actions and Azure DevOps annotations
* @param line
* @link https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-error-message
*/
export function parseAnnotations(text: string): Diagnostic[] {
if (!text) return []
const sevMap: Record<string, DiagnosticSeverity> = {
["info"]: "info",
["notice"]: "info",
["warning"]: "warning",
["error"]: "error",
}
const annotations: Record<string, Diagnostic> = {}
text.replace(
GITHUB_ANNOTATIONS_RX,
(_, severity, file, line, endLine, __, code, message) => {
const annotation: Diagnostic = {
severity: sevMap[severity] || severity,
filename: file,
range: [
[parseInt(line) - 1, 0],
[parseInt(endLine) - 1, Number.MAX_VALUE],
],
message,
code,
}
const key = JSON.stringify(annotation)
annotations[key] = annotation
return ""
}
)
text?.replace(
AZURE_DEVOPS_ANNOTATIONS_RX,
(_, severity, file, line, __, code, message) => {
const annotation: Diagnostic = {
severity: sevMap[severity] || severity,
filename: file,
range: [
[parseInt(line) - 1, 0],
[parseInt(line) - 1, Number.MAX_VALUE],
],
message,
code,
}
const key = JSON.stringify(annotation)
annotations[key] = annotation
return ""
const addAnnotation = (m: RegExpMatchArray) => {
const { file, line, endLine, severity, code, message } = m.groups
const annotation: Diagnostic = {
severity: sevMap[severity] ?? "info",
filename: file,
range: [
[parseInt(line) - 1, 0],
[parseInt(endLine) - 1, Number.MAX_VALUE],
],
message,
code,
}
)
return Object.values(annotations)
annotations.add(annotation)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no error handling for the case when the severity is not found in the sevMap. This could lead to undefined behavior if an unexpected severity value is encountered. Consider adding error handling or a default case. 😊

generated by pr-review-commit missing_error_handling


const annotations = new Set<Diagnostic>()
for (const m of text.matchAll(TYPESCRIPT_ANNOTATIONS_RX)) addAnnotation(m)
for (const m of text.matchAll(GITHUB_ANNOTATIONS_RX)) addAnnotation(m)
for (const m of text.matchAll(AZURE_DEVOPS_ANNOTATIONS_RX)) addAnnotation(m)
return Array.from(annotations.values())
}

export function convertDiagnosticToGitHubActionCommand(d: Diagnostic) {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ export function logError(msg: string | Error | SerializedError) {
if (!/^\s*\{\s*\}\s*$/.test(se)) host.log(LogLevel.Verbose, se)
}
}
export function quoteify(a: string) {
return /\s/.test(a) ? `"${a}"` : a
}

export function concatArrays<T>(...arrays: T[][]): T[] {
if (arrays.length == 0) return []
return arrays[0].concat(...arrays.slice(1))
Expand Down
14 changes: 14 additions & 0 deletions packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@
},
"contributes": {
"markdown.markdownItPlugins": true,
"taskDefinitions": [
{
"type": "genaiscript",
"required": [
"script"
],
"properties": {
"script": {
"type": "string",
"description": "GenAIScript script to run"
}
}
}
],
"walkthroughs": [
{
"id": "genaiscript.tutorial",
Expand Down
22 changes: 22 additions & 0 deletions packages/vscode/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as vscode from "vscode"
import {
TOOL_ID,
VSCODE_CONFIG_CLI_PATH,
VSCODE_CONFIG_CLI_VERSION,
} from "../../core/src/constants"
import { CORE_VERSION } from "../../core/src/version"
import { semverParse, semverSatisfies } from "../../core/src/semver"

export async function resolveCli() {
const config = vscode.workspace.getConfiguration(TOOL_ID)
const cliPath = config.get(VSCODE_CONFIG_CLI_PATH) as string
const cliVersion =
(config.get(VSCODE_CONFIG_CLI_VERSION) as string) || CORE_VERSION
const gv = semverParse(CORE_VERSION)
if (!semverSatisfies(cliVersion, ">=" + gv.major + "." + gv.minor))
vscode.window.showWarningMessage(
TOOL_ID +
` - genaiscript cli version (${cliVersion}) outdated, please update to ${CORE_VERSION}`
)
pelikhan marked this conversation as resolved.
Show resolved Hide resolved
return { cliPath, cliVersion }
}
2 changes: 2 additions & 0 deletions packages/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { activateConnectionInfoTree } from "./connectioninfotree"
import { updateConnectionConfiguration } from "../../core/src/connection"
import { APIType } from "../../core/src/host"
import { openUrlInTab } from "./browser"
import { activeTaskProvider } from "./taskprovider"

export async function activate(context: ExtensionContext) {
const state = new ExtensionState(context)
Expand All @@ -36,6 +37,7 @@ export async function activate(context: ExtensionContext) {
activateTraceTreeDataProvider(state)
activateStatusBar(state)
activateDocsNotebook(state)
activeTaskProvider(state)

context.subscriptions.push(
registerCommand(
Expand Down
27 changes: 5 additions & 22 deletions packages/vscode/src/fragmentcommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,12 @@ import { ExtensionState } from "./state"
import { checkDirectoryExists, checkFileExists } from "./fs"
import { registerCommand } from "./commands"
import { templateGroup } from "../../core/src/ast"
import {
GENAI_ANY_REGEX,
TOOL_ID,
TOOL_NAME,
VSCODE_CONFIG_CLI_PATH,
VSCODE_CONFIG_CLI_VERSION,
} from "../../core/src/constants"
import { GENAI_ANY_REGEX, TOOL_ID, TOOL_NAME } from "../../core/src/constants"
import { NotSupportedError } from "../../core/src/error"
import { promptParameterTypeToJSONSchema } from "../../core/src/parameters"
import { Fragment } from "../../core/src/generation"
import { assert, dotGenaiscriptPath, groupBy } from "../../core/src/util"
import { CORE_VERSION } from "../../core/src/version"
import { semverParse, semverSatisfies } from "../../core/src/semver"
import { resolveCli } from "./config"

type TemplateQuickPickItem = {
template?: PromptScript
Expand Down Expand Up @@ -188,28 +181,18 @@ export function activateFragmentCommands(state: ExtensionState) {
files = [file]
}

const config = vscode.workspace.getConfiguration(TOOL_ID)
const program = config.get(VSCODE_CONFIG_CLI_PATH) as string
const { cliPath, cliVersion } = await resolveCli()
pelikhan marked this conversation as resolved.
Show resolved Hide resolved
const args = [
"run",
vscode.workspace.asRelativePath(template.filename),
...files.map((file) =>
vscode.workspace.asRelativePath(file.fsPath)
),
]
const cliVersion =
(config.get(VSCODE_CONFIG_CLI_VERSION) as string) || CORE_VERSION
const gv = semverParse(CORE_VERSION)
if (!semverSatisfies(cliVersion, ">=" + gv.major + "." + gv.minor))
vscode.window.showWarningMessage(
TOOL_ID +
` - genaiscript cli version (${cliVersion}) outdated, please update to ${CORE_VERSION}`
)

const configuration = program
const configuration = cliPath
? <vscode.DebugConfiguration>{
name: TOOL_NAME,
program,
cliPath,
request: "launch",
skipFiles: ["<node_internals>/**", dotGenaiscriptPath("**")],
type: "node",
Expand Down
14 changes: 3 additions & 11 deletions packages/vscode/src/servermanager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@ import {
OPEN,
TOOL_NAME,
ICON_LOGO_NAME,
CLIENT_RECONNECT_MAX_ATTEMPTS,
TOOL_ID,
VSCODE_CONFIG_CLI_VERSION,
VSCODE_CONFIG_CLI_PATH,
} from "../../core/src/constants"
import { ServerManager, host } from "../../core/src/host"
import { logError, logVerbose } from "../../core/src/util"
import { WebSocketClient } from "../../core/src/server/client"
import { CORE_VERSION } from "../../core/src/version"
import { createChatModelRunner } from "./lmaccess"
import { semverParse, semverSatisfies } from "../../core/src/semver"
import { resolveCli } from "./config"

export class TerminalServerManager implements ServerManager {
private _terminal: vscode.Terminal
Expand Down Expand Up @@ -86,15 +84,9 @@ export class TerminalServerManager implements ServerManager {
isTransient: true,
iconPath: new vscode.ThemeIcon(ICON_LOGO_NAME),
})
const config = vscode.workspace.getConfiguration(TOOL_ID)
const cliPath = config.get(VSCODE_CONFIG_CLI_PATH) as string
const { cliPath, cliVersion } = await resolveCli()
if (cliPath) this._terminal.sendText(`node "${cliPath}" serve`)
else {
const cliVersion =
(config.get(VSCODE_CONFIG_CLI_VERSION) as string) ||
CORE_VERSION
this._terminal.sendText(`npx --yes ${TOOL_ID}@${cliVersion} serve`)
}
else this._terminal.sendText(`npx --yes ${TOOL_ID}@${cliVersion} serve`)
pelikhan marked this conversation as resolved.
Show resolved Hide resolved
this._terminal.show()
}

Expand Down
52 changes: 52 additions & 0 deletions packages/vscode/src/taskprovider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as vscode from "vscode"
import { ExtensionState } from "./state"
import { resolveCli } from "./config"
import { TOOL_ID } from "../../core/src/constants"
import { quoteify } from "../../core/src/util"

export async function activeTaskProvider(state: ExtensionState) {
const { context, host } = state
const { subscriptions } = context

const taskProvider: vscode.TaskProvider = {
provideTasks: async () => {
const { cliPath, cliVersion } = await resolveCli()
pelikhan marked this conversation as resolved.
Show resolved Hide resolved
const exec = cliPath
? quoteify(cliPath)
: `npx --yes genaiscript@${cliVersion}`
const scripts = state.project.templates.filter((t) => !t.isSystem)
const tasks = scripts.map((script) => {
const scriptp = host.path.relative(
host.projectFolder(),
script.filename
)
const task = new vscode.Task(
{ type: TOOL_ID, script: script.filename },
vscode.TaskScope.Workspace,
script.id,
TOOL_ID,
new vscode.ShellExecution(exec, [
"run",
scriptp,
"${relativeFile}",
])
)
pelikhan marked this conversation as resolved.
Show resolved Hide resolved
task.detail = `${script.title ?? script.description} - ${scriptp}`
task.problemMatchers = ["$tsc"]
task.presentationOptions = {
echo: true,
focus: true,
showReuseMessage: false,
clear: true,
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no error handling for the case when the host.path.relative function fails or returns an unexpected result. This could lead to unexpected behavior if the relative path cannot be correctly determined. Consider adding error handling or validation to ensure the relative path can be correctly determined. 😊

generated by pr-review-commit missing_error_handling

return task
})
return tasks
},
async resolveTask(task): Promise<vscode.Task> {
return task
},
}

subscriptions.push(vscode.tasks.registerTaskProvider(TOOL_ID, taskProvider))
}
Loading