@polymorphism-tech/morph-spec 8.46.0 → 8.47.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 (140) hide show
  1. package/CLAUDE.md +5 -5
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +1 -4
  4. package/bin/validate.js +4 -25
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +1 -1
  9. package/framework/MORPH.md +8 -8
  10. package/framework/commands/create-umbraco-lp.md +2 -0
  11. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +1 -1
  14. package/framework/commands/morph-infra.md +17 -203
  15. package/framework/commands/morph-media.md +3 -0
  16. package/framework/commands/morph-preflight.md +5 -28
  17. package/framework/commands/morph-proposal.md +2 -2
  18. package/framework/commands/morph-troubleshoot.md +2 -14
  19. package/framework/docs/CLI.md +3 -3
  20. package/framework/hooks/claude-code/core/state-sync.js +13 -1
  21. package/framework/hooks/shared/e2e-check.js +3 -4
  22. package/framework/hooks/shared/state-writer.js +1 -0
  23. package/framework/rules/ai-agents-standards.md +18 -16
  24. package/framework/rules/asaas-standards.md +3 -1
  25. package/framework/rules/csharp-standards.md +10 -8
  26. package/framework/rules/data-standards.md +5 -5
  27. package/framework/rules/dotnet-templates.md +1 -1
  28. package/framework/rules/frontend-standards.md +4 -2
  29. package/framework/rules/gsap-standards.md +10 -5
  30. package/framework/rules/hangfire-standards.md +3 -1
  31. package/framework/rules/infrastructure-standards.md +4 -2
  32. package/framework/rules/morph-workflow.md +2 -2
  33. package/framework/rules/neon-auth-standards.md +3 -1
  34. package/framework/rules/neon-standards.md +6 -4
  35. package/framework/rules/nextjs-standards.md +13 -11
  36. package/framework/rules/nextjs-templates.md +1 -1
  37. package/framework/rules/resend-standards.md +3 -1
  38. package/framework/schemas/feature.schema.json +1 -3
  39. package/framework/skills/README.md +1 -1
  40. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  41. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  42. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  43. package/framework/skills/core/morph-brainstorming/SKILL.md +3 -3
  44. package/framework/skills/core/morph-hotfix/SKILL.md +4 -4
  45. package/framework/skills/core/morph-implement/SKILL.md +3 -3
  46. package/framework/skills/core/morph-machine/SKILL.md +3 -3
  47. package/framework/skills/core/morph-media/SKILL.md +3 -3
  48. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  49. package/framework/skills/core/morph-proposal/SKILL.md +3 -3
  50. package/framework/skills/core/morph-review/SKILL.md +3 -3
  51. package/framework/skills/core/morph-stack-scan/SKILL.md +3 -3
  52. package/framework/skills/core/morph-standards/SKILL.md +6 -6
  53. package/framework/skills/core/morph-uiux/SKILL.md +3 -3
  54. package/framework/standards/STANDARDS.json +59 -56
  55. package/framework/standards/ai-agents/evals-with-cache.md +1 -1
  56. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  57. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  58. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  59. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  60. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  61. package/framework/standards/ai-agents/setup.md +1 -1
  62. package/framework/standards/ai-agents/structured-output.md +1 -1
  63. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  64. package/framework/standards/architecture/code-comments.md +2 -2
  65. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  66. package/framework/standards/backend/api/validation.md +135 -47
  67. package/framework/standards/backend/database/postgresql/database.md +11 -7
  68. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  69. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  70. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  71. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  72. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  73. package/framework/standards/backend/dotnet/testing.md +1 -1
  74. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  75. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  76. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  77. package/framework/standards/data/nosql/cache/redis.md +120 -44
  78. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  79. package/framework/standards/frontend/design-system/animations.md +1 -1
  80. package/framework/standards/frontend/design-system/naming.md +1 -1
  81. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  82. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  83. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  84. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  85. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  86. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  87. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  88. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  89. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  90. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  91. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  92. package/framework/templates/infrastructure/github/README.md +2 -1
  93. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  94. package/package.json +1 -1
  95. package/src/commands/observability/telemetry.js +3 -5
  96. package/src/commands/project/init.js +2 -1
  97. package/src/commands/project/update.js +16 -18
  98. package/src/commands/state/approve.js +17 -0
  99. package/src/commands/state/gate-check.js +4 -14
  100. package/src/commands/validation/verify.js +3 -12
  101. package/src/core/paths/output-schema.js +1 -1
  102. package/src/dashboard/app/App.jsx +0 -2
  103. package/src/dashboard/app/components/CommandPalette.jsx +2 -3
  104. package/src/dashboard/app/components/EntityLink.jsx +0 -1
  105. package/src/dashboard/app/components/Markdown.jsx +1 -1
  106. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  107. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  108. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  109. package/src/dashboard/scanner/activity-scanner.js +1 -1
  110. package/src/dashboard/scanner/docs-scanner.js +5 -23
  111. package/src/dashboard/scanner/features-scanner.js +1 -1
  112. package/src/dashboard/scanner/index.js +0 -2
  113. package/src/dashboard/scanner/rules-scanner.js +15 -10
  114. package/src/dashboard/scanner/skills-scanner.js +2 -3
  115. package/src/lib/evaluator-report.js +13 -0
  116. package/src/lib/risk-detector.js +8 -0
  117. package/src/lib/stack/stack-profile.js +1 -1
  118. package/src/lib/validators/reuse/reuse-validator.js +2 -3
  119. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  120. package/src/lib/validators/validation-runner.js +8 -88
  121. package/src/lib/validators/verify-runner.js +22 -72
  122. package/src/scripts/setup-infra.js +33 -22
  123. package/src/utils/agents-installer.js +20 -8
  124. package/src/utils/file-copier.js +7 -5
  125. package/framework/commands/commit.md +0 -166
  126. package/framework/rubrics/backend.json +0 -41
  127. package/framework/rubrics/default.json +0 -36
  128. package/framework/rubrics/frontend.json +0 -36
  129. package/framework/templates/project/validate.js +0 -97
  130. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  131. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  132. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  133. package/src/lib/validators/architecture/index.js +0 -7
  134. package/src/lib/validators/css/css-validator.js +0 -340
  135. package/src/lib/validators/css/index.js +0 -7
  136. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  137. package/src/lib/validators/design-system/index.js +0 -7
  138. package/src/lib/validators/score-aggregator.js +0 -244
  139. package/src/lib/validators/ui/index.js +0 -7
  140. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -1,11 +1,13 @@
1
1
  # Vector Search + RAG — EF Core 10 + pgvector (.NET 10)
2
2
 
3
- > **Scope:** blazor-azure
3
+ > **Scope:** backend/database — pipeline RAG sobre pgvector
4
4
  > **Layer:** 2 (on keyword)
5
5
  > **Keywords:** postgres, pgvector, vector, embedding, search, rag, ai
6
6
  > **Load When:** vector search or rag keywords detected
7
7
 
8
- EF Core 10 (.NET 10) com PostgreSQL + extensão `pgvector` para vector search em workloads de AI. Para a modelagem da coluna `vector`, índices `ivfflat`/`hnsw` e os operadores de distância do `pgvector`, ver `infrastructure/neon/neon-pgvector.md` — este standard cobre o pipeline RAG end-to-end (embeddings, search, geração) sobre essa base.
8
+ **Verified against:** .NET 10 (SDK 10.0.400-preview) + Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3 + Pgvector.EntityFrameworkCore 0.3.0 + Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08). Os blocos de indexação, indexação em lote, busca+geração (RAG), endpoint e job compilam com `TreatWarningsAsErrors` sobre o `Result`/`IHandler`/`ErrorHttpMapping` de `result-pattern.md`/`vertical-slice.md` e o `Document`/`ApplicationDbContext` de `neon-pgvector.md`; o registro do `IEmbeddingGenerator` por provedor, os custos e o SQL são verificação documental. Last-verified: 2026-10-03.
9
+
10
+ EF Core 10 (.NET 10) com PostgreSQL + extensão `pgvector` para vector search em workloads de AI. A modelagem da coluna `vector` (`Document`, `DocumentConfiguration`, `UseVector()`, colunas snake_case), os índices `hnsw`/`ivfflat` e os operadores de distância estão em `infrastructure/neon/neon-pgvector.md` — este standard cobre o pipeline RAG end-to-end (embeddings, busca, geração) em slices VSA sobre essa base: cada operação é um handler que injeta o `ApplicationDbContext` e o `IEmbeddingGenerator` direto, sem `EmbeddingService`/`SearchService` no meio.
9
11
 
10
12
  ---
11
13
 
@@ -42,76 +44,23 @@ Texto → Embedding (vetor de números) → Busca por similaridade
42
44
 
43
45
  ```xml
44
46
  <!-- .csproj -->
45
- <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
46
- <PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.2.2" />
47
- <PackageReference Include="Microsoft.Extensions.AI" Version="9.9.1" />
47
+ <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
48
+ <PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
49
+ <PackageReference Include="Microsoft.Extensions.AI" Version="10.9.0" />
50
+ <PackageReference Include="Microsoft.Agents.AI" Version="1.20.0" />
48
51
  ```
49
52
 
50
- ### 2. PostgreSQL + pgvector Requerido
53
+ As versões de IA vêm do pin (`framework/ai-pin.json`). A ponte do provedor (`Microsoft.Extensions.AI.OpenAI` para `.AsIEmbeddingGenerator()`) não está no pin e tem uma armadilha de restore (NU1608 contra `OpenAI` 2.13.0): ver `ai-agents/setup.md`.
51
54
 
52
- **Requisito:** PostgreSQL com a extensão `pgvector` instalada (`CREATE EXTENSION vector;`).
53
- Neon, Supabase e Azure Database for PostgreSQL já trazem `pgvector` disponível.
55
+ ### 2. PostgreSQL + pgvector Requerido
54
56
 
55
- **Nota:** habilite a extensão via migration EF Core — `migrationBuilder.Sql("CREATE EXTENSION IF NOT EXISTS vector;")` — antes de criar as colunas de vetor.
57
+ **Requisito:** PostgreSQL com a extensão `pgvector` (`CREATE EXTENSION vector;`). Neon já traz `pgvector` disponível. Registre `modelBuilder.HasPostgresExtension("vector")` no `OnModelCreating` e a migration EF Core cria a extensão antes das colunas de vetor.
56
58
 
57
59
  ---
58
60
 
59
61
  ## 🗄️ Modelo de Dados com Vectors
60
62
 
61
- ### Entidade com Embedding
62
-
63
- ```csharp
64
- using Microsoft.EntityFrameworkCore;
65
- using Pgvector;
66
-
67
- public class Document
68
- {
69
- public int Id { get; set; }
70
- public string Title { get; set; } = null!;
71
- public string Content { get; set; } = null!;
72
- public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
73
-
74
- // Vector embedding (1536 dimensões para text-embedding-3-small).
75
- // O tipo `Vector` vem do pacote Pgvector — EF Core não mapeia `float[]`.
76
- public Vector Embedding { get; set; } = null!;
77
- }
78
- ```
79
-
80
- ### DbContext Configuração
81
-
82
- ```csharp
83
- public class AppDbContext : DbContext
84
- {
85
- public DbSet<Document> Documents { get; set; }
86
-
87
- protected override void OnModelCreating(ModelBuilder modelBuilder)
88
- {
89
- // Registra a extensão pgvector — gera `CREATE EXTENSION vector` na migration
90
- modelBuilder.HasPostgresExtension("vector");
91
-
92
- modelBuilder.Entity<Document>(entity =>
93
- {
94
- entity.HasKey(d => d.Id);
95
-
96
- // Configurar coluna de vector
97
- entity.Property(d => d.Embedding)
98
- .HasColumnType("vector(1536)") // 1536 = dimensões do embedding
99
- .IsRequired();
100
-
101
- // Índice de vector para performance — operador de distância cosseno
102
- entity.HasIndex(d => d.Embedding)
103
- .HasMethod("hnsw") // ou "ivfflat"
104
- .HasOperators("vector_cosine_ops")
105
- .HasStorageParameter("m", 16)
106
- .HasStorageParameter("ef_construction", 64);
107
- });
108
- }
109
- }
110
- ```
111
-
112
- > O provider Npgsql precisa de `UseVector()` no `NpgsqlDataSourceBuilder` (ou
113
- > no `UseNpgsql(...)`) para mapear o tipo `vector` — ver a configuração do
114
- > `Program.cs` abaixo e `infrastructure/neon/neon-pgvector.md`.
63
+ Use o `Document` e o `DocumentConfiguration` de `infrastructure/neon/neon-pgvector.md`: `Id` `Guid` gerado no handler com `Guid.CreateVersion7()`, `Embedding` do tipo `Pgvector.Vector` com `vector(1536)` (EF Core não mapeia `float[]`), índice `hnsw` com `vector_cosine_ops`, `UseVector()` dentro do `UseNpgsql(...)` e colunas snake_case via `HasColumnName`.
115
64
 
116
65
  ### Migration
117
66
 
@@ -120,149 +69,60 @@ dotnet ef migrations add AddVectorSearch
120
69
  dotnet ef database update
121
70
  ```
122
71
 
123
- **SQL Gerado:**
72
+ **SQL Gerado (essencial):**
124
73
  ```sql
125
74
  CREATE EXTENSION IF NOT EXISTS vector;
126
- ALTER TABLE "Documents" ADD "Embedding" vector(1536) NOT NULL;
127
- CREATE INDEX "IX_Documents_Embedding" ON "Documents"
128
- USING hnsw ("Embedding" vector_cosine_ops) WITH (m = 16, ef_construction = 64);
75
+ CREATE TABLE documents (
76
+ id uuid NOT NULL PRIMARY KEY,
77
+ title text NOT NULL,
78
+ content text NOT NULL,
79
+ metadata jsonb,
80
+ embedding vector(1536),
81
+ created_at timestamp with time zone NOT NULL
82
+ );
83
+ CREATE INDEX ix_documents_embedding ON documents
84
+ USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
129
85
  ```
130
86
 
131
87
  ---
132
88
 
133
89
  ## 🔢 Gerando Embeddings
134
90
 
135
- ### Serviço de Embedding
91
+ O handler injeta `IEmbeddingGenerator<string, Embedding<float>>` (de `Microsoft.Extensions.AI`) e converte o resultado em `Pgvector.Vector`:
136
92
 
137
93
  ```csharp
138
- using Microsoft.Extensions.AI;
139
- using Pgvector;
140
-
141
- public interface IEmbeddingService
142
- {
143
- Task<Vector> GenerateEmbeddingAsync(string text, CancellationToken ct = default);
144
- }
145
-
146
- public class EmbeddingService : IEmbeddingService
147
- {
148
- private readonly IEmbeddingGenerator<string, Embedding<float>> _embeddingGenerator;
149
-
150
- public EmbeddingService(IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator)
151
- {
152
- _embeddingGenerator = embeddingGenerator;
153
- }
154
-
155
- public async Task<Vector> GenerateEmbeddingAsync(
156
- string text,
157
- CancellationToken ct = default)
158
- {
159
- // GenerateVectorAsync devolve o ReadOnlyMemory<float> diretamente
160
- var vector = await _embeddingGenerator.GenerateVectorAsync(text, cancellationToken: ct);
161
- return new Vector(vector);
162
- }
163
- }
94
+ var vector = new Vector(await embeddings.GenerateVectorAsync(text, cancellationToken: cancellationToken));
164
95
  ```
165
96
 
166
- ### Configuração no Program.cs
167
-
168
- ```csharp
169
- using Azure.AI.OpenAI;
170
- using Microsoft.Extensions.AI;
171
- using System.ClientModel;
172
-
173
- // DbContext com o type handler do pgvector registrado
174
- builder.Services.AddDbContext<AppDbContext>(options =>
175
- options.UseNpgsql(
176
- builder.Configuration.GetConnectionString("Default"),
177
- npgsql => npgsql.UseVector()));
97
+ `GenerateVectorAsync` devolve `ReadOnlyMemory<float>` para um texto; para muitos textos use `GenerateAsync(IEnumerable<string>)`, que faz **uma** chamada ao provedor (ver *Indexação em lote*).
178
98
 
179
- // Embedding generator do Azure OpenAI exposto como IEmbeddingGenerator
180
- builder.Services.AddSingleton<IEmbeddingGenerator<string, Embedding<float>>>(sp =>
181
- {
182
- var config = sp.GetRequiredService<IConfiguration>();
183
-
184
- var azureClient = new AzureOpenAIClient(
185
- new Uri(config["AzureOpenAI:Endpoint"]!),
186
- new ApiKeyCredential(config["AzureOpenAI:ApiKey"]!));
187
-
188
- return azureClient
189
- .GetEmbeddingClient("text-embedding-3-small") // 1536 dimensões
190
- .AsIEmbeddingGenerator();
191
- });
192
-
193
- builder.Services.AddScoped<IEmbeddingService, EmbeddingService>();
194
- ```
99
+ **Registro no DI:** o projeto registra o `IEmbeddingGenerator` do provedor escolhido pelo alias `embeddings` do Model Registry — o exemplo de registro OpenAI está em `ai-agents/rag-custom-pgvector.md` (*EmbeddingService*, a parte do `AddSingleton<IEmbeddingGenerator<...>>`), e a escolha de modelo em `ai-agents-providers-model-registry`. A chave do provedor vem de configuração (user-secrets em dev, variável de ambiente no Coolify), nunca do código. O `DbContext` é registrado como em `neon-pgvector.md` (*DbContext Setup*).
195
100
 
196
101
  ---
197
102
 
198
103
  ## 🔍 Vector Search com EF Core 10
199
104
 
200
- ### Query de Similaridade
105
+ A busca semântica pura (endpoint de busca, sem LLM) é a slice `SearchDocuments` de `infrastructure/neon/neon-pgvector.md` (*Search handler*). O padrão de query, que o RAG abaixo repete dentro da própria slice:
201
106
 
202
107
  ```csharp
203
- using Microsoft.EntityFrameworkCore;
204
-
205
- public class DocumentSearchService
206
- {
207
- private readonly AppDbContext _context;
208
- private readonly IEmbeddingService _embeddingService;
209
-
210
- public DocumentSearchService(
211
- AppDbContext context,
212
- IEmbeddingService embeddingService)
213
- {
214
- _context = context;
215
- _embeddingService = embeddingService;
216
- }
217
-
218
- public async Task<List<DocumentSearchResult>> SearchAsync(
219
- string query,
220
- int limit = 5,
221
- CancellationToken ct = default)
222
- {
223
- // 1. Gerar embedding da query
224
- var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(query, ct);
225
-
226
- // 2. Buscar documentos similares — EF.Functions.CosineDistance traduz para
227
- // o operador `<=>` do pgvector e usa o índice HNSW vector_cosine_ops
228
- var results = await _context.Documents
229
- .Select(d => new DocumentSearchResult
230
- {
231
- Document = d,
232
- // Distância cosseno (menor = mais similar)
233
- Distance = EF.Functions.CosineDistance(d.Embedding, queryEmbedding)
234
- })
235
- .OrderBy(r => r.Distance)
236
- .Take(limit)
237
- .ToListAsync(ct);
238
-
239
- return results;
240
- }
241
- }
242
-
243
- public class DocumentSearchResult
244
- {
245
- public Document Document { get; set; } = null!;
246
- public double Distance { get; set; }
247
- public double Similarity => 1 - Distance; // Converter distância em similaridade
248
- }
108
+ .Where(d => d.Embedding != null)
109
+ .Select(d => new { d.Id, d.Title, d.Content, Distance = d.Embedding!.CosineDistance(query) })
110
+ .OrderBy(x => x.Distance)
111
+ .Take(limit)
249
112
  ```
250
113
 
114
+ Traduz para `ORDER BY d.embedding <=> @query` (ASC), que usa o índice HNSW `vector_cosine_ops`. Similaridade = `1 - distância`.
115
+
251
116
  ### Funções de Distância
252
117
 
253
- O `Pgvector.EntityFrameworkCore` expõe as funções de distância como métodos
254
- `EF.Functions.*`, cada uma traduzida para o operador correspondente do `pgvector`:
118
+ O `Pgvector.EntityFrameworkCore` expõe as distâncias como métodos de extensão sobre `Vector` (namespace `Pgvector.EntityFrameworkCore`), cada um traduzido para o operador correspondente do `pgvector`:
255
119
 
256
- | Função (`EF.Functions.*`) | Operador pgvector | Quando Usar |
257
- |---------------------------|-------------------|-------------|
120
+ | Método | Operador pgvector | Quando Usar |
121
+ |--------|-------------------|-------------|
258
122
  | `CosineDistance` | `<=>` | **Recomendado** para texto / embeddings normalizados |
259
123
  | `L2Distance` | `<->` | Distância euclidiana — dados numéricos |
260
124
  | `MaxInnerProduct` | `<#>` | Produto interno negativo — vetores normalizados |
261
125
 
262
- ```csharp
263
- EF.Functions.CosineDistance(d.Embedding, queryEmbedding)
264
- ```
265
-
266
126
  > O `HasOperators` do índice precisa casar com a função usada na query —
267
127
  > `vector_cosine_ops` para `CosineDistance`, `vector_l2_ops` para `L2Distance`,
268
128
  > `vector_ip_ops` para `MaxInnerProduct`. Operador e índice divergentes fazem o
@@ -272,201 +132,224 @@ EF.Functions.CosineDistance(d.Embedding, queryEmbedding)
272
132
 
273
133
  ## 🤖 RAG Pattern Completo
274
134
 
275
- ### Implementação RAG com Agent Framework
135
+ ### Slice `AskDocuments` (retrieval + augmentation + generation)
276
136
 
277
137
  ```csharp
138
+ // Features/KnowledgeFeature/AskDocuments/AskDocumentsHandler.cs
139
+ using Microsoft.Agents.AI;
140
+ using Microsoft.EntityFrameworkCore;
278
141
  using Microsoft.Extensions.AI;
142
+ using Pgvector;
143
+ using Pgvector.EntityFrameworkCore;
144
+
145
+ public sealed record AskDocumentsRequest(string Question);
146
+ public sealed record AskDocumentsResponse(string Answer, IReadOnlyList<Guid> SourceDocumentIds);
279
147
 
280
- public interface IDocumentAssistantAgent
148
+ public static class KnowledgeErrors
281
149
  {
282
- Task<string> AskQuestionAsync(string question, CancellationToken ct = default);
150
+ public static readonly Error NoRelevantDocuments =
151
+ Error.NotFound("Knowledge.NoRelevantDocuments", "Nenhum documento relevante para a pergunta.");
283
152
  }
284
153
 
285
- public class DocumentAssistantAgent : IDocumentAssistantAgent
154
+ public sealed class AskDocumentsHandler(
155
+ ApplicationDbContext db,
156
+ IEmbeddingGenerator<string, Embedding<float>> embeddings,
157
+ IChatClient chatClient)
158
+ : IHandler<AskDocumentsRequest, Result<AskDocumentsResponse>>
286
159
  {
287
- private readonly IChatClient _chatClient;
288
- private readonly DocumentSearchService _searchService;
289
- private readonly ILogger<DocumentAssistantAgent> _logger;
290
-
291
- public DocumentAssistantAgent(
292
- IChatClient chatClient,
293
- DocumentSearchService searchService,
294
- ILogger<DocumentAssistantAgent> logger)
295
- {
296
- _chatClient = chatClient;
297
- _searchService = searchService;
298
- _logger = logger;
299
- }
160
+ private const int TopK = 3;
161
+ private const double MaxDistance = 0.5;
300
162
 
301
- public async Task<string> AskQuestionAsync(string question, CancellationToken ct = default)
163
+ public async Task<Result<AskDocumentsResponse>> HandleAsync(
164
+ AskDocumentsRequest request, CancellationToken cancellationToken)
302
165
  {
303
- // 1. Retrieval: Buscar documentos relevantes
304
- var relevantDocs = await _searchService.SearchAsync(question, limit: 3, ct);
305
-
306
- _logger.LogInformation(
307
- "Encontrados {Count} documentos relevantes para: {Question}",
308
- relevantDocs.Count,
309
- question
310
- );
311
-
312
- // 2. Construir contexto
313
- var context = string.Join("\n\n", relevantDocs.Select(r =>
314
- $"[Documento {r.Document.Id} - Similaridade: {r.Similarity:P0}]\n" +
315
- $"Título: {r.Document.Title}\n" +
316
- $"Conteúdo: {r.Document.Content}"
317
- ));
318
-
319
- // 3. Augmentation: Criar agente com contexto
320
- // VERIFY: _chatClient.CreateAgent / agent.RunAsync — superfície MAF; alinhar com o MAF Framework Revamp em andamento
321
- var agent = _chatClient.CreateAgent(
322
- instructions: """
166
+ var query = new Vector(await embeddings.GenerateVectorAsync(request.Question, cancellationToken: cancellationToken));
167
+
168
+ var relevant = await db.Documents
169
+ .Where(d => d.Embedding != null)
170
+ .Select(d => new { d.Id, d.Title, d.Content, Distance = d.Embedding!.CosineDistance(query) })
171
+ .Where(x => x.Distance <= MaxDistance)
172
+ .OrderBy(x => x.Distance)
173
+ .Take(TopK)
174
+ .ToListAsync(cancellationToken);
175
+
176
+ if (relevant.Count == 0)
177
+ return KnowledgeErrors.NoRelevantDocuments;
178
+
179
+ var context = string.Join("\n\n", relevant.Select(r =>
180
+ $"[Documento {r.Id} - Similaridade: {1 - r.Distance:P0}]\nTítulo: {r.Title}\nConteúdo: {r.Content}"));
181
+
182
+ var agent = chatClient.AsAIAgent(
183
+ instructions: $"""
323
184
  Você é um assistente que responde perguntas baseado em documentos fornecidos.
324
185
 
325
186
  Regras:
326
- 1. Use APENAS informações dos documentos fornecidos
327
- 2. Se a informação não estiver nos documentos, diga "Não encontrei informação sobre isso"
328
- 3. Cite o número do documento ao responder
329
- 4. Seja conciso e objetivo
187
+ - Use APENAS informações dos documentos fornecidos
188
+ - Se a informação não estiver nos documentos, diga "Não encontrei informação sobre isso"
189
+ - Cite o identificador do documento ao responder
190
+ - Seja conciso e objetivo
330
191
 
331
192
  Documentos disponíveis:
332
- """ + context,
333
- name: "DocumentAssistant"
334
- );
193
+ {context}
194
+ """,
195
+ name: "DocumentAssistant");
335
196
 
336
- // 4. Generation: Gerar resposta
337
- var response = await agent.RunAsync(question, cancellationToken: ct);
197
+ var response = await agent.RunAsync(request.Question, cancellationToken: cancellationToken);
338
198
 
339
- return response.Content;
199
+ return new AskDocumentsResponse(response.Text, relevant.Select(r => r.Id).ToList());
340
200
  }
341
201
  }
342
202
  ```
343
203
 
344
- ### Uso no Blazor
345
-
346
- ```razor
347
- @page "/ask"
348
- @inject IDocumentAssistantAgent Assistant
204
+ - Sem documento relevante, a slice devolve `NotFound` **antes** de chamar o LLM — não paga uma geração para responder "não sei".
205
+ - O `IChatClient` vem do DI; com Model Registry, resolva-o pelo alias (`ai-agents/rag-custom-pgvector.md` → *Exemplo de uso*). Se o agente deve decidir **quando** buscar, exponha a busca como tool (`AIFunctionFactory.Create`) em vez de injetar o contexto — também em `rag-custom-pgvector.md`.
206
+ - O prompt fica inline aqui por brevidade; em produção ele vem de `IPromptRepository` (`ai-agents/prompt-sources.md`).
349
207
 
350
- <h3>Assistente de Documentos</h3>
208
+ ### Endpoint
351
209
 
352
- <EditForm Model="_input" OnValidSubmit="AskQuestion">
353
- <InputText @bind-Value="_input.Question" placeholder="Faça uma pergunta..." />
354
- <button type="submit" disabled="@_isLoading">Perguntar</button>
355
- </EditForm>
210
+ ```csharp
211
+ // Features/KnowledgeFeature/AskDocuments/AskDocumentsEndpoint.cs
212
+ using ProjectName.Abstractions;
213
+ using ProjectName.Extensions;
356
214
 
357
- @if (_isLoading)
358
- {
359
- <p>Buscando resposta...</p>
360
- }
361
- else if (!string.IsNullOrEmpty(_answer))
215
+ internal sealed class AskDocumentsEndpoint : IApiEndpoint
362
216
  {
363
- <div class="answer">
364
- <strong>Resposta:</strong>
365
- <p>@_answer</p>
366
- </div>
367
- }
368
-
369
- @code {
370
- private QuestionInput _input = new();
371
- private string _answer = "";
372
- private bool _isLoading;
373
-
374
- private async Task AskQuestion()
217
+ public void MapEndpoint(IEndpointRouteBuilder app)
375
218
  {
376
- _isLoading = true;
377
- _answer = "";
378
-
379
- try
219
+ app.MapPost("knowledge/ask", async (
220
+ IHandler<AskDocumentsRequest, Result<AskDocumentsResponse>> handler,
221
+ AskDocumentsRequest request,
222
+ CancellationToken cancellationToken) =>
380
223
  {
381
- _answer = await Assistant.AskQuestionAsync(_input.Question);
382
- }
383
- finally
384
- {
385
- _isLoading = false;
386
- }
387
- }
388
-
389
- public class QuestionInput
390
- {
391
- public string Question { get; set; } = "";
224
+ var result = await handler.HandleAsync(request, cancellationToken);
225
+ return result.Match(
226
+ onSuccess: response => Results.Ok(response),
227
+ onFailure: error => error.ToHttpResult());
228
+ })
229
+ .Produces<AskDocumentsResponse>(StatusCodes.Status200OK)
230
+ .ProducesProblem(StatusCodes.Status400BadRequest)
231
+ .ProducesProblem(StatusCodes.Status404NotFound);
392
232
  }
393
233
  }
394
234
  ```
395
235
 
236
+ O frontend Next.js chama `knowledge/ask` pelo proxy com TanStack Query (`useMutation`), como qualquer outro endpoint — a UI não fala com o banco nem com o provedor de IA.
237
+
396
238
  ---
397
239
 
398
240
  ## 📥 Indexação de Documentos
399
241
 
400
- ### Serviço de Indexação
242
+ ### Slice `IndexDocument`
401
243
 
402
244
  ```csharp
403
- public class DocumentIndexingService
404
- {
405
- private readonly AppDbContext _context;
406
- private readonly IEmbeddingService _embeddingService;
245
+ // Features/KnowledgeFeature/IndexDocument/IndexDocumentHandler.cs
246
+ using Microsoft.Extensions.AI;
247
+ using Pgvector;
248
+
249
+ public sealed record IndexDocumentRequest(string Title, string Content);
250
+ public sealed record IndexDocumentResponse(Guid Id);
407
251
 
408
- public async Task IndexDocumentAsync(
409
- string title,
410
- string content,
411
- CancellationToken ct = default)
252
+ public sealed class IndexDocumentHandler(
253
+ ApplicationDbContext db,
254
+ IEmbeddingGenerator<string, Embedding<float>> embeddings,
255
+ TimeProvider timeProvider)
256
+ : IHandler<IndexDocumentRequest, Result<IndexDocumentResponse>>
257
+ {
258
+ public async Task<Result<IndexDocumentResponse>> HandleAsync(
259
+ IndexDocumentRequest request, CancellationToken cancellationToken)
412
260
  {
413
- // 1. Gerar embedding do conteúdo
414
- var embedding = await _embeddingService.GenerateEmbeddingAsync(
415
- $"{title}\n{content}", // Combinar título e conteúdo
416
- ct
417
- );
261
+ var embedding = await embeddings.GenerateVectorAsync(
262
+ $"{request.Title}\n{request.Content}", cancellationToken: cancellationToken);
418
263
 
419
- // 2. Criar documento
420
264
  var document = new Document
421
265
  {
422
- Title = title,
423
- Content = content,
424
- Embedding = embedding,
425
- CreatedAt = DateTime.UtcNow
266
+ Id = Guid.CreateVersion7(),
267
+ Title = request.Title,
268
+ Content = request.Content,
269
+ Embedding = new Vector(embedding),
270
+ CreatedAt = timeProvider.GetUtcNow()
426
271
  };
427
272
 
428
- // 3. Salvar no banco
429
- _context.Documents.Add(document);
430
- await _context.SaveChangesAsync(ct);
273
+ db.Documents.Add(document);
274
+ await db.SaveChangesAsync(cancellationToken);
275
+
276
+ return new IndexDocumentResponse(document.Id);
431
277
  }
278
+ }
279
+ ```
280
+
281
+ ### Indexação em lote
432
282
 
433
- public async Task BulkIndexAsync(
434
- List<(string Title, string Content)> documents,
435
- CancellationToken ct = default)
283
+ Uma chamada ao provedor por lote, um `SaveChangesAsync` por lote — nunca um loop que chama a slice unitária N vezes:
284
+
285
+ ```csharp
286
+ // Features/KnowledgeFeature/IndexDocuments/IndexDocumentsHandler.cs
287
+ using Microsoft.Extensions.AI;
288
+ using Pgvector;
289
+
290
+ public sealed record IndexDocumentsRequest(IReadOnlyList<IndexDocumentRequest> Documents);
291
+
292
+ public sealed class IndexDocumentsHandler(
293
+ ApplicationDbContext db,
294
+ IEmbeddingGenerator<string, Embedding<float>> embeddings,
295
+ TimeProvider timeProvider)
296
+ : IHandler<IndexDocumentsRequest, Result<int>>
297
+ {
298
+ public async Task<Result<int>> HandleAsync(IndexDocumentsRequest request, CancellationToken cancellationToken)
436
299
  {
437
- foreach (var (title, content) in documents)
300
+ var generated = await embeddings.GenerateAsync(
301
+ request.Documents.Select(d => $"{d.Title}\n{d.Content}"), cancellationToken: cancellationToken);
302
+
303
+ var now = timeProvider.GetUtcNow();
304
+ db.Documents.AddRange(request.Documents.Zip(generated, (doc, embedding) => new Document
438
305
  {
439
- await IndexDocumentAsync(title, content, ct);
440
- }
306
+ Id = Guid.CreateVersion7(),
307
+ Title = doc.Title,
308
+ Content = doc.Content,
309
+ Embedding = new Vector(embedding.Vector),
310
+ CreatedAt = now
311
+ }));
312
+
313
+ return await db.SaveChangesAsync(cancellationToken);
441
314
  }
442
315
  }
443
316
  ```
444
317
 
445
- ### Job de Indexação com Hangfire
318
+ Lotes grandes: fatie a lista (ex.: 100 textos por chamada) para ficar abaixo do limite de tokens por request do provedor. Chunking por tipo de documento e ingestão delta por lookup-key: `ai-agents/rag-custom-pgvector.md`.
319
+
320
+ ### Job de Indexação com Hangfire (só com razão real)
321
+
322
+ Reindexar uma fonte externa todo dia é razão real para um job; indexar o upload de um usuário não é — chame a slice no próprio request. O job é um ponto de entrada fino que chama o handler (`backend/integrations/hangfire/hangfire-jobs.md`):
446
323
 
447
324
  ```csharp
448
- public class DocumentIndexingJob
325
+ // Features/KnowledgeFeature/IndexDocuments/ReindexKnowledgeJob.cs
326
+ using Microsoft.Extensions.Logging;
327
+
328
+ public interface IKnowledgeSource
449
329
  {
450
- private readonly DocumentIndexingService _indexingService;
451
- private readonly IDocumentProvider _documentProvider;
330
+ Task<IReadOnlyList<IndexDocumentRequest>> FetchAllAsync(CancellationToken cancellationToken);
331
+ }
452
332
 
453
- public async Task IndexAllDocumentsAsync()
333
+ public sealed class ReindexKnowledgeJob(
334
+ IKnowledgeSource source,
335
+ IHandler<IndexDocumentsRequest, Result<int>> handler,
336
+ ILogger<ReindexKnowledgeJob> logger)
337
+ {
338
+ public async Task RunAsync(CancellationToken cancellationToken)
454
339
  {
455
- // Buscar documentos de fonte externa (API, arquivos, etc.)
456
- var documents = await _documentProvider.GetAllDocumentsAsync();
340
+ var documents = await source.FetchAllAsync(cancellationToken);
341
+ var result = await handler.HandleAsync(new IndexDocumentsRequest(documents), cancellationToken);
457
342
 
458
- await _indexingService.BulkIndexAsync(documents);
343
+ if (result.IsFailure)
344
+ logger.LogWarning("Reindexação falhou: {Code}", result.Error.Code);
345
+ else
346
+ logger.LogInformation("Reindexados {Count} documentos", result.Value);
459
347
  }
460
348
  }
461
-
462
- // Program.cs - Agendar job diário
463
- RecurringJob.AddOrUpdate<DocumentIndexingJob>(
464
- "index-documents",
465
- job => job.IndexAllDocumentsAsync(),
466
- Cron.Daily
467
- );
468
349
  ```
469
350
 
351
+ Agendamento diário (UTC), no `Program.cs`: `RecurringJob.AddOrUpdate<ReindexKnowledgeJob>("reindex-knowledge", job => job.RunAsync(CancellationToken.None), Cron.Daily());` — o Hangfire troca o `CancellationToken.None` pelo token de cancelamento do job.
352
+
470
353
  ---
471
354
 
472
355
  ## 📊 Índices de Performance
@@ -522,7 +405,7 @@ HNSW é o padrão recomendado: melhor recall e não exige dados pré-carregados.
522
405
  | text-embedding-3-small | 1536 | $0.02 / 1M tokens |
523
406
  | text-embedding-3-large | 3072 | $0.13 / 1M tokens |
524
407
 
525
- **Recomendação:** Use `text-embedding-3-small` (melhor custo-benefício).
408
+ **Recomendação:** Use `text-embedding-3-small` (melhor custo-benefício). Confira o preço vigente no provedor antes de orçar.
526
409
 
527
410
  ### Storage
528
411
 
@@ -535,15 +418,15 @@ HNSW é o padrão recomendado: melhor recall e não exige dados pré-carregados.
535
418
 
536
419
  ## ✅ Checklist de Implementação
537
420
 
538
- - [ ] EF Core 10 + `Npgsql.EntityFrameworkCore.PostgreSQL` + `Pgvector.EntityFrameworkCore` instalados
421
+ - [ ] `Npgsql.EntityFrameworkCore.PostgreSQL` + `Pgvector.EntityFrameworkCore` instalados; versões de IA do pin
539
422
  - [ ] PostgreSQL com extensão `pgvector` (`HasPostgresExtension("vector")`)
540
423
  - [ ] `UseVector()` chamado no `UseNpgsql(...)`
541
- - [ ] Entidade com propriedade `Pgvector.Vector`
424
+ - [ ] Entidade com propriedade `Pgvector.Vector`, `Id` via `Guid.CreateVersion7()`, `CreatedAt` via `TimeProvider`
542
425
  - [ ] Índice de vector criado (`hnsw` ou `ivfflat`) com `HasOperators("vector_cosine_ops")`
543
- - [ ] `IEmbeddingService` configurado
544
- - [ ] Serviço de search implementado com `EF.Functions.CosineDistance`
545
- - [ ] RAG pattern com Agent Framework
546
- - [ ] Job de indexação configurado
426
+ - [ ] `IEmbeddingGenerator<string, Embedding<float>>` registrado no DI e injetado **direto** nos handlers (sem `EmbeddingService`/`SearchService`)
427
+ - [ ] Busca com `CosineDistance` + `OrderBy` da distância
428
+ - [ ] Slice RAG devolve `Result`, com `NotFound` quando não há documento relevante; endpoint mapeia com `error.ToHttpResult()`
429
+ - [ ] Indexação em lote com `GenerateAsync(IEnumerable<string>)`; job Hangfire só com razão real
547
430
  - [ ] Testes de similaridade funcionando
548
431
 
549
432
  ---
@@ -554,22 +437,21 @@ HNSW é o padrão recomendado: melhor recall e não exige dados pré-carregados.
554
437
 
555
438
  **Causa:** extensão `pgvector` não instalada no banco.
556
439
 
557
- **Solução:** rode `CREATE EXTENSION vector;` — ou registre `HasPostgresExtension("vector")`
558
- no `OnModelCreating` para que a migration EF Core a crie automaticamente.
440
+ **Solução:** registre `HasPostgresExtension("vector")` no `OnModelCreating` para que a migration EF Core a crie (ou rode `CREATE EXTENSION vector;`).
559
441
 
560
442
  ### Erro: "column ... is of type vector but expression is of type ..."
561
443
 
562
444
  **Causa:** `UseVector()` não foi chamado — o Npgsql não mapeia o tipo `vector`.
563
445
 
564
446
  **Solução:** chame `npgsql.UseVector()` no `UseNpgsql(...)` (ou `UseVector()` no
565
- `NpgsqlDataSourceBuilder`).
447
+ `NpgsqlDataSourceBuilder` quando usa Npgsql sem EF Core).
566
448
 
567
449
  ### Performance lenta em queries
568
450
 
569
451
  **Causa:** índice não criado, ou operador da query difere do `HasOperators` do índice.
570
452
 
571
453
  **Solução:**
572
- - Verifique se o índice existe: `SELECT * FROM pg_indexes WHERE indexname LIKE '%Embedding%'`
454
+ - Verifique se o índice existe: `SELECT * FROM pg_indexes WHERE indexname LIKE '%embedding%'`
573
455
  - Confirme com `EXPLAIN ANALYZE` que a query usa o índice (e não `Seq Scan`)
574
456
  - Garanta que o operador casa: `CosineDistance` ↔ `vector_cosine_ops`
575
457
  - Ajuste parâmetros `m`/`ef_construction` (HNSW) ou `lists` (IVFFlat)
@@ -581,18 +463,19 @@ no `OnModelCreating` para que a migration EF Core a crie automaticamente.
581
463
  **Solução:**
582
464
  - `text-embedding-3-small` → 1536 dimensões
583
465
  - `text-embedding-3-large` → 3072 dimensões
584
- - Atualize `vector(N)` na migration
466
+ - Atualize `vector(N)` na configuração e gere nova migration
585
467
 
586
468
  ---
587
469
 
588
470
  ## 📚 Referências
589
471
 
590
472
  - [pgvector](https://github.com/pgvector/pgvector) — extensão PostgreSQL para vector search
591
- - [Pgvector.EntityFrameworkCore](https://github.com/pgvector/pgvector-dotnet) — type handler + `EF.Functions` de distância
473
+ - [Pgvector.EntityFrameworkCore](https://github.com/pgvector/pgvector-dotnet) — type handler + métodos de distância
592
474
  - [Npgsql EF Core Provider](https://www.npgsql.org/efcore/) — `UseVector()`, `HasPostgresExtension`
593
475
  - [Microsoft.Extensions.AI - Embeddings](https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai)
594
476
  - [OpenAI Embeddings](https://platform.openai.com/docs/guides/embeddings)
595
- - `infrastructure/neon/neon-pgvector.md` — modelagem da coluna, índices e operadores pgvector
477
+ - `infrastructure/neon/neon-pgvector.md` — modelagem da coluna, índices, operadores e a slice de busca
478
+ - `ai-agents/rag-custom-pgvector.md` — chunking, ingestão delta, RAG como tool, registro do `IEmbeddingGenerator`
596
479
 
597
480
  ---
598
481