Skip to content

Commit 8c0b18f

Browse files
authored
Support for direct call of genaiscript from js (#925)
* worker plugin * adding small test * refactor: update script execution and logging process 🔧 * refactor: update script exports & tsconfig settings ✨ * use yaml api * use api in test harness * feat: ✨ add Node.JS API support for CLI * refactor: change imports to use type-only imports ✨ * refactor: 🔄 rename runScript to run across codebase * simplify signature * feat: 🎉 support env vars for worker threads * refactor: update model and clean script comments ✨ * override small model too * ci: 🔧 update command to run summarize-cached script
1 parent d40b712 commit 8c0b18f

File tree

16 files changed

+196
-106
lines changed

16 files changed

+196
-106
lines changed

.github/workflows/anthropic.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@ jobs:
3333
env:
3434
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
3535
- name: cache
36-
run: yarn test:scripts summarize-cached --model anthropic:claude-3-5-sonnet-20240620
36+
run: yarn run:script summarize-cached --model anthropic:claude-3-5-sonnet-20240620 --small-model anthropic:claude-3-5-sonnet-20240620
3737
env:
3838
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
title: Node.JS API
3+
sidebar:
4+
order: 50
5+
---
6+
7+
import { Tabs, TabItem } from "@astrojs/starlight/components"
8+
9+
This page describes how to import and use the [cli](/genaiscript/reference/cli) as an API in your Node.JS application.
10+
11+
Assuming you have have added the cli as a dependency in your project, you can import the cli as follows:
12+
13+
<Tabs>
14+
<TabItem value="npm" label="npm">
15+
16+
```sh
17+
npm install -D genaiscript
18+
```
19+
20+
</TabItem>
21+
22+
<TabItem value="yarn" label="yarn">
23+
24+
```sh
25+
yarn add -D genaiscript
26+
```
27+
28+
</TabItem>
29+
30+
</Tabs>
31+
32+
The API can be imported using imports from **"genaiscript/api"**.
33+
34+
```javascript
35+
import { runScript } from "genaiscript/api"
36+
```
37+
38+
The imported `api.mjs` wrapper is a tiny, zero dependency loader that
39+
spawns a [Node.JS worker thread](https://nodejs.org/api/worker_threads.html) to run GenAIScript.
40+
41+
- No pollutation of the globals
42+
- No side effects on the process
43+
44+
## `run`
45+
46+
The `run` function wraps the [cli run](/genaiscript/reference/cli/run) command.
47+
48+
```javascript
49+
import { run } from "genaiscript/api"
50+
51+
const results = await run("summarize", ["myfile.txt"])
52+
```
53+
54+
### Environment variables
55+
56+
You can set the environment variables for the GenAIScript process by passing an object as the `env` field in the options. By default, the worker will inherit `process.env`.
57+
58+
```javascript
59+
const results = await run("summarize", ["myfile.txt"], {
60+
env: {
61+
MY_ENV_VAR: "value",
62+
},
63+
})
64+
```

docs/src/content/docs/reference/cli/index.mdx

+10-6
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ where `--yes` skips the confirmation prompt to install the package.
2323

2424
The CLI is a Node.JS package hosted on [npm](https://www.npmjs.com/package/genaiscript).
2525

26-
- Install [Node.JS LTS](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) (Node.JS includes npm and npx).
26+
- Install [Node.JS LTS](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) (Node.JS includes npm and npx).
2727

2828
## Installation
2929

30-
- Install locally as a `devDependency` in your project.
30+
- Install locally as a `devDependency` in your project.
3131

3232
<Tabs>
3333
<TabItem label="npm" icon="seti:npm">
@@ -46,13 +46,13 @@ yarn add -D genaiscript
4646
</TabItem>
4747
</Tabs>
4848

49-
- Install it globally.
49+
- Install it globally.
5050

5151
```sh "-g"
5252
npm install -g genaiscript
5353
```
5454

55-
- Check that your node version is at least 20._ and npm 10._ by running this command.
55+
- Check that your node version is at least 20._ and npm 10._ by running this command.
5656

5757
```sh
5858
node -v
@@ -77,13 +77,13 @@ system issues where the tool is not found in the path.
7777
npx genaiscript ...
7878
```
7979

80-
- Add `--yes` to skip the confirmation prompt, which is useful in a CI scenario.
80+
- Add `--yes` to skip the confirmation prompt, which is useful in a CI scenario.
8181

8282
```sh "--yes"
8383
npx --yes genaiscript ...
8484
```
8585

86-
- Specify the version range to avoid unexpected behavior with cached installations of the CLI using npx.
86+
- Specify the version range to avoid unexpected behavior with cached installations of the CLI using npx.
8787

8888
```sh "@^1.16.0"
8989
npx --yes genaiscript@^1.16.0 ...
@@ -132,6 +132,10 @@ npx genaiscript scripts model [script]
132132

133133
where [script] can be a script id or a file path.
134134

135+
## Using a the CLI as a Node.JS API
136+
137+
The CLI can be imported and [used as an API in your Node.JS application](/genaiscript/reference/cli/api).
138+
135139
## Topics
136140

137141
<DirectoryLinks directory="reference/cli" />

packages/cli/package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
"bin": {
77
"genaiscript": "built/genaiscript.cjs"
88
},
9+
"exports": {
10+
".": "./built/genaiscript.cjs",
11+
"./package.json": "./package.json"
12+
},
913
"files": [
14+
"built/api.mjs",
1015
"built/genaiscript.cjs"
1116
],
1217
"publisher": "Microsoft",
@@ -98,7 +103,7 @@
98103
"zx": "^8.2.4"
99104
},
100105
"scripts": {
101-
"compile": "esbuild src/main.ts --metafile=./esbuild.meta.json --bundle --platform=node --target=node20 --outfile=built/genaiscript.cjs --external:tsx --external:esbuild --external:get-tsconfig --external:resolve-pkg-maps --external:dockerode --external:pdfjs-dist --external:web-tree-sitter --external:tree-sitter-wasms --external:promptfoo --external:typescript --external:@lvce-editor/ripgrep --external:gpt-3-encoder --external:mammoth --external:xlsx --external:mathjs --external:@azure/identity --external:gpt-tokenizer --external:playwright --external:@inquirer/prompts --external:jimp --external:turndown --external:turndown-plugin-gfm --external:vectra --external:tabletojson --external:html-to-text --external:@octokit/rest --external:@octokit/plugin-throttling --external:@octokit/plugin-retry --external:@octokit/plugin-paginate-rest --external:skia-canvas --external:@huggingface/transformers --external:@modelcontextprotocol/sdk --external:@anthropic-ai/sdk && node ../../scripts/patch-cli.mjs",
106+
"compile": "esbuild src/api.ts --outfile=built/api.mjs && esbuild src/main.ts --metafile=./esbuild.meta.json --bundle --platform=node --target=node20 --outfile=built/genaiscript.cjs --external:tsx --external:esbuild --external:get-tsconfig --external:resolve-pkg-maps --external:dockerode --external:pdfjs-dist --external:web-tree-sitter --external:tree-sitter-wasms --external:promptfoo --external:typescript --external:@lvce-editor/ripgrep --external:gpt-3-encoder --external:mammoth --external:xlsx --external:mathjs --external:@azure/identity --external:gpt-tokenizer --external:playwright --external:@inquirer/prompts --external:jimp --external:turndown --external:turndown-plugin-gfm --external:vectra --external:tabletojson --external:html-to-text --external:@octokit/rest --external:@octokit/plugin-throttling --external:@octokit/plugin-retry --external:@octokit/plugin-paginate-rest --external:skia-canvas --external:@huggingface/transformers --external:@modelcontextprotocol/sdk --external:@anthropic-ai/sdk && node ../../scripts/patch-cli.mjs",
102107
"compile-debug": "esbuild src/main.ts --sourcemap --metafile=./esbuild.meta.json --bundle --platform=node --target=node20 --outfile=built/genaiscript.cjs --external:tsx --external:esbuild --external:get-tsconfig --external:resolve-pkg-maps --external:dockerode --external:pdfjs-dist --external:web-tree-sitter --external:tree-sitter-wasms --external:promptfoo --external:typescript --external:@lvce-editor/ripgrep --external:gpt-3-encoder --external:mammoth --external:xlsx --external:mathjs --external:@azure/identity --external:gpt-tokenizer --external:playwright --external:@inquirer/prompts --external:jimp --external:turndown --external:turndown-plugin-gfm --external:vectra --external:tabletojson --external:html-to-text --external:@octokit/rest --external:@octokit/plugin-throttling --external:@octokit/plugin-retry --external:@octokit/plugin-paginate-rest --external:skia-canvas --external:@huggingface/transformers --external:@modelcontextprotocol/sdk --external:@anthropic-ai/sdk",
103108
"postcompile": "node built/genaiscript.cjs info help > ../../docs/src/content/docs/reference/cli/commands.md",
104109
"vis:treemap": "npx --yes esbuild-visualizer --metadata esbuild.meta.json --filename esbuild.treemap.html",

packages/cli/src/api.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { GenerationResult } from "../../core/src/generation"
2+
import type { PromptScriptRunOptions } from "../../core/src/server/messages"
3+
import { Worker } from "node:worker_threads"
4+
import { fileURLToPath } from "url"
5+
import { dirname, join } from "node:path"
6+
7+
/**
8+
* Runs a GenAIScript script with the given files and options.
9+
* This function acts similarly to the `run` command in the CLI.
10+
* @param scriptId script identifier or full file path
11+
* @param files list of file paths to run the script on, leave empty if not needed
12+
* @param options
13+
* @returns
14+
*/
15+
export async function run(
16+
scriptId: string,
17+
files?: string[],
18+
options?: Partial<PromptScriptRunOptions> & {
19+
env?: Record<string, string>
20+
}
21+
): Promise<{
22+
exitCode: number
23+
result?: GenerationResult
24+
}> {
25+
const { env, ...rest } = options || {}
26+
const workerData = {
27+
type: "run",
28+
scriptId,
29+
files: files || [],
30+
options: rest,
31+
}
32+
33+
const filename =
34+
typeof __filename === "undefined"
35+
? join(dirname(fileURLToPath(import.meta.url)), "genaiscript.cjs") // ignore esbuild warning
36+
: __filename
37+
const worker = new Worker(filename, { workerData, name: options?.label })
38+
return new Promise((resolve, reject) => {
39+
worker.on("online", () => process.stderr.write(`worker: online\n`))
40+
worker.on("message", resolve)
41+
worker.on("error", reject)
42+
})
43+
}

packages/cli/src/main.ts

+20-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
1-
// Main entry point for the CLI application
2-
3-
// Import necessary modules and functions
41
import { installGlobals } from "../../core/src/globals"
52
import { cli } from "./cli"
3+
import { workerData } from "node:worker_threads"
4+
import { worker } from "./worker"
5+
import { run } from "./api"
6+
import { PromptScriptRunOptions } from "../../core/src/server/messages"
7+
import { GenerationResult } from "../../core/src/generation"
68

7-
// Initialize global settings or variables for the application
8-
// This might include setting up global error handlers or configurations
9-
installGlobals()
9+
export { run, type PromptScriptRunOptions, type GenerationResult }
1010

11-
// Execute the command-line interface logic
12-
// This function likely handles parsing input arguments and executing commands
13-
cli()
11+
// if this file is not the entry point, skip cli
12+
if (require.main === module) {
13+
// Initialize global settings or variables for the application
14+
// This might include setting up global error handlers or configurations
15+
installGlobals()
16+
if (workerData) {
17+
// Executes a worker
18+
worker()
19+
} else {
20+
// Execute the command-line interface logic
21+
// This function likely handles parsing input arguments and executing commands
22+
cli()
23+
}
24+
}

packages/cli/src/parse.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export async function parseAnyToJSON(
146146
else if (YAML_REGEX.test(file)) data = YAMLParse(src)
147147
else if (XML_REGEX.test(file)) data = XMLParse(src)
148148
else if (MD_REGEX.test(file) || MDX_REGEX.test(file))
149-
data = YAML.parse(splitMarkdown(src).frontmatter)
149+
data = YAMLParse(splitMarkdown(src).frontmatter)
150150
else throw new Error("Unsupported file format")
151151
}
152152

packages/cli/src/run.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export async function runScriptWithExitCode(
143143
host.path.basename(scriptId).replace(GENAI_ANYTS_REGEX, ""),
144144
`${new Date().toISOString().replace(/[:.]/g, "-")}.trace.md`
145145
)
146-
const res = await runScript(scriptId, files, { ...options, outTrace })
146+
const res = await runScriptInternal(scriptId, files, { ...options, outTrace })
147147
exitCode = res.exitCode
148148
if (
149149
exitCode === SUCCESS_ERROR_CODE ||
@@ -162,7 +162,7 @@ export async function runScriptWithExitCode(
162162
process.exit(exitCode)
163163
}
164164

165-
export async function runScript(
165+
export async function runScriptInternal(
166166
scriptId: string,
167167
files: string[],
168168
options: Partial<PromptScriptRunOptions> &

packages/cli/src/server.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { WebSocketServer } from "ws"
22
import { runPromptScriptTests } from "./test"
33
import { PROMPTFOO_VERSION } from "./version"
4-
import { runScript } from "./run"
4+
import { runScriptInternal } from "./run"
55
import { AbortSignalCancellationController } from "../../core/src/cancellation"
66
import {
77
SERVER_PORT,
@@ -304,7 +304,7 @@ export async function startServer(options: { port: string; apiKey?: string }) {
304304
)
305305
})
306306
logVerbose(`run ${runId}: starting ${script}`)
307-
const runner = runScript(script, files, {
307+
const runner = runScriptInternal(script, files, {
308308
...options,
309309
trace,
310310
cancellationToken: canceller.token,

packages/cli/src/tsconfig.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
"compilerOptions": {
44
"types": ["node"],
55
"declarationDir": "../built/types",
6-
"outDir": "../built"
6+
"outDir": "../built",
7+
"skipLibCheck": true,
8+
"emitDeclarationOnly": true
79
},
810
"include": [".", "../../core/src/types/*.d.ts"]
911
}

packages/cli/src/worker.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { workerData, parentPort } from "node:worker_threads"
2+
import { runScriptInternal } from "./run"
3+
import { NodeHost } from "./nodehost"
4+
5+
export async function worker() {
6+
await NodeHost.install(undefined) // Install NodeHost with environment options
7+
8+
const { type, ...data } = workerData as {
9+
type: string
10+
} & object
11+
switch (type) {
12+
case "run": {
13+
const { scriptId, files, ...options } = data as {
14+
scriptId: string
15+
files: string[]
16+
} & object
17+
const { result } = await runScriptInternal(scriptId, files, options)
18+
parentPort.postMessage(result)
19+
break
20+
}
21+
}
22+
}

packages/core/src/constants.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export const DEFAULT_SMALL_MODEL_CANDIDATES = [
8181
"azure_serverless:gpt-4o-mini",
8282
DEFAULT_SMALL_MODEL,
8383
"google:gemini-1.5-flash-002",
84-
"anthropic:claude-instant-1",
84+
"anthropic:claude-instant-1.2",
8585
"mistral:mistral-small-latest",
8686
"github:gpt-4o-mini",
8787
"client:gpt-4-mini",

0 commit comments

Comments
 (0)