Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 59c661f

Browse files
committedMay 7, 2024··
feat: add client and ability to make state reactive
1 parent cc56263 commit 59c661f

File tree

2 files changed

+214
-161
lines changed

2 files changed

+214
-161
lines changed
 

Diff for: ‎rollup.config.js

+15-15
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ import commonjs from 'rollup-plugin-commonjs';
33
import typescript from '@rollup/plugin-typescript';
44

55
export default [{
6-
input: 'dist/gptscript.js',
7-
output: {
8-
name: "GPTScript",
9-
file: "dist/gptscript.browser.js",
10-
format: 'iife',
11-
sourcemap: true,
12-
},
13-
external: [
14-
'net','http','path','child_process','sse.js',
15-
],
16-
plugins: [
17-
typescript(),
18-
commonjs(),
19-
resolve(),
20-
],
6+
input: 'dist/gptscript.js',
7+
output: {
8+
name: "GPTScript",
9+
file: "dist/gptscript.browser.js",
10+
format: 'iife',
11+
sourcemap: true,
12+
},
13+
external: [
14+
'net', 'http', 'path', 'child_process', 'sse.js',
15+
],
16+
plugins: [
17+
typescript(),
18+
commonjs(),
19+
resolve({preferBuiltins: true}),
20+
],
2121
}];

Diff for: ‎src/gptscript.ts

+199-146
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import type {SSE} from "sse.js"
33

44
export interface RunOpts {
5-
gptscriptURL?: string
65
input?: string
76
cacheDir?: string
87
disableCache?: boolean
@@ -40,15 +39,151 @@ export enum RunEventType {
4039
CallFinish = "callFinish",
4140
}
4241

42+
export class Client {
43+
public readonly gptscriptURL?: string
44+
public gptscriptBin?: string
45+
public readonly key?: string
46+
public objectFactory: (obj: any) => any
47+
48+
constructor(gptscriptURL?: string, gptscriptBin?: string, key?: string, objectFactory?: (obj: any) => any) {
49+
this.gptscriptURL = gptscriptURL
50+
this.gptscriptBin = gptscriptBin
51+
this.key = key
52+
53+
if (objectFactory) {
54+
this.objectFactory = objectFactory
55+
} else {
56+
this.objectFactory = (obj: any) => obj
57+
}
58+
59+
// if (!this.gptscriptBin) {
60+
// getCmdPath().then(
61+
// (path) => (this.gptscriptBin = path),
62+
// ).catch(err => {
63+
// console.error(`Failed to get cmd path: ${err}`)
64+
// })
65+
// }
66+
}
67+
68+
listTools(): Promise<string> {
69+
return this.runBasicCommand("list-tools")
70+
}
71+
72+
listModels(): Promise<string> {
73+
return this.runBasicCommand("list-models")
74+
}
75+
76+
version(): Promise<string> {
77+
return this.runBasicCommand("version")
78+
}
79+
80+
async runBasicCommand(cmd: string): Promise<string> {
81+
const r = new RunSubcommand(cmd, "", "", {}, this.gptscriptBin, this.gptscriptURL)
82+
if (this.gptscriptURL) {
83+
r.request(null)
84+
} else {
85+
await r.exec(["--" + cmd])
86+
}
87+
return r.text()
88+
}
89+
90+
/**
91+
* Runs a tool with the specified name and options.
92+
*
93+
* @param {string} toolName - The name of the tool to run. Can be a file path, URL, or GitHub URL.
94+
* @param {RunOpts} [opts={}] - The options for running the tool.
95+
* @return {Run} The Run object representing the running tool.
96+
*/
97+
run(toolName: string, opts: RunOpts = {}): Run {
98+
return (new Run("run-file-stream-with-events", toolName, "", opts)).nextChat(opts.input)
99+
}
100+
101+
/**
102+
* Evaluates the given tool and returns a Run object.
103+
*
104+
* @param {ToolDef | ToolDef[] | string} tool - The tool to be evaluated. Can be a single ToolDef object, an array of ToolDef objects, or a string representing the tool contents.
105+
* @param {RunOpts} [opts={}] - Optional options for the evaluation.
106+
* @return {Run} The Run object representing the evaluation.
107+
*/
108+
evaluate(tool: ToolDef | ToolDef[] | string, opts: RunOpts = {}): Run {
109+
let toolString: string = ""
110+
111+
if (Array.isArray(tool)) {
112+
toolString = toolArrayToContents(tool)
113+
} else if (typeof tool === "string") {
114+
toolString = tool
115+
} else {
116+
toolString = toolDefToString(tool)
117+
}
118+
119+
return (new Run("run-tool-stream-with-event", "", toolString, opts)).nextChat(opts.input)
120+
}
121+
122+
async parse(fileName: string, gptscriptURL?: string): Promise<Block[]> {
123+
const r: Run = new RunSubcommand("parse", fileName, "", {}, this.gptscriptBin, this.gptscriptURL)
124+
if (gptscriptURL) {
125+
r.request({file: fileName})
126+
} else {
127+
await r.exec(["parse"])
128+
}
129+
return parseBlocksFromNodes((await r.json()).nodes)
130+
}
131+
132+
async parseTool(toolContent: string, gptscriptURL?: string): Promise<Block[]> {
133+
const r: Run = new RunSubcommand("parse", "", toolContent, {}, this.gptscriptBin, this.gptscriptURL)
134+
if (gptscriptURL) {
135+
r.request({input: toolContent})
136+
} else {
137+
await r.exec(["parse"])
138+
}
139+
return parseBlocksFromNodes((await r.json()).nodes)
140+
}
141+
142+
async stringify(blocks: Block[], gptscriptURL?: string): Promise<string> {
143+
const nodes: any[] = []
144+
145+
for (const block of blocks) {
146+
if (block.type === "tool") {
147+
nodes.push({
148+
toolNode: {
149+
tool: block
150+
}
151+
})
152+
} else if (block.type === "text") {
153+
nodes.push({
154+
textNode: {
155+
text: "!" + (block.format || "text") + "\n" + block.content
156+
}
157+
})
158+
}
159+
}
160+
161+
const r: Run = new RunSubcommand("fmt", "", JSON.stringify({nodes: nodes}), {}, this.gptscriptBin, this.gptscriptURL)
162+
if (gptscriptURL) {
163+
r.request({nodes: nodes})
164+
} else {
165+
await r.exec(["fmt"])
166+
}
167+
168+
return r.text()
169+
}
170+
}
171+
43172
export class Run {
44173
public readonly id: string
45174
public readonly opts: RunOpts
46-
public state: RunState = RunState.Creating
47-
public calls: Call[] = []
48-
public err = ""
49175
public readonly filePath: string
50176
public readonly content: string
177+
public readonly reactive: { state: RunState, calls: Call[], err: string } = {
178+
state: RunState.Creating,
179+
calls: [],
180+
err: ""
181+
}
182+
51183
protected stdout?: string
184+
185+
private readonly bin?: string
186+
private readonly gptscriptURL?: string
52187
private readonly requestPath: string = ""
53188
private promise?: Promise<string>
54189
private process?: any
@@ -58,12 +193,33 @@ export class Run {
58193
private callbacks: Record<string, ((f: Frame) => void)[]> = {}
59194
private chatState: string | undefined
60195

61-
constructor(subCommand: string, path: string, content: string, opts: RunOpts) {
196+
constructor(subCommand: string, path: string, content: string, opts: RunOpts, bin?: string, gptscriptURL?: string, stateTracker?: (obj: any) => any) {
62197
this.id = randomId("run-")
63198
this.requestPath = subCommand
64199
this.opts = opts
65200
this.filePath = path
66201
this.content = content
202+
203+
if (bin) {
204+
this.bin = bin
205+
}
206+
this.gptscriptURL = gptscriptURL
207+
208+
if (stateTracker) {
209+
this.reactive = stateTracker(this.reactive)
210+
}
211+
}
212+
213+
get state(): RunState {
214+
return this.reactive.state
215+
}
216+
217+
get err(): string {
218+
return this.reactive.err
219+
}
220+
221+
get calls(): Call[] {
222+
return this.reactive.calls
67223
}
68224

69225
nextChat(input: string = ""): Run {
@@ -78,16 +234,16 @@ export class Run {
78234

79235
run.chatState = this.chatState
80236
run.opts.input = input
81-
if (run.opts.gptscriptURL) {
237+
if (run.gptscriptURL) {
82238
if (run.content !== "") {
83239
run.request({content: this.content})
84240
} else {
85241
run.request({file: this.filePath})
86242
}
87243
} else {
88244
run.exec().catch((e) => {
89-
run.state = RunState.Error
90-
run.err = e.toString()
245+
run.reactive.err = e.toString()
246+
run.reactive.state = RunState.Error
91247
}
92248
)
93249
}
@@ -146,7 +302,7 @@ export class Run {
146302

147303
const child_process = await import("child_process")
148304

149-
this.process = child_process.spawn(await getCmdPath(), extraArgs, spawnOptions as any)
305+
this.process = child_process.spawn(this.bin || await getCmdPath(), extraArgs, spawnOptions as any)
150306
if (process.platform !== "win32") {
151307
// We don't need the named pipe for streaming events.
152308
server.close()
@@ -165,8 +321,8 @@ export class Run {
165321
}
166322

167323
if (!this.process) {
168-
this.state = RunState.Error
169-
this.err = "Run failed to start"
324+
this.reactive.err = "Run failed to start"
325+
this.reactive.state = RunState.Error
170326
server.close()
171327
this.promise = Promise.reject(this.err)
172328
return
@@ -181,7 +337,7 @@ export class Run {
181337
this.process.stdin.end()
182338
}
183339

184-
this.state = RunState.Running
340+
this.reactive.state = RunState.Running
185341

186342
if (this.process.stdout) {
187343
this.process.stdout.on("data", (data: any) => {
@@ -199,17 +355,17 @@ export class Run {
199355
server.close()
200356

201357
if (signal) {
202-
this.state = RunState.Error
203-
this.err = "Run has been aborted"
358+
this.reactive.err = "Run has been aborted"
359+
this.reactive.state = RunState.Error
204360
} else if (code !== 0) {
205-
this.state = RunState.Error
206-
this.err = this.stderr || ""
361+
this.reactive.err = this.stderr || ""
362+
this.reactive.state = RunState.Error
207363
} else if (this.state !== RunState.Continue) {
208-
this.state = RunState.Finished
364+
this.reactive.state = RunState.Finished
209365
}
210366

211367
if (this.err) {
212-
this.state = RunState.Error
368+
this.reactive.state = RunState.Error
213369
reject(this.err)
214370
} else {
215371
resolve(this.stdout || "")
@@ -227,40 +383,40 @@ export class Run {
227383
try {
228384
data = JSON.parse(data)
229385
} catch (e) {
230-
this.err = `Failed to parse stdout: "${data}"`
386+
this.reactive.err = `Failed to parse stdout: "${data}"`
231387
return
232388
}
233389
}
234390

235391
const out = data as ChatState
236392
if (out.done !== undefined && !out.done) {
237393
this.chatState = out.state
238-
this.state = RunState.Continue
394+
this.reactive.state = RunState.Continue
239395
} else {
240-
this.state = RunState.Finished
396+
this.reactive.state = RunState.Finished
241397
this.chatState = undefined
242398
}
243399
}
244400

245401
request(tool: any) {
246-
if (!this.opts.gptscriptURL) {
402+
if (!this.gptscriptURL) {
247403
throw new Error("request() requires gptscriptURL to be set")
248404
}
249405
const postData = JSON.stringify({...tool, ...this.opts})
250-
const options = this.requestOptions(this.opts.gptscriptURL, this.requestPath, postData, tool)
406+
const options = this.requestOptions(this.gptscriptURL, this.requestPath, postData, tool)
251407

252408
this.promise = new Promise<string>(async (resolve, reject) => {
253409
// This checks that the code is running in a browser. If it is, then we use SSE.
254410
if (typeof window !== "undefined" && typeof window.document !== "undefined") {
255411
// @ts-ignore
256412
const {SSE} = await import("sse.js")
257-
this.sse = new SSE(this.opts.gptscriptURL + "/" + this.filePath, {
413+
this.sse = new SSE(this.gptscriptURL + "/" + this.filePath, {
258414
headers: {"Content-Type": "application/json"},
259415
payload: postData
260416
} as any)
261417

262418
this.sse.addEventListener("open", () => {
263-
this.state = RunState.Running
419+
this.reactive.state = RunState.Running
264420
})
265421

266422
this.sse.addEventListener("message", (data: any) => {
@@ -281,16 +437,16 @@ export class Run {
281437

282438
this.sse.addEventListener("close", () => {
283439
if (this.state === RunState.Running || this.state === RunState.Finished) {
284-
this.state = RunState.Finished
440+
this.reactive.state = RunState.Finished
285441
resolve(this.stdout || "")
286442
} else if (this.state === RunState.Error) {
287443
reject(this.err)
288444
}
289445
})
290446

291447
this.sse.addEventListener("error", (err: any) => {
292-
this.state = RunState.Error
293-
this.err = err
448+
this.reactive.state = RunState.Error
449+
this.reactive.err = err
294450
reject(err)
295451
})
296452
} else {
@@ -300,7 +456,7 @@ export class Run {
300456
// Use frag to keep track of partial object writes.
301457
let frag = ""
302458
this.req = http.request(options, (res: any) => {
303-
this.state = RunState.Running
459+
this.reactive.state = RunState.Running
304460
res.on("data", (chunk: any) => {
305461
for (let line of (chunk.toString() + frag).split("\n")) {
306462
const c = line.replace(/^(data: )/, "").trim()
@@ -333,7 +489,7 @@ export class Run {
333489

334490
res.on("end", () => {
335491
if (this.state === RunState.Running || this.state === RunState.Finished) {
336-
this.state = RunState.Finished
492+
this.reactive.state = RunState.Finished
337493
resolve(this.stdout || "")
338494
} else if (this.state === RunState.Error) {
339495
reject(this.err)
@@ -342,22 +498,22 @@ export class Run {
342498

343499
res.on("aborted", () => {
344500
if (this.state !== RunState.Finished) {
345-
this.state = RunState.Error
346-
this.err = "Run has been aborted"
501+
this.reactive.state = RunState.Error
502+
this.reactive.err = "Run has been aborted"
347503
reject(this.err)
348504
}
349505
})
350506

351507
res.on("error", (error: Error) => {
352-
this.state = RunState.Error
353-
this.err = error.message || ""
508+
this.reactive.state = RunState.Error
509+
this.reactive.err = error.message || ""
354510
reject(this.err)
355511
})
356512
})
357513

358514
this.req.on("error", (error: Error) => {
359-
this.state = RunState.Error
360-
this.err = error.message || ""
515+
this.reactive.state = RunState.Error
516+
this.reactive.err = error.message || ""
361517
reject(this.err)
362518
})
363519

@@ -403,17 +559,17 @@ export class Run {
403559
}
404560

405561
if (!this.state) {
406-
this.state = RunState.Creating
562+
this.reactive.state = RunState.Creating
407563
}
408564

409565
if (f.type === RunEventType.RunStart) {
410-
this.state = RunState.Running
566+
this.reactive.state = RunState.Running
411567
} else if (f.type === RunEventType.RunFinish) {
412568
if (f.err) {
413-
this.state = RunState.Error
414-
this.err = f.err || ""
569+
this.reactive.state = RunState.Error
570+
this.reactive.err = f.err || ""
415571
} else {
416-
this.state = RunState.Finished
572+
this.reactive.state = RunState.Finished
417573
this.stdout = f.output || ""
418574
}
419575
} else if ((f.type as string).startsWith("call")) {
@@ -530,8 +686,8 @@ export class Run {
530686
}
531687

532688
class RunSubcommand extends Run {
533-
constructor(subCommand: string, path: string, content: string, opts: RunOpts) {
534-
super(subCommand, path, content, opts)
689+
constructor(subCommand: string, path: string, content: string, opts: RunOpts, bin?: string, gptscriptURL?: string) {
690+
super(subCommand, path, content, opts, bin, gptscriptURL)
535691
}
536692

537693
processStdout(data: string | object) {
@@ -777,109 +933,6 @@ async function getCmdPath(): Promise<string> {
777933
return path.join(path.dirname(url.fileURLToPath(import.meta.url)), "..", "bin", "gptscript")
778934
}
779935

780-
export function listTools(gptscriptURL?: string): Promise<string> {
781-
return runBasicCommand("list-tools", gptscriptURL)
782-
}
783-
784-
export function listModels(gptscriptURL?: string): Promise<string> {
785-
return runBasicCommand("list-models", gptscriptURL)
786-
}
787-
788-
export function version(gptscriptURL?: string): Promise<string> {
789-
return runBasicCommand("version", gptscriptURL)
790-
}
791-
792-
async function runBasicCommand(cmd: string, gptscriptURL?: string): Promise<string> {
793-
const r = new RunSubcommand(cmd, "", "", {gptscriptURL: gptscriptURL})
794-
if (gptscriptURL) {
795-
r.request(null)
796-
} else {
797-
await r.exec(["--" + cmd])
798-
}
799-
return r.text()
800-
}
801-
802-
/**
803-
* Runs a tool with the specified name and options.
804-
*
805-
* @param {string} toolName - The name of the tool to run. Can be a file path, URL, or GitHub URL.
806-
* @param {RunOpts} [opts={}] - The options for running the tool.
807-
* @return {Run} The Run object representing the running tool.
808-
*/
809-
export function run(toolName: string, opts: RunOpts = {}): Run {
810-
return (new Run("run-file-stream-with-events", toolName, "", opts)).nextChat(opts.input)
811-
}
812-
813-
/**
814-
* Evaluates the given tool and returns a Run object.
815-
*
816-
* @param {ToolDef | ToolDef[] | string} tool - The tool to be evaluated. Can be a single ToolDef object, an array of ToolDef objects, or a string representing the tool contents.
817-
* @param {RunOpts} [opts={}] - Optional options for the evaluation.
818-
* @return {Run} The Run object representing the evaluation.
819-
*/
820-
export function evaluate(tool: ToolDef | ToolDef[] | string, opts: RunOpts = {}): Run {
821-
let toolString: string = ""
822-
823-
if (Array.isArray(tool)) {
824-
toolString = toolArrayToContents(tool)
825-
} else if (typeof tool === "string") {
826-
toolString = tool
827-
} else {
828-
toolString = toolDefToString(tool)
829-
}
830-
831-
return (new Run("run-tool-stream-with-event", "", toolString, opts)).nextChat(opts.input)
832-
}
833-
834-
export async function parse(fileName: string, gptscriptURL?: string): Promise<Block[]> {
835-
const r: Run = new RunSubcommand("parse", fileName, "", {gptscriptURL: gptscriptURL})
836-
if (gptscriptURL) {
837-
r.request({file: fileName})
838-
} else {
839-
await r.exec(["parse"])
840-
}
841-
return parseBlocksFromNodes((await r.json()).nodes)
842-
}
843-
844-
export async function parseTool(toolContent: string, gptscriptURL?: string): Promise<Block[]> {
845-
const r: Run = new RunSubcommand("parse", "", toolContent, {gptscriptURL: gptscriptURL})
846-
if (gptscriptURL) {
847-
r.request({input: toolContent})
848-
} else {
849-
await r.exec(["parse"])
850-
}
851-
return parseBlocksFromNodes((await r.json()).nodes)
852-
}
853-
854-
export async function stringify(blocks: Block[], gptscriptURL?: string): Promise<string> {
855-
const nodes: any[] = []
856-
857-
for (const block of blocks) {
858-
if (block.type === "tool") {
859-
nodes.push({
860-
toolNode: {
861-
tool: block
862-
}
863-
})
864-
} else if (block.type === "text") {
865-
nodes.push({
866-
textNode: {
867-
text: "!" + (block.format || "text") + "\n" + block.content
868-
}
869-
})
870-
}
871-
}
872-
873-
const r: Run = new RunSubcommand("fmt", "", JSON.stringify({nodes: nodes}), {gptscriptURL: gptscriptURL})
874-
if (gptscriptURL) {
875-
r.request({nodes: nodes})
876-
} else {
877-
await r.exec(["fmt"])
878-
}
879-
880-
return r.text()
881-
}
882-
883936
function parseBlocksFromNodes(nodes: any[]): Block[] {
884937
const blocks: Block[] = []
885938
for (const node of nodes) {

0 commit comments

Comments
 (0)
Please sign in to comment.