A progressive Node.js framework for building efficient and scalable server-side applications.
Este projeto expoe uma API REST (NestJS) que:
- indexa documentos de politica (PDF/.txt) no Pinecone como embeddings vetoriais
- responde perguntas de politica usando Retrieval-Augmented Generation (RAG)
O fluxo e dividido em dois recursos principais:
- Indexacao:
POST /documents/upload(PDF ou texto em multipart) - Perguntas:
POST /chat(embed da pergunta + busca no Pinecone + geracao no OpenAI)
O endpoint GET /api serve o Swagger UI com base nos DTOs do projeto.
Principais modulos e responsabilidade:
OpenAIModule: cria o clientOpenAIe disponibilizaEmbeddingServicePineconeModule: cria oIndexdo Pinecone a partir dePINECONE_API_KEYePINECONE_INDEX_NAMEDocumentsModule:DocumentsServicefaz extracao de texto, chunking, embeddings eupsertno PineconeChatModule:ChatServicefaz busca vetorial no Pinecone, monta contexto e chamachat.completions
Arquivo de referencia: ./.env.example.
Variaveis esperadas:
OPENAI_API_KEY=...
PINECONE_API_KEY=...
PINECONE_ENVIRONMENT=... # presente no .env.example, mas nao e referenciado no PineconeModule atual
PINECONE_INDEX_NAME=...O projeto roda a API NestJS localmente e usa Docker Compose apenas para a infraestrutura:
docker compose up -d
npm install
npm run prisma:push
npm run prisma:generate
npm run start:devServicos locais:
- API:
http://localhost:8080 - Swagger:
http://localhost:8080/api - Postgres:
localhost:5432, databaserag_policy - Redis:
localhost:6379 - MinIO API:
http://localhost:9000 - MinIO Console:
http://localhost:9001
O arquivo .env.example ja contem valores compativeis com o Compose. Para fluxos de RAG, configure tambem OPENAI_API_KEY, PINECONE_API_KEY e PINECONE_INDEX_NAME.
Em src/main.ts:
- Swagger UI em
GET /api - Porta:
process.env.PORT ?? 8080 - CORS:
- origin:
http://localhost:3000 - methods:
GET, POST, PUT, PATCH, DELETE, OPTIONS - allowedHeaders:
Content-Type, Authorization
- origin:
Retorna um health check simples:
"Hello World!"Descricao: recebe uma pergunta de politica e retorna a resposta gerada, embasada em trechos recuperados do Pinecone.
Request (JSON, application/json):
{
"question": "Qual e a politica para reembolso?",
"conversationId": "opcional"
}Validacoes:
questione obrigatoria, deve serstringe nao pode ser vazia (trim).- Se
questionfor ausente ou vazia, retorna400com mensagem:question is required and must be non-empty
Response (200):
{
"answer": "texto gerado pelo modelo",
"sources": [
{
"documentTitle": "titulo do documento",
"sourceLink": "link da fonte"
}
]
}Notas de RAG:
- Pinecone query usa
topK = 5eincludeMetadata = true. - Trechos sao filtrados por relevancia:
score >= 0.5(MIN_SCORE). - Se nao houver trechos apos o filtro (ou se nao houver
metadata.text), retorna mensagem de nao encontrado esources: []. conversationIdexiste no DTO, mas nao e utilizado noChatServiceatual (reservado para historico futuro).
Descricao: indexa um arquivo (PDF ou texto) via multipart/form-data.
Request:
file: arquivo (mime permitido:application/pdfoutext/plain)- Campos:
title: string (obrigatorio)sourceLink: string (obrigatorio)createdById: string (opcional)
Validacoes:
- Tamanho maximo:
10 MB - Mime types permitidos:
application/pdf,text/plain
Response (200):
Retorna o objeto doc criado no DocumentsService (metadados persistidos via metadata dos chunks no Pinecone):
{
"id": "uuid",
"title": "string",
"sourceLink": "string",
"fileName": null,
"status": "active",
"createdById": null,
"createdAt": "ISO_DATE_STRING",
"updatedAt": "ISO_DATE_STRING"
}graph LR
Client[Client] -->|POST /documents/upload| Upload[DocumentsController.upload]
Upload --> Extract[extractTextFromFile]
Extract --> Chunk[chunkText]
Chunk --> EmbedMany[EmbeddingService.embedMany]
EmbedMany --> Upsert[PineconeService.upsert]
Extracao de texto:
text/plain:buffer.toString('utf-8').trim()application/pdf:pdf-parseeresult.text.trim()
Chunking:
- normaliza whitespace:
trim()+ colapsa\\s+para espaco - default
maxChunkSize = 800eoverlap = 100 - quando possivel, ajusta
endpara olastIndexOf(' ', end)para reduzir cortes no meio de palavras
Estrutura enviada ao Pinecone (records):
id:${documentId}-${chunkIndex}values: vetor de embeddingmetadata:documentIdchunkIndextexttitlesourceLink
graph LR
Client[Client] -->|POST /chat| ChatCtrl[ChatController.chat]
ChatCtrl --> EmbedQ[EmbeddingService.embed(question)]
EmbedQ --> Query[PineconeService.query topK=5 includeMetadata=true]
Query --> Filter[score >= 0.5]
Filter --> Context[context = join(textChunks, \"\\n\\n---\\n\\n\")]
Filter --> Sources[dedup by documentId -> (title, sourceLink)]
Context --> LLM[OpenAI chat.completions gpt-4o-mini]
LLM --> Resp[ChatResponseDto]
Prompt/geracao:
- Modelo de chat:
gpt-4o-mini - Sistema: instrucoes para responder apenas com base nas politicas fornecidas (em ingles ou portugues conforme idioma da pergunta) e sempre citar fonte com link; recusa perguntas fora do escopo de politicas.
Fontes retornadas:
- As
sourcessao montadas a partir demetadatarecuperada do Pinecone (deduplicadas pordocumentId). - As
sourcesnao sao extraidas automaticamente da resposta do modelo.
- Chat:
CHAT_MODEL = gpt-4o-miniTOP_K = 5MIN_SCORE = 0.5
- Embeddings:
EMBEDDING_MODEL = text-embedding-3-smallEMBEDDING_DIMENSIONS = 512
- Nao ha persistencia relacional; os metadados do documento sao reconstruidos a partir dos
metadatados chunks no Pinecone. - Nao existe autenticacao/autorizar nos endpoints (todos sao publicos).
PINECONE_ENVIRONMENTesta no.env.example, mas nao e usado no codigo atual doPineconeModule.- O limiar de score (
MIN_SCORE) depende do index Pinecone e pode exigir ajuste.
- Index policies:
POST /documents/upload(multipart PDF or plain text) -> extract -> chunk -> embed -> Pinecone upsert. - Ask policy questions:
POST /chat-> embed question -> Pinecone topK search with metadata -> filter byscore >= 0.5-> build context + sources -> call OpenAI chat completions (gpt-4o-mini).
GET /->"Hello World!"POST /chat->{ "question": "string", "conversationId": "optional" }POST /documents/upload->file(PDF or text/plain, <= 10MB), plustitle,sourceLink,createdById?
OPENAI_API_KEY=...
PINECONE_API_KEY=...
PINECONE_ENVIRONMENT=... # present in .env.example, not used in PineconeModule
PINECONE_INDEX_NAME=...- Pinecone query:
topK=5,includeMetadata=true - Relevance filter:
score >= 0.5 - Embeddings:
text-embedding-3-small, dimensions512 - Chunking defaults:
maxChunkSize=800,overlap=100