@polymorphism-tech/morph-spec 7.0.0 → 7.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 (79) hide show
  1. package/README.md +367 -367
  2. package/framework/agents.json +132 -127
  3. package/framework/rubrics/backend.json +41 -36
  4. package/framework/rules/ai-agents-standards.md +45 -15
  5. package/framework/rules/neon-standards.md +25 -26
  6. package/framework/standards/STANDARDS.json +1729 -1133
  7. package/framework/standards/ai-agents/agent-session.md +160 -0
  8. package/framework/standards/ai-agents/batching.md +193 -0
  9. package/framework/standards/ai-agents/context-providers.md +188 -0
  10. package/framework/standards/ai-agents/mcp-server.md +250 -0
  11. package/framework/standards/ai-agents/mcp-tools.md +197 -0
  12. package/framework/standards/ai-agents/middleware-patterns.md +207 -0
  13. package/framework/standards/ai-agents/modalities-image-gen.md +173 -0
  14. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -0
  15. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -0
  16. package/framework/standards/ai-agents/modalities-vision.md +201 -0
  17. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -0
  18. package/framework/standards/ai-agents/production.md +114 -258
  19. package/framework/standards/ai-agents/prompts-in-db.md +214 -0
  20. package/framework/standards/ai-agents/providers/model-registry.md +158 -0
  21. package/framework/standards/ai-agents/rag-custom-pgvector.md +243 -0
  22. package/framework/standards/ai-agents/service-tier-flex.md +135 -0
  23. package/framework/standards/ai-agents/setup.md +142 -316
  24. package/framework/standards/ai-agents/structured-output.md +249 -0
  25. package/framework/standards/ai-agents/sweet-spot.md +109 -0
  26. package/framework/standards/ai-agents/workflows.md +114 -284
  27. package/framework/standards/architecture/vertical-slice/vertical-slice.md +433 -429
  28. package/framework/standards/backend/api/validation.md +92 -88
  29. package/framework/standards/backend/database/postgresql/database.md +336 -332
  30. package/framework/standards/backend/database/vector-search-rag.md +599 -541
  31. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +4 -0
  32. package/framework/standards/backend/dotnet/result-pattern.md +4 -0
  33. package/framework/standards/backend/dotnet/scalar-ui.md +176 -172
  34. package/framework/standards/backend/dotnet/scrutor.md +4 -0
  35. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +278 -274
  36. package/framework/standards/backend/integrations/asaas/asaas-api.md +260 -216
  37. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +354 -350
  38. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +30 -19
  39. package/framework/standards/backend/integrations/resend/resend-email.md +400 -385
  40. package/framework/standards/data/nosql/cache/redis.md +106 -97
  41. package/framework/standards/data/nosql/cosmos-db.md +127 -118
  42. package/framework/standards/data/vector-search/rag-chunking.md +113 -104
  43. package/framework/standards/frontend/design-system/ai-image-generation.md +4 -0
  44. package/framework/standards/frontend/design-system/animations.md +164 -160
  45. package/framework/standards/frontend/design-system/naming.md +68 -64
  46. package/framework/standards/frontend/gsap/gsap-core.md +14 -8
  47. package/framework/standards/frontend/gsap/gsap-plugins.md +26 -21
  48. package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +9 -7
  49. package/framework/standards/frontend/nextjs/app-router.md +127 -123
  50. package/framework/standards/frontend/nextjs/components.md +136 -132
  51. package/framework/standards/frontend/nextjs/data-fetching.md +130 -126
  52. package/framework/standards/frontend/nextjs/forms.md +134 -128
  53. package/framework/standards/frontend/nextjs/motion-patterns.md +328 -322
  54. package/framework/standards/frontend/nextjs/naming-conventions.md +71 -67
  55. package/framework/standards/frontend/nextjs/nextjs-patterns.md +229 -225
  56. package/framework/standards/frontend/nextjs/project-structure.md +106 -102
  57. package/framework/standards/frontend/nextjs/state-management.md +76 -72
  58. package/framework/standards/frontend/nextjs/testing.md +115 -111
  59. package/framework/standards/infrastructure/docker/easypanel-deploy.md +196 -192
  60. package/framework/standards/infrastructure/neon/neon-pgrag.md +6 -2
  61. package/framework/standards/infrastructure/neon/neon-pgvector.md +4 -0
  62. package/framework/standards/infrastructure/neon/neon-setup.md +5 -1
  63. package/framework/standards/integration/mcp/mcp-tools.md +471 -462
  64. package/framework/templates/REGISTRY.json +1745 -1239
  65. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -0
  66. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -0
  67. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -0
  68. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -0
  69. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -0
  70. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -0
  71. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -0
  72. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -0
  73. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -0
  74. package/package.json +89 -83
  75. package/src/core/templates/template-registry.js +289 -256
  76. package/src/lib/generators/context-generator.js +516 -518
  77. package/src/lib/generators/scaffold-generator.js +23 -26
  78. package/framework/standards/infrastructure/neon/neon-semantic-kernel.md +0 -310
  79. package/framework/templates/integrations/semantic-kernel-neon.cs +0 -125
@@ -0,0 +1,214 @@
1
+ # Prompts in DB — Editable, versioned agent instructions
2
+
3
+ > **Scope:** stacks=["dotnet"]
4
+ > **Layer:** 1 (on-keyword)
5
+ > **Keywords:** prompts in db, agent prompts, prompt versioning, prompt hot-reload, editable instructions, prompt management
6
+ > **Read by Claude in:** implement (when an agent's instructions should be runtime-editable)
7
+
8
+ **Verified against:** MAF 1.0 GA. Last-verified: 2026-05-19.
9
+
10
+ ---
11
+
12
+ ## Por que prompts no DB
13
+
14
+ Hardcoded instructions in source code mean every prompt tweak requires a full build and deploy cycle. With prompts in the database the team can tune agent behavior without touching code, pushing a commit, or triggering CI.
15
+
16
+ Key benefits:
17
+
18
+ | Benefit | Explanation |
19
+ |---------|-------------|
20
+ | **No deploy to tune** | Product/ML team edits prompts via a backoffice or SQL — the agent picks up the change on next load |
21
+ | **Versioning and rollback** | Every version is kept as an immutable row; rollback = flip `is_active` |
22
+ | **A/B testing** | Two rows for the same `agent_key`, activate one; compare metrics; flip back if needed |
23
+ | **Audit trail** | `created_at` per version gives a full timeline of what prompt was active when |
24
+
25
+ The agent receives its `instructions:` value from the repository at construction time — not from a string literal in code.
26
+
27
+ ---
28
+
29
+ ## O pattern
30
+
31
+ ```
32
+ ┌─────────────────┐ ┌──────────────────┐ ┌──────────────────────┐
33
+ │ Agent Handler │──────▶│ PromptRepository │──────▶│ agent_prompts table │
34
+ │ (constructs │ │ (IMemoryCache │ │ (PostgreSQL) │
35
+ │ AIAgent) │◀──────│ TTL: 5 min) │◀──────│ │
36
+ └─────────────────┘ └──────────────────┘ └──────────────────────┘
37
+ ```
38
+
39
+ 1. The handler (or DI factory) calls `IPromptRepository.GetActivePromptAsync(agentKey, ct)`.
40
+ 2. `PromptRepository` checks `IMemoryCache`. On a hit, returns the cached string. On a miss, queries the DB for the single row where `agent_key = @key AND is_active = true`, caches it with a short TTL (e.g. 5 minutes), and returns it.
41
+ 3. The handler constructs the agent: `chatClient.AsAIAgent(instructions: prompt, name: "MyAgent")`.
42
+ 4. **Agents are cheap to construct** — recreate on every request (or per scoped lifetime). Do NOT attempt to mutate a running agent's instructions.
43
+
44
+ ---
45
+
46
+ ## Schema da tabela
47
+
48
+ The `agent_prompts` table stores one row per prompt version. See the full migration in:
49
+
50
+ ```
51
+ templates/code/sql/ai-agents/agent-prompts-migration.sql.template
52
+ ```
53
+
54
+ Column summary:
55
+
56
+ | Column | Type | Notes |
57
+ |--------|------|-------|
58
+ | `id` | `uuid` | PK, `gen_random_uuid()` |
59
+ | `agent_key` | `text NOT NULL` | Logical identifier, e.g. `"proposal-agent"` |
60
+ | `version` | `int NOT NULL` | Monotonically increasing per `agent_key` |
61
+ | `content` | `text NOT NULL` | The full system-prompt text |
62
+ | `is_active` | `boolean NOT NULL DEFAULT false` | Only one row active per `agent_key` |
63
+ | `created_at` | `timestamptz NOT NULL DEFAULT now()` | Immutable audit timestamp |
64
+
65
+ ### Unique-partial-index trick
66
+
67
+ ```sql
68
+ CREATE UNIQUE INDEX ix_agent_prompts_active
69
+ ON {{SCHEMA}}.agent_prompts (agent_key)
70
+ WHERE is_active;
71
+ ```
72
+
73
+ This index enforces at the database level that only one row per `agent_key` can have `is_active = true`. An attempt to activate a second row for the same key will fail with a unique-constraint violation — no application logic needed to guard this invariant.
74
+
75
+ ---
76
+
77
+ ## O repository
78
+
79
+ Reference implementation:
80
+
81
+ ```
82
+ templates/code/dotnet/ai-agents/PromptRepository.cs.template
83
+ ```
84
+
85
+ Interface contract:
86
+
87
+ ```csharp
88
+ public interface IPromptRepository
89
+ {
90
+ /// <summary>
91
+ /// Returns the content of the currently active prompt for the given agent key.
92
+ /// Throws <see cref="InvalidOperationException"/> if no active prompt exists.
93
+ /// </summary>
94
+ Task<string> GetActivePromptAsync(string agentKey, CancellationToken ct = default);
95
+ }
96
+ ```
97
+
98
+ The concrete `PromptRepository`:
99
+
100
+ - Receives `DbContext` and `IMemoryCache` via primary constructor.
101
+ - Uses a cache key of `$"agent_prompt:{agentKey}"` with a sliding expiration of 5 minutes.
102
+ - On a cache miss, queries `AgentPrompts.Where(p => p.AgentKey == agentKey && p.IsActive)` via EF Core.
103
+ - Throws `InvalidOperationException($"No active prompt found for agent '{agentKey}'.")` when no row matches — fail fast so misconfiguration is immediately visible.
104
+ - Does NOT catch DB exceptions — let the caller decide how to handle transient failures.
105
+
106
+ DI registration (in `Program.cs` or a bootstrap extension):
107
+
108
+ ```csharp
109
+ builder.Services.AddMemoryCache();
110
+ builder.Services.AddScoped<IPromptRepository, PromptRepository>();
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Integração com o agente
116
+
117
+ ### Scoped construction (recommended)
118
+
119
+ ```csharp
120
+ public sealed class ProposalAgentHandler(
121
+ IPromptRepository promptRepo,
122
+ ModelRegistry modelRegistry)
123
+ {
124
+ public async Task<string> RunAsync(string userMessage, CancellationToken ct)
125
+ {
126
+ var prompt = await promptRepo.GetActivePromptAsync("proposal-agent", ct);
127
+
128
+ var agent = modelRegistry
129
+ .GetChatClient("text-default")
130
+ .AsAIAgent(instructions: prompt, name: "ProposalAgent");
131
+
132
+ var response = await agent.RunAsync(userMessage, ct);
133
+ return response.Text;
134
+ }
135
+ }
136
+ ```
137
+
138
+ ### Hot-reload semantics
139
+
140
+ When a new prompt is saved with `is_active = true`:
141
+
142
+ 1. The old active row is set to `is_active = false` (see §Versionamento).
143
+ 2. The cache entry expires within the configured TTL (5 min by default).
144
+ 3. The next agent construction after expiry picks up the new prompt automatically.
145
+
146
+ **Do NOT mutate a running agent.** Agents are lightweight value-like objects in MAF — recreate on each scoped lifetime instead of attempting to patch `instructions` on a live instance.
147
+
148
+ ### Keyed singleton (long-lived, explicit refresh)
149
+
150
+ For agents registered as keyed singletons (e.g. via `builder.AddAIAgent`), hot-reload requires explicit invalidation. In that pattern, call `IMemoryCache.Remove($"agent_prompt:{key}")` from an admin endpoint or background job after saving a new version, then recreate the agent in the factory. This is more complex; prefer scoped construction unless the agent carries expensive state (e.g. an embedded tool pipeline that is costly to rebuild).
151
+
152
+ ---
153
+
154
+ ## Versionamento
155
+
156
+ **Rule: never UPDATE the `content` column.** The content of a prompt row is immutable once written.
157
+
158
+ To release a new prompt version:
159
+
160
+ ```sql
161
+ -- 1. Deactivate the current active version
162
+ UPDATE {{SCHEMA}}.agent_prompts
163
+ SET is_active = false
164
+ WHERE agent_key = 'proposal-agent' AND is_active = true;
165
+
166
+ -- 2. Insert the new version (version = previous max + 1)
167
+ INSERT INTO {{SCHEMA}}.agent_prompts (agent_key, version, content, is_active)
168
+ VALUES (
169
+ 'proposal-agent',
170
+ (SELECT COALESCE(MAX(version), 0) + 1 FROM {{SCHEMA}}.agent_prompts WHERE agent_key = 'proposal-agent'),
171
+ 'Your new system prompt text here...',
172
+ true
173
+ );
174
+ ```
175
+
176
+ This keeps a complete, auditable history. Rollback = deactivate the current row and reactivate any previous row.
177
+
178
+ ---
179
+
180
+ ## Anti-patterns
181
+
182
+ | Anti-pattern | Por quê é problema | Solução |
183
+ |---|---|---|
184
+ | Hardcoded prompt string in production code | Every tune requires a build + deploy | Move to `agent_prompts` table |
185
+ | `UPDATE agent_prompts SET content = '...'` without a version | Destroys history; no rollback | INSERT new row with version+1, flip `is_active` |
186
+ | No cache — DB query on every agent invocation | High latency + DB load under traffic | Use `IMemoryCache` with a short TTL |
187
+ | `appsettings.json` prompts | Still requires a deploy to change; no versioning | Move to DB or environment-variable-agnostic store |
188
+ | Single global `IMemoryCache` key collisions | Two agents with similar keys overwrite each other | Always prefix: `$"agent_prompt:{agentKey}"` |
189
+ | Activating a new row without deactivating the old one | Two active rows for the same key — undefined behavior | The partial unique index prevents this at DB level; always deactivate first in app code |
190
+
191
+ ---
192
+
193
+ ## Checklist (verifiable by morph-eval)
194
+
195
+ - [ ] `agent_prompts` table exists with all required columns (`id`, `agent_key`, `version`, `content`, `is_active`, `created_at`)
196
+ - [ ] Unique partial index `ix_agent_prompts_active` on `(agent_key) WHERE is_active` is present
197
+ - [ ] `IPromptRepository` registered in DI (`AddScoped`)
198
+ - [ ] `IMemoryCache` registered (`builder.Services.AddMemoryCache()`)
199
+ - [ ] No prompt string literals in production agent construction code
200
+ - [ ] `GetActivePromptAsync` throws `InvalidOperationException` (not returns null) when no active prompt found
201
+ - [ ] Versionamento pattern followed: INSERT + flip, never UPDATE content
202
+ - [ ] At least one seed row per `agent_key` in migrations or seed script
203
+
204
+ ---
205
+
206
+ ## References
207
+
208
+ - `ai-agents-setup` — how to construct an `AIAgent` via `AsAIAgent(instructions:)`
209
+ - `ai-agents-providers-model-registry` — how to obtain the `IChatClient` from the Model Registry
210
+ - `backend-database-postgresql-database` — naming conventions, timestamptz, uuid PKs, partial indexes
211
+
212
+ ---
213
+
214
+ *MORPH-SPEC by Polymorphism Tech — ai-agents/prompts-in-db.md v1.0 (2026-05-19)*
@@ -0,0 +1,158 @@
1
+ # Model Registry — Provider-agnostic agent configuration
2
+
3
+ > **Scope:** stacks=["dotnet"]
4
+ > **Layer:** 0 (always-load via persona maf-expert)
5
+ > **Keywords:** model registry, provider agnostic, model alias, IChatClient factory, multi-model, model selection, model config
6
+ > **Read by Claude in:** plan (cita este standard quando feature menciona >1 modelo ou multi-provider) + implement (copia o template ModelRegistry.cs e gera config no projeto)
7
+
8
+ **Verified against:** MAF 1.0 GA + tier list research (2026-05-18). Last-verified: 2026-05-19.
9
+
10
+ ---
11
+
12
+ ## Pattern
13
+
14
+ The project consuming morph-spec maintains a **single JSON file** mapping aliases (`text-default`, `vision`, `reasoning`, etc.) to `{ provider, model, options }`. Agents reference aliases — never hardcoded model strings. Switching provider or model is a one-file change.
15
+
16
+ ```
17
+ Standard (this file, in framework) — explains the pattern + canonical schema
18
+ │ Claude reads + copies template
19
+ ▼
20
+ Project (consumer) — config/model-registry.json + src/.../ModelRegistry.cs
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Schema (canonical)
26
+
27
+ ```jsonc
28
+ // config/model-registry.json — IN THE PROJECT (not in framework)
29
+ {
30
+ "$schema": "./model-registry.schema.json",
31
+ "version": "1.0",
32
+ "defaultAlias": "text-default",
33
+ "aliases": {
34
+ "text-default": { "provider": "openai", "model": "gpt-4o-mini", "options": { "temperature": 0.7 } },
35
+ "text-heavy": { "provider": "openai", "model": "gpt-4o", "options": { "temperature": 0.5 } },
36
+ "reasoning": { "provider": "openai", "model": "o3-mini", "options": { "reasoningEffort": "medium" } },
37
+ "long-form": { "provider": "anthropic", "model": "claude-sonnet-4-6", "options": {} },
38
+ "vision": { "provider": "google", "model": "gemini-2.5-flash", "options": {} },
39
+ "embeddings": { "provider": "openai", "model": "text-embedding-3-small", "options": { "dimensions": 1536 } },
40
+ "transcription": { "provider": "openai", "model": "whisper-1", "options": {} },
41
+ "local-dev": { "provider": "ollama", "model": "llama3.2:3b", "options": { "endpoint": "http://localhost:11434/v1" } }
42
+ },
43
+ "fallback": {
44
+ "text-default": "anthropic/claude-haiku-4-5",
45
+ "text-heavy": "anthropic/claude-sonnet-4-6"
46
+ }
47
+ }
48
+ ```
49
+
50
+ > The aliases above are an **example**. Each project defines its own — provider choices vary per feature.
51
+
52
+ **Field semantics:**
53
+ - `provider`: SDK family — one of `openai`, `google`, `anthropic`, `ollama` (first-class). `azure-openai` is supported as an opt-in branch (see §providers).
54
+ - `model`: model id for that provider.
55
+ - `options`: provider-specific extras (temperature, reasoning effort, `endpoint` for Ollama, etc.).
56
+ - `fallback`: when the primary provider fails (rate limit, outage), switch to this alias. Format `provider/model`.
57
+
58
+ ---
59
+
60
+ ## Providers (first-class)
61
+
62
+ > **This section is the single source of truth for provider SDKs.** Provider SDKs change names/versions often — keep this section verified with a date; templates stay stable.
63
+
64
+ | Provider | `provider` value | NuGet SDK (LAST-VERIFIED 2026-05-19) | Notes |
65
+ |----------|------------------|--------------------------------------|-------|
66
+ | **OpenAI** | `openai` | `OpenAI` 2.* | `new ChatClient(model, apiKey).AsIChatClient()` |
67
+ | **Ollama** | `ollama` | `OpenAI` 2.* (OpenAI-compatible endpoint) | Same `ChatClient` with custom `Endpoint` + dummy key |
68
+ | **Google Gemini** | `google` | Confirm current SDK on NuGet before use | Adapt provider client to `IChatClient` |
69
+ | **Anthropic Claude** | `anthropic` | Confirm current SDK on NuGet before use | Adapt provider client to `IChatClient` |
70
+ | **Azure OpenAI** (opt-in, not default) | `azure-openai` | `Azure.AI.OpenAI` 2.* + `Azure.Identity` | Add only when a client requires data residency |
71
+
72
+ **Adding a provider branch:** the `ModelRegistry.cs` template implements `openai` + `ollama`. To add `google`, `anthropic`, or `azure-openai`:
73
+ 1. Confirm the current NuGet SDK — community SDKs evolve; check NuGet/context7 before coding.
74
+ 2. Add a `case "<provider>":` branch in `ModelRegistry.GetChatClient`'s switch, following the `openai`/`ollama` pattern.
75
+ 3. Add the SDK `PackageReference` to the project's `.csproj`.
76
+ 4. The branch returns an `IChatClient`. If the SDK doesn't expose `IChatClient` natively, wrap it with an adapter.
77
+
78
+ **Direct models only:** never use a hosted agent service. Every provider above is reached as a direct model.
79
+
80
+ ---
81
+
82
+ ## Implementation pattern
83
+
84
+ ```csharp
85
+ // WRONG — hardcoded model + provider
86
+ var agent = new ChatClient("gpt-4o-mini", apiKey)
87
+ .AsIChatClient()
88
+ .AsAIAgent(instructions: prompt);
89
+
90
+ // RIGHT — alias-based, provider-agnostic
91
+ var agent = _modelRegistry.GetChatClient("text-default")
92
+ .AsAIAgent(instructions: prompt);
93
+ ```
94
+
95
+ The `ModelRegistry` class in the project owns the mapping. See template `templates/code/dotnet/ai-agents/ModelRegistry.cs.template`.
96
+
97
+ ---
98
+
99
+ ## How Claude implements this in a project
100
+
101
+ When a task says "register the X agent" and the project has >1 model or >1 provider, Claude:
102
+
103
+ 1. Read this standard.
104
+ 2. Copy `templates/code/dotnet/ai-agents/ModelRegistry.cs.template` to `src/.../ModelRegistry.cs` in the project. Substitute placeholders.
105
+ 3. Copy `templates/code/dotnet/ai-agents/model-registry.json.template` to `config/model-registry.json`. Adjust aliases to project needs.
106
+ 4. Wire `ModelRegistry` into DI (see `AgentBootstrap.cs.template`).
107
+ 5. Future agents reference `_modelRegistry.GetChatClient(alias)`.
108
+
109
+ ---
110
+
111
+ ## Recommended aliases & models (orientativo — maio/2026)
112
+
113
+ > Tabela orientativa. Actual values live in the project's `model-registry.json`. Models evolve fast.
114
+
115
+ | Alias | Provider sugerido | Modelo (maio/2026) | Use case |
116
+ |-------|-------------------|--------------------|----------|
117
+ | `text-default` | openai | gpt-4o-mini | Geração padrão de texto |
118
+ | `text-heavy` | openai / anthropic | gpt-4o / claude-sonnet-4-6 | Análise complexa |
119
+ | `reasoning` | openai | o3-mini | Decisões, planejamento, math |
120
+ | `long-form` | anthropic | claude-sonnet-4-6 | Escrita longa, análise de documento |
121
+ | `vision` | google / openai | gemini-2.5-flash / gpt-4o | Image → Text |
122
+ | `embeddings` | openai | text-embedding-3-small | RAG, similarity |
123
+ | `transcription` | openai | whisper-1 | Audio → Text |
124
+ | `local-dev` | ollama | llama3.2:3b | Dev local, sem custo |
125
+
126
+ ---
127
+
128
+ ## Anti-patterns
129
+
130
+ | Anti-pattern | Why | Right way |
131
+ |--------------|-----|-----------|
132
+ | Hardcode `"gpt-4o-mini"` in agent code | Switching model = grep + replace | Use `_modelRegistry.GetChatClient("text-default")` |
133
+ | Put `model-registry.json` in `.morph/` | `.morph/framework` is framework-only; `.morph/config/config.json` is reserved for morph-spec metadata | Put it in `config/` of the project |
134
+ | Different `ModelRegistry` class per agent | Defeats the purpose | One class per project, singleton in DI |
135
+ | Use a hosted agent service to "simplify" | Hosted lags 6-9 months, removes control | Always direct models |
136
+ | Mix providers without a `fallback` block | Hard to diagnose outages | Declare `fallback` in `model-registry.json` |
137
+
138
+ ---
139
+
140
+ ## Checklist (verifiable by morph-eval)
141
+
142
+ - [ ] Project has `config/model-registry.json` (or declared equivalent).
143
+ - [ ] Project has a `ModelRegistry` class registered as singleton in DI.
144
+ - [ ] No agent code has hardcoded model strings — `grep -rE 'gpt-[0-9]|claude-|gemini-|whisper-' src/` matches only `ModelRegistry.cs`.
145
+ - [ ] Aliases used in agent code exist in `model-registry.json`.
146
+
147
+ ---
148
+
149
+ ## References
150
+
151
+ - `ai-agents-setup` — provider SDKs
152
+ - Template: `templates/code/dotnet/ai-agents/ModelRegistry.cs.template`
153
+ - Template: `templates/code/dotnet/ai-agents/model-registry.json.template`
154
+ - Template: `templates/code/dotnet/ai-agents/AgentBootstrap.cs.template`
155
+
156
+ ---
157
+
158
+ *MORPH-SPEC by Polymorphism Tech — ai-agents/providers/model-registry.md v1.0 (2026-05-19)*
@@ -0,0 +1,243 @@
1
+ # RAG Custom with pgvector — Knowledge retrieval you control
2
+
3
+ > **Scope:** stacks=["dotnet"]
4
+ > **Layer:** 1 (on-keyword)
5
+ > **Keywords:** rag, vector search, pgvector, embeddings, custom rag, retrieval, semantic search, knowledge base, RAG tool
6
+ > **Read by Claude in:** implement (when an agent needs to retrieve knowledge)
7
+
8
+ **Verified against:** MAF 1.0 GA + Microsoft.Extensions.AI. Last-verified: 2026-05-19.
9
+
10
+ ---
11
+
12
+ ## Por que RAG custom
13
+
14
+ Hosted RAG (uploading PDFs to a service) hands control of chunking, embedding, and storage to a third party. You cannot tune chunk size for your document type, cannot switch embedding models without re-indexing, and cannot add tenant isolation at the storage layer. Custom RAG gives you all three.
15
+
16
+ - **Chunking:** you choose the strategy per document type (catalog, contract, technical doc). See `data-vector-search-rag-chunking`.
17
+ - **Embedding:** provider-agnostic. The embedding model is chosen via the `embeddings` alias in the project's Model Registry (see `ai-agents-providers-model-registry`). The project wires an `IEmbeddingGenerator<string, Embedding<float>>` for that alias — switching from `text-embedding-3-small` to another model is a one-file config change.
18
+ - **Storage:** pgvector on Neon — a column you own, indexed by HNSW, with RLS for tenant isolation.
19
+
20
+ ---
21
+
22
+ ## Arquitetura
23
+
24
+ ```
25
+ [Document ingestion]
26
+ │
27
+ ▼
28
+ EmbeddingService (generates float vectors via IEmbeddingGenerator)
29
+ │
30
+ ▼
31
+ kb_embeddings table (pgvector column, HNSW index, tenant_id partition)
32
+ │
33
+ ▼
34
+ VectorSearchTool ◄── MAF agent (tool registered via AIFunctionFactory.Create)
35
+ │
36
+ ▼
37
+ Top-k chunks → agent context (agent decides when to call the tool)
38
+ ```
39
+
40
+ Three pieces:
41
+
42
+ 1. **EmbeddingService** — wraps `IEmbeddingGenerator<string, Embedding<float>>` from `Microsoft.Extensions.AI`. Called during indexing (batch) and at query time (single text).
43
+ 2. **`kb_embeddings` table** — stores chunk text + `vector(1536)` + `tenant_id` + `metadata jsonb`. HNSW index with `vector_cosine_ops`.
44
+ 3. **VectorSearchTool** — a typed MAF tool. A `[Description]`-decorated method the agent can call. Embeds the query, runs cosine-distance SQL, returns top-k chunks as concatenated text.
45
+
46
+ ---
47
+
48
+ ## Chunking por tipo de documento
49
+
50
+ Chunk strategy varies by document type:
51
+
52
+ | Document type | Recommended strategy | Chunk size |
53
+ |---------------|----------------------|------------|
54
+ | Product catalog | Fixed-size | 256–512 tokens |
55
+ | Legal contract | Sentence-aware | 256 tokens |
56
+ | Technical docs | Semantic (split on headers) | Variable |
57
+ | Q&A pairs | Keep whole pair | Whole Q+A |
58
+ | Code files | Function boundary | Whole function |
59
+
60
+ For implementation of each strategy, see `data-vector-search-rag-chunking`. Do not duplicate chunking code here — that standard is the single source of truth for chunk implementations and the `Chunk` metadata record.
61
+
62
+ ---
63
+
64
+ ## EmbeddingService
65
+
66
+ Reference template: `templates/code/dotnet/ai-agents/EmbeddingService.cs.template`
67
+
68
+ The service wraps `IEmbeddingGenerator<string, Embedding<float>>` (from `Microsoft.Extensions.AI`) and exposes two methods:
69
+
70
+ ```csharp
71
+ // Single text → embedding vector (used at query time)
72
+ Task<ReadOnlyMemory<float>> EmbedAsync(string text, CancellationToken ct = default);
73
+
74
+ // Batch of texts → list of vectors (used during document indexing)
75
+ Task<IReadOnlyList<ReadOnlyMemory<float>>> EmbedBatchAsync(
76
+ IReadOnlyList<string> texts, CancellationToken ct = default);
77
+ ```
78
+
79
+ Both call `GenerateAsync(IEnumerable<string>, ...)` on the injected `IEmbeddingGenerator` (verified via context7 / Microsoft Learn). `GenerateAsync` returns `GeneratedEmbeddings<Embedding<float>>`; each element's `.Vector` property is a `ReadOnlyMemory<float>`.
80
+
81
+ **Wiring the `IEmbeddingGenerator`:** the project registers its chosen embedding provider in DI (e.g., via the OpenAI embedding client's `.AsIEmbeddingGenerator()` extension, or the equivalent for another provider). The `embeddings` alias in `model-registry.json` declares the model (`text-embedding-3-small`, dimensions 1536). The `EmbeddingService` is registered as a scoped or singleton service and injected wherever indexing or search happens.
82
+
83
+ ```csharp
84
+ // Example DI registration (Program.cs, in the project — not in framework)
85
+ builder.Services.AddSingleton<IEmbeddingGenerator<string, Embedding<float>>>(sp =>
86
+ {
87
+ var cfg = sp.GetRequiredService<IConfiguration>();
88
+ return new OpenAIClient(cfg["OpenAI:ApiKey"]!)
89
+ .GetEmbeddingClient("text-embedding-3-small")
90
+ .AsIEmbeddingGenerator();
91
+ });
92
+ builder.Services.AddScoped<EmbeddingService>();
93
+ ```
94
+
95
+ ---
96
+
97
+ ## A tabela pgvector
98
+
99
+ Reference template: `templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template`
100
+
101
+ ```sql
102
+ CREATE TABLE kb_embeddings (
103
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
104
+ tenant_id uuid NOT NULL,
105
+ source text NOT NULL, -- document identifier / URL
106
+ chunk_index int NOT NULL, -- position within source document
107
+ content text NOT NULL, -- chunk text returned to the agent
108
+ embedding vector(1536) NOT NULL, -- matches embedding model dimensions
109
+ metadata jsonb NOT NULL DEFAULT '{}',
110
+ created_at timestamptz NOT NULL DEFAULT now()
111
+ );
112
+
113
+ -- HNSW index — cosine distance, production-grade recall
114
+ CREATE INDEX ix_kb_embeddings_hnsw
115
+ ON kb_embeddings
116
+ USING hnsw (embedding vector_cosine_ops);
117
+
118
+ -- Index for tenant filter (applied in every query)
119
+ CREATE INDEX ix_kb_embeddings_tenant
120
+ ON kb_embeddings (tenant_id);
121
+ ```
122
+
123
+ For pgvector extension setup (`CREATE EXTENSION IF NOT EXISTS vector;`, `UseVector()` on `NpgsqlDataSourceBuilder`, HNSW parameter tuning), see `infrastructure-neon-pgvector`.
124
+
125
+ > **Dimension match:** `vector(1536)` matches `text-embedding-3-small` (default 1536 dimensions). If the project switches models, re-create the column with the new dimension. Never mix models on a single column.
126
+
127
+ ---
128
+
129
+ ## VectorSearchTool (tool MAF)
130
+
131
+ Reference template: `templates/code/dotnet/ai-agents/VectorSearchTool.cs.template`
132
+
133
+ A sealed class with a `[Description]`-decorated method the agent calls when it needs context from the knowledge base.
134
+
135
+ ```csharp
136
+ // Registration — in the agent setup code
137
+ var tool = sp.GetRequiredService<VectorSearchTool>();
138
+ var agent = _modelRegistry.GetChatClient("text-default")
139
+ .AsAIAgent(
140
+ instructions: systemPrompt,
141
+ tools: [AIFunctionFactory.Create(tool.SearchAsync)]);
142
+ ```
143
+
144
+ The tool:
145
+ 1. Takes a natural-language `query` string (the only parameter the LLM sees).
146
+ 2. Calls `EmbeddingService.EmbedAsync(query)` to get a query vector.
147
+ 3. Runs a parameterized SQL `SELECT content FROM kb_embeddings WHERE tenant_id = @tenant ORDER BY embedding <=> @queryVec LIMIT 5`.
148
+ 4. Concatenates chunk contents and returns them as a single string — the agent uses this as grounding context.
149
+
150
+ The `tenant_id` filter is **never a tool parameter** (that would allow the LLM to cross tenant boundaries). It is injected via a constructor dependency — e.g., a scoped `ITenantContext` or a `Guid currentTenantId` resolved from the authenticated request. See §Multi-tenant below.
151
+
152
+ Note on raw Npgsql vs EF Core: this template uses `NpgsqlDataSource` (raw SQL) for the cosine-distance query, since pgvector operators (`<=>`) are easier to express and verify in raw SQL. If the project already wires pgvector through EF Core (`UseVector()`, `HasMethod("hnsw")`), the equivalent `db.Database.SqlQuery<>` form per `infrastructure-neon-pgvector` is equally valid — choose whichever is already in use.
153
+
154
+ ---
155
+
156
+ ## Exemplo de uso
157
+
158
+ ```csharp
159
+ // In an agent feature handler (the project's code)
160
+ using Microsoft.Agents.AI;
161
+ using Microsoft.Extensions.AI;
162
+
163
+ public sealed class KnowledgeAgentHandler(
164
+ ModelRegistry modelRegistry,
165
+ VectorSearchTool searchTool,
166
+ IPromptRepository prompts)
167
+ {
168
+ public async Task<string> AnswerAsync(string question, CancellationToken ct = default)
169
+ {
170
+ var systemPrompt = await prompts.GetActivePromptAsync("knowledge-agent", ct);
171
+
172
+ var agent = modelRegistry.GetChatClient("text-default")
173
+ .AsAIAgent(
174
+ instructions: systemPrompt,
175
+ tools: [AIFunctionFactory.Create(searchTool.SearchAsync)]);
176
+
177
+ var response = await agent.RunAsync(question, ct);
178
+ return response.Text;
179
+ }
180
+ }
181
+ ```
182
+
183
+ The agent decides autonomously when to call `VectorSearchTool.SearchAsync`. It receives the top-k chunks as a string and incorporates them into its response. The handler code stays clean — no manual retrieval loop.
184
+
185
+ ---
186
+
187
+ ## Multi-tenant
188
+
189
+ Each tenant's knowledge is partitioned by `tenant_id`:
190
+
191
+ - Every row in `kb_embeddings` has a `tenant_id UUID NOT NULL`.
192
+ - Every query filters on `tenant_id = @tenant` before any cosine-distance ordering.
193
+ - `VectorSearchTool` resolves `tenant_id` from the authenticated request context (injected at construction time) — **never from a tool parameter the LLM supplies**.
194
+ - For database-level enforcement, enable Row-Level Security:
195
+
196
+ ```sql
197
+ ALTER TABLE kb_embeddings ENABLE ROW LEVEL SECURITY;
198
+ -- See the project's RLS policy template (templates/code/sql/postgresql-rls-policy.sql)
199
+ -- for the policy pattern that binds app.current_tenant_id to the session.
200
+ ```
201
+
202
+ > **Rule:** never query across tenants without the filter. One missing `WHERE tenant_id = @tenant` leaks every tenant's data to the requester.
203
+
204
+ ---
205
+
206
+ ## Anti-patterns
207
+
208
+ | Anti-pattern | Why it's wrong | Right way |
209
+ |--------------|----------------|-----------|
210
+ | Hosted RAG (upload PDFs to a service) | Loses control of chunking, embedding, and storage; vendor lock-in | Custom RAG: own all three layers |
211
+ | Embeddings column without an index | Full table scan — 100 ms becomes 30 s at 100k rows | HNSW index on the embedding column always |
212
+ | One fixed chunk size for all document types | Contracts need dense sentences; catalogs need short snippets; mixed chunks degrade recall | Choose strategy per document type (see `data-vector-search-rag-chunking`) |
213
+ | `tenant_id` as a tool parameter the LLM passes | LLM can be prompted to pass any GUID — cross-tenant data leak | Inject `tenant_id` from auth context at construction; never expose as a model-facing parameter |
214
+ | Embedding with model A, querying with model B | Vectors live in different spaces — cosine distance is meaningless | One embedding model per column; re-index when switching models |
215
+ | `ORDER BY similarity DESC` in SQL | `<=>` returns distance (0 = identical), not similarity | `ORDER BY embedding <=> @queryVec` (ascending distance = most similar first) |
216
+
217
+ ---
218
+
219
+ ## Checklist (verifiable by morph-eval)
220
+
221
+ - [ ] EmbeddingService (concrete class) registered in DI; it injects IEmbeddingGenerator<string, Embedding<float>> for the project's chosen embedding model.
222
+ - [ ] Embedding model declared in `model-registry.json` under the `embeddings` alias.
223
+ - [ ] `kb_embeddings` table has `embedding vector(1536)` and HNSW index with `vector_cosine_ops`.
224
+ - [ ] `tenant_id` column present and filtered in every query.
225
+ - [ ] `VectorSearchTool.SearchAsync` takes only `query` + `ct` — no tenant param visible to the LLM.
226
+ - [ ] `VectorSearchTool` registered on the agent via `AIFunctionFactory.Create(tool.SearchAsync)`.
227
+ - [ ] No `Microsoft.SemanticKernel` references in the project.
228
+ - [ ] Vector dimensions consistent between embedding model and `vector(N)` column definition.
229
+ - [ ] RLS enabled on `kb_embeddings` (or equivalent app-level guard documented in `decisions.md`).
230
+ - [ ] Re-indexing strategy documented when document content changes (content hash check).
231
+
232
+ ---
233
+
234
+ ## References
235
+
236
+ - `ai-agents-setup` — MAF packages, `AsAIAgent`, `AIFunctionFactory.Create`
237
+ - `ai-agents-providers-model-registry` — `embeddings` alias, `IEmbeddingGenerator` wiring
238
+ - `data-vector-search-rag-chunking` — chunk strategies per document type
239
+ - `infrastructure-neon-pgvector` — extension setup, `UseVector()`, HNSW parameter tuning
240
+
241
+ ---
242
+
243
+ *MORPH-SPEC by Polymorphism Tech — ai-agents/rag-custom-pgvector.md v1.0 (2026-05-19)*