@mgvdev/nestjs-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +655 -0
- package/dist/ai-module-options.interface-B5X4AFLC.d.cts +192 -0
- package/dist/ai-module-options.interface-Ca6y_MY2.d.ts +192 -0
- package/dist/conversation-store.interface-CtQY-qcc.d.cts +30 -0
- package/dist/conversation-store.interface-CtQY-qcc.d.ts +30 -0
- package/dist/index.cjs +3209 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1441 -0
- package/dist/index.d.ts +1441 -0
- package/dist/index.js +3163 -0
- package/dist/index.js.map +1 -0
- package/dist/stream-to-socket-fJphU0WN.d.cts +47 -0
- package/dist/stream-to-socket-fJphU0WN.d.ts +47 -0
- package/dist/testing.cjs +2395 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +46 -0
- package/dist/testing.d.ts +46 -0
- package/dist/testing.js +2391 -0
- package/dist/testing.js.map +1 -0
- package/dist/typeorm.cjs +115 -0
- package/dist/typeorm.cjs.map +1 -0
- package/dist/typeorm.d.cts +44 -0
- package/dist/typeorm.d.ts +44 -0
- package/dist/typeorm.js +113 -0
- package/dist/typeorm.js.map +1 -0
- package/dist/websocket.cjs +154 -0
- package/dist/websocket.cjs.map +1 -0
- package/dist/websocket.d.cts +25 -0
- package/dist/websocket.d.ts +25 -0
- package/dist/websocket.js +152 -0
- package/dist/websocket.js.map +1 -0
- package/documentation/README.md +44 -0
- package/documentation/agents-and-tools.md +102 -0
- package/documentation/api-reference.md +117 -0
- package/documentation/configuration.md +87 -0
- package/documentation/content-safety.md +51 -0
- package/documentation/embeddings-and-rag.md +105 -0
- package/documentation/evals-and-testing.md +56 -0
- package/documentation/getting-started.md +96 -0
- package/documentation/guardrails-events-telemetry.md +72 -0
- package/documentation/jobs-and-realtime.md +69 -0
- package/documentation/memory.md +101 -0
- package/documentation/multimodal.md +49 -0
- package/documentation/orchestration-and-mcp.md +68 -0
- package/documentation/prompts.md +51 -0
- package/documentation/reliability.md +90 -0
- package/documentation/structured-output-and-streaming.md +81 -0
- package/package.json +146 -0
- package/skill/nestjs-ai/SKILL.md +154 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maxence Guyonvarho
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
# @mgvdev/nestjs-ai
|
|
2
|
+
|
|
3
|
+
Structured AI toolkit for **NestJS** — build agents with tools, structured
|
|
4
|
+
output, streaming and embeddings on top of the [Vercel AI SDK](https://ai-sdk.dev),
|
|
5
|
+
with first-class dependency injection.
|
|
6
|
+
|
|
7
|
+
Inspired by [Laravel AI](https://github.com/laravel/ai): a consistent,
|
|
8
|
+
framework-native interface over multiple providers (OpenAI, Anthropic, Google).
|
|
9
|
+
|
|
10
|
+
> **Docs:** full guides live in [`documentation/`](./documentation/README.md).
|
|
11
|
+
> A bundled Claude Code skill lives at [`skill/nestjs-ai/SKILL.md`](./skill/nestjs-ai/SKILL.md) —
|
|
12
|
+
> copy it to `.claude/skills/nestjs-ai/` (or symlink from `node_modules/@mgvdev/nestjs-ai/skill/nestjs-ai`)
|
|
13
|
+
> to let Claude build with this library.
|
|
14
|
+
|
|
15
|
+
- 🧩 **`@Agent` / `@Tool` decorators** — agents and tools are ordinary NestJS
|
|
16
|
+
providers, so they can inject repositories, services, config, anything.
|
|
17
|
+
- 🔧 **Automatic tool discovery** — tools are found across your app and wired to
|
|
18
|
+
the model; execution runs on the DI-managed instance.
|
|
19
|
+
- 🧠 **Pluggable conversation memory** — a `ConversationStore` interface with an
|
|
20
|
+
in-memory default; bring your own DB.
|
|
21
|
+
- 📦 **Structured output & streaming** — Zod-validated objects and token streams.
|
|
22
|
+
- 🔢 **Embeddings** — one injectable service for `embed` / `embedMany`.
|
|
23
|
+
- 🖼️ **Multimodal** — image generation, speech synthesis, transcription.
|
|
24
|
+
- 📚 **RAG** — vector store interface, in-memory impl, retrieval tool for agents.
|
|
25
|
+
- 📝 **Prompt registry** — named, versioned templates with interpolation.
|
|
26
|
+
- 🛡️ **Events & guardrails** — lifecycle events (`@OnEvent`) and blocking hooks.
|
|
27
|
+
- 💾 **Durable memory** — TypeORM and Prisma conversation-store adapters.
|
|
28
|
+
- 🤝 **Multi-agent** — agents as tools (supervisor / handoff).
|
|
29
|
+
- 🔌 **MCP** — adapt Model Context Protocol servers into agent tools.
|
|
30
|
+
- ♻️ **Resilience** — model fallback chains, retries, response/embedding caching.
|
|
31
|
+
- ✋ **Human-in-the-loop** — tool approval gates.
|
|
32
|
+
- ⏱️ **Background jobs** — run agents asynchronously on BullMQ.
|
|
33
|
+
- 🌐 **HTTP/SSE** — stream agents straight to the response.
|
|
34
|
+
- 💰 **Cost & budgets** — token/cost tracking with per-conversation budget guards.
|
|
35
|
+
- 🚦 **Rate limiting** — throttle runs per conversation/user.
|
|
36
|
+
- 🧠 **Semantic memory** — long-term recall via embeddings.
|
|
37
|
+
- 🛡️ **Content safety** — PII redaction and moderation guardrails.
|
|
38
|
+
- 🥇 **Reranking** — heuristic or model reranking for RAG.
|
|
39
|
+
- 🧪 **Testing & evals** — mock helpers and an LLM-as-judge harness.
|
|
40
|
+
- 🔌 **More vector stores** — Qdrant, Pinecone (plus pgvector).
|
|
41
|
+
- 📡 **WebSocket** — realtime agent streaming over socket.io.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install @mgvdev/nestjs-ai ai zod
|
|
47
|
+
# plus the provider SDK(s) you use:
|
|
48
|
+
npm install @ai-sdk/openai # and/or @ai-sdk/anthropic, @ai-sdk/google
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`@nestjs/common`, `@nestjs/core` and `reflect-metadata` are peer dependencies
|
|
52
|
+
(already present in every Nest app).
|
|
53
|
+
|
|
54
|
+
Optional peers, only needed for the matching feature:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install @nestjs/event-emitter # events & guardrails
|
|
58
|
+
npm install @nestjs/typeorm typeorm # TypeORM conversation store
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { Module } from '@nestjs/common';
|
|
65
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
66
|
+
import { AiModule } from '@mgvdev/nestjs-ai';
|
|
67
|
+
|
|
68
|
+
@Module({
|
|
69
|
+
imports: [
|
|
70
|
+
ConfigModule.forRoot(),
|
|
71
|
+
AiModule.forRootAsync({
|
|
72
|
+
imports: [ConfigModule],
|
|
73
|
+
inject: [ConfigService],
|
|
74
|
+
useFactory: (config: ConfigService) => ({
|
|
75
|
+
providers: {
|
|
76
|
+
openai: { apiKey: config.getOrThrow('OPENAI_API_KEY') },
|
|
77
|
+
},
|
|
78
|
+
defaultModel: 'openai:gpt-4o',
|
|
79
|
+
defaultEmbeddingModel: 'openai:text-embedding-3-small',
|
|
80
|
+
}),
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
})
|
|
84
|
+
export class AppModule {}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`AiModule.forRoot(options)` is available for static configuration. The module is
|
|
88
|
+
global, so agents/tools/services are injectable everywhere.
|
|
89
|
+
|
|
90
|
+
Model ids are `"provider:model"` (e.g. `"openai:gpt-4o"`). A bare `"gpt-4o"`
|
|
91
|
+
works when a single provider is configured or a `defaultModel` sets the provider.
|
|
92
|
+
|
|
93
|
+
## Tools
|
|
94
|
+
|
|
95
|
+
Declare tools as methods on an injectable provider. The Zod `schema` types the
|
|
96
|
+
arguments; the return value is fed back to the model.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { Injectable } from '@nestjs/common';
|
|
100
|
+
import { Tool } from '@mgvdev/nestjs-ai';
|
|
101
|
+
import { z } from 'zod';
|
|
102
|
+
|
|
103
|
+
@Injectable()
|
|
104
|
+
export class WeatherTools {
|
|
105
|
+
constructor(private readonly weather: WeatherApi) {} // regular DI
|
|
106
|
+
|
|
107
|
+
@Tool({
|
|
108
|
+
description: 'Get the current weather for a city',
|
|
109
|
+
schema: z.object({ city: z.string() }),
|
|
110
|
+
})
|
|
111
|
+
getWeather({ city }: { city: string }) {
|
|
112
|
+
return this.weather.lookup(city); // uses the injected dependency
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Agents
|
|
118
|
+
|
|
119
|
+
Extend `AiAgent` and annotate with `@Agent`. Reference tool provider classes in
|
|
120
|
+
`tools`.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { Agent, AiAgent } from '@mgvdev/nestjs-ai';
|
|
124
|
+
|
|
125
|
+
@Agent({
|
|
126
|
+
model: 'openai:gpt-4o',
|
|
127
|
+
system: 'You are a helpful weather assistant.',
|
|
128
|
+
tools: [WeatherTools],
|
|
129
|
+
maxSteps: 5,
|
|
130
|
+
})
|
|
131
|
+
export class WeatherAgent extends AiAgent {}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Register the classes (or use `AiModule.forFeature`):
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
@Module({
|
|
138
|
+
imports: [AiModule.forFeature({ agents: [WeatherAgent], tools: [WeatherTools] })],
|
|
139
|
+
providers: [WeatherApi],
|
|
140
|
+
})
|
|
141
|
+
export class WeatherModule {}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Run it:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
@Injectable()
|
|
148
|
+
export class ChatService {
|
|
149
|
+
constructor(private readonly agent: WeatherAgent) {}
|
|
150
|
+
|
|
151
|
+
async ask(question: string) {
|
|
152
|
+
const { text } = await this.agent.run(question);
|
|
153
|
+
return text;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Conversation memory
|
|
159
|
+
|
|
160
|
+
Pass a `conversationId` to load prior turns and persist the new exchange:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
await this.agent.run('What about tomorrow?', { conversationId: user.id });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The default store is in-memory. Provide your own by implementing
|
|
167
|
+
`ConversationStore` and passing it to `AiModule.forRoot({ conversationStore })`.
|
|
168
|
+
|
|
169
|
+
### Structured output
|
|
170
|
+
|
|
171
|
+
Declare an `output` schema on the agent (or pass `schema` per call) to get a
|
|
172
|
+
validated object instead of text:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
@Agent({
|
|
176
|
+
model: 'openai:gpt-4o',
|
|
177
|
+
system: 'Extract the order details.',
|
|
178
|
+
output: z.object({ product: z.string(), quantity: z.number() }),
|
|
179
|
+
})
|
|
180
|
+
export class OrderAgent extends AiAgent {}
|
|
181
|
+
|
|
182
|
+
const { object } = await orderAgent.run<{ product: string; quantity: number }>(text);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Streaming
|
|
186
|
+
|
|
187
|
+
`.stream()` returns the raw Vercel AI SDK stream result, so you can pipe it to an
|
|
188
|
+
HTTP response or iterate `textStream`:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
@Controller('chat')
|
|
192
|
+
export class ChatController {
|
|
193
|
+
constructor(private readonly agent: WeatherAgent) {}
|
|
194
|
+
|
|
195
|
+
@Post()
|
|
196
|
+
stream(@Body('prompt') prompt: string, @Res() res: Response) {
|
|
197
|
+
const result = this.agent.stream(prompt);
|
|
198
|
+
result.pipeUIMessageStreamToResponse(res);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Embeddings
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { EmbeddingsService } from '@mgvdev/nestjs-ai';
|
|
207
|
+
|
|
208
|
+
@Injectable()
|
|
209
|
+
export class SearchService {
|
|
210
|
+
constructor(private readonly embeddings: EmbeddingsService) {}
|
|
211
|
+
|
|
212
|
+
async index(docs: string[]) {
|
|
213
|
+
const { embeddings } = await this.embeddings.embedMany(docs);
|
|
214
|
+
return embeddings;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Multimodal
|
|
220
|
+
|
|
221
|
+
`ImageService`, `SpeechService`, and `TranscriptionService` wrap the SDK's
|
|
222
|
+
image/speech/transcription APIs. Set `defaultImageModel` / `defaultSpeechModel` /
|
|
223
|
+
`defaultTranscriptionModel` in options, or pass `model` per call.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const { image } = await this.images.generate('a fox in the snow', { size: '1024x1024' });
|
|
227
|
+
const { audio } = await this.speech.generate('Hello there', { model: 'openai:tts-1', voice: 'alloy' });
|
|
228
|
+
const { text } = await this.transcription.transcribe(buffer, { model: 'openai:whisper-1' });
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## RAG (retrieval-augmented generation)
|
|
232
|
+
|
|
233
|
+
Ingest documents (chunked + embedded) into a `VectorStore`, then retrieve the
|
|
234
|
+
most relevant chunks. The default store is `InMemoryVectorStore`
|
|
235
|
+
(cosine similarity); override `vectorStore` in options for pgvector etc.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
import { RagService, createRetrievalTool } from '@mgvdev/nestjs-ai';
|
|
239
|
+
|
|
240
|
+
await this.rag.ingest([
|
|
241
|
+
{ id: 'doc1', content: longText, metadata: { source: 'handbook' } },
|
|
242
|
+
]);
|
|
243
|
+
const hits = await this.rag.retrieve('vacation policy', { topK: 4 });
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Expose retrieval to an agent as a tool — either a `@Tool` method that calls
|
|
247
|
+
`RagService`, or the `createRetrievalTool` factory:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
@Injectable()
|
|
251
|
+
class KnowledgeTools {
|
|
252
|
+
constructor(private readonly rag: RagService) {}
|
|
253
|
+
|
|
254
|
+
@Tool({ description: 'Search the handbook', schema: z.object({ query: z.string() }) })
|
|
255
|
+
async search({ query }: { query: string }) {
|
|
256
|
+
const hits = await this.rag.retrieve(query);
|
|
257
|
+
return hits.map((h) => h.content).join('\n\n');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Prompt registry
|
|
263
|
+
|
|
264
|
+
Register named, versioned templates and render them with `{{var}}` interpolation.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
AiModule.forRoot({
|
|
268
|
+
prompts: [
|
|
269
|
+
{ name: 'support', version: 'v1', template: 'Help {{user}} with {{topic}}.' },
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// render directly…
|
|
274
|
+
const system = this.prompts.render('support', { user: 'Ada', topic: 'billing' });
|
|
275
|
+
// …or resolve an agent's system prompt per call:
|
|
276
|
+
await agent.run(question, { systemPrompt: { name: 'support', vars: { user, topic } } });
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Events & guardrails
|
|
280
|
+
|
|
281
|
+
With `@nestjs/event-emitter` installed and `EventEmitterModule.forRoot()`
|
|
282
|
+
imported, the library emits lifecycle events (no-op otherwise):
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { AI_EVENTS } from '@mgvdev/nestjs-ai';
|
|
286
|
+
|
|
287
|
+
@Injectable()
|
|
288
|
+
class AiListener {
|
|
289
|
+
@OnEvent(AI_EVENTS.agentRunFinish)
|
|
290
|
+
onFinish(payload: { agent: string; result: AgentResult }) {
|
|
291
|
+
// log usage, persist traces, …
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Events: `ai.agent.run.start|finish|error`, `ai.tool.call|result`, `ai.stream.finish`.
|
|
297
|
+
|
|
298
|
+
Guardrails inspect, mutate, or block runs and tool calls. Throw to abort:
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import { Guardrail, type GuardrailContext } from '@mgvdev/nestjs-ai';
|
|
302
|
+
|
|
303
|
+
@Guardrail()
|
|
304
|
+
export class ProfanityGuard {
|
|
305
|
+
beforeRun(ctx: GuardrailContext) {
|
|
306
|
+
if (isProfane(ctx.messages)) throw new ForbiddenException('Blocked');
|
|
307
|
+
}
|
|
308
|
+
onToolCall(tool: string, args: unknown) {
|
|
309
|
+
// veto specific tool calls
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Register guardrails as providers (auto-discovered) or via
|
|
315
|
+
`AiModule.forRoot({ guardrails: [ProfanityGuard] })`.
|
|
316
|
+
|
|
317
|
+
## Telemetry
|
|
318
|
+
|
|
319
|
+
Forward OpenTelemetry spans to the AI SDK by enabling telemetry (requires an OTel
|
|
320
|
+
setup in your app):
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
AiModule.forRoot({ telemetry: { isEnabled: true, functionId: 'support-agent' } });
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Persistent conversation stores
|
|
327
|
+
|
|
328
|
+
The in-memory store is the default. For durable history:
|
|
329
|
+
|
|
330
|
+
**TypeORM** (subpath `@mgvdev/nestjs-ai/typeorm`, needs `typeorm` + `@nestjs/typeorm`):
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { getRepositoryToken, TypeOrmModule } from '@nestjs/typeorm';
|
|
334
|
+
import {
|
|
335
|
+
ConversationMessageEntity,
|
|
336
|
+
TypeOrmConversationStore,
|
|
337
|
+
} from '@mgvdev/nestjs-ai/typeorm';
|
|
338
|
+
|
|
339
|
+
@Module({
|
|
340
|
+
imports: [
|
|
341
|
+
TypeOrmModule.forFeature([ConversationMessageEntity]),
|
|
342
|
+
AiModule.forRoot({
|
|
343
|
+
providers: { openai: { apiKey: process.env.OPENAI_API_KEY } },
|
|
344
|
+
conversationStore: {
|
|
345
|
+
useFactory: (repo) => new TypeOrmConversationStore(repo),
|
|
346
|
+
inject: [getRepositoryToken(ConversationMessageEntity)],
|
|
347
|
+
},
|
|
348
|
+
}),
|
|
349
|
+
],
|
|
350
|
+
})
|
|
351
|
+
export class AppModule {}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Prisma** (no build-time dependency — pass a model delegate):
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import { PrismaConversationStore } from '@mgvdev/nestjs-ai';
|
|
358
|
+
|
|
359
|
+
AiModule.forRoot({
|
|
360
|
+
conversationStore: {
|
|
361
|
+
useFactory: (prisma: PrismaService) => new PrismaConversationStore(prisma.aiMessage),
|
|
362
|
+
inject: [PrismaService],
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
```prisma
|
|
368
|
+
model AiMessage {
|
|
369
|
+
id String @id @default(cuid())
|
|
370
|
+
conversationId String
|
|
371
|
+
role String
|
|
372
|
+
content Json
|
|
373
|
+
position Int
|
|
374
|
+
createdAt DateTime @default(now())
|
|
375
|
+
@@index([conversationId])
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Multi-agent orchestration
|
|
380
|
+
|
|
381
|
+
Reference an `@Agent` class in another agent's `tools` to delegate to it
|
|
382
|
+
(supervisor / handoff). The sub-agent's output is returned to the caller.
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
@Agent({ model: 'openai:gpt-4o', system: 'Research facts.' })
|
|
386
|
+
export class ResearchAgent extends AiAgent {}
|
|
387
|
+
|
|
388
|
+
@Agent({
|
|
389
|
+
model: 'openai:gpt-4o',
|
|
390
|
+
system: 'Coordinate specialists to answer the user.',
|
|
391
|
+
tools: [ResearchAgent, WriterAgent],
|
|
392
|
+
})
|
|
393
|
+
export class SupervisorAgent extends AiAgent {}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`AgentRegistry` indexes all agents by class name (used by orchestration and
|
|
397
|
+
background jobs); `createAgentTool(agent, opts)` wraps any agent as a tool
|
|
398
|
+
manually.
|
|
399
|
+
|
|
400
|
+
## MCP (Model Context Protocol)
|
|
401
|
+
|
|
402
|
+
Adapt tools from an MCP server into an agent tool set. Bring your own client
|
|
403
|
+
from `@modelcontextprotocol/sdk` (a `listTools`/`callTool`/`close` object):
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
const set = await this.mcp.connect('filesystem', mcpClient);
|
|
407
|
+
// tools are now available; reference them via AiService or merge into an agent
|
|
408
|
+
await this.ai.generateText({ model: 'openai:gpt-4o', tools: set, prompt });
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Resilience: fallback models & retries
|
|
412
|
+
|
|
413
|
+
Pass a model **array** to configure a fallback chain (each tried in order), and
|
|
414
|
+
`maxRetries` for SDK-native retries:
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
@Agent({ model: ['openai:gpt-4o', 'anthropic:claude-sonnet-5'] })
|
|
418
|
+
export class ResilientAgent extends AiAgent {}
|
|
419
|
+
|
|
420
|
+
await agent.run(prompt, { maxRetries: 3 });
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Caching
|
|
424
|
+
|
|
425
|
+
Configure a cache to memoize model responses (via middleware) and single
|
|
426
|
+
embeddings. Implement `AiCache` (e.g. Redis) or use the in-memory default:
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
AiModule.forRoot({
|
|
430
|
+
providers: { openai: { apiKey } },
|
|
431
|
+
cache: InMemoryAiCache,
|
|
432
|
+
cacheTtlMs: 60_000,
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
## Human-in-the-loop (tool approval)
|
|
437
|
+
|
|
438
|
+
Flag a tool with `requiresApproval` and register an `ApprovalGate`. The gate is
|
|
439
|
+
consulted before the tool executes; returning `false` blocks it.
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
@Tool({ description: 'Delete a record', schema: z.object({ id: z.string() }), requiresApproval: true })
|
|
443
|
+
deleteRecord({ id }: { id: string }) { /* … */ }
|
|
444
|
+
|
|
445
|
+
@Injectable()
|
|
446
|
+
class QueueApprovalGate implements ApprovalGate {
|
|
447
|
+
async requestApproval({ tool, args }: ApprovalContext) {
|
|
448
|
+
return askAHuman(tool, args); // resolve true/false
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
AiModule.forRoot({ providers: { openai: { apiKey } }, approvalGate: QueueApprovalGate });
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Background jobs (BullMQ)
|
|
456
|
+
|
|
457
|
+
Run agents asynchronously. Import `AgentJobsModule` (needs the optional peer
|
|
458
|
+
`bullmq` + Redis); a worker resolves agents from `AgentRegistry`.
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
@Module({ imports: [AgentJobsModule.forRoot({ connection: { host: 'localhost', port: 6379 } })] })
|
|
462
|
+
export class JobsModule {}
|
|
463
|
+
|
|
464
|
+
// enqueue
|
|
465
|
+
await this.queue.enqueue({ agent: 'SupportAgent', input: 'summarize ticket 42' });
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## HTTP / SSE streaming
|
|
469
|
+
|
|
470
|
+
Pipe an agent stream straight to the HTTP response:
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
@Post('chat')
|
|
474
|
+
chat(@Body('prompt') prompt: string, @Res() res: Response) {
|
|
475
|
+
pipeAgentStream(this.agent.stream(prompt), res, { protocol: 'ui' });
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Or use the interceptor and just return the stream result:
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
@UseInterceptors(new AgentStreamInterceptor({ protocol: 'ui' }))
|
|
483
|
+
@Post('chat')
|
|
484
|
+
chat(@Body('prompt') prompt: string) {
|
|
485
|
+
return this.agent.stream(prompt);
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## pgvector
|
|
490
|
+
|
|
491
|
+
A Postgres/pgvector `VectorStore`. Pass your own `pg` Pool:
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
import { PgVectorStore } from '@mgvdev/nestjs-ai';
|
|
495
|
+
|
|
496
|
+
AiModule.forRoot({
|
|
497
|
+
providers: { openai: { apiKey } },
|
|
498
|
+
vectorStore: { useFactory: () => new PgVectorStore(pool, { table: 'ai_documents' }) },
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
```sql
|
|
503
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
504
|
+
CREATE TABLE ai_documents (
|
|
505
|
+
id text PRIMARY KEY, content text NOT NULL,
|
|
506
|
+
embedding vector(1536) NOT NULL, metadata jsonb
|
|
507
|
+
);
|
|
508
|
+
CREATE INDEX ON ai_documents USING hnsw (embedding vector_cosine_ops);
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## Cost tracking & budgets
|
|
512
|
+
|
|
513
|
+
Token usage and USD cost are tracked per conversation. Set a budget to block
|
|
514
|
+
runaway spend:
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
AiModule.forRoot({
|
|
518
|
+
providers: { openai: { apiKey } },
|
|
519
|
+
maxCostPerConversation: 0.5, // blocks the next run once $0.50 is reached
|
|
520
|
+
pricing: { 'gpt-4o': { input: 2.5, output: 10 } }, // override defaults
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// inspect anytime
|
|
524
|
+
const { cost, inputTokens } = this.usage.totals(conversationId);
|
|
525
|
+
// or listen: @OnEvent('ai.usage') onUsage(record) { ... }
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
## Rate limiting
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
AiModule.forRoot({
|
|
532
|
+
providers: { openai: { apiKey } },
|
|
533
|
+
rateLimiter: {
|
|
534
|
+
useValue: new InMemoryRateLimiter({ capacity: 10, refillTokens: 10, intervalMs: 60_000 }),
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Runs are throttled per `conversationId`. Implement `RateLimiter` for a
|
|
540
|
+
distributed (Redis) limiter.
|
|
541
|
+
|
|
542
|
+
## Semantic memory
|
|
543
|
+
|
|
544
|
+
Store and recall context across turns via embeddings:
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
await this.memory.remember(conversationId, 'The user prefers dark mode.');
|
|
548
|
+
// recall automatically on the next run:
|
|
549
|
+
await agent.run('what are my settings?', { conversationId, recall: { topK: 3 } });
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
## Content safety
|
|
553
|
+
|
|
554
|
+
Register guardrails to redact PII and moderate content:
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
AiModule.forFeature({
|
|
558
|
+
guardrails: [
|
|
559
|
+
PiiRedactionGuardrail,
|
|
560
|
+
createModerationGuardrail({ blocked: ['secret-project'] }),
|
|
561
|
+
],
|
|
562
|
+
});
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## Reranking
|
|
566
|
+
|
|
567
|
+
Improve RAG precision by reranking candidates:
|
|
568
|
+
|
|
569
|
+
```ts
|
|
570
|
+
// heuristic (no dependency), or configure a model reranker:
|
|
571
|
+
const hits = await this.rag.retrieve('query', { topK: 4, rerank: true });
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
## More vector stores
|
|
575
|
+
|
|
576
|
+
`QdrantVectorStore` and `PineconeVectorStore` join `PgVectorStore` and
|
|
577
|
+
`InMemoryVectorStore`. Pass your own client:
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
AiModule.forRoot({
|
|
581
|
+
providers: { openai: { apiKey } },
|
|
582
|
+
vectorStore: { useFactory: () => new QdrantVectorStore(qdrant, { collection: 'docs' }) },
|
|
583
|
+
});
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
## Evals (LLM-as-judge)
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
const report = await this.evals.run(myAgent, [
|
|
590
|
+
{ input: 'capital of France?', expected: 'Paris' },
|
|
591
|
+
], { judge: createLlmJudge(this.ai, { scale: 5 }) });
|
|
592
|
+
// report.averageScore, report.passRate, report.results
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
## Testing utilities
|
|
596
|
+
|
|
597
|
+
From `@mgvdev/nestjs-ai/testing` (needs `msw`):
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
import { createTestingAiModule, createMockModel } from '@mgvdev/nestjs-ai/testing';
|
|
601
|
+
|
|
602
|
+
const app = await createTestingAiModule({
|
|
603
|
+
model: createMockModel('mocked answer'),
|
|
604
|
+
providers: [MyAgent],
|
|
605
|
+
});
|
|
606
|
+
expect((await app.get(MyAgent).run('hi')).text).toBe('mocked answer');
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## Realtime (WebSocket)
|
|
610
|
+
|
|
611
|
+
From `@mgvdev/nestjs-ai/websocket` (needs `@nestjs/websockets`, `socket.io`):
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
@Module({ providers: [AgentGateway] })
|
|
615
|
+
export class RealtimeModule {}
|
|
616
|
+
// client: socket.emit('agent:run', { agent: 'ChatAgent', input });
|
|
617
|
+
// socket.on('agent:chunk', ({ delta }) => …); socket.on('agent:done', ({ text }) => …);
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
## Raw generation (no agent class)
|
|
621
|
+
|
|
622
|
+
`AiService` is a thin, DI-friendly facade over the SDK when you don't want an
|
|
623
|
+
agent class. It resolves `model` strings and `tools` references:
|
|
624
|
+
|
|
625
|
+
```ts
|
|
626
|
+
import { AiService } from '@mgvdev/nestjs-ai';
|
|
627
|
+
|
|
628
|
+
const { text } = await this.ai.generateText({
|
|
629
|
+
model: 'openai:gpt-4o',
|
|
630
|
+
tools: [WeatherTools],
|
|
631
|
+
prompt: 'Weather in Paris?',
|
|
632
|
+
});
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
## API surface
|
|
636
|
+
|
|
637
|
+
| Export | Purpose |
|
|
638
|
+
| --- | --- |
|
|
639
|
+
| `AiModule` | `forRoot` / `forRootAsync` / `forFeature` |
|
|
640
|
+
| `@Agent`, `AiAgent` | Declare and run agents (`.run` / `.stream`) |
|
|
641
|
+
| `@Tool` | Declare a tool method on a provider |
|
|
642
|
+
| `AiService` | Raw `generateText` / `streamText` / `generateObject` / `streamObject` |
|
|
643
|
+
| `EmbeddingsService` | `embed` / `embedMany` |
|
|
644
|
+
| `ImageService`, `SpeechService`, `TranscriptionService` | Multimodal generation |
|
|
645
|
+
| `RagService`, `VectorStore`, `InMemoryVectorStore`, `createRetrievalTool` | RAG |
|
|
646
|
+
| `PromptRegistry` | Named, versioned prompt templates |
|
|
647
|
+
| `@Guardrail`, `GuardrailRegistry`, `AiEventEmitter`, `AI_EVENTS` | Events & guardrails |
|
|
648
|
+
| `ProviderRegistry` | Resolve `"provider:model"` ids to models |
|
|
649
|
+
| `ConversationStore`, `InMemoryConversationStore` | Conversation memory |
|
|
650
|
+
| `TypeOrmConversationStore` (`/typeorm`), `PrismaConversationStore` | Durable memory |
|
|
651
|
+
| `ToolRegistry` | Introspect discovered tools |
|
|
652
|
+
|
|
653
|
+
## License
|
|
654
|
+
|
|
655
|
+
MIT © Maxence Guyonvarho
|