Anonymous View
Skip to content

Commit 96f23f1

Browse files
committed
fix: parsing LLM response when code is not wrapped
1 parent f25e425 commit 96f23f1

6 files changed

Lines changed: 149 additions & 24 deletions

File tree

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './inferAgent.js';
44

55
export * from './parser/parseScript.js';
66
export * from './parser/astTypes.js';
7+
export * from './parser/ParseError.js';
78

89
export * from './defineAgent.js';
910
export * from './runtime/createAgent.js';

packages/core/src/inferAgent.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
AgentTools,
1212
} from './defineAgent.js';
1313
import { renderRuntime } from './modules/renderRuntime.js';
14+
import { parseCodeResponse } from './parser/parseCodeResponse.js';
1415
import { parseScript } from './parser/parseScript.js';
1516
import { createAgent } from './runtime/createAgent.js';
1617

@@ -54,8 +55,6 @@ Then create a valid AgentScript code.
5455
Don't wrap code in a function.
5556
Don't explain the code later.`;
5657

57-
const RESPONSE_REGEX = /^([\s\S]*)```(\w*)?\n([\s\S]*)\n```/m;
58-
5958
const debug = createDebug('agentscript:inferAgent');
6059

6160
/**
@@ -79,7 +78,7 @@ export async function inferAgent<
7978
messages: [{ role: 'user', content: params.prompt }],
8079
});
8180

82-
const { plan, code } = parseResponse(response.content);
81+
const { plan, code } = parseCodeResponse(response.content);
8382

8483
debug('plan', plan);
8584
debug('code', code);
@@ -96,18 +95,3 @@ export async function inferAgent<
9695

9796
return agent;
9897
}
99-
100-
function parseResponse(response: string) {
101-
const match = response.match(RESPONSE_REGEX);
102-
if (!match) {
103-
debug('response', response);
104-
throw new Error('No code found in response', {
105-
cause: response,
106-
});
107-
}
108-
109-
return {
110-
plan: match[1].trim(),
111-
code: match[3].trim(),
112-
};
113-
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Error thrown when parsing fails.
3+
*/
4+
export class ParseError extends Error {
5+
/**
6+
* @param message - Error message.
7+
* @param options - Error options.
8+
*/
9+
constructor(message: string, options: ErrorOptions) {
10+
super(message, options);
11+
}
12+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { expect, test } from 'vitest';
2+
3+
import { joinLines } from '@agentscript-ai/utils';
4+
5+
import { ParseError } from './ParseError.js';
6+
import { parseCodeResponse } from './parseCodeResponse.js';
7+
8+
test('code is wrapped in ```typescript```', () => {
9+
const response = joinLines([
10+
'This is a plan:',
11+
'1. Create a function that logs "Hello, world!"',
12+
'2. Call the function',
13+
'',
14+
'Here is the code:',
15+
'```typescript',
16+
'// Create a function that logs "Hello, world!"',
17+
'const hello = "Hello, world!";',
18+
'// Call the function',
19+
'console.log(hello);',
20+
'```',
21+
]);
22+
23+
const { plan, code } = parseCodeResponse(response);
24+
25+
expect(plan).toBe(
26+
joinLines([
27+
'This is a plan:',
28+
'1. Create a function that logs "Hello, world!"',
29+
'2. Call the function',
30+
'',
31+
'Here is the code:',
32+
]),
33+
);
34+
35+
expect(code).toBe(
36+
joinLines([
37+
'// Create a function that logs "Hello, world!"',
38+
'const hello = "Hello, world!";',
39+
'// Call the function',
40+
'console.log(hello);',
41+
]),
42+
);
43+
});
44+
45+
test('code is nod wrapped in ```typescript```', () => {
46+
const response = joinLines([
47+
'This is a plan:',
48+
'1. Create a function that logs "Hello, world!"',
49+
'2. Call the function',
50+
'',
51+
'Here is the code:',
52+
'// Create a function that logs "Hello, world!"',
53+
'const hello = "Hello, world!";',
54+
'// Call the function',
55+
'console.log(hello);',
56+
]);
57+
58+
const { plan, code } = parseCodeResponse(response);
59+
60+
expect(plan).toBe(
61+
joinLines([
62+
'This is a plan:',
63+
'1. Create a function that logs "Hello, world!"',
64+
'2. Call the function',
65+
'',
66+
'Here is the code:',
67+
]),
68+
);
69+
70+
expect(code).toBe(
71+
joinLines([
72+
'// Create a function that logs "Hello, world!"',
73+
'const hello = "Hello, world!";',
74+
'// Call the function',
75+
'console.log(hello);',
76+
]),
77+
);
78+
});
79+
80+
test('no code found', () => {
81+
const response = 'This is a plan:';
82+
83+
expect(() => parseCodeResponse(response)).toThrow(ParseError);
84+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ParseError } from './ParseError.js';
2+
3+
const RESPONSE_WRAPPED_REGEX = /^([\s\S]*)```(\w*)?\n([\s\S]*)\n```/m;
4+
const RESPONSE_UNWRAPPED_REGEX = /^([\s\S]*?)\n(\/\/[\s\S]*)/m;
5+
6+
/**
7+
* Parse the response from the LLM into a plan and code.
8+
* @param response - Response from the LLM.
9+
* @returns Plan and code.
10+
*/
11+
export function parseCodeResponse(response: string) {
12+
let match = response.match(RESPONSE_WRAPPED_REGEX);
13+
if (match) {
14+
return {
15+
plan: match[1].trim(),
16+
code: match[3].trim(),
17+
};
18+
}
19+
20+
match = response.match(RESPONSE_UNWRAPPED_REGEX);
21+
if (match) {
22+
return {
23+
plan: match[1].trim(),
24+
code: match[2].trim(),
25+
};
26+
}
27+
28+
throw new ParseError('No code found in response', {
29+
cause: response,
30+
});
31+
}

packages/core/src/parser/parseScript.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { parse } from '@babel/parser';
22
import type * as babel from '@babel/types';
33

4+
import { ParseError } from './ParseError.js';
45
import type { Assignment, AstNode, Expression, ObjectProperty, Script } from './astTypes.js';
56

67
/**
@@ -33,7 +34,9 @@ function parseStatement(statement: babel.Statement): AstNode {
3334
case 'VariableDeclaration': {
3435
const declaration = statement.declarations[0];
3536
if (declaration.id.type !== 'Identifier') {
36-
throw new Error('Invalid variable declaration');
37+
throw new ParseError('Invalid variable declaration', {
38+
cause: statement,
39+
});
3740
}
3841

3942
return {
@@ -51,7 +54,9 @@ function parseStatement(statement: babel.Statement): AstNode {
5154
}
5255
}
5356

54-
throw new Error(`Unknown statement type: ${statement.type}`);
57+
throw new ParseError(`Unknown statement type: ${statement.type}`, {
58+
cause: statement,
59+
});
5560
}
5661

5762
function parseExpression(expression: babel.Expression): Expression {
@@ -138,7 +143,9 @@ function parseExpression(expression: babel.Expression): Expression {
138143
}
139144
}
140145

141-
throw new Error(`Unknown expression type: ${expression.type}`);
146+
throw new ParseError(`Unknown expression type: ${expression.type}`, {
147+
cause: expression,
148+
});
142149
}
143150

144151
function parseLeftValue(left: babel.LVal): Assignment['left'] {
@@ -150,7 +157,9 @@ function parseLeftValue(left: babel.LVal): Assignment['left'] {
150157
return expression;
151158
}
152159

153-
throw new Error(`Invalid left value: ${expression.type}`);
160+
throw new ParseError(`Invalid left value: ${expression.type}`, {
161+
cause: expression,
162+
});
154163
}
155164

156165
function parseComment(comments: babel.Comment[] | undefined | null): string | undefined {
@@ -173,12 +182,16 @@ function parseArgument(
173182
): Expression {
174183
if (arg.type === 'SpreadElement') {
175184
// TODO: Implement spread elements
176-
throw new Error('Spread element not supported');
185+
throw new ParseError('Spread element not supported', {
186+
cause: arg,
187+
});
177188
}
178189

179190
if (arg.type === 'ArgumentPlaceholder') {
180191
// TODO: Implement argument placeholders
181-
throw new Error('Argument placeholder not supported');
192+
throw new ParseError('Argument placeholder not supported', {
193+
cause: arg,
194+
});
182195
}
183196

184197
return parseExpression(arg);

0 commit comments

Comments
 (0)