Knowledge Base Service — Ingestão¶
API de upload e worker de indexação para a knowledge base customizada por affiliate (ver Affiliate → Knowledge Base para o mecanismo completo, incluindo o lado de consulta).
Por que dois componentes deployáveis¶
Este serviço vive no mesmo monorepo ifriend-agents, mas como duas unidades
deployáveis separadas — mesmo padrão de duas infraestruturas já usadas neste
repo (Cloud Run tipo apps/tenant-dashboard, Cloud Function Gen2 tipo
runtime/workers/analytics):
- API (
apps/knowledge-base-service/, Cloud Run): recebe o upload, responde rápido, não faz o processamento pesado. - Worker (
runtime/workers/knowledge_base/, Cloud Function Gen2, trigger Pub/Sub): faz o processamento (potencialmente lento — parse, chunking, embedding, escrita no Qdrant) desacoplado do tempo de vida da instância que recebeu o upload.
Motivo de manter GCS + Pub/Sub em vez de uma task in-process: documentos grandes podem levar mais tempo para processar do que o razoável manter uma request HTTP aberta, e o Pub/Sub já dá retry nativo em caso de falha transitória.
Fluxo¶
sequenceDiagram
actor Affiliate as Affiliate (via Site)
participant API as knowledge-base-api (Cloud Run)
participant GCS as Cloud Storage
participant FS as Firestore (kb_jobs)
participant PS as Pub/Sub (kb-indexing-jobs)
participant W as knowledge-base-worker (Cloud Function)
participant QD as Qdrant
participant IF as iFriend API
Affiliate->>API: POST /kb/documents (JWT + arquivo)
API->>API: valida JWT + autorização (partner_code == affiliate_id)
API->>GCS: upload do arquivo bruto
API->>FS: cria job (status=queued)
API->>PS: publica {job_id, affiliate_id, gcs_path}
API-->>Affiliate: {job_id}
Affiliate->>API: GET /kb/jobs/{job_id} (polling)
API->>FS: lê status/progresso
API-->>Affiliate: status atual
PS->>W: entrega mensagem
W->>FS: status=processing (downloading)
W->>GCS: baixa o documento
W->>W: extrai texto (PDF/DOCX/TXT)
W->>W: chunking (char-based, com overlap)
W->>FS: status=processing (embedding), progresso por lote
W->>W: embedding via fastembed (KB_EMBEDDING_MODEL)
W->>QD: ensure_collection + upsert (payload.text, id determinístico)
W->>FS: status=completed
W->>W: autentica sistemicamente (ifriend_api_auth)
W->>IF: PATCH /affiliates/{id}/root_agent {knowledgeBaseCollection}
W->>FS: writeback_status=success|failed
Onde IF é a iFriend API (PHP/Symfony, api.theifriend.com).
Endpoints¶
| Método | Path | O que faz |
|---|---|---|
POST |
/kb/documents?affiliate_id=X |
Upload de documento (multipart), cria job e publica no Pub/Sub |
GET |
/kb/jobs/{job_id} |
Status/progresso de um job específico |
GET |
/kb/documents?affiliate_id=X |
Lista os documentos (jobs) já enviados por um affiliate, mais recentes primeiro |
DELETE |
/kb/documents/{job_id} |
Remove um documento específico: pontos no Qdrant (por document_id), arquivo no GCS, job no Firestore |
DELETE |
/kb/collection?affiliate_id=X |
Reset — apaga a collection inteira no Qdrant, todos os arquivos e todos os jobs do affiliate |
As operações de remoção (DELETE) são síncronas na própria API — não
passam pelo Pub/Sub/worker, diferente do upload. São rápidas
(delete-by-filter no Qdrant, delete de blobs), sem o custo de
parse+chunk+embedding que justifica o upload ser assíncrono.
Para viabilizar a remoção seletiva, cada ponto no Qdrant carrega um
document_id (== job_id) no payload, além do text já documentado no
contrato de leitura — runtime/workers/knowledge_base/qdrant_writer.py::upsert_chunks
e o módulo equivalente apps/knowledge-base-service/qdrant_ops.py (duplicado
deliberadamente, mesmo padrão de isolamento entre os dois componentes).
Limitação aceita: remover um job que ainda está processing pode, em
teoria, deixar pontos reaparecerem no Qdrant se o worker terminar de
escrever depois do delete (corrida rara) — sem tratamento especial para
isso hoje.
Idempotência e retry¶
- Point ID determinístico: cada ponto no Qdrant usa um ID derivado de
job_id+ índice do chunk (qdrant_writer._point_id). Reprocessar o mesmo job (retry automático do Pub/Sub) faz upsert, não duplica pontos. - Falha definitiva vs transitória: erro de parsing (arquivo corrompido/
formato inválido) marca o job como
failede não relança a exceção — não faz sentido o Pub/Sub tentar de novo, o resultado seria o mesmo. Erros potencialmente transitórios (timeout do Qdrant, GCS indisponível) marcamfailede relançam, para o Pub/Sub tentar de novo conforme a política de retry da subscription.
Autenticação¶
Duas autenticações distintas, para dois sentidos diferentes de chamada:
- Affiliate → API (
apps/knowledge-base-service/auth.py): reaproveita o mesmo JWT que a iFriend já emite (JWT_PUBLIC_KEY, mesmo secret usado pelounified_bot.py) — não é um mecanismo de auth novo. Autorização: opartner_codedo JWT precisa bater com oaffiliate_iddo upload, ou o usuário precisa ter role admin. - Worker → iFriend API (
runtime/workers/knowledge_base/ifriend_api_auth.py): autenticação sistêmica (admin), não ligada a nenhum usuário — mesmo padrão deAuthTokenManagerjá usado no agente principal (ifriend_agent/tools/booking/auth.py):POST /authentication_tokencomIFRIEND_API_EMAIL/IFRIEND_API_PASSWORD, token cacheado ~55min, invalidação/retry em 401.
Por que não reaproveitar os módulos do agente principal diretamente
runtime/sessions/__init__.py importa CloudSQL/Redis/Firestore session
services como efeito colateral do import, e o worker (Cloud Function
Gen2) não tem ifriend_agent/ no seu build source — dependências
pesadas/inacessíveis para estes dois componentes enxutos. auth.py e
ifriend_api_auth.py portam apenas a lógica necessária (mesmas env
vars, mesmo comportamento), sem essas dependências transitivas.
Dependências isoladas por componente¶
Nem functions-framework (worker) nem fastapi/google-adk (agente
principal) podem coexistir no mesmo ambiente Python — functions-framework
exige starlette>=1.0, incompatível com o que fastapi/google-adk
exigem (starlette<1.0). Por isso:
apps/knowledge-base-service/requirements.txteruntime/workers/knowledge_base/requirements.txtsão isolados, cada um instalado apenas no seu próprio ambiente de deploy (build da imagem Docker / build da Cloud Function).- A lógica de processamento do worker vive em
processing.py(sem importarfunctions_framework/cloudevents), emain.pyé só o wrapper decorado usado no deploy real — isso permite testarprocessing.pyno venv principal do repo sem esse conflito de dependências.
Contrato com o lado de consulta¶
Ver a tabela completa em Affiliate → Knowledge Base.
Write-back no affiliate_config¶
Ao concluir a indexação, o worker chama
PATCH {IFRIEND_API_BASE_URL}/affiliates/{affiliate_id}/root_agent com
{"knowledgeBaseCollection": collection_name} (runtime/workers/knowledge_base/affiliate_api.py).
O endpoint foi liberado primeiro em ambiente de dev — o kill-switch
IFRIEND_API_KB_WRITEBACK_ENABLED (padrão false) precisa ser habilitado
explicitamente por ambiente, e só depois de confirmar que o endpoint
também está disponível em produção.
Uma falha nesta chamada não falha o job — indexação e busca já
funcionam independente do resultado deste último passo (reprocessar o job
inteiro só para repetir o write-back seria desperdício: embedding e upsert
já teriam sido feitos de novo à toa). O resultado fica registrado no campo
writeback_status (success/failed, + writeback_error se falhou) do
job no Firestore, sem alterar o status geral do job.
Isolamento por ambiente (stage vs produção)¶
Antes desta seção existir, não havia nenhum indicador de ambiente em lugar
nenhum do repo (APP_NAME era idêntico em stage e produção). O env var
ENVIRONMENT (default production, valor stage nos deploys de
stage) resolve isso para o pipeline de KB, prefixando todos os recursos
que poderiam se misturar entre ambientes:
| Recurso | Produção | Stage |
|---|---|---|
| Collection Qdrant | affiliate_<id>_kb |
stage_affiliate_<id>_kb |
| Collection Firestore (jobs) | kb_jobs |
kb_jobs_stage |
| Prefixo GCS | kb-uploads/ |
kb-uploads-stage/ |
| Tópico Pub/Sub | kb-indexing-jobs |
kb-indexing-jobs-stage |
| Host da iFriend API (write-back) | api.theifriend.com |
stage.api.theifriend.com |
A iFriend API tem um host dedicado para stage
(stage.api.theifriend.com) — por isso o isolamento é completo, não
parcial: o agente em stage (cloudbuild.stage.yaml, _API_BASE_URL) lê e
escreve affiliate_config nesse host, e o worker da KB em stage grava o
write-back no mesmo host — as duas pontas (leitura via chat, escrita via
ingestão) ficam olhando o mesmo banco de stage. Sem essa correção, o
agente em stage continuaria lendo affiliate_config de produção mesmo com
a KB isolada, tornando o write-back de stage invisível a quem testasse via
chat.
A collection do Qdrant/Firestore é a mesma instância compartilhada em todos os ambientes (não uma instância separada) — o isolamento vem inteiramente do prefixo, não de infraestrutura duplicada.
Credencial sistêmica precisa funcionar no host de stage
cloudbuild-knowledge-base-worker.stage.yaml reaproveita o mesmo
secret trip-ifriend-api-password usado em produção, presumindo que
essa credencial também autentica em stage.api.theifriend.com. Se não
autenticar, trocar _IFRIEND_API_PASSWORD_SECRET nesse arquivo por um
secret válido para stage.
Deploy¶
cloudbuild-knowledge-base-api.yaml/cloudbuild-knowledge-base-worker.yaml— produção.cloudbuild-knowledge-base-api.stage.yaml/cloudbuild-knowledge-base-worker.stage.yaml— stage, mesmo padrão de nome já usado para o agente principal (cloudbuild.yaml→cloudbuild.stage.yaml). Serviço/function com nome próprio (knowledge-base-api-stage,knowledge-base-worker-stage), tópico Pub/Sub próprio (kb-indexing-jobs-stage).- O worker inclui
IFRIEND_API_BASE_URL/IFRIEND_API_EMAIL(env vars) eIFRIEND_API_PASSWORD(secret) para o write-back — reaproveita a mesma credencial sistêmica já usada pelocloudbuild.yamlprincipal (trip-ifriend-api-password), sem criar uma conta admin nova.
Infra a criar uma vez (fora deste repo, via gcloud):
gcloud pubsub topics create kb-indexing-jobs
gcloud pubsub topics create kb-indexing-jobs-stage