@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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
All exports are from `@mgvdev/nestjs-ai` unless a subpath is noted.
|
|
4
|
+
|
|
5
|
+
## Module
|
|
6
|
+
|
|
7
|
+
| Export | Kind | Notes |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `AiModule` | module | `forRoot` / `forRootAsync` / `forFeature` |
|
|
10
|
+
| `AI_MODULE_OPTIONS` | token | injected options |
|
|
11
|
+
| `DEFAULT_MAX_STEPS` | const | default tool-loop bound (5) |
|
|
12
|
+
|
|
13
|
+
### Tokens
|
|
14
|
+
|
|
15
|
+
`CONVERSATION_STORE` · `VECTOR_STORE` · `AI_CACHE` · `APPROVAL_GATE` ·
|
|
16
|
+
`AGENT_QUEUE` · `RATE_LIMITER` · `RERANKER`.
|
|
17
|
+
|
|
18
|
+
## Agents & tools
|
|
19
|
+
|
|
20
|
+
| Export | Kind |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `@Agent(options)` | class decorator |
|
|
23
|
+
| `AiAgent` | base class (`.run` / `.stream`) |
|
|
24
|
+
| `AgentExecutorService` | runtime |
|
|
25
|
+
| `@Tool(options)` | method decorator |
|
|
26
|
+
| `ToolRegistry` | discovered-tool registry |
|
|
27
|
+
| `AiService` | raw `generateText`/`streamText`/`generateObject`/`streamObject` |
|
|
28
|
+
| `ProviderRegistry` | model resolution |
|
|
29
|
+
|
|
30
|
+
Types: `AgentOptions`, `AgentRunOptions`, `AgentResult`, `ToolOptions`,
|
|
31
|
+
`ToolMetadata`, `ToolEntry`, `ToolRef`, `AiMessage`, `AiInput`, `toMessages`.
|
|
32
|
+
|
|
33
|
+
## Embeddings & RAG
|
|
34
|
+
|
|
35
|
+
| Export | Kind |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `EmbeddingsService` | `embed` / `embedMany` |
|
|
38
|
+
| `RagService` | `ingest` / `retrieve` |
|
|
39
|
+
| `createRetrievalTool(rag, opts)` | tool factory |
|
|
40
|
+
| `InMemoryVectorStore` | default store |
|
|
41
|
+
| `PgVectorStore` | Postgres/pgvector (structural `pg` Pool) |
|
|
42
|
+
| `QdrantVectorStore` | Qdrant (structural client) |
|
|
43
|
+
| `PineconeVectorStore` | Pinecone (structural index) |
|
|
44
|
+
| `HeuristicReranker` / `ModelReranker` | reranking |
|
|
45
|
+
| `splitText(text, size, overlap)` | chunker |
|
|
46
|
+
|
|
47
|
+
Types: `VectorStore`, `VectorDocument`, `VectorQueryResult`, `VectorQueryOptions`,
|
|
48
|
+
`IngestItem`, `IngestOptions`, `RetrieveOptions`, `Reranker`.
|
|
49
|
+
|
|
50
|
+
## Multimodal
|
|
51
|
+
|
|
52
|
+
`ImageService` · `SpeechService` · `TranscriptionService`. Option types:
|
|
53
|
+
`GenerateImageOptions`, `GenerateSpeechOptions`, `TranscribeOptions`, `AudioInput`.
|
|
54
|
+
|
|
55
|
+
## Prompts
|
|
56
|
+
|
|
57
|
+
`PromptRegistry` · `interpolate(template, vars)`. Types: `PromptDefinition`,
|
|
58
|
+
`PromptRef`.
|
|
59
|
+
|
|
60
|
+
## Memory
|
|
61
|
+
|
|
62
|
+
`InMemoryConversationStore` · `SemanticMemory`. Interface: `ConversationStore`.
|
|
63
|
+
Subpath `@mgvdev/nestjs-ai/typeorm`: `TypeOrmConversationStore`,
|
|
64
|
+
`ConversationMessageEntity`. Main: `PrismaConversationStore`,
|
|
65
|
+
`PrismaConversationDelegate`.
|
|
66
|
+
|
|
67
|
+
## Guardrails, events & safety
|
|
68
|
+
|
|
69
|
+
| Export | Kind |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `@Guardrail()` | class decorator |
|
|
72
|
+
| `GuardrailRegistry` | runs the chain |
|
|
73
|
+
| `AiEventEmitter` / `AI_EVENTS` | events |
|
|
74
|
+
| `PiiRedactionGuardrail` / `createPiiRedactionGuardrail` | PII |
|
|
75
|
+
| `redactPii` / `redactMessages` / `DEFAULT_PII_PATTERNS` | helpers |
|
|
76
|
+
| `createModerationGuardrail` / `ContentBlockedError` | moderation |
|
|
77
|
+
|
|
78
|
+
Types: `Guardrail` (as `GuardrailContract`), `GuardrailContext`, event payloads.
|
|
79
|
+
|
|
80
|
+
## Orchestration & MCP
|
|
81
|
+
|
|
82
|
+
`AgentRegistry` · `createAgentTool` · `McpService`. Types: `AgentEntry`,
|
|
83
|
+
`AgentToolOptions`, `McpClientLike`, `McpToolInfo`, `McpCallResult`.
|
|
84
|
+
|
|
85
|
+
## Reliability
|
|
86
|
+
|
|
87
|
+
| Export | Kind |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `createFallbackModel(models, opts)` | composite model |
|
|
90
|
+
| `InMemoryAiCache` / `createCacheMiddleware` / `cacheKey` | caching |
|
|
91
|
+
| `InMemoryRateLimiter` / `RateLimitGuardrail` / `RateLimitedError` | rate limit |
|
|
92
|
+
| `AutoApproveGate` / `DenyApproveGate` / `ToolApprovalDeniedError` | approval |
|
|
93
|
+
| `UsageTracker` / `BudgetGuard` / `BudgetExceededError` | cost |
|
|
94
|
+
| `costOf` / `DEFAULT_PRICING` / `bareModelId` | pricing |
|
|
95
|
+
|
|
96
|
+
Interfaces: `AiCache`, `RateLimiter`, `ApprovalGate`, `ApprovalContext`. Types:
|
|
97
|
+
`ModelPricing`, `PricingTable`, `UsageTotals`, `UsageRecord`.
|
|
98
|
+
|
|
99
|
+
## Jobs, HTTP & realtime
|
|
100
|
+
|
|
101
|
+
| Export | Kind |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `AgentJobsModule` / `AgentQueueService` / `AgentJobProcessor` | BullMQ |
|
|
104
|
+
| `pipeAgentStream` / `AgentStreamInterceptor` | HTTP/SSE |
|
|
105
|
+
| `streamAgentToSocket` | WebSocket helper (main entry) |
|
|
106
|
+
| `AgentGateway` | WebSocket gateway (`@mgvdev/nestjs-ai/websocket`) |
|
|
107
|
+
|
|
108
|
+
Types: `AgentJobData`, `QueueLike`, `AgentJobsOptions`, `PipeAgentStreamOptions`,
|
|
109
|
+
`SocketLike`, `AgentRunMessage`.
|
|
110
|
+
|
|
111
|
+
## Evals & testing
|
|
112
|
+
|
|
113
|
+
`EvalRunner` · `createLlmJudge` · `defaultJudge`. Types: `EvalCase`, `EvalScore`,
|
|
114
|
+
`EvalResult`, `EvalReport`, `Judge`, `RunnableAgent`, `JudgeAi`.
|
|
115
|
+
|
|
116
|
+
Subpath `@mgvdev/nestjs-ai/testing`: `createTestingAiModule`, `createMockModel`,
|
|
117
|
+
`createEmbeddingMock`.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
## `AiModule.forRoot(options)`
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
AiModule.forRoot({
|
|
7
|
+
providers: {
|
|
8
|
+
openai: { apiKey: '…', baseURL?: '…', headers?: {} },
|
|
9
|
+
anthropic: { apiKey: '…' },
|
|
10
|
+
google: { apiKey: '…' },
|
|
11
|
+
},
|
|
12
|
+
defaultModel: 'openai:gpt-4o', // string | string[] (fallback chain)
|
|
13
|
+
defaultEmbeddingModel: 'openai:text-embedding-3-small',
|
|
14
|
+
defaultImageModel: 'openai:dall-e-3',
|
|
15
|
+
defaultSpeechModel: 'openai:tts-1',
|
|
16
|
+
defaultTranscriptionModel: 'openai:whisper-1',
|
|
17
|
+
defaultMaxSteps: 5,
|
|
18
|
+
maxRetries: 2,
|
|
19
|
+
|
|
20
|
+
// memory
|
|
21
|
+
conversationStore: MyStore, // class | { useClass | useFactory | useValue }
|
|
22
|
+
// rag
|
|
23
|
+
vectorStore: { useFactory: () => new PgVectorStore(pool) },
|
|
24
|
+
rerankingModel: 'cohere:rerank-v3.5',
|
|
25
|
+
// prompts / guardrails
|
|
26
|
+
prompts: [{ name: 'support', template: 'Help {{user}}.' }],
|
|
27
|
+
guardrails: [MyGuardrail],
|
|
28
|
+
// reliability
|
|
29
|
+
cache: InMemoryAiCache,
|
|
30
|
+
cacheTtlMs: 60_000,
|
|
31
|
+
approvalGate: MyApprovalGate,
|
|
32
|
+
rateLimiter: { useValue: new InMemoryRateLimiter({ capacity: 10, refillTokens: 10, intervalMs: 60_000 }) },
|
|
33
|
+
// cost
|
|
34
|
+
pricing: { 'gpt-4o': { input: 2.5, output: 10 } },
|
|
35
|
+
maxCostPerConversation: 1.0,
|
|
36
|
+
// telemetry
|
|
37
|
+
telemetry: { isEnabled: true, functionId: 'my-app' },
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Every field is optional. Providers you don't list are simply unavailable; their
|
|
42
|
+
`@ai-sdk/*` package only needs to be installed if you configure that provider.
|
|
43
|
+
|
|
44
|
+
## `AiModule.forRootAsync(options)`
|
|
45
|
+
|
|
46
|
+
Build options from injected dependencies (e.g. `ConfigService`):
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
AiModule.forRootAsync({
|
|
50
|
+
imports: [ConfigModule],
|
|
51
|
+
inject: [ConfigService],
|
|
52
|
+
useFactory: (config: ConfigService) => ({
|
|
53
|
+
providers: { openai: { apiKey: config.getOrThrow('OPENAI_API_KEY') } },
|
|
54
|
+
defaultModel: 'openai:gpt-4o',
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## `AiModule.forFeature(feature)`
|
|
60
|
+
|
|
61
|
+
Convenience registration so agents/tools/guardrails don't have to be added to a
|
|
62
|
+
consuming module's own `providers`:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
AiModule.forFeature({
|
|
66
|
+
agents: [SupportAgent],
|
|
67
|
+
tools: [WeatherTools],
|
|
68
|
+
guardrails: [ProfanityGuard],
|
|
69
|
+
prompts: [{ name: 'greeting', template: 'Hi {{name}}' }],
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Discovery finds providers globally regardless of where they're registered — but
|
|
74
|
+
they **must** be registered somewhere (here, or in your own module `providers`).
|
|
75
|
+
|
|
76
|
+
## Model ids
|
|
77
|
+
|
|
78
|
+
- `"provider:model"` — explicit, e.g. `"openai:gpt-4o"`, `"anthropic:claude-sonnet-4"`.
|
|
79
|
+
- Bare `"model"` — works only when a single provider is configured, or when
|
|
80
|
+
`defaultModel` carries the provider prefix.
|
|
81
|
+
- `string[]` — a **fallback chain**: each is tried in order (see
|
|
82
|
+
[Reliability](./reliability.md)).
|
|
83
|
+
|
|
84
|
+
## Environment
|
|
85
|
+
|
|
86
|
+
AI SDK v7 requires **Node.js ≥ 22**. Provider SDKs also read their own env vars
|
|
87
|
+
(e.g. `OPENAI_API_KEY`) if you omit `apiKey`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Content safety
|
|
2
|
+
|
|
3
|
+
Two guardrails: **PII redaction** and **moderation**. Both integrate with the
|
|
4
|
+
guardrail pipeline — register them as providers or via `forFeature({ guardrails })`.
|
|
5
|
+
|
|
6
|
+
## PII redaction
|
|
7
|
+
|
|
8
|
+
Redacts emails, phone numbers, credit cards, and US SSNs from the messages sent
|
|
9
|
+
to the model.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { PiiRedactionGuardrail } from '@mgvdev/nestjs-ai';
|
|
13
|
+
|
|
14
|
+
AiModule.forFeature({ guardrails: [PiiRedactionGuardrail] });
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Custom patterns / replacement:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createPiiRedactionGuardrail } from '@mgvdev/nestjs-ai';
|
|
21
|
+
|
|
22
|
+
const MyRedaction = createPiiRedactionGuardrail({
|
|
23
|
+
patterns: [/\bACC-\d{6}\b/g],
|
|
24
|
+
replacement: '[HIDDEN]',
|
|
25
|
+
});
|
|
26
|
+
AiModule.forFeature({ guardrails: [MyRedaction] });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Standalone helpers are exported too: `redactPii(text, patterns?, replacement?)`
|
|
30
|
+
and `redactMessages(messages, …)`.
|
|
31
|
+
|
|
32
|
+
## Moderation
|
|
33
|
+
|
|
34
|
+
Blocks a run when the input contains a deny-listed term or fails a custom check.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { createModerationGuardrail } from '@mgvdev/nestjs-ai';
|
|
38
|
+
|
|
39
|
+
const Moderation = createModerationGuardrail({
|
|
40
|
+
blocked: ['secret-project', 'internal-only'],
|
|
41
|
+
moderate: async (text) => (await callModerationApi(text)).flagged, // optional
|
|
42
|
+
});
|
|
43
|
+
AiModule.forFeature({ guardrails: [Moderation] });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A blocked run throws `ContentBlockedError`.
|
|
47
|
+
|
|
48
|
+
## Ordering
|
|
49
|
+
|
|
50
|
+
Guardrails run in registration order. Put PII redaction before moderation if you
|
|
51
|
+
want moderation to see the redacted text, or after if it should see the raw text.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Embeddings & RAG
|
|
2
|
+
|
|
3
|
+
## Embeddings
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { EmbeddingsService } from '@mgvdev/nestjs-ai';
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class SearchService {
|
|
10
|
+
constructor(private readonly embeddings: EmbeddingsService) {}
|
|
11
|
+
|
|
12
|
+
async run(docs: string[]) {
|
|
13
|
+
const { embedding } = await this.embeddings.embed('hello');
|
|
14
|
+
const { embeddings } = await this.embeddings.embedMany(docs);
|
|
15
|
+
return embeddings;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Model resolves from `defaultEmbeddingModel` or the `model` option. When a
|
|
21
|
+
[cache](./reliability.md#caching) is configured, single `embed` calls are cached.
|
|
22
|
+
|
|
23
|
+
## RAG
|
|
24
|
+
|
|
25
|
+
`RagService` chunks, embeds, and stores documents, then retrieves the most
|
|
26
|
+
relevant chunks for a query.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
await this.rag.ingest(
|
|
30
|
+
[{ id: 'handbook', content: longText, metadata: { source: 'hr' } }],
|
|
31
|
+
{ chunkSize: 1000, chunkOverlap: 100 },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const hits = await this.rag.retrieve('vacation policy', { topK: 4 });
|
|
35
|
+
// hits: { id, content, score, metadata }[]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Expose retrieval to an agent
|
|
39
|
+
|
|
40
|
+
A `@Tool` method that calls `RagService`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
@Injectable()
|
|
44
|
+
export class KnowledgeTools {
|
|
45
|
+
constructor(private readonly rag: RagService) {}
|
|
46
|
+
|
|
47
|
+
@Tool({ description: 'Search the handbook', schema: z.object({ query: z.string() }) })
|
|
48
|
+
async search({ query }: { query: string }) {
|
|
49
|
+
const hits = await this.rag.retrieve(query);
|
|
50
|
+
return hits.map((h) => h.content).join('\n\n');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or use the factory:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createRetrievalTool } from '@mgvdev/nestjs-ai';
|
|
59
|
+
const tool = createRetrievalTool(rag, { topK: 5 });
|
|
60
|
+
await this.ai.generateText({ model: 'openai:gpt-4o', tools: { search: tool }, prompt });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Reranking
|
|
64
|
+
|
|
65
|
+
Fetch more candidates then rerank down to `topK`:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const hits = await this.rag.retrieve('query', { topK: 4, rerank: true }); // heuristic default
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `rerank: true` uses the configured default (`HeuristicReranker`, no dependency).
|
|
72
|
+
- Pass a `Reranker` to override; `ModelReranker` uses the AI SDK `rerank()` with a
|
|
73
|
+
rerank-capable provider (`rerankingModel`, e.g. Cohere).
|
|
74
|
+
|
|
75
|
+
## Vector stores
|
|
76
|
+
|
|
77
|
+
The default store is `InMemoryVectorStore` (cosine similarity). Swap it via
|
|
78
|
+
`vectorStore`:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// pgvector — pass your own `pg` Pool
|
|
82
|
+
AiModule.forRoot({ vectorStore: { useFactory: () => new PgVectorStore(pool, { table: 'ai_documents' }) } });
|
|
83
|
+
|
|
84
|
+
// Qdrant
|
|
85
|
+
AiModule.forRoot({ vectorStore: { useFactory: () => new QdrantVectorStore(qdrant, { collection: 'docs' }) } });
|
|
86
|
+
|
|
87
|
+
// Pinecone
|
|
88
|
+
AiModule.forRoot({ vectorStore: { useFactory: () => new PineconeVectorStore(index) } });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Implement the `VectorStore` interface (`upsert` / `query` / `delete` / `clear`)
|
|
92
|
+
for any other backend.
|
|
93
|
+
|
|
94
|
+
### pgvector schema
|
|
95
|
+
|
|
96
|
+
```sql
|
|
97
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
98
|
+
CREATE TABLE ai_documents (
|
|
99
|
+
id text PRIMARY KEY,
|
|
100
|
+
content text NOT NULL,
|
|
101
|
+
embedding vector(1536) NOT NULL,
|
|
102
|
+
metadata jsonb
|
|
103
|
+
);
|
|
104
|
+
CREATE INDEX ON ai_documents USING hnsw (embedding vector_cosine_ops);
|
|
105
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Evals & testing
|
|
2
|
+
|
|
3
|
+
## Testing
|
|
4
|
+
|
|
5
|
+
Never hit real providers in tests. Use `@mgvdev/nestjs-ai/testing` (needs the
|
|
6
|
+
optional peer `msw`, which `ai/test` requires).
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { createTestingAiModule, createMockModel } from '@mgvdev/nestjs-ai/testing';
|
|
10
|
+
|
|
11
|
+
it('answers', async () => {
|
|
12
|
+
const app = await createTestingAiModule({
|
|
13
|
+
model: createMockModel('mocked answer'),
|
|
14
|
+
providers: [SupportAgent],
|
|
15
|
+
});
|
|
16
|
+
const { text } = await app.get(SupportAgent).run('hi');
|
|
17
|
+
expect(text).toBe('mocked answer');
|
|
18
|
+
await app.close();
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `createMockModel(reply | replies[])` — a mock language model; an array yields
|
|
23
|
+
one reply per successive call (multi-step).
|
|
24
|
+
- `createEmbeddingMock(fn)` — a mock embedding model from `value => number[]`.
|
|
25
|
+
- `createTestingAiModule({ model, embedding, providers, imports, aiOptions })` —
|
|
26
|
+
boots `AiModule` with `ProviderRegistry` overridden to serve the mocks.
|
|
27
|
+
|
|
28
|
+
To exercise tool calls or multi-step loops with full control, build a
|
|
29
|
+
`MockLanguageModelV3` from `ai/test` directly and pass it via
|
|
30
|
+
`createTestingAiModule({ model })`.
|
|
31
|
+
|
|
32
|
+
## Evals (LLM-as-judge)
|
|
33
|
+
|
|
34
|
+
`EvalRunner` runs an agent over a set of cases and scores each output.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { EvalRunner, createLlmJudge } from '@mgvdev/nestjs-ai';
|
|
38
|
+
|
|
39
|
+
const report = await this.evals.run(
|
|
40
|
+
supportAgent,
|
|
41
|
+
[
|
|
42
|
+
{ input: 'capital of France?', expected: 'Paris' },
|
|
43
|
+
{ input: 'summarize X', rubric: 'accurate and concise' },
|
|
44
|
+
],
|
|
45
|
+
{ judge: createLlmJudge(this.ai, { scale: 5, passThreshold: 0.6 }) },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// report.averageScore, report.passRate, report.results[]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **Default judge** (no `judge` option): substring match against `case.expected`.
|
|
52
|
+
- **`createLlmJudge(ai, { model?, scale?, passThreshold? })`**: an LLM scores each
|
|
53
|
+
output 0..`scale` (normalized to `[0, 1]`) using the case's `rubric` / `expected`.
|
|
54
|
+
|
|
55
|
+
Each `EvalResult` has `{ name, input, output, score, passed, reasoning? }`.
|
|
56
|
+
`EvalReport` aggregates `averageScore` and `passRate`.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @mgvdev/nestjs-ai ai zod
|
|
7
|
+
npm install @ai-sdk/openai # and/or @ai-sdk/anthropic, @ai-sdk/google
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`@nestjs/common`, `@nestjs/core`, `reflect-metadata`, and `rxjs` are already in
|
|
11
|
+
every Nest app. Ensure `reflect-metadata` is imported once at your app entry.
|
|
12
|
+
|
|
13
|
+
## Register the module
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Module } from '@nestjs/common';
|
|
17
|
+
import { AiModule } from '@mgvdev/nestjs-ai';
|
|
18
|
+
|
|
19
|
+
@Module({
|
|
20
|
+
imports: [
|
|
21
|
+
AiModule.forRoot({
|
|
22
|
+
providers: { openai: { apiKey: process.env.OPENAI_API_KEY } },
|
|
23
|
+
defaultModel: 'openai:gpt-4o',
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
})
|
|
27
|
+
export class AppModule {}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`AiModule` is global — its services are injectable everywhere.
|
|
31
|
+
|
|
32
|
+
## Your first agent
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// support.agent.ts
|
|
36
|
+
import { Agent, AiAgent } from '@mgvdev/nestjs-ai';
|
|
37
|
+
|
|
38
|
+
@Agent({ model: 'openai:gpt-4o', system: 'You are a concise assistant.' })
|
|
39
|
+
export class SupportAgent extends AiAgent {}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// support.module.ts
|
|
44
|
+
import { Module } from '@nestjs/common';
|
|
45
|
+
import { AiModule } from '@mgvdev/nestjs-ai';
|
|
46
|
+
import { SupportAgent } from './support.agent';
|
|
47
|
+
|
|
48
|
+
@Module({ imports: [AiModule.forFeature({ agents: [SupportAgent] })] })
|
|
49
|
+
export class SupportModule {}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// support.service.ts
|
|
54
|
+
import { Injectable } from '@nestjs/common';
|
|
55
|
+
import { SupportAgent } from './support.agent';
|
|
56
|
+
|
|
57
|
+
@Injectable()
|
|
58
|
+
export class SupportService {
|
|
59
|
+
constructor(private readonly agent: SupportAgent) {}
|
|
60
|
+
|
|
61
|
+
ask(question: string) {
|
|
62
|
+
return this.agent.run(question).then((r) => r.text);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Add a tool
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { Injectable } from '@nestjs/common';
|
|
71
|
+
import { Tool } from '@mgvdev/nestjs-ai';
|
|
72
|
+
import { z } from 'zod';
|
|
73
|
+
|
|
74
|
+
@Injectable()
|
|
75
|
+
export class ClockTools {
|
|
76
|
+
@Tool({ description: 'Get the current time', schema: z.object({}) })
|
|
77
|
+
now() {
|
|
78
|
+
return new Date().toISOString();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Reference it on the agent and register both:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
@Agent({ model: 'openai:gpt-4o', system: '…', tools: [ClockTools] })
|
|
87
|
+
export class SupportAgent extends AiAgent {}
|
|
88
|
+
|
|
89
|
+
AiModule.forFeature({ agents: [SupportAgent], tools: [ClockTools] });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Next
|
|
93
|
+
|
|
94
|
+
- [Configuration](./configuration.md) — providers, defaults, async config.
|
|
95
|
+
- [Agents & tools](./agents-and-tools.md) — the full agent/tool model.
|
|
96
|
+
- [Evals & testing](./evals-and-testing.md) — test without hitting providers.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Guardrails, events & telemetry
|
|
2
|
+
|
|
3
|
+
## Guardrails
|
|
4
|
+
|
|
5
|
+
A guardrail inspects, mutates, or blocks agent runs and tool calls. Throw from
|
|
6
|
+
any hook to abort.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { Guardrail, type GuardrailContext } from '@mgvdev/nestjs-ai';
|
|
10
|
+
|
|
11
|
+
@Guardrail()
|
|
12
|
+
export class ProfanityGuard {
|
|
13
|
+
beforeRun(ctx: GuardrailContext) {
|
|
14
|
+
// ctx.messages is mutable; throw to block
|
|
15
|
+
if (isProfane(ctx.messages)) throw new ForbiddenException('Blocked');
|
|
16
|
+
}
|
|
17
|
+
afterRun(ctx: GuardrailContext, result) { /* inspect the result */ }
|
|
18
|
+
onToolCall(tool: string, args: unknown) { /* veto specific tool calls */ }
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Register as a provider (auto-discovered) or via
|
|
23
|
+
`AiModule.forRoot({ guardrails: [ProfanityGuard] })` /
|
|
24
|
+
`AiModule.forFeature({ guardrails })`.
|
|
25
|
+
|
|
26
|
+
Hooks run in registration order:
|
|
27
|
+
- `beforeRun(ctx)` — before generation; mutate `ctx.messages` or throw.
|
|
28
|
+
- `afterRun(ctx, result)` — after generation.
|
|
29
|
+
- `onToolCall(tool, args)` — before each tool executes; throw to block.
|
|
30
|
+
|
|
31
|
+
Built-in guardrails ship for cost budgets, rate limiting, PII redaction, and
|
|
32
|
+
moderation — see [Reliability](./reliability.md) and
|
|
33
|
+
[Content safety](./content-safety.md).
|
|
34
|
+
|
|
35
|
+
## Events
|
|
36
|
+
|
|
37
|
+
With `@nestjs/event-emitter` installed and `EventEmitterModule.forRoot()`
|
|
38
|
+
imported, the library emits lifecycle events (no-op otherwise).
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { AI_EVENTS } from '@mgvdev/nestjs-ai';
|
|
42
|
+
|
|
43
|
+
@Injectable()
|
|
44
|
+
export class AiListener {
|
|
45
|
+
@OnEvent(AI_EVENTS.agentRunFinish)
|
|
46
|
+
onFinish(payload: { agent: string; result: AgentResult }) { /* … */ }
|
|
47
|
+
|
|
48
|
+
@OnEvent(AI_EVENTS.usage)
|
|
49
|
+
onUsage(record: { model: string; cost: number; inputTokens: number }) { /* … */ }
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Event names:
|
|
54
|
+
|
|
55
|
+
| Constant | Name | Payload |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| `agentRunStart` | `ai.agent.run.start` | `{ agent, input, options }` |
|
|
58
|
+
| `agentRunFinish` | `ai.agent.run.finish` | `{ agent, result }` |
|
|
59
|
+
| `agentRunError` | `ai.agent.run.error` | `{ agent, error }` |
|
|
60
|
+
| `toolCall` | `ai.tool.call` | `{ tool, args }` |
|
|
61
|
+
| `toolResult` | `ai.tool.result` | `{ tool, args, result }` |
|
|
62
|
+
| `streamFinish` | `ai.stream.finish` | `{ agent }` |
|
|
63
|
+
| `usage` | `ai.usage` | `{ model, agent?, inputTokens, outputTokens, cost }` |
|
|
64
|
+
|
|
65
|
+
## Telemetry (OpenTelemetry)
|
|
66
|
+
|
|
67
|
+
Forward spans to the AI SDK's `experimental_telemetry` (requires an OTel setup in
|
|
68
|
+
your app):
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
AiModule.forRoot({ telemetry: { isEnabled: true, functionId: 'support-agent' } });
|
|
72
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Background jobs & realtime
|
|
2
|
+
|
|
3
|
+
## Background jobs (BullMQ)
|
|
4
|
+
|
|
5
|
+
Run agents asynchronously. `AgentJobsModule` is a separate module (not part of
|
|
6
|
+
the global `AiModule`); it needs the optional peer `bullmq` and a Redis instance.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { AgentJobsModule } from '@mgvdev/nestjs-ai';
|
|
10
|
+
|
|
11
|
+
@Module({
|
|
12
|
+
imports: [
|
|
13
|
+
AgentJobsModule.forRoot({
|
|
14
|
+
connection: { host: 'localhost', port: 6379 },
|
|
15
|
+
queueName: 'ai-agent-runs', // optional
|
|
16
|
+
runWorker: true, // start a worker in this process (default)
|
|
17
|
+
}),
|
|
18
|
+
],
|
|
19
|
+
})
|
|
20
|
+
export class JobsModule {}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Enqueue a run:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
constructor(private readonly queue: AgentQueueService) {}
|
|
27
|
+
|
|
28
|
+
const jobId = await this.queue.enqueue({
|
|
29
|
+
agent: 'SupportAgent', // agent class name (resolved via AgentRegistry)
|
|
30
|
+
input: 'summarize ticket 42',
|
|
31
|
+
options: { conversationId: 't-42' },
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The worker resolves the agent from `AgentRegistry`, runs it, and returns the
|
|
36
|
+
`AgentResult` as the job's return value (BullMQ stores it). Query status via the
|
|
37
|
+
BullMQ job API.
|
|
38
|
+
|
|
39
|
+
For fine control, inject `AgentJobProcessor` and call `run(data)` yourself from a
|
|
40
|
+
custom worker.
|
|
41
|
+
|
|
42
|
+
## Realtime (WebSocket)
|
|
43
|
+
|
|
44
|
+
Stream agent responses over socket.io. From `@mgvdev/nestjs-ai/websocket` (needs
|
|
45
|
+
`@nestjs/websockets`, `@nestjs/platform-socket.io`, `socket.io`).
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { AgentGateway } from '@mgvdev/nestjs-ai/websocket';
|
|
49
|
+
|
|
50
|
+
@Module({ providers: [AgentGateway] })
|
|
51
|
+
export class RealtimeModule {}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Protocol:
|
|
55
|
+
- Client emits `agent:run` `{ agent, input }`.
|
|
56
|
+
- Server streams `agent:chunk` `{ delta }` per token, then `agent:done` `{ text }`.
|
|
57
|
+
- Errors arrive as `agent:error` `{ message }`.
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
socket.emit('agent:run', { agent: 'ChatAgent', input: 'Hello' });
|
|
61
|
+
socket.on('agent:chunk', ({ delta }) => append(delta));
|
|
62
|
+
socket.on('agent:done', ({ text }) => finalize(text));
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Framework-agnostic helper
|
|
66
|
+
|
|
67
|
+
`streamAgentToSocket(streamResult, socket, options?)` (from the main entry)
|
|
68
|
+
forwards a stream to any object with an `emit` method — use it to build your own
|
|
69
|
+
gateway or adapter.
|