@nestjs/ai 0.0.0-stage → 0.0.1
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 +23 -0
- package/README.md +108 -2
- package/dist/ai.constants.d.ts +22 -0
- package/dist/ai.constants.js +24 -0
- package/dist/ai.module-definition.d.ts +23 -0
- package/dist/ai.module-definition.js +30 -0
- package/dist/ai.module.d.ts +11 -0
- package/dist/ai.module.js +36 -0
- package/dist/context/ai-execution-context.d.ts +32 -0
- package/dist/context/ai-execution-context.js +41 -0
- package/dist/context/ai-params.d.ts +20 -0
- package/dist/context/ai-params.js +44 -0
- package/dist/decorators/agent-function.decorator.d.ts +22 -0
- package/dist/decorators/agent-function.decorator.js +36 -0
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.js +2 -0
- package/dist/decorators/inject-model.decorator.d.ts +10 -0
- package/dist/decorators/inject-model.decorator.js +16 -0
- package/dist/errors/ai.errors.d.ts +54 -0
- package/dist/errors/ai.errors.js +62 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/http/keep-alive.util.d.ts +6 -0
- package/dist/http/keep-alive.util.js +38 -0
- package/dist/http/node-response.util.d.ts +14 -0
- package/dist/http/node-response.util.js +104 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +15 -0
- package/dist/interfaces/agent-function.interface.d.ts +53 -0
- package/dist/interfaces/agent-function.interface.js +1 -0
- package/dist/interfaces/ai-call-options.interface.d.ts +65 -0
- package/dist/interfaces/ai-call-options.interface.js +1 -0
- package/dist/interfaces/ai-module-options.interface.d.ts +88 -0
- package/dist/interfaces/ai-module-options.interface.js +1 -0
- package/dist/interfaces/ai-result.interface.d.ts +54 -0
- package/dist/interfaces/ai-result.interface.js +1 -0
- package/dist/interfaces/ai-schema.interface.d.ts +44 -0
- package/dist/interfaces/ai-schema.interface.js +1 -0
- package/dist/interfaces/ai-tool.interface.d.ts +22 -0
- package/dist/interfaces/ai-tool.interface.js +1 -0
- package/dist/interfaces/ai-usage.interface.d.ts +30 -0
- package/dist/interfaces/ai-usage.interface.js +1 -0
- package/dist/interfaces/index.d.ts +7 -0
- package/dist/interfaces/index.js +7 -0
- package/dist/models/ai-models.service.d.ts +29 -0
- package/dist/models/ai-models.service.js +113 -0
- package/dist/models/model-requests.d.ts +2 -0
- package/dist/models/model-requests.js +12 -0
- package/dist/runtime/agent-loop.d.ts +30 -0
- package/dist/runtime/agent-loop.js +81 -0
- package/dist/runtime/call-plan.d.ts +54 -0
- package/dist/runtime/call-plan.js +105 -0
- package/dist/runtime/model-policy.d.ts +48 -0
- package/dist/runtime/model-policy.js +202 -0
- package/dist/runtime/steps.d.ts +32 -0
- package/dist/runtime/steps.js +190 -0
- package/dist/runtime/text-stream.d.ts +64 -0
- package/dist/runtime/text-stream.js +211 -0
- package/dist/services/ai.service.d.ts +57 -0
- package/dist/services/ai.service.js +139 -0
- package/dist/testing/fake-model.d.ts +96 -0
- package/dist/testing/fake-model.js +201 -0
- package/dist/testing/index.d.ts +26 -0
- package/dist/testing/index.js +19 -0
- package/dist/tools/ai-tools.service.d.ts +77 -0
- package/dist/tools/ai-tools.service.js +234 -0
- package/dist/tools/tool-output.util.d.ts +6 -0
- package/dist/tools/tool-output.util.js +38 -0
- package/dist/utils/duration.util.d.ts +3 -0
- package/dist/utils/duration.util.js +17 -0
- package/dist/utils/errors.util.d.ts +16 -0
- package/dist/utils/errors.util.js +35 -0
- package/dist/utils/names.util.d.ts +2 -0
- package/dist/utils/names.util.js +10 -0
- package/dist/utils/schema.util.d.ts +16 -0
- package/dist/utils/schema.util.js +70 -0
- package/dist/utils/usage.util.d.ts +10 -0
- package/dist/utils/usage.util.js +42 -0
- package/dist/workflows/ai-agent.interfaces.d.ts +70 -0
- package/dist/workflows/ai-agent.interfaces.js +1 -0
- package/dist/workflows/ai-agent.workflow.d.ts +15 -0
- package/dist/workflows/ai-agent.workflow.js +25 -0
- package/dist/workflows/ai-agents.service.d.ts +83 -0
- package/dist/workflows/ai-agents.service.js +277 -0
- package/dist/workflows/ai-workflows.module.d.ts +6 -0
- package/dist/workflows/ai-workflows.module.js +22 -0
- package/dist/workflows/index.d.ts +6 -0
- package/dist/workflows/index.js +5 -0
- package/package.json +100 -4
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
14
|
+
import { DiscoveryService, ExternalContextCreator, MetadataScanner } from '@nestjs/core';
|
|
15
|
+
import { AGENT_FUNCTION_METADATA, AI_CONTEXT_TYPE, AI_MODULE_OPTIONS, AI_PARAMS_METADATA } from '../ai.constants.js';
|
|
16
|
+
import { AiParamsFactory, recordHandlerParams } from '../context/ai-params.js';
|
|
17
|
+
import { AiConfigurationError, AiToolError } from '../errors/ai.errors.js';
|
|
18
|
+
import { toToolError } from '../utils/errors.util.js';
|
|
19
|
+
import { assertProviderToolName } from '../utils/names.util.js';
|
|
20
|
+
import { EMPTY_OBJECT_SCHEMA, toModelSchema } from '../utils/schema.util.js';
|
|
21
|
+
import { toToolOutput } from './tool-output.util.js';
|
|
22
|
+
const NEVER_ABORTED = new AbortController().signal;
|
|
23
|
+
/**
|
|
24
|
+
* The application's agent functions: the `@AgentFunction()` methods of its providers (or of the `include` modules),
|
|
25
|
+
* found once at startup, each wrapped by Nest's enhancers with the context type `'ai'`, and called with an
|
|
26
|
+
* `AgentFunctionContext`. `select()` turns some of them into `ai` tools for one caller.
|
|
27
|
+
*/
|
|
28
|
+
let AiTools = class AiTools {
|
|
29
|
+
constructor(discovery, scanner, externalContextCreator, options) {
|
|
30
|
+
this.discovery = discovery;
|
|
31
|
+
this.scanner = scanner;
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.logger = new Logger('AiModule');
|
|
34
|
+
this.entries = new Map();
|
|
35
|
+
this.paramsFactory = new AiParamsFactory();
|
|
36
|
+
this.scanned = false;
|
|
37
|
+
this.contextCreator = withOwnHandlerMetadata(externalContextCreator);
|
|
38
|
+
}
|
|
39
|
+
onModuleInit() {
|
|
40
|
+
this.scan();
|
|
41
|
+
}
|
|
42
|
+
/** Every agent function, by name. */
|
|
43
|
+
get tools() {
|
|
44
|
+
this.scan();
|
|
45
|
+
return this.entries;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `ai` tools for `names` (agent function names, or provider classes for all their agent functions), whose calls run
|
|
49
|
+
* as `caller`: what `AiService` calls pass the model, and what code that calls `ai` itself can pass `generateText()`.
|
|
50
|
+
*/
|
|
51
|
+
select(selection, { caller = {}, onExecute, onProgress, execute = true, } = {}) {
|
|
52
|
+
this.scan();
|
|
53
|
+
const tools = {};
|
|
54
|
+
for (const entry of this.resolve(selection)) {
|
|
55
|
+
// An `ai` tool is a plain object; `ai` validates the input against the schema before `execute`.
|
|
56
|
+
tools[entry.name] = {
|
|
57
|
+
...(entry.description !== undefined && { description: entry.description }),
|
|
58
|
+
inputSchema: entry.inputSchema,
|
|
59
|
+
...(execute && {
|
|
60
|
+
execute: (input, options) => {
|
|
61
|
+
onExecute?.();
|
|
62
|
+
return this.call(entry, input, caller, { ...options, ...(onProgress && { onProgress }) });
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return tools;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Runs the agent function `name` with `input` (validated by its schema) as `caller`, through its enhancers, as an
|
|
71
|
+
* agent's call does. Resolves with what the model would read; rejects with the `AiToolError` it would read.
|
|
72
|
+
*/
|
|
73
|
+
async call(entryOrName, input, caller = {}, options = {}) {
|
|
74
|
+
const entry = typeof entryOrName === 'string' ? this.resolve([entryOrName])[0] : entryOrName;
|
|
75
|
+
if (typeof entryOrName === 'string') {
|
|
76
|
+
const validated = await entry.inputSchema.validate?.(input);
|
|
77
|
+
if (validated && !validated.success) {
|
|
78
|
+
throw new AiToolError(`Invalid input: ${validated.error.message}`, entry.name);
|
|
79
|
+
}
|
|
80
|
+
input = validated ? validated.value : input;
|
|
81
|
+
}
|
|
82
|
+
if (entry.scopes?.length) {
|
|
83
|
+
const held = new Set(caller.scopes ?? []);
|
|
84
|
+
const missing = entry.scopes.filter((scope) => !held.has(scope));
|
|
85
|
+
if (missing.length > 0) {
|
|
86
|
+
throw new AiToolError(`Forbidden: this tool needs the scope(s) ${missing.join(', ')}`, entry.name);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const toolCallId = options.toolCallId ?? '';
|
|
90
|
+
// Where `@nestjs/authentication` and `@nestjs/authorization` look on an `'ai'` call, as on an `'mcp'` one: the
|
|
91
|
+
// request it runs in, else the context itself, with its `user`.
|
|
92
|
+
const context = {
|
|
93
|
+
name: entry.name,
|
|
94
|
+
...(caller.request !== undefined && { request: caller.request }),
|
|
95
|
+
...(caller.user !== undefined && { user: caller.user }),
|
|
96
|
+
...(caller.scopes !== undefined && { scopes: caller.scopes }),
|
|
97
|
+
signal: options.abortSignal ?? NEVER_ABORTED,
|
|
98
|
+
reportProgress: async (progress, total, message) => options.onProgress?.({
|
|
99
|
+
toolCallId,
|
|
100
|
+
toolName: entry.name,
|
|
101
|
+
progress,
|
|
102
|
+
...(total !== undefined && { total }),
|
|
103
|
+
...(message !== undefined && { message }),
|
|
104
|
+
}),
|
|
105
|
+
toolCallId,
|
|
106
|
+
...(options.idempotencyKey !== undefined && { idempotencyKey: options.idempotencyKey }),
|
|
107
|
+
messages: (options.messages ?? []),
|
|
108
|
+
};
|
|
109
|
+
let value;
|
|
110
|
+
try {
|
|
111
|
+
value = await entry.handler(input ?? {}, context);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
// Nest's exception handler has run the filters, and logged the error.
|
|
115
|
+
throw toToolError(error, entry.name);
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return toToolOutput(value, entry.name, entry.where);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (!(error instanceof AiToolError)) {
|
|
122
|
+
this.logger.error(`${entry.where} returned a value that is not JSON`, error?.stack);
|
|
123
|
+
}
|
|
124
|
+
throw toToolError(error, entry.name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** The agent functions a selection names, each once, in order. Unknown names throw. */
|
|
128
|
+
resolve(selection) {
|
|
129
|
+
this.scan();
|
|
130
|
+
if (!Array.isArray(selection)) {
|
|
131
|
+
throw new AiConfigurationError("`tools` lists agent function names and provider classes, e.g. ['find-order', OrdersTools].");
|
|
132
|
+
}
|
|
133
|
+
const picked = new Map();
|
|
134
|
+
for (const item of selection) {
|
|
135
|
+
if (typeof item === 'string') {
|
|
136
|
+
const entry = this.entries.get(item);
|
|
137
|
+
if (!entry) {
|
|
138
|
+
throw new AiConfigurationError(`There is no agent function named "${item}". ${this.known()}`);
|
|
139
|
+
}
|
|
140
|
+
picked.set(entry.name, entry);
|
|
141
|
+
}
|
|
142
|
+
else if (typeof item === 'function') {
|
|
143
|
+
const matches = [...this.entries.values()].filter((entry) => entry.host === item || entry.host.prototype instanceof item);
|
|
144
|
+
if (matches.length === 0) {
|
|
145
|
+
throw new AiConfigurationError(`${item.name} has no agent functions, or is not a provider of the application. ${this.known()}`);
|
|
146
|
+
}
|
|
147
|
+
for (const entry of matches) {
|
|
148
|
+
picked.set(entry.name, entry);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
throw new AiConfigurationError(`\`tools\` lists agent function names and provider classes; got ${String(item)}.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return [...picked.values()];
|
|
156
|
+
}
|
|
157
|
+
/** Finds the agent functions, once. */
|
|
158
|
+
scan() {
|
|
159
|
+
if (this.scanned) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.scanned = true;
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
const wrappers = this.discovery.getProviders(this.options.include ? { include: this.options.include } : undefined);
|
|
165
|
+
for (const wrapper of wrappers) {
|
|
166
|
+
const { instance } = wrapper;
|
|
167
|
+
if (!instance || typeof instance !== 'object' || wrapper.isAlias || seen.has(instance)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
seen.add(instance);
|
|
171
|
+
const prototype = Object.getPrototypeOf(instance);
|
|
172
|
+
for (const methodName of this.scanner.getAllMethodNames(prototype)) {
|
|
173
|
+
const method = prototype[methodName];
|
|
174
|
+
const definition = Reflect.getMetadata(AGENT_FUNCTION_METADATA, method);
|
|
175
|
+
if (!definition) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const where = `${wrapper.metatype?.name ?? instance.constructor.name}.${methodName}`;
|
|
179
|
+
if (!wrapper.isDependencyTreeStatic()) {
|
|
180
|
+
throw new AiConfigurationError(`${where}: agent functions must be on singleton providers, and this one is request-scoped or transient (or ` +
|
|
181
|
+
'depends on a request-scoped provider). Pass what a call needs through `caller` instead.');
|
|
182
|
+
}
|
|
183
|
+
const name = definition.name ?? methodName;
|
|
184
|
+
assertProviderToolName(name, where);
|
|
185
|
+
const existing = this.entries.get(name);
|
|
186
|
+
if (existing) {
|
|
187
|
+
throw new AiConfigurationError(`${where}: there is already an agent function named "${name}" (${existing.where}). Give one of them ` +
|
|
188
|
+
'another { name }.');
|
|
189
|
+
}
|
|
190
|
+
recordHandlerParams(instance.constructor, methodName);
|
|
191
|
+
const handler = this.contextCreator.create(instance, method, methodName, AI_PARAMS_METADATA, this.paramsFactory, undefined, undefined, undefined, AI_CONTEXT_TYPE);
|
|
192
|
+
this.entries.set(name, {
|
|
193
|
+
name,
|
|
194
|
+
...(definition.description !== undefined && { description: definition.description }),
|
|
195
|
+
...(definition.scopes?.length && { scopes: [...definition.scopes] }),
|
|
196
|
+
inputSchema: definition.inputSchema
|
|
197
|
+
? toModelSchema(definition.inputSchema, `${where} inputSchema`, { object: true })
|
|
198
|
+
: EMPTY_OBJECT_SCHEMA,
|
|
199
|
+
handler,
|
|
200
|
+
host: instance.constructor,
|
|
201
|
+
where,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
known() {
|
|
207
|
+
const names = [...this.entries.keys()];
|
|
208
|
+
return names.length
|
|
209
|
+
? `Agent functions: ${names.join(', ')}.`
|
|
210
|
+
: 'The application has no agent functions (@AgentFunction() methods).';
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
AiTools = __decorate([
|
|
214
|
+
Injectable(),
|
|
215
|
+
__param(3, Inject(AI_MODULE_OPTIONS)),
|
|
216
|
+
__metadata("design:paramtypes", [DiscoveryService,
|
|
217
|
+
MetadataScanner,
|
|
218
|
+
ExternalContextCreator, Object])
|
|
219
|
+
], AiTools);
|
|
220
|
+
export { AiTools };
|
|
221
|
+
/**
|
|
222
|
+
* Nest's `ExternalContextCreator`, with a cache of handler metadata of its own. Nest's keeps what it reads of a
|
|
223
|
+
* method's parameters by class and method name, and builds the context type of the first `create()` for the method into
|
|
224
|
+
* it: the `ExecutionContext` that `createParamDecorator()` decorators receive. A method that is an `@nestjs/mcp` tool
|
|
225
|
+
* too is wrapped by both packages, and its parameter decorators would see the type of whichever wrapped it first,
|
|
226
|
+
* `'ai'` or `'mcp'`, on every call. Guards, interceptors and filters get the type of each `create()` either way. Each
|
|
227
|
+
* agent function is wrapped once, at startup, so nothing needs caching here.
|
|
228
|
+
*/
|
|
229
|
+
function withOwnHandlerMetadata(creator) {
|
|
230
|
+
// `handlerMetadataStorage` is the private field `getMetadata()` reads, in every 12.x release.
|
|
231
|
+
return Object.create(creator, {
|
|
232
|
+
handlerMetadataStorage: { value: { get: () => undefined, set: () => { } } },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the model reads of an agent function's return value: JSON (a `Date` becomes its ISO string; `undefined` is
|
|
3
|
+
* `null`). An MCP `CallToolResult` becomes its `structuredContent`, else its text blocks joined; `isError: true` is a
|
|
4
|
+
* tool error. Blocks other than text (images, audio, resources) are dropped.
|
|
5
|
+
*/
|
|
6
|
+
export declare function toToolOutput(value: unknown, toolName: string, where: string): unknown;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Logger } from '@nestjs/common';
|
|
2
|
+
import { AiToolError } from '../errors/ai.errors.js';
|
|
3
|
+
const logger = new Logger('AiModule');
|
|
4
|
+
function isCallToolResult(value) {
|
|
5
|
+
if (typeof value !== 'object' || value === null || !Array.isArray(value.content)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const keys = Object.keys(value);
|
|
9
|
+
return (keys.every((key) => ['content', 'structuredContent', 'isError', '_meta'].includes(key)) &&
|
|
10
|
+
value.content.every((block) => typeof block?.type === 'string'));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* What the model reads of an agent function's return value: JSON (a `Date` becomes its ISO string; `undefined` is
|
|
14
|
+
* `null`). An MCP `CallToolResult` becomes its `structuredContent`, else its text blocks joined; `isError: true` is a
|
|
15
|
+
* tool error. Blocks other than text (images, audio, resources) are dropped.
|
|
16
|
+
*/
|
|
17
|
+
export function toToolOutput(value, toolName, where) {
|
|
18
|
+
if (isCallToolResult(value)) {
|
|
19
|
+
const text = value.content
|
|
20
|
+
.filter((block) => block.type === 'text' && typeof block.text === 'string')
|
|
21
|
+
.map((block) => block.text)
|
|
22
|
+
.join('\n');
|
|
23
|
+
if (value.isError) {
|
|
24
|
+
throw new AiToolError(text || 'The tool failed', toolName);
|
|
25
|
+
}
|
|
26
|
+
if (value.content.some((block) => block.type !== 'text')) {
|
|
27
|
+
logger.warn(`${where} returned content other than text, which agents are not sent; only its text is.`);
|
|
28
|
+
}
|
|
29
|
+
return value.structuredContent !== undefined ? toJson(value.structuredContent) : text;
|
|
30
|
+
}
|
|
31
|
+
return value === undefined ? null : toJson(value);
|
|
32
|
+
}
|
|
33
|
+
function toJson(value) {
|
|
34
|
+
if (typeof value !== 'object' || value === null) {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
return JSON.parse(JSON.stringify(value));
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AiConfigurationError } from '../errors/ai.errors.js';
|
|
2
|
+
const UNITS = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
|
|
3
|
+
/** A duration (`@nestjs/resilience`'s: milliseconds, or `'30s'`, `'2m'`, ...) in milliseconds. */
|
|
4
|
+
export function toMilliseconds(value, option) {
|
|
5
|
+
if (typeof value === 'number') {
|
|
6
|
+
if (Number.isFinite(value) && value >= 0) {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
else if (typeof value === 'string') {
|
|
11
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/.exec(value.trim());
|
|
12
|
+
if (match) {
|
|
13
|
+
return Number(match[1]) * UNITS[match[2]];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
throw new AiConfigurationError(`\`${option}\` must be a number of milliseconds or a duration such as "30s" or "2m". Got ${JSON.stringify(value)}.`);
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { HttpException } from '@nestjs/common';
|
|
2
|
+
import { AiToolError } from '../errors/ai.errors.js';
|
|
3
|
+
/** What the model is told about an error that is not meant for it. */
|
|
4
|
+
export declare const INTERNAL_ERROR_MESSAGE = "Internal server error";
|
|
5
|
+
/**
|
|
6
|
+
* The message of an `HttpException`, as its response body carries it: a string response, the body's `message`
|
|
7
|
+
* (`ValidationPipe`'s list joined), else the exception's own.
|
|
8
|
+
*/
|
|
9
|
+
export declare function httpExceptionMessage(exception: HttpException): string;
|
|
10
|
+
/**
|
|
11
|
+
* An agent function's failure, as the model reads it: an `HttpException`'s message, `Internal server error` for
|
|
12
|
+
* anything else (Nest's exception handler has logged it). A raw error message never reaches the model.
|
|
13
|
+
*/
|
|
14
|
+
export declare function toToolError(error: unknown, toolName: string): AiToolError;
|
|
15
|
+
/** Whether `error` is an abort: the caller's signal, or a request cancelled with it. */
|
|
16
|
+
export declare function isAbortError(error: unknown): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { HttpException } from '@nestjs/common';
|
|
2
|
+
import { AiToolError } from '../errors/ai.errors.js';
|
|
3
|
+
/** What the model is told about an error that is not meant for it. */
|
|
4
|
+
export const INTERNAL_ERROR_MESSAGE = 'Internal server error';
|
|
5
|
+
/**
|
|
6
|
+
* The message of an `HttpException`, as its response body carries it: a string response, the body's `message`
|
|
7
|
+
* (`ValidationPipe`'s list joined), else the exception's own.
|
|
8
|
+
*/
|
|
9
|
+
export function httpExceptionMessage(exception) {
|
|
10
|
+
const response = exception.getResponse();
|
|
11
|
+
if (typeof response === 'string') {
|
|
12
|
+
return response;
|
|
13
|
+
}
|
|
14
|
+
const message = response?.message;
|
|
15
|
+
if (Array.isArray(message)) {
|
|
16
|
+
return message.map(String).join('; ');
|
|
17
|
+
}
|
|
18
|
+
return typeof message === 'string' ? message : exception.message;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* An agent function's failure, as the model reads it: an `HttpException`'s message, `Internal server error` for
|
|
22
|
+
* anything else (Nest's exception handler has logged it). A raw error message never reaches the model.
|
|
23
|
+
*/
|
|
24
|
+
export function toToolError(error, toolName) {
|
|
25
|
+
if (error instanceof AiToolError) {
|
|
26
|
+
return error;
|
|
27
|
+
}
|
|
28
|
+
const message = error instanceof HttpException ? httpExceptionMessage(error) : INTERNAL_ERROR_MESSAGE;
|
|
29
|
+
return new AiToolError(message, toolName, { cause: error });
|
|
30
|
+
}
|
|
31
|
+
/** Whether `error` is an abort: the caller's signal, or a request cancelled with it. */
|
|
32
|
+
export function isAbortError(error) {
|
|
33
|
+
return (error instanceof Error &&
|
|
34
|
+
(error.name === 'AbortError' || error.code === 'ABORT_ERR'));
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AiConfigurationError } from '../errors/ai.errors.js';
|
|
2
|
+
/** What OpenAI, Anthropic and Google accept as a tool name, which is what an agent function's name is to the model. */
|
|
3
|
+
const PROVIDER_TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
4
|
+
/** Throws for an agent function name model providers refuse, naming the method. */
|
|
5
|
+
export function assertProviderToolName(name, where) {
|
|
6
|
+
if (typeof name !== 'string' || !PROVIDER_TOOL_NAME.test(name)) {
|
|
7
|
+
throw new AiConfigurationError(`${where}: the name ${JSON.stringify(name)} is not one model providers accept as a tool name: 1 to 64 ` +
|
|
8
|
+
'characters out of A-Z a-z 0-9 _ -. Give @AgentFunction() a { name } that fits.');
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Schema } from 'ai';
|
|
2
|
+
import type { AiSchema, AiSchemaResult } from '../interfaces/ai-schema.interface.js';
|
|
3
|
+
/** The `ai` schema of a tool without arguments. */
|
|
4
|
+
export declare const EMPTY_OBJECT_SCHEMA: Schema<Record<string, never>>;
|
|
5
|
+
/**
|
|
6
|
+
* The `ai` schema for a Standard Schema with JSON Schema: its JSON Schema (the input side: what the model writes),
|
|
7
|
+
* computed once, and its validation, whose issues become the error the model reads. `object: true` requires an
|
|
8
|
+
* object at the root (tool arguments).
|
|
9
|
+
*/
|
|
10
|
+
export declare function toModelSchema<T = unknown>(schema: AiSchema, where: string, { object }?: {
|
|
11
|
+
object?: boolean | undefined;
|
|
12
|
+
}): Schema<T>;
|
|
13
|
+
/** Validation issues as one line each, with their path: what the model reads to correct itself. */
|
|
14
|
+
export declare function formatIssues(issues: NonNullable<Extract<AiSchemaResult<unknown>, {
|
|
15
|
+
issues: unknown;
|
|
16
|
+
}>['issues']>): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsonSchema } from 'ai';
|
|
2
|
+
import { AiConfigurationError } from '../errors/ai.errors.js';
|
|
3
|
+
/** JSON Schema drafts, in order of preference: what `ai` sends providers, then what MCP uses. */
|
|
4
|
+
const TARGETS = ['draft-07', 'draft-2020-12'];
|
|
5
|
+
const converted = new WeakMap();
|
|
6
|
+
/** The `ai` schema of a tool without arguments. */
|
|
7
|
+
export const EMPTY_OBJECT_SCHEMA = jsonSchema({
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {},
|
|
10
|
+
additionalProperties: false,
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* The `ai` schema for a Standard Schema with JSON Schema: its JSON Schema (the input side: what the model writes),
|
|
14
|
+
* computed once, and its validation, whose issues become the error the model reads. `object: true` requires an
|
|
15
|
+
* object at the root (tool arguments).
|
|
16
|
+
*/
|
|
17
|
+
export function toModelSchema(schema, where, { object = false } = {}) {
|
|
18
|
+
const cached = converted.get(schema);
|
|
19
|
+
if (cached) {
|
|
20
|
+
return cached;
|
|
21
|
+
}
|
|
22
|
+
const standard = schema?.['~standard'];
|
|
23
|
+
if (!standard || typeof standard.validate !== 'function') {
|
|
24
|
+
throw new AiConfigurationError(`${where}: not a Standard Schema. Pass a Zod 4.2+ schema (z.object({ ... })), an ArkType type, a Valibot schema ` +
|
|
25
|
+
'wrapped with toStandardJsonSchema(), or a JSON Schema wrapped with fromJsonSchema() from @modelcontextprotocol/server.');
|
|
26
|
+
}
|
|
27
|
+
if (typeof standard.jsonSchema?.input !== 'function') {
|
|
28
|
+
throw new AiConfigurationError(`${where}: the "${standard.vendor}" schema cannot produce JSON Schema (no ~standard.jsonSchema), which the ` +
|
|
29
|
+
'model needs. Zod needs 4.2 or later (Zod 3 is not supported); Valibot needs toStandardJsonSchema() from ' +
|
|
30
|
+
'@valibot/to-json-schema.');
|
|
31
|
+
}
|
|
32
|
+
let root;
|
|
33
|
+
let failure;
|
|
34
|
+
for (const target of TARGETS) {
|
|
35
|
+
try {
|
|
36
|
+
root = standard.jsonSchema.input({ target });
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
failure = error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!root) {
|
|
44
|
+
throw new AiConfigurationError(`${where}: the schema cannot be converted to JSON Schema: ${failure?.message ?? failure}`);
|
|
45
|
+
}
|
|
46
|
+
if (object && root.type !== undefined && root.type !== 'object') {
|
|
47
|
+
throw new AiConfigurationError(`${where}: arguments are an object, and this schema describes ${JSON.stringify(root.type)}. Wrap it in z.object({ ... }).`);
|
|
48
|
+
}
|
|
49
|
+
const result = jsonSchema(root, {
|
|
50
|
+
validate: async (value) => {
|
|
51
|
+
const outcome = (await standard.validate(value));
|
|
52
|
+
return outcome.issues
|
|
53
|
+
? { success: false, error: new TypeError(formatIssues(outcome.issues)) }
|
|
54
|
+
: { success: true, value: outcome.value };
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
converted.set(schema, result);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
/** Validation issues as one line each, with their path: what the model reads to correct itself. */
|
|
61
|
+
export function formatIssues(issues) {
|
|
62
|
+
return issues
|
|
63
|
+
.map((issue) => {
|
|
64
|
+
const path = (issue.path ?? [])
|
|
65
|
+
.map((segment) => String(typeof segment === 'object' && segment !== null ? segment.key : segment))
|
|
66
|
+
.join('.');
|
|
67
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
68
|
+
})
|
|
69
|
+
.join('\n');
|
|
70
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LanguageModelUsage } from 'ai';
|
|
2
|
+
import type { AiModelPricing } from '../interfaces/ai-module-options.interface.js';
|
|
3
|
+
import type { AiUsage } from '../interfaces/ai-usage.interface.js';
|
|
4
|
+
export declare const ZERO_USAGE: AiUsage;
|
|
5
|
+
/** `ai`'s usage of one model call, with what the provider did not report as 0. */
|
|
6
|
+
export declare function toUsage(usage: LanguageModelUsage | undefined): AiUsage;
|
|
7
|
+
export declare function addUsage(a: AiUsage, b: AiUsage): AiUsage;
|
|
8
|
+
/** The cost of `usage` at `pricing` (per million tokens); `undefined` without pricing. */
|
|
9
|
+
export declare function costOf(usage: AiUsage, pricing: AiModelPricing | undefined): number | undefined;
|
|
10
|
+
export declare function addCost(a: number | undefined, b: number | undefined): number | undefined;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const ZERO_USAGE = Object.freeze({
|
|
2
|
+
inputTokens: 0,
|
|
3
|
+
outputTokens: 0,
|
|
4
|
+
totalTokens: 0,
|
|
5
|
+
cachedInputTokens: 0,
|
|
6
|
+
reasoningTokens: 0,
|
|
7
|
+
});
|
|
8
|
+
/** `ai`'s usage of one model call, with what the provider did not report as 0. */
|
|
9
|
+
export function toUsage(usage) {
|
|
10
|
+
const inputTokens = usage?.inputTokens ?? 0;
|
|
11
|
+
const outputTokens = usage?.outputTokens ?? 0;
|
|
12
|
+
return {
|
|
13
|
+
inputTokens,
|
|
14
|
+
outputTokens,
|
|
15
|
+
totalTokens: usage?.totalTokens ?? inputTokens + outputTokens,
|
|
16
|
+
cachedInputTokens: usage?.inputTokenDetails?.cacheReadTokens ?? 0,
|
|
17
|
+
reasoningTokens: usage?.outputTokenDetails?.reasoningTokens ?? 0,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function addUsage(a, b) {
|
|
21
|
+
return {
|
|
22
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
23
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
24
|
+
totalTokens: a.totalTokens + b.totalTokens,
|
|
25
|
+
cachedInputTokens: a.cachedInputTokens + b.cachedInputTokens,
|
|
26
|
+
reasoningTokens: a.reasoningTokens + b.reasoningTokens,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** The cost of `usage` at `pricing` (per million tokens); `undefined` without pricing. */
|
|
30
|
+
export function costOf(usage, pricing) {
|
|
31
|
+
if (!pricing) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const cached = Math.min(usage.cachedInputTokens, usage.inputTokens);
|
|
35
|
+
const cost = (usage.inputTokens - cached) * pricing.input +
|
|
36
|
+
cached * (pricing.cachedInput ?? pricing.input) +
|
|
37
|
+
usage.outputTokens * pricing.output;
|
|
38
|
+
return cost / 1_000_000;
|
|
39
|
+
}
|
|
40
|
+
export function addCost(a, b) {
|
|
41
|
+
return a === undefined && b === undefined ? undefined : (a ?? 0) + (b ?? 0);
|
|
42
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Duration } from '@nestjs/workflows';
|
|
2
|
+
import type { ModelMessage } from 'ai';
|
|
3
|
+
import type { AiToolChoice } from '../interfaces/ai-call-options.interface.js';
|
|
4
|
+
import type { AiBudget } from '../interfaces/ai-module-options.interface.js';
|
|
5
|
+
import type { AiTextResult } from '../interfaces/ai-result.interface.js';
|
|
6
|
+
import type { AiToolSelection } from '../interfaces/ai-tool.interface.js';
|
|
7
|
+
/** Who a durable run acts for: data, stored with the run, since a request does not survive a restart. */
|
|
8
|
+
export interface AiAgentCaller<TUser = unknown> {
|
|
9
|
+
/** JSON: an id, the claims the agent functions and their guards need. */
|
|
10
|
+
user?: TUser;
|
|
11
|
+
scopes?: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The options of `AiAgents.run()`: those of a call that a run stores and resumes with, so JSON only. Models and agent
|
|
15
|
+
* functions are named; what the module configures for a model (pricing, fallback, retries) applies as for any call.
|
|
16
|
+
*/
|
|
17
|
+
export interface AiAgentRunOptions {
|
|
18
|
+
/** A model's name. Default `'default'`. */
|
|
19
|
+
model?: string;
|
|
20
|
+
instructions?: string;
|
|
21
|
+
prompt?: string;
|
|
22
|
+
messages?: ModelMessage[];
|
|
23
|
+
/** Agent function names and provider classes (all their agent functions), stored as names. */
|
|
24
|
+
tools?: AiToolSelection;
|
|
25
|
+
/**
|
|
26
|
+
* The agent functions, among `tools`, whose calls wait for a person's approval (`AiAgents.approve()` or `reject()`)
|
|
27
|
+
* before they run. A rejected call, or one nobody answered within `approvalTimeout`, is a tool error the model reads.
|
|
28
|
+
*/
|
|
29
|
+
approve?: string[];
|
|
30
|
+
/** How long a call waits for its approval. Default: until it gets one (or the workflow's own timeout). */
|
|
31
|
+
approvalTimeout?: Duration;
|
|
32
|
+
caller?: AiAgentCaller;
|
|
33
|
+
/** Model calls the run may make while the model asks for tools. Default: `defaults.maxSteps`, else 25. */
|
|
34
|
+
maxSteps?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Model calls per workflow instance: after that many, the run goes on in a child workflow, which keeps each
|
|
37
|
+
* instance's journal small (a journal is replayed on every resume, and capped). Default 10.
|
|
38
|
+
*/
|
|
39
|
+
stepsPerInstance?: number;
|
|
40
|
+
/** `maxTokens` and `maxCost`, over the whole run. Default: the module's `defaults.budget`. */
|
|
41
|
+
budget?: AiBudget;
|
|
42
|
+
/** Names the run's model calls in usage events. */
|
|
43
|
+
label?: string;
|
|
44
|
+
toolChoice?: AiToolChoice;
|
|
45
|
+
/** Models tried in order when the model fails after its retries, by name; `false` for none. */
|
|
46
|
+
fallback?: string | string[] | false;
|
|
47
|
+
maxOutputTokens?: number;
|
|
48
|
+
temperature?: number;
|
|
49
|
+
providerOptions?: Record<string, Record<string, any>>;
|
|
50
|
+
}
|
|
51
|
+
/** What `AiAgents.run()` resolves with: a call's result, as JSON. */
|
|
52
|
+
export type AiAgentRunResult = AiTextResult;
|
|
53
|
+
/** A person's answer to a tool call that waits for approval: the `TOOL_APPROVAL` signal's payload. */
|
|
54
|
+
export interface AiToolApproval {
|
|
55
|
+
approved: boolean;
|
|
56
|
+
/** Told to the model when the call is rejected. */
|
|
57
|
+
reason?: string;
|
|
58
|
+
/** Who answered, for your records. */
|
|
59
|
+
by?: unknown;
|
|
60
|
+
}
|
|
61
|
+
/** A tool call waiting for approval, as `AiAgents.pendingApprovals()` lists it. */
|
|
62
|
+
export interface AiPendingApproval {
|
|
63
|
+
/** What `approve()` and `reject()` take. */
|
|
64
|
+
key: string;
|
|
65
|
+
/** The run (the instance `AiAgents.run()` runs in). */
|
|
66
|
+
runId: string;
|
|
67
|
+
toolCallId: string;
|
|
68
|
+
toolName: string;
|
|
69
|
+
input: unknown;
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type WorkflowContext, type WorkflowRunner } from '@nestjs/workflows';
|
|
2
|
+
import { AiAgents, type AgentSettings, type AgentState } from './ai-agents.service.js';
|
|
3
|
+
import type { AiAgentRunResult } from './ai-agent.interfaces.js';
|
|
4
|
+
/** A run of `AiAgents.run()` going on in a child instance, from where its parent stopped. Internal. */
|
|
5
|
+
export declare class AiAgentSegment implements WorkflowRunner<{
|
|
6
|
+
settings: AgentSettings;
|
|
7
|
+
state: AgentState;
|
|
8
|
+
}, AiAgentRunResult> {
|
|
9
|
+
private readonly agents;
|
|
10
|
+
constructor(agents: AiAgents);
|
|
11
|
+
run(ctx: WorkflowContext, { settings, state }: {
|
|
12
|
+
settings: AgentSettings;
|
|
13
|
+
state: AgentState;
|
|
14
|
+
}): Promise<AiAgentRunResult>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { Workflow } from '@nestjs/workflows';
|
|
11
|
+
import { AGENT_WORKFLOW, AiAgents } from './ai-agents.service.js';
|
|
12
|
+
/** A run of `AiAgents.run()` going on in a child instance, from where its parent stopped. Internal. */
|
|
13
|
+
let AiAgentSegment = class AiAgentSegment {
|
|
14
|
+
constructor(agents) {
|
|
15
|
+
this.agents = agents;
|
|
16
|
+
}
|
|
17
|
+
run(ctx, { settings, state }) {
|
|
18
|
+
return this.agents.resume(ctx, settings, state);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
AiAgentSegment = __decorate([
|
|
22
|
+
Workflow(AGENT_WORKFLOW),
|
|
23
|
+
__metadata("design:paramtypes", [AiAgents])
|
|
24
|
+
], AiAgentSegment);
|
|
25
|
+
export { AiAgentSegment };
|