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.
Files changed (89) hide show
  1. package/README.md +67 -5
  2. package/dist/agent.d.ts +16 -2
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +31 -23
  5. package/dist/agent.js.map +1 -1
  6. package/dist/constants.d.ts +2 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +6 -0
  9. package/dist/constants.js.map +1 -1
  10. package/dist/errors/index.d.ts +2 -0
  11. package/dist/errors/index.d.ts.map +1 -1
  12. package/dist/errors/index.js +2 -0
  13. package/dist/errors/index.js.map +1 -1
  14. package/dist/errors/messages.d.ts +7 -0
  15. package/dist/errors/messages.d.ts.map +1 -1
  16. package/dist/errors/messages.js +18 -3
  17. package/dist/errors/messages.js.map +1 -1
  18. package/dist/errors/structured-output.error.d.ts +22 -0
  19. package/dist/errors/structured-output.error.d.ts.map +1 -0
  20. package/dist/errors/structured-output.error.js +23 -0
  21. package/dist/errors/structured-output.error.js.map +1 -0
  22. package/dist/errors/unwrap-middleware-error.util.d.ts +10 -0
  23. package/dist/errors/unwrap-middleware-error.util.d.ts.map +1 -0
  24. package/dist/errors/unwrap-middleware-error.util.js +17 -0
  25. package/dist/errors/unwrap-middleware-error.util.js.map +1 -0
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/interfaces/completed-run.interface.d.ts +1 -1
  31. package/dist/interfaces/completed-run.interface.d.ts.map +1 -1
  32. package/dist/interfaces/run-options.interface.d.ts +21 -0
  33. package/dist/interfaces/run-options.interface.d.ts.map +1 -0
  34. package/dist/interfaces/run-options.interface.js +2 -0
  35. package/dist/interfaces/run-options.interface.js.map +1 -0
  36. package/dist/output/index.d.ts +3 -0
  37. package/dist/output/index.d.ts.map +1 -0
  38. package/dist/output/index.js +3 -0
  39. package/dist/output/index.js.map +1 -0
  40. package/dist/output/run-output.d.ts +43 -0
  41. package/dist/output/run-output.d.ts.map +1 -0
  42. package/dist/output/run-output.js +107 -0
  43. package/dist/output/run-output.js.map +1 -0
  44. package/dist/output/structured-output.middleware.d.ts +22 -0
  45. package/dist/output/structured-output.middleware.d.ts.map +1 -0
  46. package/dist/output/structured-output.middleware.js +40 -0
  47. package/dist/output/structured-output.middleware.js.map +1 -0
  48. package/dist/run/answered-calls.util.d.ts +10 -0
  49. package/dist/run/answered-calls.util.d.ts.map +1 -0
  50. package/dist/run/answered-calls.util.js +12 -0
  51. package/dist/run/answered-calls.util.js.map +1 -0
  52. package/dist/run/index.d.ts +2 -0
  53. package/dist/run/index.d.ts.map +1 -1
  54. package/dist/run/index.js +2 -0
  55. package/dist/run/index.js.map +1 -1
  56. package/dist/run/run-summary.util.d.ts +10 -3
  57. package/dist/run/run-summary.util.d.ts.map +1 -1
  58. package/dist/run/run-summary.util.js +18 -6
  59. package/dist/run/run-summary.util.js.map +1 -1
  60. package/dist/run/usage-aside.d.ts +23 -0
  61. package/dist/run/usage-aside.d.ts.map +1 -0
  62. package/dist/run/usage-aside.js +33 -0
  63. package/dist/run/usage-aside.js.map +1 -0
  64. package/dist/tools/tool-errors.middleware.d.ts +7 -1
  65. package/dist/tools/tool-errors.middleware.d.ts.map +1 -1
  66. package/dist/tools/tool-errors.middleware.js +81 -16
  67. package/dist/tools/tool-errors.middleware.js.map +1 -1
  68. package/dist/tools/tool-name.util.d.ts.map +1 -1
  69. package/dist/tools/tool-name.util.js +1 -3
  70. package/dist/tools/tool-name.util.js.map +1 -1
  71. package/lib/agent.ts +61 -15
  72. package/lib/constants.ts +8 -0
  73. package/lib/errors/index.ts +2 -0
  74. package/lib/errors/messages.ts +43 -3
  75. package/lib/errors/structured-output.error.ts +23 -0
  76. package/lib/errors/unwrap-middleware-error.util.ts +19 -0
  77. package/lib/index.ts +6 -1
  78. package/lib/interfaces/completed-run.interface.ts +1 -1
  79. package/lib/interfaces/run-options.interface.ts +21 -0
  80. package/lib/output/index.ts +5 -0
  81. package/lib/output/run-output.ts +154 -0
  82. package/lib/output/structured-output.middleware.ts +44 -0
  83. package/lib/run/answered-calls.util.ts +16 -0
  84. package/lib/run/index.ts +2 -0
  85. package/lib/run/run-summary.util.ts +33 -11
  86. package/lib/run/usage-aside.ts +41 -0
  87. package/lib/tools/tool-errors.middleware.ts +104 -10
  88. package/lib/tools/tool-name.util.ts +1 -4
  89. package/package.json +28 -9
@@ -0,0 +1,41 @@
1
+ import { UsageMetadata } from '@langchain/core/messages';
2
+
3
+ // Our own key in `configurable`, like the run's output. See #296.
4
+ const USAGE_ASIDE_KEY = 'nestjs-langchain:usage-aside';
5
+
6
+ /**
7
+ * The usage of the model's replies a run leaves out of its messages, which its
8
+ * `tokens` still count: those the `unknownTools` re-ask asks again about. One
9
+ * per run, handed to the middlewares in `configurable`. See #296.
10
+ *
11
+ * @internal
12
+ */
13
+ export class UsageAside {
14
+ readonly usage: UsageMetadata[] = [];
15
+
16
+ /** The `configurable` entry that hands it to the middlewares. */
17
+ get config(): Record<string, UsageAside> {
18
+ return { [USAGE_ASIDE_KEY]: this };
19
+ }
20
+
21
+ /**
22
+ * @param usage A left-out reply's usage, when the provider reported it.
23
+ */
24
+ add(usage: UsageMetadata | undefined): void {
25
+ if (usage) {
26
+ this.usage.push(usage);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * @param configurable A middleware's `runtime.configurable`.
32
+ * @returns The run's aside, when it set one.
33
+ */
34
+ static of(
35
+ configurable: Record<string, unknown> | undefined,
36
+ ): UsageAside | undefined {
37
+ const aside = configurable?.[USAGE_ASIDE_KEY];
38
+
39
+ return aside instanceof UsageAside ? aside : undefined;
40
+ }
41
+ }
@@ -1,10 +1,33 @@
1
+ import {
2
+ AIMessage,
3
+ BaseMessage,
4
+ ToolCall,
5
+ ToolMessage,
6
+ } from '@langchain/core/messages';
1
7
  import {
2
8
  createMiddleware,
3
9
  toolErrorMiddleware,
4
10
  ToolInvocationError,
5
11
  } from 'langchain';
6
- import { unknownTool } from '../errors/messages.js';
12
+ import { OUTPUT_TOOL, STRUCTURED_OUTPUT_PREFIX } from '../constants.js';
13
+ import { invalidTool, toolNotRun, unknownTool } from '../errors/messages.js';
7
14
  import { reportToolError, RunReporter } from '../logging/index.js';
15
+ import { answeredCalls, UsageAside } from '../run/index.js';
16
+
17
+ // The default of LangChain's own retry middlewares. See #151.
18
+ const MAX_REASKS = 2;
19
+
20
+ // LangChain routes every `extract-` call to structured output, never to the
21
+ // tools: one the model invents, which is not the run's output, neither runs
22
+ // nor gets an answer. See #264.
23
+ const isInvented = ({ name }: ToolCall): boolean =>
24
+ name.startsWith(STRUCTURED_OUTPUT_PREFIX) && name !== OUTPUT_TOOL;
25
+
26
+ const invented = (calls: readonly ToolCall[]): ToolCall[] =>
27
+ calls.filter(isInvented);
28
+
29
+ const callsOf = (message: BaseMessage | undefined): ToolCall[] =>
30
+ AIMessage.isInstance(message) ? (message.tool_calls ?? []) : [];
8
31
 
9
32
  /**
10
33
  * Turns a failed tool call into the text the model receives, LangChain's own,
@@ -48,21 +71,92 @@ export const toolErrors = (reporter: RunReporter) =>
48
71
  /**
49
72
  * Reports a call to a tool the agent does not have. LangChain's tool node
50
73
  * answers the model itself, without throwing, so `toolErrors` never sees it.
74
+ * One the model invents with the `extract-` prefix never reaches the tool
75
+ * node: it is answered here, as the tool node answers any other, and so is
76
+ * any call LangChain leaves unanswered.
77
+ *
78
+ * @param reporter The agent's reporter.
79
+ * @param tools The names of the agent's tools, which the answer lists.
51
80
  */
52
- export const unknownTools = (reporter: RunReporter) =>
53
- createMiddleware({
81
+ export const unknownTools = (reporter: RunReporter, tools: string[]) => {
82
+ const report = ({ name, id }: ToolCall) =>
83
+ reportToolError(reporter, {
84
+ tool: name,
85
+ callId: id ?? '',
86
+ reason: 'unknown-tool',
87
+ error: new Error(unknownTool()),
88
+ durationMs: 0,
89
+ });
90
+
91
+ const answer = (call: ToolCall) =>
92
+ new ToolMessage({
93
+ content: isInvented(call)
94
+ ? invalidTool(call.name, tools)
95
+ : toolNotRun(call.name),
96
+ tool_call_id: call.id ?? '',
97
+ name: call.name,
98
+ status: 'error',
99
+ });
100
+
101
+ // Every call without an answer gets one right after its message, which
102
+ // providers require: an invented one (see #264), or one LangChain left out
103
+ // next to the output (see #290). In the request, which the output's retry
104
+ // reaches without `beforeModel`; rebuilt on every model call, so only the
105
+ // reply just made is reported. See #297.
106
+ const answerLeft = (messages: BaseMessage[]): BaseMessage[] => {
107
+ const done = answeredCalls(messages);
108
+ const left = (message: BaseMessage | undefined) =>
109
+ callsOf(message).filter(({ id }) => id !== undefined && !done.has(id));
110
+
111
+ invented(left(messages.findLast((m) => AIMessage.isInstance(m)))).forEach(
112
+ report,
113
+ );
114
+
115
+ return messages.flatMap((message) => [
116
+ message,
117
+ ...left(message).map(answer),
118
+ ]);
119
+ };
120
+
121
+ return createMiddleware({
54
122
  name: 'unknownTools',
123
+ // Alone, LangChain would end the run: answered, and the model asked again,
124
+ // as `modelRetryMiddleware` asks it again. See #264.
125
+ wrapModelCall: async (request, handler) => {
126
+ let current = { ...request, messages: answerLeft(request.messages) };
127
+
128
+ for (let asked = 0; ; asked += 1) {
129
+ const reply = await handler(current);
130
+ const calls = callsOf(reply);
131
+ const left = invented(calls);
132
+
133
+ if (calls.length === 0 || left.length < calls.length) {
134
+ return reply;
135
+ }
136
+
137
+ left.forEach(report);
138
+
139
+ if (asked === MAX_REASKS) {
140
+ return reply;
141
+ }
142
+
143
+ // Left out of the run's messages, but paid for. See #296.
144
+ UsageAside.of(request.runtime.configurable)?.add(
145
+ AIMessage.isInstance(reply) ? reply.usage_metadata : undefined,
146
+ );
147
+
148
+ current = {
149
+ ...current,
150
+ messages: [...current.messages, reply, ...left.map(answer)],
151
+ };
152
+ }
153
+ },
55
154
  wrapToolCall: (request, handler) => {
56
155
  if (!request.tool) {
57
- reportToolError(reporter, {
58
- tool: request.toolCall.name,
59
- callId: request.toolCall.id ?? '',
60
- reason: 'unknown-tool',
61
- error: new Error(unknownTool()),
62
- durationMs: 0,
63
- });
156
+ report(request.toolCall);
64
157
  }
65
158
 
66
159
  return handler(request);
67
160
  },
68
161
  });
162
+ };
@@ -1,3 +1,4 @@
1
+ import { STRUCTURED_OUTPUT_PREFIX } from '../constants.js';
1
2
  import {
2
3
  duplicateToolName,
3
4
  invalidToolName,
@@ -8,10 +9,6 @@ import {
8
9
  // it. See #90.
9
10
  const TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
10
11
 
11
- // The agent routes a call with this prefix to structured output, never to the
12
- // tool. Restated: LangChain does not export it. See #213.
13
- const STRUCTURED_OUTPUT_PREFIX = 'extract-';
14
-
15
12
  export const resolveToolName = (
16
13
  declared: string | undefined,
17
14
  method: string,
package/package.json CHANGED
@@ -1,13 +1,32 @@
1
1
  {
2
2
  "name": "nestjs-langchain",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
- "description": "NestJS library to build agents with LangChain",
5
+ "description": "AI agents for NestJS, powered by LangChain: declare agents as modules, give them your services as tools.",
6
6
  "keywords": [
7
7
  "nestjs",
8
+ "nest",
9
+ "nestjs-module",
8
10
  "langchain",
11
+ "langchainjs",
12
+ "ai",
13
+ "agent",
9
14
  "agents",
10
- "ai"
15
+ "ai-agent",
16
+ "ai-agents",
17
+ "llm",
18
+ "tools",
19
+ "tool-calling",
20
+ "function-calling",
21
+ "structured-output",
22
+ "openai",
23
+ "anthropic",
24
+ "mistral",
25
+ "gemini",
26
+ "ollama",
27
+ "zod",
28
+ "decorators",
29
+ "typescript"
11
30
  ],
12
31
  "author": "Martin Bruel",
13
32
  "license": "MIT",
@@ -64,20 +83,20 @@
64
83
  },
65
84
  "devDependencies": {
66
85
  "@arethetypeswrong/cli": "^0.18.5",
67
- "@commitlint/cli": "21.2.3",
86
+ "@commitlint/cli": "21.2.4",
68
87
  "@commitlint/config-conventional": "^21.2.3",
69
88
  "@langchain/core": "^1.2.3",
70
89
  "@langchain/openai": "^1.2.10",
71
- "@nestjs/common": "^11.1.18",
72
- "@nestjs/core": "^11.1.18",
73
- "@nestjs/testing": "^11.1.18",
90
+ "@nestjs/common": "^12.0.1",
91
+ "@nestjs/core": "^12.0.1",
92
+ "@nestjs/testing": "^12.0.1",
74
93
  "@release-it/conventional-changelog": "12.0.2",
75
- "@types/node": "^25.1.0",
94
+ "@types/node": "^26.6.4",
76
95
  "@vitest/coverage-v8": "^4.1.11",
77
96
  "eslint-plugin-jsdoc": "^65.2.0",
78
97
  "husky": "9.1.7",
79
98
  "langchain": "^1.5.4",
80
- "lint-staged": "16.4.0",
99
+ "lint-staged": "17.6.0",
81
100
  "oxlint": "^1.83.0",
82
101
  "oxlint-tsgolint": "^7.0.2001",
83
102
  "prettier": "^3.8.1",