- ASP.NET Core 9 Razor Pages + Minimal API hybrid - 14 validators (CPF, CEP, CNPJ, email, phone, name, yes-no, birthdate, handoff, cancel-intent, company-name, plate-br, postal-code, validate_reply) - OAuth login (Google, Microsoft, GitHub) + cookie auth - MongoDB usage tracking + CEP cache collection - Stripe checkout with inline PriceData (no Price IDs) - MCP server for Claude Code / Cursor integration - Playground (10 calls/IP/day, no auth) - Docs: Quickstart, API Reference, N8N, MCP, Créditos, Erros, Fluxos - Credit system: 3 cr standard validators, 5 cr validate_reply - SmartSuggestion: contextual re-ask via IA when obtained=false - Per-IP rate limiting + daily cap + shared-IP abuse detection - Lightbox for comic images - Validadores page split: Brasileiros / Universais + Em breve Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15 KiB
# Prompt 1 — NALU AI: Estrutura + Pipeline + Primeiro validador
## O que é o projeto
NALU AI (Natural Language Understanding) é uma API + MCP server em ASP.NET Core 9 (C#) que recebe um trecho de diálogo entre agente e usuário e retorna a **intenção real** extraída, com validação determinística e sugestão de fala para o agente.
Resolve problemas universais de chatbots:
- Usuário responde "Bom dia" quando perguntado o nome → bot grava "Bom Dia" como nome.
- Usuário "zoa" e diz "Meu nome é Xilofone" → bot aceita sem questionar.
- Agente oferece "20x de R$100", usuário responde "Bora em 48?" → bot extrai R$48 em vez de 48 parcelas.
- Usuário digita CEP errado → bot aceita sem validar.
---
## Princípio fundamental: 1 validador = 1 endpoint = 1 MCP tool
Cada validador tem seu **próprio endpoint** e sua **própria tool MCP**. A IA consumidora vê uma lista clara de endpoints/tools e sabe exatamente qual chamar sem ler parâmetros.
Internamente todos os endpoints compartilham o mesmo pipeline de extração — a diferença é o validador .md carregado.
### Endpoints da API
POST /v1/extract/name → validate\_full\_name.md
POST /v1/extract/cpf → validate\_cpf.md
POST /v1/extract/cep → validate\_cep.md
POST /v1/extract/phone → validate\_phone\_br.md
POST /v1/extract/email → validate\_email.md
POST /v1/extract/yes-no → validate\_yes\_no.md
GET /v1/validators → lista todos os validadores com descrição
### Tools MCP (espelho dos endpoints)
nalu\_extract\_name → "Extrai o nome completo do usuário"
nalu\_extract\_cpf → "Extrai e valida CPF do usuário"
nalu\_extract\_cep → "Extrai CEP e retorna endereço completo"
nalu\_extract\_phone → "Extrai telefone brasileiro com DDD"
nalu\_extract\_email → "Extrai email com correção de typos"
nalu\_extract\_yes\_no → "Detecta se o usuário disse sim ou não"
Todos aceitam o **mesmo body** — sem campo validator, o endpoint já define qual usar:
{
  "agent\_input": "Bom dia! Qual seu nome?",
  "user\_input": "Bom dia!",
  "agent\_context": "Negociador de parcelamento.",
  "language": "pt-BR"
}
---
## Arquitetura
### Stack
- ASP.NET Core 9 (C#), .NET 10
- LLM principal: Groq (Llama 3.3 70B, free tier) via API compatível OpenAI
- LLM fallback: OpenRouter (modelos baratos)
- Cache: in-memory (V1)
- Banco: PostgreSQL (logs, billing, API keys) — EF Core
- Container Docker único
### Pipeline de extração (compartilhado)
Request chega no endpoint específico (ex: /v1/extract/name)
  ↓
Auth (API key via header Authorization: Bearer {key})
  ↓
Rate limit por plano
  ↓
Resolve qual validador (.md) usar baseado no endpoint
  ↓
Cache lookup (hash de: validator\_id + agent\_input + user\_input + language)
  ↓ cache miss
Carregar validador (.md do disco, com cache em memória)
  ↓
Camada 1 — Regras determinísticas (regex, stop words, constraints)
  ↓ não resolveu
Camada 2 — LLM com prompt do validador (Groq → fallback OpenRouter)
  ↓
Pós-processamento (normalização, dígito verificador, etc.)
  ↓
Enriquecimento externo (ViaCEP etc., se o validador declarar)
  ↓
Montar sugestão de fala (template do validador)
  ↓
Salvar log + retornar resposta
---
## Estrutura de pastas
src/
  Nalu.Api/
  Program.cs
  appsettings.json
  appsettings.Development.json
  Endpoints/
  ExtractEndpoints.cs # Registra todos os POST /v1/extract/{tipo}
  ValidatorsEndpoints.cs # GET /v1/validators
  Models/
  ExtractionRequest.cs # Body compartilhado por todos os endpoints
  ExtractionResponse.cs # Response compartilhada
  ValidatorInfo.cs # Modelo para GET /v1/validators
  ValidatorDefinition.cs # Validador parseado do .md
  Services/
  ExtractionPipeline.cs # Orquestra o pipeline (recebe validator\_id)
  ValidatorLoader.cs # Parseia .md, cache em memória, FileSystemWatcher
  DeterministicLayer.cs # Camada 1: stop words, regex, constraints
  LlmExtractionService.cs # Camada 2: Groq/OpenRouter
  PostProcessorRegistry.cs # Registry de IPostProcessor por nome
  EnrichmentService.cs # Orquestra enrichers
  SuggestionBuilder.cs # Monta suggestion\_to\_agent
  CacheService.cs # IMemoryCache com hash
  AuthService.cs # Valida API key, retorna plano
  RateLimitService.cs # Rate limit por plano
  PostProcessors/
  IPostProcessor.cs
  CapitalizeProperName.cs
  RemoveTitles.cs
  Enrichers/
  IEnricher.cs
  Infrastructure/
  GroqClient.cs
  OpenRouterClient.cs
  NaluDbContext.cs
  Validators/ # ← CADA .md É UM VALIDADOR
  validate\_full\_name.md
tests/
  Nalu.Tests/
  ValidatorLoaderTests.cs
  DeterministicLayerTests.cs
  PipelineIntegrationTests.cs
Dockerfile
### Registro dos endpoints (Minimal APIs)
// ExtractEndpoints.cs
public static class ExtractEndpoints
{
  public static void MapExtractEndpoints(this WebApplication app)
  {
  var group = app.MapGroup("/v1/extract")
  .RequireAuthorization();
  group.MapPost("/name", async (ExtractionRequest req, ExtractionPipeline pipeline) =>
  await pipeline.ExecuteAsync("validate\_full\_name", req));
  // Prompt 2 adiciona os demais aqui — mesma estrutura, 2 linhas cada
  }
}
---
## Formato dos validadores (.md)
O ValidatorLoader parseia seções ## do markdown. Cada seção vira uma propriedade do ValidatorDefinition.
### Exemplo completo: validate\_full\_name.md
\# validate\_full\_name
Extrai o nome completo do usuário a partir do diálogo.
\## config
\- type: extraction
\- version: 1.0
\- languages: pt-BR, es-ES, en-US
\- endpoint: /v1/extract/name
\- mcp\_tool: nalu\_extract\_name
\- mcp\_description: Extrai o nome completo do usuário a partir da conversa. Use quando o agente perguntou o nome e o usuário respondeu. Retorna o nome extraído, nível de certeza e sugestão de fala para o agente. Se certain=true, aceite o valor. Se certain=false e suggestion\_to\_agent não é null, use a sugestão como próxima mensagem. Se obtained=false, use a sugestão para re-pedir o dado.
\## deterministic\_rules
\### stop\_words
bom dia, boa tarde, boa noite, olá, oi, tudo bem, e aí, fala, eae, opa
\### reject\_patterns
\- ^(não|nao|sei la|sei lá|tanto faz|qualquer|nenhum|nada)$
\- ^\\d+$
\### accept\_patterns
\- ^meu nome é\\s+(.+)$
\- ^me chamo\\s+(.+)$
\- ^sou o\\s+(.+)$
\- ^sou a\\s+(.+)$
\- ^pode me chamar de\\s+(.+)$
\### constraints
\- min\_length: 2
\- must\_have\_alpha: true
\- max\_length: 120
\## prompt
Você é um extrator de nomes. Dado o diálogo abaixo entre um agente e um usuário, extraia o nome completo que o usuário informou.
Regras:
1\. Se o usuário respondeu com saudação (bom dia, oi, etc.) e NÃO disse o nome, retorne extracted\_value: null.
2\. Se o usuário deu um nome que parece falso, zueira ou ofensivo (ex: "Xilofone", "Ninguém", "Seu Pai", "Não tenho"), retorne extracted\_value com o nome mas certain: false.
3\. Se o usuário deu um nome comum/plausível, retorne extracted\_value com o nome e certain: true.
4\. Nomes incomuns mas reais (ex: "Céu", "Lua", "Sol", "Índigo") devem retornar certain: false para o agente confirmar.
5\. Normalize o nome com capitalização adequada (primeira letra maiúscula de cada palavra).
Diálogo:
Agente: {{agent\_input}}
Usuário: {{user\_input}}
Contexto do agente: {{agent\_context}}
Responda SOMENTE com JSON válido, sem markdown, sem explicação:
{
  "extracted\_value": "nome extraído ou null",
  "certain": true/false,
  "reasoning": "explicação curta de 1 linha"
}
\## few\_shot\_examples
\### example 1
\- agent\_input: Bom dia! Qual seu nome completo?
\- user\_input: Bom dia!
\- output: {"extracted\_value": null, "certain": false, "reasoning": "Usuário respondeu com saudação, não informou o nome"}
\### example 2
\- agent\_input: Qual seu nome?
\- user\_input: Meu nome é xilofone
\- output: {"extracted\_value": "Xilofone", "certain": false, "reasoning": "Nome aparenta ser zueira, precisa confirmação"}
\### example 3
\- agent\_input: Para continuar, preciso do seu nome completo.
\- user\_input: Maria Silva dos Santos
\- output: {"extracted\_value": "Maria Silva Dos Santos", "certain": true, "reasoning": "Nome completo plausível informado diretamente"}
\### example 4
\- agent\_input: Qual seu nome?
\- user\_input: sei la
\- output: {"extracted\_value": null, "certain": false, "reasoning": "Usuário foi evasivo, não informou nome"}
\### example 5
\- agent\_input: Tem certeza que seu nome é Cebola?
\- user\_input: Sim, quero que me chame de Cebola.
\- output: {"extracted\_value": "Cebola", "certain": true, "reasoning": "Usuário confirmou o nome após questionamento"}
\### example 6
\- agent\_input: Qual seu nome?
\- user\_input: Céu Azul de Oliveira
\- output: {"extracted\_value": "Céu Azul De Oliveira", "certain": false, "reasoning": "Nome incomum, pode ser real mas precisa confirmação"}
\## post\_processors
\- capitalize\_proper\_name
\- remove\_titles
\## enrichment
(nenhum)
\## suggestions
\### when\_null\_greeting
{{greeting\_response}} Mas preciso do seu nome completo para continuar. Pode me dizer?
\### when\_null\_evasive
Sem problemas, mas preciso do seu nome para prosseguir. Qual seu nome completo?
\### when\_uncertain
Só confirmando: seu nome é {{extracted\_value}}? Pode confirmar?
\### when\_certain
(sem sugestão — agente segue o fluxo)
---
## Contrato da API
### Request (mesmo para todos os endpoints)
POST /v1/extract/name
Authorization: Bearer {api\_key}
Content-Type: application/json
{
  "agent\_input": "Bom dia! Qual seu nome completo?",
  "user\_input": "Bom dia!",
  "agent\_context": "Negociador de parcelamento. Precisa de nome e CPF.",
  "language": "pt-BR"
}
### Response (mesma estrutura para todos)
{
  "obtained": false,
  "extracted\_value": null,
  "confidence": 0.15,
  "certain": false,
  "reasoning": "Usuário respondeu com saudação, não informou o nome",
  "suggestion\_to\_agent": "Bom dia! Mas preciso do seu nome completo para continuar. Pode me dizer?",
  "validator\_used": "validate\_full\_name",
  "engine": "deterministic"
}
### Lógica obtained × certain × suggestion
| Cenário | obtained | certain | suggestion_to_agent |
|---|---|---|---|
| Valor extraído e confiável | true | true | null |
| Valor extraído mas duvidoso | true | false | Frase de confirmação |
| Nada extraído (saudação, evasão) | false | false | Frase re-pedindo o dado |
| Erro de formato (CPF inválido) | false | false | Frase explicando o erro |
### GET /v1/validators
{
  "validators": \[
  {
  "id": "validate\_full\_name",
  "endpoint": "/v1/extract/name",
  "mcp\_tool": "nalu\_extract\_name",
  "description": "Extrai o nome completo do usuário",
  "version": "1.0",
  "languages": \["pt-BR", "es-ES", "en-US"]
  }
  ]
}
---
## O que gerar
1. Projeto ASP.NET Core 9 com Minimal APIs e a estrutura de pastas acima
2. ExtractEndpoints.cs com POST /v1/extract/name
3. ValidatorsEndpoints.cs com GET /v1/validators
4. ExtractionPipeline.cs — orquestra o pipeline, recebe validator\_id + ExtractionRequest
5. ValidatorLoader.cs — parseia .md, ConcurrentDictionary + FileSystemWatcher
6. DeterministicLayer.cs — stop_words, reject/accept patterns, constraints
7. LlmExtractionService.cs — monta messages e chama Groq /chat/completions com response\_format: json\_object
8. PostProcessorRegistry.cs + CapitalizeProperName.cs + RemoveTitles.cs
9. SuggestionBuilder.cs — templates com {{extracted\_value}}, {{greeting\_response}}
10. CacheService.cs — IMemoryCache, TTL configurável
11. AuthService.cs — valida API key contra lista em appsettings (sem banco por agora)
12. RateLimitService.cs — rate limit por plano
13. GroqClient.cs + OpenRouterClient.cs — HttpClients tipados, fallback automático em 429/5xx
14. validate\_full\_name.md completo
15. Testes para ValidatorLoader, DeterministicLayer, integração do pipeline
16. appsettings.json com Groq, OpenRouter, planos, cache, API keys de teste
17. Dockerfile
18. Models (ExtractionRequest, ExtractionResponse, ValidatorInfo, ValidatorDefinition)
### appsettings.json
{
  "Groq": {
  "ApiKey": "YOUR\_GROQ\_API\_KEY",
  "BaseUrl": "https://api.groq.com/openai/v1",
  "Model": "llama-3.3-70b-versatile",
  "MaxTokens": 500,
  "Temperature": 0.1
  },
  "OpenRouter": {
  "ApiKey": "YOUR\_OPENROUTER\_API\_KEY",
  "BaseUrl": "https://openrouter.ai/api/v1",
  "Model": "mistralai/mistral-7b-instruct",
  "MaxTokens": 500,
  "Temperature": 0.1
  },
  "Plans": {
  "free": { "monthly\_limit": 2000, "daily\_limit": 100 },
  "hobby": { "monthly\_limit": 5000, "daily\_limit": null },
  "indie": { "monthly\_limit": 25000, "daily\_limit": null },
  "pro": { "monthly\_limit": 100000, "daily\_limit": null }
  },
  "Cache": { "DefaultTtlMinutes": 60 },
  "ApiKeys": \[
  { "key": "nalu-test-key-001", "plan": "free", "owner": "test" }
  ]
}
### Notas de implementação
- ValidatorLoader: regex por seção ##. ConcurrentDictionary como cache. FileSystemWatcher invalida ao mudar .md.
- GroqClient: /chat/completions, system message = prompt + few-shot, user message = diálogo. response\_format: { type: "json\_object" }.
- Fallback: Groq 429 ou 5xx → retry no OpenRouter.
- SuggestionBuilder: detectar saudação no user\_input para {{greeting\_response}}.
- DI para tudo no Program.cs.
### NÃO incluir
- MCP server (Prompt 2)
- Validadores além do validate_full_name (Prompt 2)
- Painel admin, billing, PostgreSQL