nestjs-langchain 1.0.2 → 2.0.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 (151) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +344 -254
  3. package/dist/agent.d.ts +63 -0
  4. package/dist/agent.d.ts.map +1 -0
  5. package/dist/agent.js +189 -0
  6. package/dist/agent.js.map +1 -0
  7. package/dist/constants.d.ts +5 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/constants.js +14 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/decorators/index.d.ts +4 -2
  12. package/dist/decorators/index.d.ts.map +1 -0
  13. package/dist/decorators/index.js +3 -18
  14. package/dist/decorators/index.js.map +1 -0
  15. package/dist/decorators/inject-agent.decorator.d.ts +12 -0
  16. package/dist/decorators/inject-agent.decorator.d.ts.map +1 -0
  17. package/dist/decorators/inject-agent.decorator.js +15 -7
  18. package/dist/decorators/inject-agent.decorator.js.map +1 -0
  19. package/dist/decorators/tool.decorator.d.ts +54 -6
  20. package/dist/decorators/tool.decorator.d.ts.map +1 -0
  21. package/dist/decorators/tool.decorator.js +40 -16
  22. package/dist/decorators/tool.decorator.js.map +1 -0
  23. package/dist/errors/agent-run.error.d.ts +27 -0
  24. package/dist/errors/agent-run.error.d.ts.map +1 -0
  25. package/dist/errors/agent-run.error.js +26 -0
  26. package/dist/errors/agent-run.error.js.map +1 -0
  27. package/dist/errors/index.d.ts +3 -0
  28. package/dist/errors/index.d.ts.map +1 -0
  29. package/dist/errors/index.js +3 -0
  30. package/dist/errors/index.js.map +1 -0
  31. package/dist/errors/messages.d.ts +26 -0
  32. package/dist/errors/messages.d.ts.map +1 -0
  33. package/dist/errors/messages.js +67 -0
  34. package/dist/errors/messages.js.map +1 -0
  35. package/dist/errors/tool-configuration.error.d.ts +23 -0
  36. package/dist/errors/tool-configuration.error.d.ts.map +1 -0
  37. package/dist/errors/tool-configuration.error.js +27 -0
  38. package/dist/errors/tool-configuration.error.js.map +1 -0
  39. package/dist/index.d.ts +10 -3
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +8 -19
  42. package/dist/index.js.map +1 -0
  43. package/dist/interfaces/agent-observer.interface.d.ts +107 -0
  44. package/dist/interfaces/agent-observer.interface.d.ts.map +1 -0
  45. package/dist/interfaces/agent-observer.interface.js +2 -0
  46. package/dist/interfaces/agent-observer.interface.js.map +1 -0
  47. package/dist/interfaces/completed-run.interface.d.ts +14 -0
  48. package/dist/interfaces/completed-run.interface.d.ts.map +1 -0
  49. package/dist/interfaces/completed-run.interface.js +2 -0
  50. package/dist/interfaces/completed-run.interface.js.map +1 -0
  51. package/dist/interfaces/langchain-module-options.interface.d.ts +73 -5
  52. package/dist/interfaces/langchain-module-options.interface.d.ts.map +1 -0
  53. package/dist/interfaces/langchain-module-options.interface.js +2 -2
  54. package/dist/interfaces/langchain-module-options.interface.js.map +1 -0
  55. package/dist/langchain.module-definition.d.ts +26 -6
  56. package/dist/langchain.module-definition.d.ts.map +1 -0
  57. package/dist/langchain.module-definition.js +26 -13
  58. package/dist/langchain.module-definition.js.map +1 -0
  59. package/dist/langchain.module.d.ts +60 -3
  60. package/dist/langchain.module.d.ts.map +1 -0
  61. package/dist/langchain.module.js +87 -24
  62. package/dist/langchain.module.js.map +1 -0
  63. package/dist/logging/index.d.ts +3 -0
  64. package/dist/logging/index.d.ts.map +1 -0
  65. package/dist/logging/index.js +3 -0
  66. package/dist/logging/index.js.map +1 -0
  67. package/dist/logging/observe-tool-call.util.d.ts +13 -0
  68. package/dist/logging/observe-tool-call.util.d.ts.map +1 -0
  69. package/dist/logging/observe-tool-call.util.js +34 -0
  70. package/dist/logging/observe-tool-call.util.js.map +1 -0
  71. package/dist/logging/run-reporter.d.ts +26 -0
  72. package/dist/logging/run-reporter.d.ts.map +1 -0
  73. package/dist/logging/run-reporter.js +39 -0
  74. package/dist/logging/run-reporter.js.map +1 -0
  75. package/dist/run/index.d.ts +3 -0
  76. package/dist/run/index.d.ts.map +1 -0
  77. package/dist/run/index.js +2 -0
  78. package/dist/run/index.js.map +1 -0
  79. package/dist/run/run-summary.util.d.ts +31 -0
  80. package/dist/run/run-summary.util.d.ts.map +1 -0
  81. package/dist/run/run-summary.util.js +28 -0
  82. package/dist/run/run-summary.util.js.map +1 -0
  83. package/dist/tools/index.d.ts +3 -0
  84. package/dist/tools/index.d.ts.map +1 -0
  85. package/dist/tools/index.js +3 -0
  86. package/dist/tools/index.js.map +1 -0
  87. package/dist/tools/tool-discovery.service.d.ts +31 -0
  88. package/dist/tools/tool-discovery.service.d.ts.map +1 -0
  89. package/dist/tools/tool-discovery.service.js +132 -0
  90. package/dist/tools/tool-discovery.service.js.map +1 -0
  91. package/dist/tools/tool-errors.middleware.d.ts +21 -0
  92. package/dist/tools/tool-errors.middleware.d.ts.map +1 -0
  93. package/dist/tools/tool-errors.middleware.js +55 -0
  94. package/dist/tools/tool-errors.middleware.js.map +1 -0
  95. package/dist/tools/tool-method.util.d.ts +21 -0
  96. package/dist/tools/tool-method.util.d.ts.map +1 -0
  97. package/dist/tools/tool-method.util.js +39 -0
  98. package/dist/tools/tool-method.util.js.map +1 -0
  99. package/dist/tools/tool-modules.util.d.ts +14 -0
  100. package/dist/tools/tool-modules.util.d.ts.map +1 -0
  101. package/dist/tools/tool-modules.util.js +61 -0
  102. package/dist/tools/tool-modules.util.js.map +1 -0
  103. package/dist/tools/tool-name.util.d.ts +10 -0
  104. package/dist/tools/tool-name.util.d.ts.map +1 -0
  105. package/dist/tools/tool-name.util.js +35 -0
  106. package/dist/tools/tool-name.util.js.map +1 -0
  107. package/dist/tools/tool-schema.factory.d.ts +4 -0
  108. package/dist/tools/tool-schema.factory.d.ts.map +1 -0
  109. package/dist/tools/tool-schema.factory.js +87 -0
  110. package/dist/tools/tool-schema.factory.js.map +1 -0
  111. package/dist/tools/tool.factory.d.ts +9 -0
  112. package/dist/tools/tool.factory.d.ts.map +1 -0
  113. package/dist/tools/tool.factory.js +34 -0
  114. package/dist/tools/tool.factory.js.map +1 -0
  115. package/lib/agent.ts +257 -0
  116. package/lib/constants.ts +15 -0
  117. package/lib/decorators/index.ts +3 -0
  118. package/lib/decorators/inject-agent.decorator.ts +15 -0
  119. package/lib/decorators/tool.decorator.ts +86 -0
  120. package/lib/errors/agent-run.error.ts +29 -0
  121. package/lib/errors/index.ts +2 -0
  122. package/lib/errors/messages.ts +158 -0
  123. package/lib/errors/tool-configuration.error.ts +24 -0
  124. package/lib/index.ts +26 -0
  125. package/lib/interfaces/agent-observer.interface.ts +113 -0
  126. package/lib/interfaces/completed-run.interface.ts +14 -0
  127. package/lib/interfaces/langchain-module-options.interface.ts +91 -0
  128. package/lib/langchain.module-definition.ts +36 -0
  129. package/lib/langchain.module.ts +138 -0
  130. package/lib/logging/index.ts +2 -0
  131. package/lib/logging/observe-tool-call.util.ts +59 -0
  132. package/lib/logging/run-reporter.ts +56 -0
  133. package/lib/run/index.ts +2 -0
  134. package/lib/run/run-summary.util.ts +51 -0
  135. package/lib/tools/index.ts +2 -0
  136. package/lib/tools/tool-discovery.service.ts +191 -0
  137. package/lib/tools/tool-errors.middleware.ts +68 -0
  138. package/lib/tools/tool-method.util.ts +81 -0
  139. package/lib/tools/tool-modules.util.ts +104 -0
  140. package/lib/tools/tool-name.util.ts +57 -0
  141. package/lib/tools/tool-schema.factory.ts +142 -0
  142. package/lib/tools/tool.factory.ts +56 -0
  143. package/package.json +112 -91
  144. package/dist/factories/message.factory.d.ts +0 -7
  145. package/dist/factories/message.factory.js +0 -28
  146. package/dist/langchain.service.d.ts +0 -18
  147. package/dist/langchain.service.js +0 -69
  148. package/dist/tool-discovery.service.d.ts +0 -11
  149. package/dist/tool-discovery.service.js +0 -79
  150. package/dist/types/message.d.ts +0 -69
  151. package/dist/types/message.js +0 -104
@@ -0,0 +1,191 @@
1
+ import { Injectable, Scope, Type } from '@nestjs/common';
2
+ // From `langchain`, not `@langchain/core`: core's declaration gives an
3
+ // identity `createAgent` rejects. See #52.
4
+ import { DynamicStructuredTool } from 'langchain';
5
+ import {
6
+ DiscoveryService,
7
+ MetadataScanner,
8
+ ModulesContainer,
9
+ } from '@nestjs/core';
10
+ import { ToolConfigurationError } from '../errors/index.js';
11
+ import {
12
+ toolModuleWithoutTools,
13
+ toolOnNonSingleton,
14
+ } from '../errors/messages.js';
15
+ import { ToolModule } from '../interfaces/langchain-module-options.interface.js';
16
+ import { RunReporter } from '../logging/index.js';
17
+ import { buildTool } from './tool.factory.js';
18
+ import { readToolMethod, ToolMethod } from './tool-method.util.js';
19
+ import { findDuplicateToolNames } from './tool-name.util.js';
20
+ import { resolveToolModules } from './tool-modules.util.js';
21
+
22
+ /**
23
+ * Maps each item, skipping `undefined`. An item that throws adds its message
24
+ * to `problems` rather than stopping the others, so one bad tool does not hide
25
+ * the next.
26
+ */
27
+ const collect = <T, R>(
28
+ items: T[],
29
+ map: (item: T) => R | undefined,
30
+ problems: string[],
31
+ ): R[] =>
32
+ items.flatMap((item) => {
33
+ try {
34
+ const result = map(item);
35
+ return result === undefined ? [] : [result];
36
+ } catch (error) {
37
+ problems.push(error instanceof Error ? error.message : String(error));
38
+ return [];
39
+ }
40
+ });
41
+
42
+ type ProviderWrapper = ReturnType<DiscoveryService['getProviders']>[number];
43
+
44
+ /**
45
+ * Why a provider is not a singleton, or `undefined` when it is. An agent is
46
+ * one, and a provider built per request or per consumer leaves only an
47
+ * unconstructed placeholder in `instance`. See #127.
48
+ */
49
+ const nonSingletonScope = (
50
+ wrapper: ProviderWrapper,
51
+ ): 'request' | 'transient' | 'request-dependency' | undefined => {
52
+ // `TRANSIENT` reports a static dependency tree.
53
+ if (wrapper.isTransient) {
54
+ return 'transient';
55
+ }
56
+
57
+ if (wrapper.isDependencyTreeStatic()) {
58
+ return undefined;
59
+ }
60
+
61
+ return wrapper.scope === Scope.REQUEST ? 'request' : 'request-dependency';
62
+ };
63
+
64
+ /** Finds the `@Tool()` methods of an agent's tool modules and builds their tools. */
65
+ @Injectable()
66
+ export class ToolDiscoveryService {
67
+ constructor(
68
+ private readonly discoveryService: DiscoveryService,
69
+ private readonly metadataScanner: MetadataScanner,
70
+ private readonly modulesContainer: ModulesContainer,
71
+ ) {}
72
+
73
+ private modulesInContext(): Map<Type, number> {
74
+ const modules = new Map<Type, number>();
75
+
76
+ this.modulesContainer.forEach(({ metatype }) =>
77
+ modules.set(metatype, (modules.get(metatype) ?? 0) + 1),
78
+ );
79
+
80
+ return modules;
81
+ }
82
+
83
+ /**
84
+ * Every `@Tool()` method of the providers that `modules` host. Adds to
85
+ * `declaring` each module declaring one, accepted or not.
86
+ */
87
+ private toolMethods(
88
+ modules: Set<Type>,
89
+ declaring: Set<Type>,
90
+ problems: string[],
91
+ ): ToolMethod[] {
92
+ return this.discoveryService.getProviders().flatMap((wrapper) => {
93
+ const { instance, host } = wrapper;
94
+
95
+ // A `useValue` primitive carries no decorator, nor metadata to read.
96
+ if (
97
+ typeof instance !== 'object' ||
98
+ !instance ||
99
+ !host ||
100
+ !modules.has(host.metatype)
101
+ ) {
102
+ return [];
103
+ }
104
+
105
+ const reported = problems.length;
106
+ const methods = collect(
107
+ this.metadataScanner.getAllMethodNames(Object.getPrototypeOf(instance)),
108
+ (method) => readToolMethod(instance, method),
109
+ problems,
110
+ );
111
+
112
+ // A method already reported as a problem is one this module declares.
113
+ if (methods.length > 0 || problems.length > reported) {
114
+ declaring.add(host.metatype);
115
+ }
116
+
117
+ const scope = nonSingletonScope(wrapper);
118
+
119
+ if (scope && methods.length > 0) {
120
+ problems.push(
121
+ toolOnNonSingleton(
122
+ instance.constructor.name,
123
+ scope,
124
+ methods.map(({ method }) => method),
125
+ ),
126
+ );
127
+ return [];
128
+ }
129
+
130
+ return methods;
131
+ });
132
+ }
133
+
134
+ /** One tool per method, each reporting its calls to `reporter`. */
135
+ private buildTools(
136
+ methods: ToolMethod[],
137
+ reporter: RunReporter,
138
+ problems: string[],
139
+ ): { tool: DynamicStructuredTool; where: string }[] {
140
+ return collect(
141
+ methods,
142
+ (method) => ({ tool: buildTool(method, reporter), where: method.where }),
143
+ problems,
144
+ );
145
+ }
146
+
147
+ /**
148
+ * The tools of the modules listed in an agent's `tools` option, each one
149
+ * reporting its calls to `reporter`. Throws a `ToolConfigurationError`
150
+ * listing every problem found: a module that is not imported, imported
151
+ * several times or declaring no tool, a name or a schema that is invalid,
152
+ * two tools sharing a name.
153
+ *
154
+ * Async because an entry may be a `Promise<DynamicModule>`, which Nest's
155
+ * own `imports` accepts.
156
+ */
157
+ async getToolsFromModules(
158
+ entries: ToolModule[],
159
+ reporter: RunReporter,
160
+ ): Promise<DynamicStructuredTool[]> {
161
+ // By constructor, not `host.name`: two modules may share a class name.
162
+ // See #58.
163
+ const { modules, problems } = await resolveToolModules(
164
+ entries,
165
+ this.modulesInContext(),
166
+ );
167
+
168
+ const declaring = new Set<Type>();
169
+ const methods = this.toolMethods(modules, declaring, problems);
170
+
171
+ modules.forEach((module) => {
172
+ if (!declaring.has(module)) {
173
+ problems.push(toolModuleWithoutTools(module.name));
174
+ }
175
+ });
176
+
177
+ const built = this.buildTools(methods, reporter, problems);
178
+ // Per agent: the same name in two agents is legitimate.
179
+ problems.push(
180
+ ...findDuplicateToolNames(
181
+ built.map(({ tool, where }) => ({ name: tool.name, where })),
182
+ ),
183
+ );
184
+
185
+ if (problems.length > 0) {
186
+ throw new ToolConfigurationError(problems, reporter.agent);
187
+ }
188
+
189
+ return built.map(({ tool }) => tool);
190
+ }
191
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ createMiddleware,
3
+ toolErrorMiddleware,
4
+ ToolInvocationError,
5
+ } from 'langchain';
6
+ import { unknownTool } from '../errors/messages.js';
7
+ import { reportToolError, RunReporter } from '../logging/index.js';
8
+
9
+ /**
10
+ * Turns a failed tool call into the text the model receives, LangChain's own,
11
+ * without the stack (and its server paths) LangChain puts in a schema failure.
12
+ * Every error is answered: past a middleware, LangChain's tool node rethrows
13
+ * any error left unhandled and fails the run. See #176.
14
+ *
15
+ * Also the only place that sees arguments the schema rejects, which never
16
+ * reach the `@Tool` wrapper: they are reported here. See #177.
17
+ *
18
+ * Once langchain-ai/langchainjs#11830 ships, LangChain's own text has no stack
19
+ * and `tool-errors.middleware.spec.ts` fails: the schema text can be left to
20
+ * LangChain, the reporting stays.
21
+ */
22
+ export const toolErrors = (reporter: RunReporter) =>
23
+ toolErrorMiddleware({
24
+ onError: (error) => {
25
+ if (!ToolInvocationError.isInstance(error)) {
26
+ // LangChain's default for a method that throws.
27
+ return `${String(error)}\n Please fix your mistakes.`;
28
+ }
29
+
30
+ const { toolCall, toolError } = error;
31
+
32
+ reportToolError(reporter, {
33
+ tool: toolCall.name,
34
+ callId: toolCall.id ?? '',
35
+ reason: 'invalid-arguments',
36
+ error: toolError,
37
+ durationMs: 0,
38
+ });
39
+
40
+ return (
41
+ `Error invoking tool '${toolCall.name}' with kwargs ` +
42
+ `${JSON.stringify(toolCall.args)} with error: ${String(toolError)}\n` +
43
+ ' Please fix the error and try again.'
44
+ );
45
+ },
46
+ });
47
+
48
+ /**
49
+ * Reports a call to a tool the agent does not have. LangChain's tool node
50
+ * answers the model itself, without throwing, so `toolErrors` never sees it.
51
+ */
52
+ export const unknownTools = (reporter: RunReporter) =>
53
+ createMiddleware({
54
+ name: 'unknownTools',
55
+ wrapToolCall: (request, handler) => {
56
+ 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
+ });
64
+ }
65
+
66
+ return handler(request);
67
+ },
68
+ });
@@ -0,0 +1,81 @@
1
+ import {
2
+ PARAM_TYPES_METADATA,
3
+ TOOL_METADATA,
4
+ TOOL_PARAMS_METADATA,
5
+ } from '../constants.js';
6
+ import {
7
+ ToolOptions,
8
+ ToolParamMetadata,
9
+ } from '../decorators/tool.decorator.js';
10
+ import { toolParamWithoutTool } from '../errors/messages.js';
11
+
12
+ type Provider = Record<string, (...args: unknown[]) => unknown>;
13
+
14
+ /** A provider method decorated with `@Tool()`, and what its decorators recorded. */
15
+ export interface ToolMethod {
16
+ /** `MathService.add`, to name the method in error messages. */
17
+ where: string;
18
+ /** The method name, the tool's name when `options` gives none. */
19
+ method: string;
20
+ options: ToolOptions;
21
+ /** The `@ToolParam()` metadata, in declaration order. */
22
+ params: ToolParamMetadata[];
23
+ /** The declared parameter types, from TypeScript's `design:paramtypes`. */
24
+ paramTypes: unknown[];
25
+ /** Calls the method on its provider, with its arguments by position. */
26
+ call: (args: unknown[]) => unknown;
27
+ }
28
+
29
+ // The prototype that defines `method`: its metadata belongs to the function
30
+ // `instance[method]` resolves to, the one carrying `@Tool()` or not.
31
+ const definingPrototype = (instance: object, method: string): object => {
32
+ let prototype: object | null = Object.getPrototypeOf(instance);
33
+
34
+ while (
35
+ prototype &&
36
+ !Object.prototype.hasOwnProperty.call(prototype, method)
37
+ ) {
38
+ prototype = Object.getPrototypeOf(prototype);
39
+ }
40
+
41
+ return prototype ?? instance;
42
+ };
43
+
44
+ /**
45
+ * Reads `method` of a provider instance, or `undefined` if it is not a
46
+ * `@Tool()`. Throws when it carries a `@ToolParam()` but no `@Tool()`.
47
+ */
48
+ export const readToolMethod = (
49
+ instance: object,
50
+ method: string,
51
+ ): ToolMethod | undefined => {
52
+ const provider = instance as Provider;
53
+ const options: ToolOptions | undefined = Reflect.getMetadata(
54
+ TOOL_METADATA,
55
+ provider[method],
56
+ );
57
+
58
+ const owner = definingPrototype(instance, method);
59
+ const declared: ToolParamMetadata[] =
60
+ Reflect.getOwnMetadata(TOOL_PARAMS_METADATA, owner, method) ?? [];
61
+ const where = `${instance.constructor.name}.${method}`;
62
+
63
+ if (!options) {
64
+ if (declared.length > 0) {
65
+ throw new Error(toolParamWithoutTool(where));
66
+ }
67
+
68
+ return undefined;
69
+ }
70
+
71
+ return {
72
+ where,
73
+ method,
74
+ options,
75
+ // Parameter decorators run right to left.
76
+ params: [...declared].sort((a, b) => a.index - b.index),
77
+ paramTypes:
78
+ Reflect.getOwnMetadata(PARAM_TYPES_METADATA, owner, method) ?? [],
79
+ call: (args) => provider[method](...args),
80
+ };
81
+ };
@@ -0,0 +1,104 @@
1
+ import { DynamicModule, ForwardReference, Type } from '@nestjs/common';
2
+ import { MODULE_METADATA_KEYS } from '../constants.js';
3
+ import {
4
+ notAToolModule,
5
+ severalToolModuleInstances,
6
+ toolModuleNotImported,
7
+ } from '../errors/messages.js';
8
+ import { ToolModule } from '../interfaces/langchain-module-options.interface.js';
9
+
10
+ // `@Module({})` writes no metadata at all, so an unimported empty module and a
11
+ // plain class are indistinguishable. Only used to sharpen the message.
12
+ const looksLikeAModule = (entry: Type): boolean =>
13
+ MODULE_METADATA_KEYS.some(
14
+ (key) => Reflect.getMetadata(key, entry) !== undefined,
15
+ );
16
+
17
+ interface Resolved {
18
+ type?: Type;
19
+ // A `{ module: Class }` entry says it is a module whatever its metadata
20
+ // holds, which a `@Module({})` host of a dynamic module never holds.
21
+ declaresItself: boolean;
22
+ }
23
+
24
+ /**
25
+ * Reduce an entry to the class Nest registered it under, the way the scanner
26
+ * resolves `imports`: await it, call a forward reference, take `module` off a
27
+ * dynamic module.
28
+ */
29
+ const toModuleClass = async (entry: ToolModule): Promise<Resolved> => {
30
+ const awaited: unknown = await entry;
31
+
32
+ if (typeof awaited === 'function') {
33
+ return { type: awaited as Type, declaresItself: false };
34
+ }
35
+
36
+ if (typeof awaited !== 'object' || awaited === null) {
37
+ return { declaresItself: false };
38
+ }
39
+
40
+ const reference = awaited as Partial<ForwardReference<() => ToolModule>>;
41
+
42
+ if (typeof reference.forwardRef === 'function') {
43
+ return toModuleClass(reference.forwardRef());
44
+ }
45
+
46
+ const dynamic = awaited as Partial<DynamicModule>;
47
+
48
+ if (!dynamic.module) {
49
+ return { declaresItself: false };
50
+ }
51
+
52
+ return { ...(await toModuleClass(dynamic.module)), declaresItself: true };
53
+ };
54
+
55
+ // Only reached when the entry resolved to no class at all.
56
+ const describeEntry = (entry: unknown): string =>
57
+ entry === null || entry === undefined
58
+ ? String(entry)
59
+ : `an entry of type ${typeof entry}`;
60
+
61
+ interface ToolModules {
62
+ modules: Set<Type>;
63
+ problems: string[];
64
+ }
65
+
66
+ /**
67
+ * Split the `tools` entries into the modules discovery can match and the
68
+ * problems to report. `inContext` counts the modules Nest registered per
69
+ * class: one per plain import, one per dynamic module.
70
+ */
71
+ export const resolveToolModules = async (
72
+ entries: readonly ToolModule[],
73
+ inContext: ReadonlyMap<Type, number>,
74
+ ): Promise<ToolModules> => {
75
+ const modules = new Set<Type>();
76
+ const problems: string[] = [];
77
+
78
+ for (const entry of entries) {
79
+ // A circular import resolves to `undefined` here rather than to a class.
80
+ const { type, declaresItself } = await toModuleClass(entry);
81
+ const instances = type ? (inContext.get(type) ?? 0) : 0;
82
+
83
+ if (type && instances === 1) {
84
+ modules.add(type);
85
+ continue;
86
+ }
87
+
88
+ // Tools are matched by class, which would merge the instances' tools.
89
+ // See #234.
90
+ if (type && instances > 1) {
91
+ problems.push(severalToolModuleInstances(type.name, instances));
92
+ continue;
93
+ }
94
+
95
+ const name = type?.name ?? describeEntry(entry);
96
+ const isModule = type && (declaresItself || looksLikeAModule(type));
97
+
98
+ problems.push(
99
+ isModule ? toolModuleNotImported(name) : notAToolModule(name),
100
+ );
101
+ }
102
+
103
+ return { modules, problems };
104
+ };
@@ -0,0 +1,57 @@
1
+ import {
2
+ duplicateToolName,
3
+ invalidToolName,
4
+ reservedToolName,
5
+ } from '../errors/messages.js';
6
+
7
+ // OpenAI's limit, the strictest published, restated: no provider SDK exports
8
+ // it. See #90.
9
+ const TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
10
+
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
+ export const resolveToolName = (
16
+ declared: string | undefined,
17
+ method: string,
18
+ where: string,
19
+ ): string => {
20
+ const name = declared ?? method;
21
+
22
+ if (!TOOL_NAME.test(name)) {
23
+ throw new Error(
24
+ invalidToolName(where, name, String(TOOL_NAME), declared === undefined),
25
+ );
26
+ }
27
+
28
+ if (name.startsWith(STRUCTURED_OUTPUT_PREFIX)) {
29
+ throw new Error(reservedToolName(where, name, STRUCTURED_OUTPUT_PREFIX));
30
+ }
31
+
32
+ return name;
33
+ };
34
+
35
+ /**
36
+ * One problem per tool whose name an earlier tool already took, naming both
37
+ * methods. `where` is the method each tool was built from.
38
+ */
39
+ export const findDuplicateToolNames = (
40
+ tools: { name: string; where: string }[],
41
+ ): string[] => {
42
+ const seen = new Map<string, string>();
43
+ const problems: string[] = [];
44
+
45
+ tools.forEach(({ name, where }) => {
46
+ const first = seen.get(name);
47
+
48
+ if (first) {
49
+ problems.push(duplicateToolName(name, first, where));
50
+ return;
51
+ }
52
+
53
+ seen.set(name, where);
54
+ });
55
+
56
+ return problems;
57
+ };
@@ -0,0 +1,142 @@
1
+ import { Type } from '@nestjs/common';
2
+ import { toJsonSchema } from '@langchain/core/utils/json_schema';
3
+ import z, { ZodObject, ZodType } from 'zod';
4
+ import { ToolParamMetadata } from '../decorators/tool.decorator.js';
5
+ import {
6
+ cannotInferSchema,
7
+ duplicateParamName,
8
+ noJsonSchemaForm,
9
+ schemaContradictsSignature,
10
+ } from '../errors/messages.js';
11
+ import { messageOf } from '../logging/index.js';
12
+
13
+ const INFERRED = new Map<unknown, () => ZodType>([
14
+ [String, () => z.string()],
15
+ [Number, () => z.number()],
16
+ [Boolean, () => z.boolean()],
17
+ ]);
18
+
19
+ // The JSON Schema types each primitive signature accepts, the first naming it.
20
+ // `integer` is a subset of `number`.
21
+ const JSON_SCHEMA_TYPES = new Map<unknown, string[]>([
22
+ [String, ['string']],
23
+ [Number, ['number', 'integer']],
24
+ [Boolean, ['boolean']],
25
+ ]);
26
+
27
+ const resolveSchema = (
28
+ param: ToolParamMetadata,
29
+ paramType: unknown,
30
+ where: string,
31
+ ): ZodType => {
32
+ if (param.schema) {
33
+ return param.schema;
34
+ }
35
+
36
+ const inferred = INFERRED.get(paramType);
37
+
38
+ if (!inferred) {
39
+ const declared = (paramType as Type | undefined)?.name ?? 'unknown';
40
+
41
+ throw new Error(cannotInferSchema(where, param.name, declared));
42
+ }
43
+
44
+ return inferred();
45
+ };
46
+
47
+ interface JsonSchemaNode {
48
+ type?: string | string[];
49
+ anyOf?: JsonSchemaNode[];
50
+ }
51
+
52
+ // The JSON Schema types a schema allows, from its `type` or from the branches
53
+ // of its `anyOf`: zod 4 gives a union either form (see #137). `undefined` when
54
+ // it has no JSON Schema form, or a branch names no type.
55
+ const typesOf = (schema: ZodType): string[] | undefined => {
56
+ let node: JsonSchemaNode;
57
+
58
+ try {
59
+ node = z.toJSONSchema(schema);
60
+ } catch {
61
+ return undefined;
62
+ }
63
+
64
+ const branches = node.type === undefined && node.anyOf ? node.anyOf : [node];
65
+
66
+ if (branches.some((branch) => branch.type === undefined)) {
67
+ return undefined;
68
+ }
69
+
70
+ return branches.flatMap((branch) => branch.type ?? []);
71
+ };
72
+
73
+ // A declared schema LangChain cannot convert to the JSON Schema a provider
74
+ // receives. Wrapped in an object, as LangChain converts a tool's schema: only
75
+ // there does it read the input side of a `.transform()`.
76
+ const checkJsonSchemaForm = (
77
+ param: ToolParamMetadata,
78
+ schema: ZodType,
79
+ where: string,
80
+ ): void => {
81
+ try {
82
+ toJsonSchema(z.object({ [param.name]: schema }));
83
+ } catch (error) {
84
+ throw new Error(noJsonSchemaForm(where, param.name, messageOf(error)));
85
+ }
86
+ };
87
+
88
+ // A declared schema that contradicts a primitive signature. Skipped for any
89
+ // other signature, where the schema is the only source of truth.
90
+ const checkAgainstSignature = (
91
+ param: ToolParamMetadata,
92
+ paramType: unknown,
93
+ schema: ZodType,
94
+ where: string,
95
+ ): void => {
96
+ const accepted = JSON_SCHEMA_TYPES.get(paramType);
97
+ const types = accepted && typesOf(schema);
98
+
99
+ if (!accepted || !types || types.some((type) => accepted.includes(type))) {
100
+ return;
101
+ }
102
+
103
+ throw new Error(
104
+ schemaContradictsSignature(where, param.name, types, accepted[0]),
105
+ );
106
+ };
107
+
108
+ export const buildToolSchema = (
109
+ params: ToolParamMetadata[],
110
+ paramTypes: unknown[],
111
+ where: string,
112
+ ): ZodObject => {
113
+ const shape: Record<string, ZodType> = {};
114
+ const indexes = new Map<string, number>();
115
+
116
+ params.forEach((param) => {
117
+ const first = indexes.get(param.name);
118
+
119
+ if (first !== undefined) {
120
+ throw new Error(
121
+ duplicateParamName(where, param.name, first, param.index),
122
+ );
123
+ }
124
+
125
+ indexes.set(param.name, param.index);
126
+
127
+ const resolved = resolveSchema(param, paramTypes[param.index], where);
128
+
129
+ if (param.schema) {
130
+ checkJsonSchemaForm(param, resolved, where);
131
+ checkAgainstSignature(param, paramTypes[param.index], resolved, where);
132
+ }
133
+
134
+ const described = param.description
135
+ ? resolved.describe(param.description)
136
+ : resolved;
137
+
138
+ shape[param.name] = param.optional ? described.optional() : described;
139
+ });
140
+
141
+ return z.object(shape);
142
+ };
@@ -0,0 +1,56 @@
1
+ // From `langchain`, not `@langchain/core`: core's declaration gives an
2
+ // identity `createAgent` rejects. See #52.
3
+ import { DynamicStructuredTool } from 'langchain';
4
+ import { ToolParamMetadata } from '../decorators/tool.decorator.js';
5
+ import { observeToolCall, RunReporter } from '../logging/index.js';
6
+ import { buildToolSchema } from './tool-schema.factory.js';
7
+ import { resolveToolName } from './tool-name.util.js';
8
+ import { ToolMethod } from './tool-method.util.js';
9
+
10
+ // What the agent's tool node adds to the config. Structural, see #52.
11
+ interface ToolCallConfig {
12
+ toolCall?: { id?: string };
13
+ }
14
+
15
+ // Placed by index: an omitted optional leaves a hole rather than shifting the
16
+ // arguments after it.
17
+ const toPositional = (
18
+ params: ToolParamMetadata[],
19
+ values: Record<string, unknown>,
20
+ ): unknown[] => {
21
+ const args: unknown[] = [];
22
+
23
+ params.forEach((param) => {
24
+ args[param.index] = values[param.name];
25
+ });
26
+
27
+ return args;
28
+ };
29
+
30
+ /**
31
+ * Builds the LangChain tool that calls a `@Tool()` method and reports each
32
+ * call to `reporter`. Throws when the name or a parameter schema is invalid.
33
+ */
34
+ export const buildTool = (
35
+ { where, method, options, params, paramTypes, call }: ToolMethod,
36
+ reporter: RunReporter,
37
+ ): DynamicStructuredTool => {
38
+ const name = resolveToolName(options.name, method, where);
39
+
40
+ return new DynamicStructuredTool({
41
+ name,
42
+ description: options.description,
43
+ schema: buildToolSchema(params, paramTypes, where),
44
+ func: (values: Record<string, unknown>, _runManager, config) =>
45
+ observeToolCall(
46
+ reporter,
47
+ {
48
+ tool: name,
49
+ // Empty only for a tool invoked outside an agent.
50
+ callId: (config as ToolCallConfig | undefined)?.toolCall?.id ?? '',
51
+ args: values,
52
+ },
53
+ () => call(toPositional(params, values)),
54
+ ),
55
+ });
56
+ };