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.
- package/LICENSE +21 -21
- package/README.md +344 -254
- package/dist/agent.d.ts +63 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +189 -0
- package/dist/agent.js.map +1 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +14 -0
- package/dist/constants.js.map +1 -0
- package/dist/decorators/index.d.ts +4 -2
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +3 -18
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/inject-agent.decorator.d.ts +12 -0
- package/dist/decorators/inject-agent.decorator.d.ts.map +1 -0
- package/dist/decorators/inject-agent.decorator.js +15 -7
- package/dist/decorators/inject-agent.decorator.js.map +1 -0
- package/dist/decorators/tool.decorator.d.ts +54 -6
- package/dist/decorators/tool.decorator.d.ts.map +1 -0
- package/dist/decorators/tool.decorator.js +40 -16
- package/dist/decorators/tool.decorator.js.map +1 -0
- package/dist/errors/agent-run.error.d.ts +27 -0
- package/dist/errors/agent-run.error.d.ts.map +1 -0
- package/dist/errors/agent-run.error.js +26 -0
- package/dist/errors/agent-run.error.js.map +1 -0
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +3 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/messages.d.ts +26 -0
- package/dist/errors/messages.d.ts.map +1 -0
- package/dist/errors/messages.js +67 -0
- package/dist/errors/messages.js.map +1 -0
- package/dist/errors/tool-configuration.error.d.ts +23 -0
- package/dist/errors/tool-configuration.error.d.ts.map +1 -0
- package/dist/errors/tool-configuration.error.js +27 -0
- package/dist/errors/tool-configuration.error.js.map +1 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -19
- package/dist/index.js.map +1 -0
- package/dist/interfaces/agent-observer.interface.d.ts +107 -0
- package/dist/interfaces/agent-observer.interface.d.ts.map +1 -0
- package/dist/interfaces/agent-observer.interface.js +2 -0
- package/dist/interfaces/agent-observer.interface.js.map +1 -0
- package/dist/interfaces/completed-run.interface.d.ts +14 -0
- package/dist/interfaces/completed-run.interface.d.ts.map +1 -0
- package/dist/interfaces/completed-run.interface.js +2 -0
- package/dist/interfaces/completed-run.interface.js.map +1 -0
- package/dist/interfaces/langchain-module-options.interface.d.ts +73 -5
- package/dist/interfaces/langchain-module-options.interface.d.ts.map +1 -0
- package/dist/interfaces/langchain-module-options.interface.js +2 -2
- package/dist/interfaces/langchain-module-options.interface.js.map +1 -0
- package/dist/langchain.module-definition.d.ts +26 -6
- package/dist/langchain.module-definition.d.ts.map +1 -0
- package/dist/langchain.module-definition.js +26 -13
- package/dist/langchain.module-definition.js.map +1 -0
- package/dist/langchain.module.d.ts +60 -3
- package/dist/langchain.module.d.ts.map +1 -0
- package/dist/langchain.module.js +87 -24
- package/dist/langchain.module.js.map +1 -0
- package/dist/logging/index.d.ts +3 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +3 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/observe-tool-call.util.d.ts +13 -0
- package/dist/logging/observe-tool-call.util.d.ts.map +1 -0
- package/dist/logging/observe-tool-call.util.js +34 -0
- package/dist/logging/observe-tool-call.util.js.map +1 -0
- package/dist/logging/run-reporter.d.ts +26 -0
- package/dist/logging/run-reporter.d.ts.map +1 -0
- package/dist/logging/run-reporter.js +39 -0
- package/dist/logging/run-reporter.js.map +1 -0
- package/dist/run/index.d.ts +3 -0
- package/dist/run/index.d.ts.map +1 -0
- package/dist/run/index.js +2 -0
- package/dist/run/index.js.map +1 -0
- package/dist/run/run-summary.util.d.ts +31 -0
- package/dist/run/run-summary.util.d.ts.map +1 -0
- package/dist/run/run-summary.util.js +28 -0
- package/dist/run/run-summary.util.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/tool-discovery.service.d.ts +31 -0
- package/dist/tools/tool-discovery.service.d.ts.map +1 -0
- package/dist/tools/tool-discovery.service.js +132 -0
- package/dist/tools/tool-discovery.service.js.map +1 -0
- package/dist/tools/tool-errors.middleware.d.ts +21 -0
- package/dist/tools/tool-errors.middleware.d.ts.map +1 -0
- package/dist/tools/tool-errors.middleware.js +55 -0
- package/dist/tools/tool-errors.middleware.js.map +1 -0
- package/dist/tools/tool-method.util.d.ts +21 -0
- package/dist/tools/tool-method.util.d.ts.map +1 -0
- package/dist/tools/tool-method.util.js +39 -0
- package/dist/tools/tool-method.util.js.map +1 -0
- package/dist/tools/tool-modules.util.d.ts +14 -0
- package/dist/tools/tool-modules.util.d.ts.map +1 -0
- package/dist/tools/tool-modules.util.js +61 -0
- package/dist/tools/tool-modules.util.js.map +1 -0
- package/dist/tools/tool-name.util.d.ts +10 -0
- package/dist/tools/tool-name.util.d.ts.map +1 -0
- package/dist/tools/tool-name.util.js +35 -0
- package/dist/tools/tool-name.util.js.map +1 -0
- package/dist/tools/tool-schema.factory.d.ts +4 -0
- package/dist/tools/tool-schema.factory.d.ts.map +1 -0
- package/dist/tools/tool-schema.factory.js +87 -0
- package/dist/tools/tool-schema.factory.js.map +1 -0
- package/dist/tools/tool.factory.d.ts +9 -0
- package/dist/tools/tool.factory.d.ts.map +1 -0
- package/dist/tools/tool.factory.js +34 -0
- package/dist/tools/tool.factory.js.map +1 -0
- package/lib/agent.ts +257 -0
- package/lib/constants.ts +15 -0
- package/lib/decorators/index.ts +3 -0
- package/lib/decorators/inject-agent.decorator.ts +15 -0
- package/lib/decorators/tool.decorator.ts +86 -0
- package/lib/errors/agent-run.error.ts +29 -0
- package/lib/errors/index.ts +2 -0
- package/lib/errors/messages.ts +158 -0
- package/lib/errors/tool-configuration.error.ts +24 -0
- package/lib/index.ts +26 -0
- package/lib/interfaces/agent-observer.interface.ts +113 -0
- package/lib/interfaces/completed-run.interface.ts +14 -0
- package/lib/interfaces/langchain-module-options.interface.ts +91 -0
- package/lib/langchain.module-definition.ts +36 -0
- package/lib/langchain.module.ts +138 -0
- package/lib/logging/index.ts +2 -0
- package/lib/logging/observe-tool-call.util.ts +59 -0
- package/lib/logging/run-reporter.ts +56 -0
- package/lib/run/index.ts +2 -0
- package/lib/run/run-summary.util.ts +51 -0
- package/lib/tools/index.ts +2 -0
- package/lib/tools/tool-discovery.service.ts +191 -0
- package/lib/tools/tool-errors.middleware.ts +68 -0
- package/lib/tools/tool-method.util.ts +81 -0
- package/lib/tools/tool-modules.util.ts +104 -0
- package/lib/tools/tool-name.util.ts +57 -0
- package/lib/tools/tool-schema.factory.ts +142 -0
- package/lib/tools/tool.factory.ts +56 -0
- package/package.json +112 -91
- package/dist/factories/message.factory.d.ts +0 -7
- package/dist/factories/message.factory.js +0 -28
- package/dist/langchain.service.d.ts +0 -18
- package/dist/langchain.service.js +0 -69
- package/dist/tool-discovery.service.d.ts +0 -11
- package/dist/tool-discovery.service.js +0 -79
- package/dist/types/message.d.ts +0 -69
- package/dist/types/message.js +0 -104
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { AgentRunError } from '../errors/agent-run.error.js';
|
|
2
|
+
import type { RunSummary } from '../run/index.js';
|
|
3
|
+
|
|
4
|
+
interface ToolEvent {
|
|
5
|
+
/** `'default'` when the agent was registered without a name. */
|
|
6
|
+
agent: string;
|
|
7
|
+
/** The tool's name, as the model called it. */
|
|
8
|
+
tool: string;
|
|
9
|
+
/**
|
|
10
|
+
* The id the model gave this call. Pairs a start with its end or error. A
|
|
11
|
+
* call that never reached the tool has an error and no start.
|
|
12
|
+
*/
|
|
13
|
+
callId: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A tool call about to run, its arguments validated. */
|
|
17
|
+
export interface ToolStartEvent extends ToolEvent {
|
|
18
|
+
/** The arguments, parsed and validated against the tool's schema. */
|
|
19
|
+
args: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A tool call that returned. */
|
|
23
|
+
export interface ToolEndEvent extends ToolEvent {
|
|
24
|
+
/** What the method returned, before it is serialised for the model. */
|
|
25
|
+
output: unknown;
|
|
26
|
+
/** How long the method took. */
|
|
27
|
+
durationMs: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A tool call that failed, or never reached its tool. */
|
|
31
|
+
export interface ToolErrorEvent extends ToolEvent {
|
|
32
|
+
/**
|
|
33
|
+
* Why the call failed: the agent has no such tool, the schema rejected the
|
|
34
|
+
* arguments, or the tool threw. Only `'threw'` follows an `onToolStart`.
|
|
35
|
+
* May gain values in a minor release: keep a `default` branch when
|
|
36
|
+
* switching on it.
|
|
37
|
+
*/
|
|
38
|
+
reason: 'unknown-tool' | 'invalid-arguments' | 'threw';
|
|
39
|
+
/** What the tool threw, or why the call never reached it. */
|
|
40
|
+
error: unknown;
|
|
41
|
+
/** `0` when the tool never ran. */
|
|
42
|
+
durationMs: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A model call that failed. The run may still retry or fail. */
|
|
46
|
+
export interface ModelErrorEvent {
|
|
47
|
+
/** `'default'` when the agent was registered without a name. */
|
|
48
|
+
agent: string;
|
|
49
|
+
/** What the provider threw. */
|
|
50
|
+
error: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A run that returned, with what it cost. */
|
|
54
|
+
export interface RunFinishEvent extends RunSummary {
|
|
55
|
+
/** `'default'` when the agent was registered without a name. */
|
|
56
|
+
agent: string;
|
|
57
|
+
/** How long the run took. */
|
|
58
|
+
durationMs: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A run that threw. */
|
|
62
|
+
export interface RunErrorEvent {
|
|
63
|
+
/** `'default'` when the agent was registered without a name. */
|
|
64
|
+
agent: string;
|
|
65
|
+
/** How long the run took before it failed. */
|
|
66
|
+
durationMs: number;
|
|
67
|
+
/** The same error the caller of `run()` receives. */
|
|
68
|
+
error: AgentRunError;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Called as a run goes. Every method is optional and may be async. It is
|
|
73
|
+
* never awaited, and a throw or a rejection is logged and swallowed, never
|
|
74
|
+
* propagated into the run.
|
|
75
|
+
*/
|
|
76
|
+
export interface AgentObserver {
|
|
77
|
+
/**
|
|
78
|
+
* Before a tool runs, once its arguments are validated.
|
|
79
|
+
*
|
|
80
|
+
* @param event The call and its arguments.
|
|
81
|
+
*/
|
|
82
|
+
onToolStart?(event: ToolStartEvent): void | Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* After a tool returns.
|
|
85
|
+
*
|
|
86
|
+
* @param event The call, its output and its duration.
|
|
87
|
+
*/
|
|
88
|
+
onToolEnd?(event: ToolEndEvent): void | Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* After a tool call fails: the tool threw, or the call never reached it.
|
|
91
|
+
*
|
|
92
|
+
* @param event The call, why it failed and the error.
|
|
93
|
+
*/
|
|
94
|
+
onToolError?(event: ToolErrorEvent): void | Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* After a model call fails.
|
|
97
|
+
*
|
|
98
|
+
* @param event The error the provider threw.
|
|
99
|
+
*/
|
|
100
|
+
onModelError?(event: ModelErrorEvent): void | Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Only for a run that returns.
|
|
103
|
+
*
|
|
104
|
+
* @param event The run's duration, tools and tokens.
|
|
105
|
+
*/
|
|
106
|
+
onRunFinish?(event: RunFinishEvent): void | Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* For every run that throws, whatever the cause.
|
|
109
|
+
*
|
|
110
|
+
* @param event The run's duration and the error its caller receives.
|
|
111
|
+
*/
|
|
112
|
+
onRunError?(event: RunErrorEvent): void | Promise<void>;
|
|
113
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RunSummary } from '../run/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A run that went to completion. `status` may gain values in a minor release:
|
|
5
|
+
* keep a `default` branch when switching on it.
|
|
6
|
+
*/
|
|
7
|
+
export interface CompletedRun<TOutput = string> extends RunSummary {
|
|
8
|
+
/** Always `'completed'`: a run that fails throws instead. */
|
|
9
|
+
status: 'completed';
|
|
10
|
+
/** The model's text answer. */
|
|
11
|
+
output: TOutput;
|
|
12
|
+
/** How long the run took. */
|
|
13
|
+
durationMs: number;
|
|
14
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ModuleMetadata } from '@nestjs/common';
|
|
2
|
+
import type {
|
|
3
|
+
BaseCallbackHandler,
|
|
4
|
+
CallbackHandlerMethods,
|
|
5
|
+
} from '@langchain/core/callbacks/base';
|
|
6
|
+
import type { AgentObserver } from './agent-observer.interface.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Model configuration, resolved at bootstrap through `initChatModel`. Its
|
|
10
|
+
* fields go as they are to the provider class, and a provider that names one
|
|
11
|
+
* differently ignores it (`ChatOllama` reads `numPredict`, not `maxTokens`):
|
|
12
|
+
* pass a model instance then.
|
|
13
|
+
*/
|
|
14
|
+
export type ModelConfig = {
|
|
15
|
+
/** Provider-qualified name, e.g. `openai:gpt-5-mini`. */
|
|
16
|
+
model: string;
|
|
17
|
+
/** Omit it to let the provider read its own env var, or to use IAM/ADC. */
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
/** The sampling temperature. */
|
|
20
|
+
temperature?: number;
|
|
21
|
+
/** The longest answer, in tokens. */
|
|
22
|
+
maxTokens?: number;
|
|
23
|
+
/** How long one model call may take, in milliseconds. */
|
|
24
|
+
timeout?: number;
|
|
25
|
+
/** How many times a failed model call is retried. */
|
|
26
|
+
maxRetries?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A LangChain chat model instance, by the members `createAgent` checks for.
|
|
31
|
+
* Structural rather than `BaseChatModel`: a CommonJS application types its
|
|
32
|
+
* model from `@langchain/core`'s other declaration files. See #169.
|
|
33
|
+
*/
|
|
34
|
+
interface ChatModelLike {
|
|
35
|
+
/** Calls the model. */
|
|
36
|
+
invoke(...args: never[]): Promise<unknown>;
|
|
37
|
+
/** Optional, as on `BaseChatModel`; required at bootstrap. */
|
|
38
|
+
bindTools?(...args: never[]): unknown;
|
|
39
|
+
/** Streams the model's answer. */
|
|
40
|
+
_streamResponseChunks(...args: never[]): unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A configuration to resolve, or a model that is already built. The instance
|
|
45
|
+
* form takes anything {@link ModelConfig} cannot express.
|
|
46
|
+
*/
|
|
47
|
+
export type ModelOption = ModelConfig | ChatModelLike;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What Nest's own `imports` accepts: a module class, a dynamic module, a
|
|
51
|
+
* promise of one, or a forward reference.
|
|
52
|
+
*/
|
|
53
|
+
export type ToolModule = NonNullable<ModuleMetadata['imports']>[number];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Anything LangChain accepts as a run callback. Every method is optional, so a
|
|
57
|
+
* plain object of the `handle*` you care about is enough.
|
|
58
|
+
*/
|
|
59
|
+
export type AgentCallback = BaseCallbackHandler | CallbackHandlerMethods;
|
|
60
|
+
|
|
61
|
+
/** The options of one agent, for `register` or `registerAsync`. */
|
|
62
|
+
export interface LangChainModuleOptions {
|
|
63
|
+
/** The chat model: a `provider:model` configuration, or an instance. */
|
|
64
|
+
model: ModelOption;
|
|
65
|
+
/** The instructions the model receives before every run. */
|
|
66
|
+
systemPrompt?: string;
|
|
67
|
+
/**
|
|
68
|
+
* The modules declaring this agent's `@Tool()` providers. Each must also be
|
|
69
|
+
* imported into the Nest context, as a single instance.
|
|
70
|
+
*/
|
|
71
|
+
tools?: ToolModule[];
|
|
72
|
+
/** Called as a run goes, so you decide what is worth recording. */
|
|
73
|
+
observer?: AgentObserver;
|
|
74
|
+
/** LangChain-native run callbacks, for handlers written against its API. */
|
|
75
|
+
callbacks?: AgentCallback[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A class building an agent's options, for `registerAsync({ useClass })` or
|
|
80
|
+
* `useExisting`. Declare the method's return type, so that a key the options
|
|
81
|
+
* do not declare is rejected.
|
|
82
|
+
*/
|
|
83
|
+
export interface LangChainOptionsFactory {
|
|
84
|
+
/**
|
|
85
|
+
* Builds the agent's options.
|
|
86
|
+
*
|
|
87
|
+
* @returns The options, or a promise of them.
|
|
88
|
+
*/
|
|
89
|
+
createLangChainOptions():
|
|
90
|
+
LangChainModuleOptions | Promise<LangChainModuleOptions>;
|
|
91
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ConfigurableModuleBuilder } from '@nestjs/common';
|
|
2
|
+
import { LangChainModuleOptions } from './interfaces/langchain-module-options.interface.js';
|
|
3
|
+
|
|
4
|
+
/** The base class and tokens Nest generates for `LangChainModule`. */
|
|
5
|
+
export const {
|
|
6
|
+
ConfigurableModuleClass,
|
|
7
|
+
MODULE_OPTIONS_TOKEN,
|
|
8
|
+
OPTIONS_TYPE,
|
|
9
|
+
ASYNC_OPTIONS_TYPE,
|
|
10
|
+
} = new ConfigurableModuleBuilder<LangChainModuleOptions>()
|
|
11
|
+
.setExtras<{ name?: string }>({ name: undefined })
|
|
12
|
+
.setFactoryMethodName('createLangChainOptions')
|
|
13
|
+
.build();
|
|
14
|
+
|
|
15
|
+
/** The agent's own name. */
|
|
16
|
+
export const AGENT_NAME_TOKEN = 'nestjs-langchain:agent-name';
|
|
17
|
+
|
|
18
|
+
/** The name the unnamed agent carries in logs and observer events. */
|
|
19
|
+
export const UNNAMED_AGENT = 'default';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The injection token of the agent registered under `name`, for the places
|
|
23
|
+
* `@InjectAgent` cannot go: `overrideProvider` in a test, a factory's
|
|
24
|
+
* `inject`. The string it returns is not part of the API.
|
|
25
|
+
*
|
|
26
|
+
* @param name The `name` the agent was registered with.
|
|
27
|
+
* @returns The agent's injection token.
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* Test.createTestingModule({ imports: [AppModule] })
|
|
31
|
+
* .overrideProvider(getAgentToken('MATH_AGENT'))
|
|
32
|
+
* .useValue({ run: async () => ({ status: 'completed', output: '42' }) });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const getAgentToken = (name: string): string =>
|
|
36
|
+
`nestjs-langchain:agent:${name}`;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { DynamicModule, Module, Provider, Type } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
AGENT_NAME_TOKEN,
|
|
4
|
+
ASYNC_OPTIONS_TYPE,
|
|
5
|
+
ConfigurableModuleClass,
|
|
6
|
+
getAgentToken,
|
|
7
|
+
OPTIONS_TYPE,
|
|
8
|
+
UNNAMED_AGENT,
|
|
9
|
+
} from './langchain.module-definition.js';
|
|
10
|
+
import { Agent } from './agent.js';
|
|
11
|
+
import {
|
|
12
|
+
LangChainModuleOptions,
|
|
13
|
+
LangChainOptionsFactory,
|
|
14
|
+
} from './interfaces/langchain-module-options.interface.js';
|
|
15
|
+
import { ToolDiscoveryService } from './tools/index.js';
|
|
16
|
+
import { reservedAgentName } from './errors/messages.js';
|
|
17
|
+
import { DiscoveryModule } from '@nestjs/core';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Registers agents as Nest providers: each `register` or `registerAsync`
|
|
21
|
+
* adds one. Give each a `name` when there are several.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* imports: [
|
|
26
|
+
* MathModule,
|
|
27
|
+
* LangChainModule.register({
|
|
28
|
+
* model: { model: 'openai:gpt-5-mini' },
|
|
29
|
+
* tools: [MathModule],
|
|
30
|
+
* }),
|
|
31
|
+
* ],
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
@Module({
|
|
35
|
+
imports: [DiscoveryModule],
|
|
36
|
+
providers: [ToolDiscoveryService],
|
|
37
|
+
})
|
|
38
|
+
export class LangChainModule extends ConfigurableModuleClass {
|
|
39
|
+
/**
|
|
40
|
+
* Registers an agent from its options.
|
|
41
|
+
*
|
|
42
|
+
* @param options The agent's options, with its `name` when there are several.
|
|
43
|
+
* @returns The module providing the agent.
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* LangChainModule.register({
|
|
47
|
+
* name: 'MATH_AGENT',
|
|
48
|
+
* model: { model: 'openai:gpt-5-mini' },
|
|
49
|
+
* tools: [MathModule],
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
static register(options: typeof OPTIONS_TYPE): DynamicModule {
|
|
54
|
+
const dynamicModule = super.register(options);
|
|
55
|
+
return this.addDynamicAgentProvider(dynamicModule, options.name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Registers an agent whose options other providers build, through
|
|
60
|
+
* `useFactory`, `useClass` or `useExisting`. `name` stays outside the
|
|
61
|
+
* factory: it names the token `@InjectAgent()` resolves.
|
|
62
|
+
*
|
|
63
|
+
* @param options How to build the agent's options, and its `name`.
|
|
64
|
+
* @returns The module providing the agent.
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* LangChainModule.registerAsync({
|
|
68
|
+
* name: 'MATH_AGENT',
|
|
69
|
+
* imports: [ConfigModule],
|
|
70
|
+
* inject: [ConfigService],
|
|
71
|
+
* useFactory: (config: ConfigService) => ({
|
|
72
|
+
* model: {
|
|
73
|
+
* model: 'openai:gpt-5-mini',
|
|
74
|
+
* apiKey: config.getOrThrow<string>('OPENAI_API_KEY'),
|
|
75
|
+
* },
|
|
76
|
+
* }),
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
static registerAsync<
|
|
81
|
+
// Rejects a key the options do not declare, which Nest's
|
|
82
|
+
// `T | Promise<T>` return type never checks in a returned literal.
|
|
83
|
+
T extends LangChainModuleOptions &
|
|
84
|
+
Record<Exclude<keyof T, keyof LangChainModuleOptions>, never>,
|
|
85
|
+
>(
|
|
86
|
+
options: Omit<
|
|
87
|
+
typeof ASYNC_OPTIONS_TYPE,
|
|
88
|
+
'useFactory' | 'useClass' | 'useExisting'
|
|
89
|
+
> & {
|
|
90
|
+
// Nest's own signature: `unknown[]` would reject a factory declaring
|
|
91
|
+
// its injected parameters, such as `(config: ConfigService) => …`.
|
|
92
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
93
|
+
useFactory?: (...args: any[]) => T | Promise<T>;
|
|
94
|
+
useClass?: Type<LangChainOptionsFactory>;
|
|
95
|
+
useExisting?: Type<LangChainOptionsFactory>;
|
|
96
|
+
},
|
|
97
|
+
): DynamicModule {
|
|
98
|
+
const dynamicModule = super.registerAsync(options);
|
|
99
|
+
return this.addDynamicAgentProvider(dynamicModule, options.name);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private static addDynamicAgentProvider(
|
|
103
|
+
dynamicModule: DynamicModule,
|
|
104
|
+
name: string | undefined,
|
|
105
|
+
): DynamicModule {
|
|
106
|
+
if (name === '' || name === UNNAMED_AGENT) {
|
|
107
|
+
throw new Error(reservedAgentName(name));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const nameProvider: Provider = {
|
|
111
|
+
provide: AGENT_NAME_TOKEN,
|
|
112
|
+
useValue: name ?? UNNAMED_AGENT,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// The class token belongs to the unnamed agent alone, in every module, so
|
|
116
|
+
// `overrideProvider(Agent)` and `get(Agent)` never reach a named one.
|
|
117
|
+
// See #128 and #170.
|
|
118
|
+
if (!name) {
|
|
119
|
+
return {
|
|
120
|
+
...dynamicModule,
|
|
121
|
+
providers: [...(dynamicModule.providers ?? []), nameProvider, Agent],
|
|
122
|
+
exports: [...(dynamicModule.exports ?? []), Agent],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const agentToken = getAgentToken(name);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
...dynamicModule,
|
|
130
|
+
providers: [
|
|
131
|
+
...(dynamicModule.providers ?? []),
|
|
132
|
+
nameProvider,
|
|
133
|
+
{ provide: agentToken, useClass: Agent },
|
|
134
|
+
],
|
|
135
|
+
exports: [...(dynamicModule.exports ?? []), agentToken],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ToolErrorEvent } from '../interfaces/agent-observer.interface.js';
|
|
2
|
+
import { messageOf, RunReporter } from './run-reporter.js';
|
|
3
|
+
|
|
4
|
+
interface ToolCall {
|
|
5
|
+
tool: string;
|
|
6
|
+
callId: string;
|
|
7
|
+
args: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Logs a failed tool call and hands it to the observer. */
|
|
11
|
+
export const reportToolError = (
|
|
12
|
+
reporter: RunReporter,
|
|
13
|
+
event: Omit<ToolErrorEvent, 'agent'>,
|
|
14
|
+
): void => {
|
|
15
|
+
reporter.logError(`${event.tool} failed: ${messageOf(event.error)}`);
|
|
16
|
+
reporter.notify((observer) =>
|
|
17
|
+
observer.onToolError?.({ agent: reporter.agent, ...event }),
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Runs one tool call, reporting its start, then its end or its failure. */
|
|
22
|
+
export const observeToolCall = async (
|
|
23
|
+
reporter: RunReporter,
|
|
24
|
+
{ tool, callId, args }: ToolCall,
|
|
25
|
+
call: () => unknown,
|
|
26
|
+
): Promise<unknown> => {
|
|
27
|
+
const { agent } = reporter;
|
|
28
|
+
const startedAt = Date.now();
|
|
29
|
+
|
|
30
|
+
reporter.notify((observer) =>
|
|
31
|
+
observer.onToolStart?.({ agent, tool, callId, args }),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const output = await call();
|
|
36
|
+
|
|
37
|
+
reporter.notify((observer) =>
|
|
38
|
+
observer.onToolEnd?.({
|
|
39
|
+
agent,
|
|
40
|
+
tool,
|
|
41
|
+
callId,
|
|
42
|
+
output,
|
|
43
|
+
durationMs: Date.now() - startedAt,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return output;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
reportToolError(reporter, {
|
|
50
|
+
tool,
|
|
51
|
+
callId,
|
|
52
|
+
reason: 'threw',
|
|
53
|
+
error,
|
|
54
|
+
durationMs: Date.now() - startedAt,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { LoggerService } from '@nestjs/common';
|
|
2
|
+
import { AgentObserver } from '../interfaces/agent-observer.interface.js';
|
|
3
|
+
|
|
4
|
+
export const LOG_CONTEXT = 'LangChainAgent';
|
|
5
|
+
|
|
6
|
+
interface RunReporterInput {
|
|
7
|
+
logger: LoggerService;
|
|
8
|
+
agent: string;
|
|
9
|
+
/** Empty for the unnamed agent, `'MATH '` otherwise. */
|
|
10
|
+
prefix: string;
|
|
11
|
+
observer?: AgentObserver;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const messageOf = (error: unknown): string =>
|
|
15
|
+
error instanceof Error ? error.message : String(error);
|
|
16
|
+
|
|
17
|
+
/** One agent's error log lines and its observer. */
|
|
18
|
+
export class RunReporter {
|
|
19
|
+
constructor(private readonly input: RunReporterInput) {}
|
|
20
|
+
|
|
21
|
+
get agent(): string {
|
|
22
|
+
return this.input.agent;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One line per error, whatever the message holds. */
|
|
26
|
+
logError(message: string): void {
|
|
27
|
+
const line = message
|
|
28
|
+
.split('\n')
|
|
29
|
+
.map((part) => part.trim())
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(' ');
|
|
32
|
+
|
|
33
|
+
this.input.logger.error(`${this.input.prefix}${line}`, LOG_CONTEXT);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Hands an event to the observer, if any. Never awaited: a throw or a
|
|
38
|
+
* rejection is logged and swallowed, never propagated into the run.
|
|
39
|
+
*/
|
|
40
|
+
notify(call: (observer: AgentObserver) => void | Promise<void>): void {
|
|
41
|
+
const { observer } = this.input;
|
|
42
|
+
|
|
43
|
+
if (!observer) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const onFailure = (error: unknown) =>
|
|
48
|
+
this.logError(`the observer failed: ${messageOf(error)}`);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
void Promise.resolve(call(observer)).catch(onFailure);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
onFailure(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
package/lib/run/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mergeUsageMetadata, UsageMetadata } from '@langchain/core/messages';
|
|
2
|
+
|
|
3
|
+
// Structural rather than `BaseMessage`. See #52.
|
|
4
|
+
interface MessageLike {
|
|
5
|
+
usage_metadata?: UsageMetadata;
|
|
6
|
+
tool_calls?: { name: string }[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** What a run did and cost. */
|
|
10
|
+
export interface RunSummary {
|
|
11
|
+
/** Every tool call the model made, failed and unknown ones included. */
|
|
12
|
+
tools: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Summed over the run's model calls; absent when the provider reports no
|
|
15
|
+
* usage.
|
|
16
|
+
*/
|
|
17
|
+
tokens?: { input: number; output: number; total: number };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* What a finished run cost, read off the messages it returned.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
* @param messages The messages of the finished run.
|
|
25
|
+
* @returns The tool calls the model made, and the tokens if reported.
|
|
26
|
+
*/
|
|
27
|
+
export const summariseRun = (messages: readonly MessageLike[]): RunSummary => {
|
|
28
|
+
const tools = messages.flatMap((message) =>
|
|
29
|
+
(message.tool_calls ?? []).map((call) => call.name),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const reported = messages
|
|
33
|
+
.map((message) => message.usage_metadata)
|
|
34
|
+
.filter((usage) => usage !== undefined);
|
|
35
|
+
|
|
36
|
+
if (reported.length === 0) {
|
|
37
|
+
return { tools };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Seeded with zeros, so a field a provider left out counts as zero.
|
|
41
|
+
const usage = reported.reduce(mergeUsageMetadata, mergeUsageMetadata());
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
tools,
|
|
45
|
+
tokens: {
|
|
46
|
+
input: usage.input_tokens,
|
|
47
|
+
output: usage.output_tokens,
|
|
48
|
+
total: usage.total_tokens,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|