nestjs-langchain 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -5
- package/dist/agent.d.ts +16 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +31 -23
- package/dist/agent.js.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +6 -0
- package/dist/constants.js.map +1 -1
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +2 -0
- package/dist/errors/index.js.map +1 -1
- package/dist/errors/messages.d.ts +7 -0
- package/dist/errors/messages.d.ts.map +1 -1
- package/dist/errors/messages.js +18 -3
- package/dist/errors/messages.js.map +1 -1
- package/dist/errors/structured-output.error.d.ts +22 -0
- package/dist/errors/structured-output.error.d.ts.map +1 -0
- package/dist/errors/structured-output.error.js +23 -0
- package/dist/errors/structured-output.error.js.map +1 -0
- package/dist/errors/unwrap-middleware-error.util.d.ts +10 -0
- package/dist/errors/unwrap-middleware-error.util.d.ts.map +1 -0
- package/dist/errors/unwrap-middleware-error.util.js +17 -0
- package/dist/errors/unwrap-middleware-error.util.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/completed-run.interface.d.ts +1 -1
- package/dist/interfaces/completed-run.interface.d.ts.map +1 -1
- package/dist/interfaces/run-options.interface.d.ts +21 -0
- package/dist/interfaces/run-options.interface.d.ts.map +1 -0
- package/dist/interfaces/run-options.interface.js +2 -0
- package/dist/interfaces/run-options.interface.js.map +1 -0
- package/dist/output/index.d.ts +3 -0
- package/dist/output/index.d.ts.map +1 -0
- package/dist/output/index.js +3 -0
- package/dist/output/index.js.map +1 -0
- package/dist/output/run-output.d.ts +43 -0
- package/dist/output/run-output.d.ts.map +1 -0
- package/dist/output/run-output.js +107 -0
- package/dist/output/run-output.js.map +1 -0
- package/dist/output/structured-output.middleware.d.ts +22 -0
- package/dist/output/structured-output.middleware.d.ts.map +1 -0
- package/dist/output/structured-output.middleware.js +40 -0
- package/dist/output/structured-output.middleware.js.map +1 -0
- package/dist/run/answered-calls.util.d.ts +10 -0
- package/dist/run/answered-calls.util.d.ts.map +1 -0
- package/dist/run/answered-calls.util.js +12 -0
- package/dist/run/answered-calls.util.js.map +1 -0
- package/dist/run/index.d.ts +2 -0
- package/dist/run/index.d.ts.map +1 -1
- package/dist/run/index.js +2 -0
- package/dist/run/index.js.map +1 -1
- package/dist/run/run-summary.util.d.ts +10 -3
- package/dist/run/run-summary.util.d.ts.map +1 -1
- package/dist/run/run-summary.util.js +18 -6
- package/dist/run/run-summary.util.js.map +1 -1
- package/dist/run/usage-aside.d.ts +23 -0
- package/dist/run/usage-aside.d.ts.map +1 -0
- package/dist/run/usage-aside.js +33 -0
- package/dist/run/usage-aside.js.map +1 -0
- package/dist/tools/tool-errors.middleware.d.ts +7 -1
- package/dist/tools/tool-errors.middleware.d.ts.map +1 -1
- package/dist/tools/tool-errors.middleware.js +81 -16
- package/dist/tools/tool-errors.middleware.js.map +1 -1
- package/dist/tools/tool-name.util.d.ts.map +1 -1
- package/dist/tools/tool-name.util.js +1 -3
- package/dist/tools/tool-name.util.js.map +1 -1
- package/lib/agent.ts +61 -15
- package/lib/constants.ts +8 -0
- package/lib/errors/index.ts +2 -0
- package/lib/errors/messages.ts +43 -3
- package/lib/errors/structured-output.error.ts +23 -0
- package/lib/errors/unwrap-middleware-error.util.ts +19 -0
- package/lib/index.ts +6 -1
- package/lib/interfaces/completed-run.interface.ts +1 -1
- package/lib/interfaces/run-options.interface.ts +21 -0
- package/lib/output/index.ts +5 -0
- package/lib/output/run-output.ts +154 -0
- package/lib/output/structured-output.middleware.ts +44 -0
- package/lib/run/answered-calls.util.ts +16 -0
- package/lib/run/index.ts +2 -0
- package/lib/run/run-summary.util.ts +33 -11
- package/lib/run/usage-aside.ts +41 -0
- package/lib/tools/tool-errors.middleware.ts +104 -10
- package/lib/tools/tool-name.util.ts +1 -4
- package/package.json +28 -9
|
@@ -1,10 +1,8 @@
|
|
|
1
|
+
import { STRUCTURED_OUTPUT_PREFIX } from '../constants.js';
|
|
1
2
|
import { duplicateToolName, invalidToolName, reservedToolName, } from '../errors/messages.js';
|
|
2
3
|
// OpenAI's limit, the strictest published, restated: no provider SDK exports
|
|
3
4
|
// it. See #90.
|
|
4
5
|
const TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
5
|
-
// The agent routes a call with this prefix to structured output, never to the
|
|
6
|
-
// tool. Restated: LangChain does not export it. See #213.
|
|
7
|
-
const STRUCTURED_OUTPUT_PREFIX = 'extract-';
|
|
8
6
|
export const resolveToolName = (declared, method, where) => {
|
|
9
7
|
const name = declared ?? method;
|
|
10
8
|
if (!TOOL_NAME.test(name)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-name.util.js","sourceRoot":"","sources":["../../lib/tools/tool-name.util.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAE/B,6EAA6E;AAC7E,eAAe;AACf,MAAM,SAAS,GAAG,uBAAuB,CAAC;AAE1C,
|
|
1
|
+
{"version":3,"file":"tool-name.util.js","sourceRoot":"","sources":["../../lib/tools/tool-name.util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAE/B,6EAA6E;AAC7E,eAAe;AACf,MAAM,SAAS,GAAG,uBAAuB,CAAC;AAE1C,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,QAA4B,EAC5B,MAAc,EACd,KAAa,EACL,EAAE;IACV,MAAM,IAAI,GAAG,QAAQ,IAAI,MAAM,CAAC;IAEhC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,eAAe,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE,QAAQ,KAAK,SAAS,CAAC,CACxE,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,UAAU,CAAC,wBAAwB,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,KAAK,EAAE,IAAI,EAAE,wBAAwB,CAAC,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,KAAwC,EAC9B,EAAE;IACZ,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IACvC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAE7B,IAAI,KAAK,EAAE,CAAC;YACV,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;YACrD,OAAO;QACT,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC"}
|
package/lib/agent.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
2
2
|
import { DiscoveryService } from '@nestjs/core';
|
|
3
3
|
import { createAgent, initChatModel } from 'langchain';
|
|
4
|
+
import type { z, ZodType } from 'zod';
|
|
4
5
|
import {
|
|
5
6
|
AGENT_NAME_TOKEN,
|
|
6
7
|
MODULE_OPTIONS_TOKEN,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
ModelOption,
|
|
12
13
|
} from './interfaces/langchain-module-options.interface.js';
|
|
13
14
|
import { CompletedRun } from './interfaces/completed-run.interface.js';
|
|
15
|
+
import { RunOptions } from './interfaces/run-options.interface.js';
|
|
14
16
|
import {
|
|
15
17
|
ToolDiscoveryService,
|
|
16
18
|
toolErrors,
|
|
@@ -18,8 +20,9 @@ import {
|
|
|
18
20
|
} from './tools/index.js';
|
|
19
21
|
import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
|
|
20
22
|
import { LOG_CONTEXT, messageOf, RunReporter } from './logging/index.js';
|
|
21
|
-
import { summariseRun } from './run/index.js';
|
|
22
|
-
import {
|
|
23
|
+
import { summariseRun, UsageAside } from './run/index.js';
|
|
24
|
+
import { outputConfig, RunOutput, structuredOutput } from './output/index.js';
|
|
25
|
+
import { AgentRunError, unwrapMiddlewareError } from './errors/index.js';
|
|
23
26
|
import {
|
|
24
27
|
agentNotBootstrapped,
|
|
25
28
|
agentPrefix,
|
|
@@ -106,8 +109,16 @@ export class Agent implements OnModuleInit {
|
|
|
106
109
|
model: await this.resolveModel(this.options.model),
|
|
107
110
|
tools,
|
|
108
111
|
systemPrompt: this.options.systemPrompt,
|
|
109
|
-
// The first wraps the next: `toolErrors` sits right around the tool
|
|
110
|
-
|
|
112
|
+
// The first wraps the next: `toolErrors` sits right around the tool,
|
|
113
|
+
// `structuredOutput` right around the model.
|
|
114
|
+
middleware: [
|
|
115
|
+
unknownTools(
|
|
116
|
+
this.reporter,
|
|
117
|
+
tools.map((tool) => tool.name),
|
|
118
|
+
),
|
|
119
|
+
toolErrors(this.reporter),
|
|
120
|
+
structuredOutput(),
|
|
121
|
+
],
|
|
111
122
|
});
|
|
112
123
|
|
|
113
124
|
this.logger.log(
|
|
@@ -179,21 +190,45 @@ export class Agent implements OnModuleInit {
|
|
|
179
190
|
* as an `AgentRunError`, the original error in its `cause`.
|
|
180
191
|
*
|
|
181
192
|
* @param input The user's message.
|
|
182
|
-
* @
|
|
193
|
+
* @param options With `output`, the schema the answer must match.
|
|
194
|
+
* @returns The model's answer, the object `output` parses when given, the
|
|
195
|
+
* tools it called and what the run cost.
|
|
183
196
|
* @example
|
|
184
197
|
* ```ts
|
|
185
198
|
* const { output, tools } = await agent.run('What is 6 times 7?');
|
|
199
|
+
*
|
|
200
|
+
* const { output: ticket } = await agent.run(email, {
|
|
201
|
+
* output: z.object({ category: z.enum(['billing', 'bug', 'other']) }),
|
|
202
|
+
* });
|
|
186
203
|
* ```
|
|
187
204
|
*/
|
|
188
|
-
|
|
205
|
+
run<S extends ZodType>(
|
|
206
|
+
input: string,
|
|
207
|
+
options: RunOptions & { output: S },
|
|
208
|
+
): Promise<CompletedRun<z.output<S>>>;
|
|
209
|
+
run(
|
|
210
|
+
input: string,
|
|
211
|
+
options?: RunOptions & { output?: undefined },
|
|
212
|
+
): Promise<CompletedRun>;
|
|
213
|
+
// Options typed `RunOptions`, whose schema is not known statically. See #280.
|
|
214
|
+
run(input: string, options?: RunOptions): Promise<CompletedRun<unknown>>;
|
|
215
|
+
async run(
|
|
216
|
+
input: string,
|
|
217
|
+
options?: RunOptions,
|
|
218
|
+
): Promise<CompletedRun<unknown>> {
|
|
189
219
|
const startedAt = Date.now();
|
|
190
220
|
const durationMs = () => Date.now() - startedAt;
|
|
191
221
|
|
|
192
222
|
try {
|
|
193
|
-
|
|
223
|
+
// Read in the `try`, and `null` from untyped JavaScript taken as no
|
|
224
|
+
// options, as LangChain takes a `null` config. See #291.
|
|
225
|
+
const schema = options?.output;
|
|
226
|
+
const output = schema && new RunOutput(schema, this.agentName);
|
|
227
|
+
const aside = new UsageAside();
|
|
228
|
+
const { answer, messages } = await this.complete(input, output, aside);
|
|
194
229
|
const finished = {
|
|
195
230
|
durationMs: durationMs(),
|
|
196
|
-
...summariseRun(messages),
|
|
231
|
+
...summariseRun(messages, output?.toolName, aside.usage),
|
|
197
232
|
};
|
|
198
233
|
|
|
199
234
|
this.reporter.notify((observer) =>
|
|
@@ -202,13 +237,14 @@ export class Agent implements OnModuleInit {
|
|
|
202
237
|
|
|
203
238
|
return { status: 'completed', output: answer, ...finished };
|
|
204
239
|
} catch (thrown) {
|
|
240
|
+
const cause = unwrapMiddlewareError(thrown);
|
|
205
241
|
const error =
|
|
206
|
-
|
|
207
|
-
?
|
|
242
|
+
cause instanceof AgentRunError
|
|
243
|
+
? cause
|
|
208
244
|
: new AgentRunError(
|
|
209
|
-
agentRunFailed(this.prefix,
|
|
245
|
+
agentRunFailed(this.prefix, cause),
|
|
210
246
|
this.agentName,
|
|
211
|
-
{ cause
|
|
247
|
+
{ cause },
|
|
212
248
|
);
|
|
213
249
|
|
|
214
250
|
this.reporter.notify((observer) =>
|
|
@@ -225,7 +261,9 @@ export class Agent implements OnModuleInit {
|
|
|
225
261
|
|
|
226
262
|
private async complete(
|
|
227
263
|
input: string,
|
|
228
|
-
|
|
264
|
+
output: RunOutput | undefined,
|
|
265
|
+
aside: UsageAside,
|
|
266
|
+
): Promise<{ answer: unknown; messages: AgentState['messages'] }> {
|
|
229
267
|
if (!this.#graph) {
|
|
230
268
|
throw new AgentRunError(
|
|
231
269
|
agentNotBootstrapped(this.prefix),
|
|
@@ -233,10 +271,14 @@ export class Agent implements OnModuleInit {
|
|
|
233
271
|
);
|
|
234
272
|
}
|
|
235
273
|
|
|
236
|
-
const
|
|
274
|
+
const state: AgentState = await this.#graph.invoke(
|
|
237
275
|
{ messages: [{ role: 'user', content: input }] },
|
|
238
|
-
{
|
|
276
|
+
{
|
|
277
|
+
callbacks: [this.modelErrors, ...(this.options.callbacks ?? [])],
|
|
278
|
+
configurable: { ...outputConfig(output), ...aside.config },
|
|
279
|
+
},
|
|
239
280
|
);
|
|
281
|
+
const { messages } = state;
|
|
240
282
|
|
|
241
283
|
const last = messages[messages.length - 1];
|
|
242
284
|
|
|
@@ -247,6 +289,10 @@ export class Agent implements OnModuleInit {
|
|
|
247
289
|
);
|
|
248
290
|
}
|
|
249
291
|
|
|
292
|
+
if (output) {
|
|
293
|
+
return { answer: output.read(state.structuredResponse), messages };
|
|
294
|
+
}
|
|
295
|
+
|
|
250
296
|
// `text`, not `content`: block answers are an array.
|
|
251
297
|
if (!last.text) {
|
|
252
298
|
throw new AgentRunError(modelReplyEmpty(this.prefix), this.agentName);
|
package/lib/constants.ts
CHANGED
|
@@ -5,6 +5,14 @@ export const TOOL_PARAMS_METADATA = 'nestjs-langchain:tool-params';
|
|
|
5
5
|
// Emitted by `emitDecoratorMetadata` on every decorated method.
|
|
6
6
|
export const PARAM_TYPES_METADATA = 'design:paramtypes';
|
|
7
7
|
|
|
8
|
+
// The agent routes a call with this prefix to structured output, never to the
|
|
9
|
+
// tool. Restated: LangChain does not export it. See #213.
|
|
10
|
+
export const STRUCTURED_OUTPUT_PREFIX = 'extract-';
|
|
11
|
+
|
|
12
|
+
// Ours rather than LangChain's counter or the schema's title: the same tool on
|
|
13
|
+
// every run, under the prefix no `@Tool` can take. See #281.
|
|
14
|
+
export const OUTPUT_TOOL = `${STRUCTURED_OUTPUT_PREFIX}output`;
|
|
15
|
+
|
|
8
16
|
// The keys `@Module()` writes, restated: `@nestjs/common/constants` is
|
|
9
17
|
// internal. See #97.
|
|
10
18
|
export const MODULE_METADATA_KEYS = [
|
package/lib/errors/index.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { AgentRunError } from './agent-run.error.js';
|
|
2
|
+
export { StructuredOutputError } from './structured-output.error.js';
|
|
2
3
|
export { ToolConfigurationError } from './tool-configuration.error.js';
|
|
4
|
+
export { unwrapMiddlewareError } from './unwrap-middleware-error.util.js';
|
package/lib/errors/messages.ts
CHANGED
|
@@ -96,14 +96,17 @@ export const schemaContradictsSignature = (
|
|
|
96
96
|
`${where}: the schema for "${param}" describes ${declared.join(' | ')} ` +
|
|
97
97
|
`but the signature declares ${expected}.`;
|
|
98
98
|
|
|
99
|
+
// The advice of both messages on a schema with no JSON Schema form.
|
|
100
|
+
const jsonSchemaAdvice =
|
|
101
|
+
'Declare a schema JSON Schema can express and convert it with .transform(), ' +
|
|
102
|
+
'such as z.iso.date() or z.iso.datetime() for a date.';
|
|
103
|
+
|
|
99
104
|
export const noJsonSchemaForm = (
|
|
100
105
|
where: string,
|
|
101
106
|
param: string,
|
|
102
107
|
reason: string,
|
|
103
108
|
): string =>
|
|
104
|
-
`${where}: "${param}" has no JSON Schema form (${reason}).
|
|
105
|
-
'Declare a schema JSON Schema can express and convert it with .transform(), ' +
|
|
106
|
-
'such as z.iso.datetime() for a date.';
|
|
109
|
+
`${where}: "${param}" has no JSON Schema form (${reason}). ${jsonSchemaAdvice}`;
|
|
107
110
|
|
|
108
111
|
export const duplicateParamName = (
|
|
109
112
|
where: string,
|
|
@@ -155,4 +158,41 @@ export const modelNeverReplied = (
|
|
|
155
158
|
export const modelReplyEmpty = (prefix: string): string =>
|
|
156
159
|
`The ${prefix}agent's model replied with no text content.`;
|
|
157
160
|
|
|
161
|
+
export const noOutputJsonSchemaForm = (
|
|
162
|
+
prefix: string,
|
|
163
|
+
reason: string,
|
|
164
|
+
): string =>
|
|
165
|
+
`The ${prefix}agent's \`output\` schema has no JSON Schema form (${reason}). ` +
|
|
166
|
+
jsonSchemaAdvice;
|
|
167
|
+
|
|
168
|
+
export const outputNotAnObject = (prefix: string): string =>
|
|
169
|
+
`The ${prefix}agent's \`output\` schema is not an object with named fields: ` +
|
|
170
|
+
"the model fills it in as a tool's parameters. Wrap it, such as " +
|
|
171
|
+
'`z.object({ items: z.array(...) })`.';
|
|
172
|
+
|
|
173
|
+
export const structuredOutputMissing = (prefix: string): string =>
|
|
174
|
+
`The ${prefix}agent's model replied without the requested structured output.`;
|
|
175
|
+
|
|
176
|
+
export const structuredOutputInvalid = (
|
|
177
|
+
prefix: string,
|
|
178
|
+
reason: string,
|
|
179
|
+
): string =>
|
|
180
|
+
`The ${prefix}agent's model replied with an output that does not match ` +
|
|
181
|
+
`the schema: ${reason}`;
|
|
182
|
+
|
|
183
|
+
export const structuredOutputMultiple = (
|
|
184
|
+
prefix: string,
|
|
185
|
+
count: number,
|
|
186
|
+
): string =>
|
|
187
|
+
`The ${prefix}agent's model replied with ${count} outputs instead of one.`;
|
|
188
|
+
|
|
158
189
|
export const unknownTool = (): string => 'not a tool of this agent';
|
|
190
|
+
|
|
191
|
+
// LangChain's answer to a tool the agent does not have, restated:
|
|
192
|
+
// `getInvalidToolError` is not exported. See #264.
|
|
193
|
+
export const invalidTool = (tool: string, tools: string[]): string =>
|
|
194
|
+
`Error: ${tool} is not a valid tool, try one of [${tools.join(', ')}].`;
|
|
195
|
+
|
|
196
|
+
export const toolNotRun = (tool: string): string =>
|
|
197
|
+
`Error: '${tool}' was not run: it came with the final answer. ` +
|
|
198
|
+
'Call it on its own first if it is needed.';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AgentRunError } from './agent-run.error.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The model's answer to a run with an `output` schema never came, or does not
|
|
5
|
+
* match the schema. LangChain's or Zod's error, if any, is in `cause`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* try {
|
|
10
|
+
* const { output } = await agent.run(ticket, { output: Classification });
|
|
11
|
+
* } catch (error) {
|
|
12
|
+
* if (error instanceof StructuredOutputError) {
|
|
13
|
+
* console.error(error.message, error.cause);
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export class StructuredOutputError extends AgentRunError {
|
|
19
|
+
constructor(message: string, agent: string, options?: { cause?: unknown }) {
|
|
20
|
+
super(message, agent, options);
|
|
21
|
+
this.name = 'StructuredOutputError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MiddlewareError } from 'langchain';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The error as thrown, without the `MiddlewareError` LangChain wraps it in at
|
|
5
|
+
* each `wrapModelCall` middleware it crosses, a provider error included.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
* @param error What the agent's graph threw.
|
|
9
|
+
* @returns The innermost error.
|
|
10
|
+
*/
|
|
11
|
+
export const unwrapMiddlewareError = (error: unknown): unknown => {
|
|
12
|
+
let unwrapped = error;
|
|
13
|
+
|
|
14
|
+
while (MiddlewareError.isInstance(unwrapped) && unwrapped.cause) {
|
|
15
|
+
unwrapped = unwrapped.cause;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return unwrapped;
|
|
19
|
+
};
|
package/lib/index.ts
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
export { LangChainModule } from './langchain.module.js';
|
|
4
4
|
export { getAgentToken } from './langchain.module-definition.js';
|
|
5
5
|
export { Agent } from './agent.js';
|
|
6
|
-
export {
|
|
6
|
+
export {
|
|
7
|
+
AgentRunError,
|
|
8
|
+
StructuredOutputError,
|
|
9
|
+
ToolConfigurationError,
|
|
10
|
+
} from './errors/index.js';
|
|
7
11
|
export { InjectAgent, Tool, ToolParam } from './decorators/index.js';
|
|
8
12
|
export type { ToolOptions, ToolParamOptions } from './decorators/index.js';
|
|
9
13
|
export type {
|
|
@@ -16,6 +20,7 @@ export type {
|
|
|
16
20
|
ToolStartEvent,
|
|
17
21
|
} from './interfaces/agent-observer.interface.js';
|
|
18
22
|
export type { CompletedRun } from './interfaces/completed-run.interface.js';
|
|
23
|
+
export type { RunOptions } from './interfaces/run-options.interface.js';
|
|
19
24
|
export type {
|
|
20
25
|
AgentCallback,
|
|
21
26
|
LangChainModuleOptions,
|
|
@@ -7,7 +7,7 @@ import type { RunSummary } from '../run/index.js';
|
|
|
7
7
|
export interface CompletedRun<TOutput = string> extends RunSummary {
|
|
8
8
|
/** Always `'completed'`: a run that fails throws instead. */
|
|
9
9
|
status: 'completed';
|
|
10
|
-
/** The model's text answer. */
|
|
10
|
+
/** The model's text answer, or the object the run's `output` schema parsed. */
|
|
11
11
|
output: TOutput;
|
|
12
12
|
/** How long the run took. */
|
|
13
13
|
durationMs: number;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ZodType } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The options of one run. Passed inline, they type `output` from the schema;
|
|
5
|
+
* typed `RunOptions`, `output` is `unknown`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const { output } = await agent.run(email, { output: Ticket }); // a Ticket
|
|
10
|
+
*
|
|
11
|
+
* const ask = (question: string, options?: RunOptions) =>
|
|
12
|
+
* agent.run(question, options); // `output` is unknown
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export interface RunOptions {
|
|
16
|
+
/**
|
|
17
|
+
* The schema the answer must match. `output` is then the object it parses,
|
|
18
|
+
* typed from it, instead of the model's text.
|
|
19
|
+
*/
|
|
20
|
+
output?: ZodType;
|
|
21
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { toJsonSchema } from '@langchain/core/utils/json_schema';
|
|
2
|
+
import {
|
|
3
|
+
MultipleStructuredOutputsError,
|
|
4
|
+
StructuredOutputParsingError,
|
|
5
|
+
toolStrategy,
|
|
6
|
+
TypedToolStrategy,
|
|
7
|
+
} from 'langchain';
|
|
8
|
+
import z, { ZodType } from 'zod';
|
|
9
|
+
import { OUTPUT_TOOL } from '../constants.js';
|
|
10
|
+
import { AgentRunError, StructuredOutputError } from '../errors/index.js';
|
|
11
|
+
import {
|
|
12
|
+
agentPrefix,
|
|
13
|
+
noOutputJsonSchemaForm,
|
|
14
|
+
outputNotAnObject,
|
|
15
|
+
structuredOutputInvalid,
|
|
16
|
+
structuredOutputMissing,
|
|
17
|
+
structuredOutputMultiple,
|
|
18
|
+
} from '../errors/messages.js';
|
|
19
|
+
import { messageOf } from '../logging/index.js';
|
|
20
|
+
|
|
21
|
+
// The default of LangChain's own retry middlewares. See #151.
|
|
22
|
+
const MAX_RETRIES = 2;
|
|
23
|
+
|
|
24
|
+
// LangChain's default for a Zod schema, which its JSON Schema path leaves
|
|
25
|
+
// empty. See #282.
|
|
26
|
+
const OUTPUT_DESCRIPTION =
|
|
27
|
+
"Tool for extracting structured output from the model's response.";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The structured output one run asked for: the strategy its model calls use,
|
|
31
|
+
* and the answer parsed by the schema.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export class RunOutput {
|
|
36
|
+
/**
|
|
37
|
+
* Through a tool, whatever the model (see #274). One per run, which counts
|
|
38
|
+
* its own retries (see #151).
|
|
39
|
+
*/
|
|
40
|
+
readonly strategy: TypedToolStrategy;
|
|
41
|
+
/** The tool the model answers through. */
|
|
42
|
+
readonly toolName = OUTPUT_TOOL;
|
|
43
|
+
#retries = 0;
|
|
44
|
+
private readonly prefix: string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param schema The run's `output` schema.
|
|
48
|
+
* @param agent The agent's name, for the errors.
|
|
49
|
+
* @throws AgentRunError when the schema has no JSON Schema form, or is not
|
|
50
|
+
* an object with named fields.
|
|
51
|
+
*/
|
|
52
|
+
constructor(
|
|
53
|
+
private readonly schema: ZodType,
|
|
54
|
+
private readonly agent: string,
|
|
55
|
+
) {
|
|
56
|
+
this.prefix = agentPrefix(agent);
|
|
57
|
+
|
|
58
|
+
// LangChain's own conversion, run before any model call.
|
|
59
|
+
let json: Record<string, unknown>;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
json = toJsonSchema(schema);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new AgentRunError(
|
|
65
|
+
noOutputJsonSchemaForm(this.prefix, messageOf(error)),
|
|
66
|
+
agent,
|
|
67
|
+
{ cause: error },
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A tool's parameters. LangChain takes them as a JSON Schema with
|
|
72
|
+
// `properties` at every version, as a Zod schema other than an object at
|
|
73
|
+
// recent ones only. See #282.
|
|
74
|
+
if (json.type !== 'object' || !('properties' in json)) {
|
|
75
|
+
throw new AgentRunError(outputNotAnObject(this.prefix), agent);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// LangChain names the tool after the title.
|
|
79
|
+
this.strategy = toolStrategy(
|
|
80
|
+
{
|
|
81
|
+
...json,
|
|
82
|
+
type: 'object',
|
|
83
|
+
title: OUTPUT_TOOL,
|
|
84
|
+
description: json.description ?? OUTPUT_DESCRIPTION,
|
|
85
|
+
},
|
|
86
|
+
{ handleError: (error) => this.retry(error) },
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// What LangChain sends back by default, until the retries run out.
|
|
91
|
+
private retry(error: Error): string {
|
|
92
|
+
this.#retries += 1;
|
|
93
|
+
|
|
94
|
+
if (this.#retries > MAX_RETRIES) {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return error.message;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param error What a model call threw.
|
|
103
|
+
* @returns A `StructuredOutputError` for LangChain's parsing failures, the
|
|
104
|
+
* error itself otherwise.
|
|
105
|
+
*/
|
|
106
|
+
failure(error: unknown): unknown {
|
|
107
|
+
// Never LangChain's messages, which name the internal tool.
|
|
108
|
+
if (error instanceof StructuredOutputParsingError) {
|
|
109
|
+
return this.invalid(error.errors.join(' '), error);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (error instanceof MultipleStructuredOutputsError) {
|
|
113
|
+
return new StructuredOutputError(
|
|
114
|
+
structuredOutputMultiple(this.prefix, error.toolNames.length),
|
|
115
|
+
this.agent,
|
|
116
|
+
{ cause: error },
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return error;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param structuredResponse What the run left in `structuredResponse`.
|
|
125
|
+
* @returns The answer, as the schema parses it.
|
|
126
|
+
* @throws StructuredOutputError when the answer is missing or invalid.
|
|
127
|
+
*/
|
|
128
|
+
read(structuredResponse: unknown): unknown {
|
|
129
|
+
if (structuredResponse === undefined) {
|
|
130
|
+
throw new StructuredOutputError(
|
|
131
|
+
structuredOutputMissing(this.prefix),
|
|
132
|
+
this.agent,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// LangChain checks the JSON Schema only: Zod also applies transforms and
|
|
137
|
+
// refinements. See #151.
|
|
138
|
+
const parsed = this.schema.safeParse(structuredResponse);
|
|
139
|
+
|
|
140
|
+
if (!parsed.success) {
|
|
141
|
+
throw this.invalid(z.prettifyError(parsed.error), parsed.error);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return parsed.data;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private invalid(reason: string, cause: unknown): StructuredOutputError {
|
|
148
|
+
return new StructuredOutputError(
|
|
149
|
+
structuredOutputInvalid(this.prefix, reason),
|
|
150
|
+
this.agent,
|
|
151
|
+
{ cause },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createMiddleware } from 'langchain';
|
|
2
|
+
import { RunOutput } from './run-output.js';
|
|
3
|
+
|
|
4
|
+
// The run's output travels under our own key in `configurable`, which
|
|
5
|
+
// LangGraph keeps out of the trace metadata and checkpoints. See #151.
|
|
6
|
+
const OUTPUT_KEY = 'nestjs-langchain:output';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The `configurable` entry that hands a run's output to the middleware. A
|
|
10
|
+
* text run sets it to `null`, or it would inherit the output of the run it is
|
|
11
|
+
* nested in, which LangChain hands down. See #295.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
* @param output The run's output, if it asked for one.
|
|
15
|
+
* @returns The entry to put in `configurable`.
|
|
16
|
+
*/
|
|
17
|
+
export const outputConfig = (output: RunOutput | undefined) => ({
|
|
18
|
+
[OUTPUT_KEY]: output ?? null,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sets the `responseFormat` of each model call of a run that asked for an
|
|
23
|
+
* `output`, so that one compiled agent serves text and structured runs.
|
|
24
|
+
* LangChain's parsing failures come out as a `StructuredOutputError`.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export const structuredOutput = () =>
|
|
29
|
+
createMiddleware({
|
|
30
|
+
name: 'structuredOutput',
|
|
31
|
+
wrapModelCall: async (request, handler) => {
|
|
32
|
+
const output = request.runtime.configurable?.[OUTPUT_KEY];
|
|
33
|
+
|
|
34
|
+
if (!(output instanceof RunOutput)) {
|
|
35
|
+
return handler(request);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
return await handler({ ...request, responseFormat: output.strategy });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw output.failure(error);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ids of the tool calls the messages answer: the tool node answers every
|
|
3
|
+
* call it runs. Structural rather than `BaseMessage`. See #52.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
* @param messages A run's messages.
|
|
7
|
+
* @returns The ids their `tool_call_id`s answer.
|
|
8
|
+
*/
|
|
9
|
+
export const answeredCalls = (messages: readonly object[]): Set<string> =>
|
|
10
|
+
new Set(
|
|
11
|
+
messages.flatMap((message) =>
|
|
12
|
+
'tool_call_id' in message && typeof message.tool_call_id === 'string'
|
|
13
|
+
? [message.tool_call_id]
|
|
14
|
+
: [],
|
|
15
|
+
),
|
|
16
|
+
);
|
package/lib/run/index.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { mergeUsageMetadata, UsageMetadata } from '@langchain/core/messages';
|
|
2
|
+
import { answeredCalls } from './answered-calls.util.js';
|
|
2
3
|
|
|
3
4
|
// Structural rather than `BaseMessage`. See #52.
|
|
4
5
|
interface MessageLike {
|
|
5
6
|
usage_metadata?: UsageMetadata;
|
|
6
|
-
tool_calls?: { name: string }[];
|
|
7
|
+
tool_calls?: { name: string; id?: string }[];
|
|
8
|
+
tool_call_id?: string;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
/** What a run did and cost. */
|
|
10
12
|
export interface RunSummary {
|
|
11
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Every tool call that reached the agent's tools, failed and unknown ones
|
|
15
|
+
* included, but not the one returning a structured output.
|
|
16
|
+
*/
|
|
12
17
|
tools: string[];
|
|
13
18
|
/**
|
|
14
19
|
* Summed over the run's model calls; absent when the provider reports no
|
|
@@ -22,16 +27,33 @@ export interface RunSummary {
|
|
|
22
27
|
*
|
|
23
28
|
* @internal
|
|
24
29
|
* @param messages The messages of the finished run.
|
|
25
|
-
* @
|
|
30
|
+
* @param outputTool The tool the model returned a structured output through.
|
|
31
|
+
* @param aside The usage of the replies the run left out of its messages.
|
|
32
|
+
* @returns The tool calls that reached the tools, and the tokens if reported.
|
|
26
33
|
*/
|
|
27
|
-
export const summariseRun = (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
export const summariseRun = (
|
|
35
|
+
messages: readonly MessageLike[],
|
|
36
|
+
outputTool?: string,
|
|
37
|
+
aside: readonly UsageMetadata[] = [],
|
|
38
|
+
): RunSummary => {
|
|
39
|
+
// One that did not run has no answer (see #290). One with no id cannot be
|
|
40
|
+
// told, and counts (see #295).
|
|
41
|
+
const answered = answeredCalls(messages);
|
|
42
|
+
|
|
43
|
+
const tools = messages
|
|
44
|
+
.flatMap((message) => message.tool_calls ?? [])
|
|
45
|
+
.filter(
|
|
46
|
+
({ name, id }) =>
|
|
47
|
+
name !== outputTool && (id === undefined || answered.has(id)),
|
|
48
|
+
)
|
|
49
|
+
.map((call) => call.name);
|
|
50
|
+
|
|
51
|
+
const reported = [
|
|
52
|
+
...messages
|
|
53
|
+
.map((message) => message.usage_metadata)
|
|
54
|
+
.filter((usage) => usage !== undefined),
|
|
55
|
+
...aside,
|
|
56
|
+
];
|
|
35
57
|
|
|
36
58
|
if (reported.length === 0) {
|
|
37
59
|
return { tools };
|