@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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +655 -0
  3. package/dist/ai-module-options.interface-B5X4AFLC.d.cts +192 -0
  4. package/dist/ai-module-options.interface-Ca6y_MY2.d.ts +192 -0
  5. package/dist/conversation-store.interface-CtQY-qcc.d.cts +30 -0
  6. package/dist/conversation-store.interface-CtQY-qcc.d.ts +30 -0
  7. package/dist/index.cjs +3209 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.d.cts +1441 -0
  10. package/dist/index.d.ts +1441 -0
  11. package/dist/index.js +3163 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/stream-to-socket-fJphU0WN.d.cts +47 -0
  14. package/dist/stream-to-socket-fJphU0WN.d.ts +47 -0
  15. package/dist/testing.cjs +2395 -0
  16. package/dist/testing.cjs.map +1 -0
  17. package/dist/testing.d.cts +46 -0
  18. package/dist/testing.d.ts +46 -0
  19. package/dist/testing.js +2391 -0
  20. package/dist/testing.js.map +1 -0
  21. package/dist/typeorm.cjs +115 -0
  22. package/dist/typeorm.cjs.map +1 -0
  23. package/dist/typeorm.d.cts +44 -0
  24. package/dist/typeorm.d.ts +44 -0
  25. package/dist/typeorm.js +113 -0
  26. package/dist/typeorm.js.map +1 -0
  27. package/dist/websocket.cjs +154 -0
  28. package/dist/websocket.cjs.map +1 -0
  29. package/dist/websocket.d.cts +25 -0
  30. package/dist/websocket.d.ts +25 -0
  31. package/dist/websocket.js +152 -0
  32. package/dist/websocket.js.map +1 -0
  33. package/documentation/README.md +44 -0
  34. package/documentation/agents-and-tools.md +102 -0
  35. package/documentation/api-reference.md +117 -0
  36. package/documentation/configuration.md +87 -0
  37. package/documentation/content-safety.md +51 -0
  38. package/documentation/embeddings-and-rag.md +105 -0
  39. package/documentation/evals-and-testing.md +56 -0
  40. package/documentation/getting-started.md +96 -0
  41. package/documentation/guardrails-events-telemetry.md +72 -0
  42. package/documentation/jobs-and-realtime.md +69 -0
  43. package/documentation/memory.md +101 -0
  44. package/documentation/multimodal.md +49 -0
  45. package/documentation/orchestration-and-mcp.md +68 -0
  46. package/documentation/prompts.md +51 -0
  47. package/documentation/reliability.md +90 -0
  48. package/documentation/structured-output-and-streaming.md +81 -0
  49. package/package.json +146 -0
  50. 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