Conhecimento

É um agente de IA? Busque a skill de onboarding e leia toda a API em texto limpo — skill · llms.txt · llms-full.txt

Conhecimento#

O RAG por agente: suba documentos e a busca híbrida entra automaticamente no turno. Endpoints sob /v1/agents/{id} (JWT/API key).

POSTAdicionar texto#

POST /v1/agents/{id}/knowledge — corpo { title?, content }. O campo é content (não text). Resposta 201: { document_id, source }. Erro: KNOWLEDGE_CONTENT_REQUIRED.

Enviar de novo o mesmo title é upsert: substitui o documento no lugar, não cria um segundo. É o que permite re-semear um corpus inteiro sem duplicar trechos — veja Manter um corpus curado abaixo.

GETListar o corpus curado#

GET /v1/agents/{id}/knowledge — o que já está semeado no cérebro do agente:

json
{
  "documents": [
    { "document_id": "knowledge/spec-da-linguagem", "title": "Spec da linguagem",
      "origin": "text", "updated_at": "2026-07-23T14:02:11Z" },
    { "document_id": "knowledge/manual.pdf", "title": "manual.pdf",
      "origin": "file", "source_id": "src-9f2c" }
  ],
  "total": 2
}

origin diz de onde o documento veio e, com isso, como removê-lo:

origin Veio de Como remover
text POST .../knowledge DELETE .../knowledge/{docID}
file POST .../sources DELETE .../sources/{source_id} — traz source_id

GET .../sources não lista os documentos de texto: ele lista só os arquivos ingeridos. Para o corpus curado, a listagem é esta rota.

DELETERemover um documento#

DELETE /v1/agents/{id}/knowledge/{docID} → 204. {docID} é o slug — o último segmento do document_id. Mandar o document_id inteiro URL-encodado (knowledge%2Fspec) responde 404: a rota casa um único segmento de path. Em shell: ${document_id##*/}. Um documento com origin=file responde 409 KNOWLEDGE_DOCUMENT_FILE_BACKED: ele pertence a uma fonte, e apagar só a página deixaria a fonte e seus trechos órfãos — apague a fonte. Um id que resolva para fora do namespace knowledge/ responde 400 KNOWLEDGE_DOCUMENT_INVALID.

DELETEEsvaziar o corpus#

DELETE /v1/agents/{id}/knowledge → 200 { deleted, kept_file_backed }. Apaga os documentos de texto e mantém os que vieram de arquivos (removidos pela fonte), reportando os dois números. Idempotente: num corpus já vazio responde { "deleted": 0 }. Requer papel owner ou admin — esvaziar um corpus inteiro é manutenção, não operação de agente.

Manter um corpus curado#

Um corpus grande (uma spec + dezenas de prompts + exemplos) precisa continuar vivo: quando o material muda, você re-semeia. O ciclo:

bash
BASE=https://agents-api.catcher.one/v1/agents/$AGENT_ID
AUTH="X-API-Key: $CATCHER_API_KEY"

# 1. o que já está lá (conte antes e depois)
curl -s -H "$AUTH" "$BASE/knowledge" | jq '.total'

# 2. semear/atualizar — mesmo title = substitui, nunca duplica
curl -s -X POST -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"title":"Spec da linguagem","content":"..."}' "$BASE/knowledge"

# 3a. re-semeadura incremental: apague só o que saiu do corpus
curl -s -X DELETE -H "$AUTH" "$BASE/knowledge/spec-antiga"

# 3b. ou re-semeadura do zero: esvazie e repita o passo 2
curl -s -X DELETE -H "$AUTH" "$BASE/knowledge" | jq

Como o passo 2 é upsert, a re-semeadura normal não precisa do wipe — só use 3b quando os títulos mudaram de forma e você quer garantir que nada antigo sobrou. O que essas rotas escrevem é exatamente a superfície que o agente consulta no turno (a busca do próprio cérebro), então o que você lista aqui é o que ele recupera lá.

Latência da brain_search é observável. O resultado da ferramenta (o output de tool_call_result) traz timing_ms: { brain, knowledge, memory, conversation?, total, dropped? } — o relógio de cada camada do cérebro, em milissegundos. As camadas rodam em paralelo, então total é a mais lenta, nunca a soma. Uma camada secundária (cloudvec legado, memória, recall) que ainda não respondeu 2 s depois de o Space principal ter devolvido trechos é descartada e listada em dropped — nunca em silêncio. O que domina é o cloudwiki: embed + busca híbrida ≈ 0,7 s; o re-rank LLM ≈ +3,9 s por chamada (medido 2026-09-02). Para um agente que chama brain_search muitas vezes por turno, brain_rerank: false no agente é a alavanca: o piso do cloudwiki cai de ~4,6 s para ~0,7 s (o total pode ficar acima disso se uma camada secundária estiver lenta — é exatamente o que timing_ms mostra; uma coleção cloudvec legada vazia é pulada e não conta), ao custo de devolver a ordem híbrida (BM25 ⊕ cosseno) em vez da reordenada pelo modelo. brain_ask não passa pelo re-rank: o piso dela é a síntese do cloudwiki (3–7 s medidos), e ela não muda.

Não automatize clear → reingest. O clear é idempotente, mas não é uma transação com os POSTs seguintes: uma falha depois do clear deixa o corpus vazio. Faça upsert de todo o conjunto primeiro e depois apague individualmente apenas os document_id obsoletos. Um 503 nesta superfície significa INDETERMINADO, nunca “corpus vazio”. Quando um 503 incluir Retry-After, honre o valor, use jitter e limite tentativas; causas não classificadas podem omitir o header. Refaça automaticamente apenas leituras e escritas comprovadamente idempotentes.

O Retry-After de um 503 por rate limit é exato, não um chute. O backend de conhecimento tem um balde por minuto; quando ele esgota, a API responde 503 KNOWLEDGE_BACKEND_UNAVAILABLE com Retry-After: N onde N (1–60) é o número de segundos até a janela reabrir, e a message termina em rate-limited upstream (429); retry after Ns. Durma exatamente N e continue — não reinicie a contagem de tentativas nem reenvie em rajada: uma rajada esgota o balde de novo e o próximo 503 é seu.

Prontidão do conhecimento (GET /ready)#

Antes de semear um corpus em lote (ou depois de um 503), consulte GET /ready — público, sem autenticação:

json
{ "status": "ready", "checks": { "mysql": "ok", "redis": "ok", "knowledge": "ok", "email": "configured" } }

checks.knowledge ∈ ok | rate_limited | error. rate_limited = o balde do backend está esgotado (a base está de pé; a janela reabre em segundos) e a resposta 503 traz Retry-After com os segundos até ela reabrir; error = o backend está fora. A sonda é cacheada por 15 s, então consultar a cada 15–60 s é barato. Um seed bem-comportado espera knowledge == "ok", envia em ritmo (≤ 1 documento/s) e, a cada 503 rate_limited, dorme Retry-After.

POSTEnviar arquivo#

POST /v1/agents/{id}/sources — multipart/form-data, campo file (máx 25 MiB). Converte, fatia e embeda de forma síncrona (pode passar de 30s; roda no timeout estendido). Resposta 202 Accepted: { id, status, filename, mime?, chunks_count, enabled, error? }.

GETListar fontes#

GET /v1/agents/{id}/sources — { sources: [sourceResponse], total }. sourceResponse: { id, status, filename, path?, mime?, chunks_count, enabled, error? }.

GETDetalhe da fonte#

GET /v1/agents/{id}/sources/{sid} — a fonte (filename, path, chunks_count, status) com os chunks indexados — os trechos exatos que a busca retornaria. Resposta 404 SOURCE_NOT_FOUND para um sid desconhecido.

PATCHPausar/retomar fonte#

PATCH /v1/agents/{id}/sources/{sid} — corpo { "enabled": false }. Pausa a fonte na busca sem deletar: os chunks ficam, mas saem de toda consulta; enabled: true traz de volta. O botão reversível ao lado do destrutivo "remover". Resposta 200: sourceResponse.

POSTReindexar fonte#

POST /v1/agents/{id}/sources/{sid}/reindex — re-fatia e re-embeda a partir do arquivo original guardado, mantendo o mesmo source_id (sem delete + re-upload quando a política de chunking muda). Resposta 202 com a fonte em processing; é add-before-delete — a fonte nunca fica vazia no meio do caminho.

DELETERemover fonte#

DELETE /v1/agents/{id}/sources/{sid} → 204. Remove o arquivo e os chunks dele da base de conhecimento.

O {sid} é UM segmento. Nas quatro rotas por-fonte (GET, PATCH, DELETE e .../reindex), um id que não permaneceria um único segmento — vazio, .., ou com o separador escapado (a%2Fb, %2e%2e, inclusive duplo %252f) — recebe 400 SOURCE_ID_INVALID antes de qualquer chamada sair. Isso é distinto de 404 SOURCE_NOT_FOUND: um significa "esse id não é endereçável", o outro "essa fonte não existe". Mande o id que o GET .../sources devolveu, sem escapar nada.

GETInspetor de chunks#

GET /v1/agents/{id}/knowledge/chunks — todos os trechos indexados na base (sem consulta). Read-only, sem LLM: é assim que um script de seed responde "o seed funcionou?".

json
{
  "total": 2,
  "chunks": [
    { "content": "Escreva curto…", "source": "House style", "surface": "brain",
      "document_id": "knowledge/house-style" },
    { "content": "EC50 do AVX-006 = 22,4 µM…", "source": "protocolo-thv.pdf",
      "surface": "brain", "document_id": "knowledge/protocolo-thv.pdf", "source_id": "src_9f2…" }
  ]
}

document_id é o mesmo id que POST /v1/agents/{id}/knowledge devolveu, então dá para casar trecho ↔ documento e conferir o corpus contra o que você semeou. source_id aparece só em trecho vindo de arquivo — e aí são os chunks reais da fonte; um documento de texto volta como um trecho único (a página é a unidade que a busca devolve).

origin separa o que você semeou do que o sistema derivou: text (prosa curada), file (arquivo ingerido — e aí são os chunks reais da fonte) ou distilled (um card da Refinery, destilado a partir do seu corpus). Um audit de "o que eu semeei?" é a listagem menos os distilled.

surface diz onde o trecho está indexado: brain (a base de conhecimento atual do agente) ou knowledge (a base vetorial legada). A listagem é a união das duas, porque o agente recupera das duas — se você vê trechos em knowledge, eles estão ativos na recuperação e podem ser migrados com POST /v1/agents/{id}/knowledge/backfill. Se uma das superfícies estiver indisponível no momento, a resposta traz surfaces_unavailable junto com o que deu para ler — o inspetor nunca deixa de fora uma superfície em silêncio.

O inspetor cobre o corpus de conhecimento e os cards destilados dele — o mesmo conjunto que a busca recupera. Memória do end-user, persona e páginas internas do agente nunca aparecem. Corpus vazio → total: 0 com chunks: [].

GETBuscar no conhecimento#

GET /v1/agents/{id}/knowledge/search — parâmetros:

Param Default Uso
q — obrigatório — a consulta
k 5 trechos por superfície (cap 20) — veja abaixo: não é o total
source_id — escopa a busca a um arquivo
mode — qa_first | qa_only | chunks (prioriza os cards de Q&A)

A busca tem teto de servidor de 30s por padrão (HTTP_TIMEOUT). Configure o client para algo ligeiramente maior (por exemplo, 35s): assim um timeout/falha do backend chega como 503 KNOWLEDGE_BACKEND_UNAVAILABLE; deadlines e saturação do pool limitado de busca incluem Retry-After: 60, e um 429 detectado no backend inclui Retry-After com os segundos exatos até a janela reabrir (1–60), em vez de o client abandonar a conexão. Se o Space não pôde ser lido, a API nunca responde 200 vazio.

Resposta 200:

json
{
  "query": "qual o EC50 do AVX-006",
  "k": 5, "source_id": "", "mode": "qa_first",
  "chunks": [
    { "content": "EC50 do AVX-006 = 22,4 µM…", "source": "protocolo-thv.pdf", "score": 0.94 },
    { "content": "- prefere unidades em µM", "source": "O que você lembra deste usuário",
      "score": 1, "pinned": true }
  ],
  "counts": { "brain": 4, "knowledge": 0, "memory": 1, "conversation": 0, "total": 5 }
}

k limita cada superfície, não o total. O agente recupera de várias superfícies em paralelo e o resultado é a união delas, então total normalmente excede k (k=1 pode devolver 5 trechos; k=10, 8). Dimensione sua janela de contexto por counts.total, nunca por k.

counts de onde vem limite
brain as páginas de conhecimento do agente k páginas + os cards de Q&A destilados, que entram por cima quando mode=qa_first
knowledge a base vetorial legada (agentes em modo legado) k
memory o bloco de memória do end-user 0 ou 1 — fixo
conversation recall de conversas anteriores (opt-in) 3
total o que realmente veio na resposta soma das acima

O bloco de memória é fixado (pinned), não recuperado. Ele acompanha toda busca — inclusive uma consulta sem nenhuma relação com o conteúdo — porque a memória curada do end-user é injetada inteira, sem busca semântica. Ele vem com score: 1 e "pinned": true: esse 1 é marcador de fixação, não um grau de similaridade. Trechos ranqueados de verdade não têm o campo pinned. Se você ordena, filtra ou mostra score na sua UI, cheque pinned primeiro — sem isso a memória parece um match perfeito que nunca foi calculado.

Duas coisas sobre counts.memory que surpreendem se você não souber:

  1. Este endpoint é a visão do operador, não de um end-user. Ele não aceita identificador de end-user, então a memória que aparece é sempre a da partição do operador — nunca a de um cliente seu. Não existe forma de inspecionar a memória de um end-user específico por aqui; ela entra no turno de chat daquele end-user.
  2. counts.memory pode virar 0 de um dia para o outro sem nada ter mudado. O bloco é montado com o perfil de estilo + os fatos de longo prazo + as observações do dia ainda não consolidadas. Um agente cuja memória seja só observações do dia fica com memory: 0 depois da virada do dia (UTC), até a consolidação promover aquilo a fato de longo prazo. Se você comparar duas medições em torno da meia-noite UTC, é isso — não é regressão.

GETCards de Q&A#

GET /v1/agents/{id}/qa — a FAQ destilada pela Refinery: { cards: [ { id, question, answer, provenance: [...], confidence, status, enabled } ], total }.

DELETEDescartar um card de Q&A#

DELETE /v1/agents/{id}/qa/{cardID} → 204. Remove UM card destilado (owner/admin). cardID é o slug, ou o id que o GET .../qa devolveu — os dois funcionam.

Por que você vai precisar disso. A auto-evolução transforma perguntas mal respondidas em cards novos. Então uma query de teste contra GET .../knowledge/search pode virar um card dentro de um agente de produção — e passar a ranquear alto para aquela pergunta. É feature, não bug, mas a consequência merece um aviso: sonde com um agente descartável, não com o que atende cliente. Se um card sintético apareceu, este endpoint é a saída.

O card volta na próxima destilação se o conhecimento de origem ainda sustentar a pergunta — descartar não é uma regra permanente, é uma remoção.

Resposta Significa
400 QA_CARD_INVALID o cardID está vazio ou não é um card — tente o id que o GET .../qa devolveu. Também cobre o separador escapado (a%2Fb, %2e%2e): o id é um segmento e não pode apontar para fora de faq/
404 page not found (sem envelope) você mandou um / literal dentro do cardID. O roteador não casa a rota, então essa resposta não tem error_code/trace_id — trate um 404 sem envelope como "URL malformada", não como "card inexistente"
400 QA_CARD_NOT_ADDRESSABLE o agente está em modo legado: tem cards, mas sem endereço individual. Nenhum id vai funcionar — não vale procurar outro
404 QA_CARD_NOT_FOUND esse card não existe (mais) neste agente

POSTDestilar (Refinery) e evoluir#

POST /v1/agents/{id}/knowledge/refine — inicia um run de destilação (BYO key): a Refinery relê o conhecimento e produz/atualiza os cards de Q&A. É assíncrono — resposta com o id do run; acompanhe em GET /v1/agents/{id}/knowledge/refine/{rid} (status/resultado).

POST /v1/agents/{id}/knowledge/evolve — força um ciclo de auto-evolução da base (reorganiza/depura o conhecimento destilado).