Skip to content

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 failed e 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) marcam failed e 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 pelo unified_bot.py) — não é um mecanismo de auth novo. Autorização: o partner_code do JWT precisa bater com o affiliate_id do 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 de AuthTokenManager já usado no agente principal (ifriend_agent/tools/booking/auth.py): POST /authentication_token com IFRIEND_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.txt e runtime/workers/knowledge_base/requirements.txt sã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 importar functions_framework/cloudevents), e main.py é só o wrapper decorado usado no deploy real — isso permite testar processing.py no 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) e IFRIEND_API_PASSWORD (secret) para o write-back — reaproveita a mesma credencial sistêmica já usada pelo cloudbuild.yaml principal (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

Ver também