- 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>
839 lines
15 KiB
Markdown
839 lines
15 KiB
Markdown
\# 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:
|
||
|
||
|
||
|
||
```json
|
||
|
||
{
|
||
|
||
  "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)
|
||
|
||
|
||
|
||
```csharp
|
||
|
||
// 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`
|
||
|
||
|
||
|
||
```markdown
|
||
|
||
\# 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
|
||
|
||
```
|
||
|
||
|
||
|
||
```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)
|
||
|
||
|
||
|
||
```json
|
||
|
||
{
|
||
|
||
  "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
|
||
|
||
|
||
|
||
```json
|
||
|
||
{
|
||
|
||
  "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
|
||
|
||
|
||
|
||
```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
|
||
|