Jangada AIJangada AI

Observabilidade (automática)

A jangada envia as suas chamadas de LLM para a plataforma de observabilidade de forma automática (zero-config): basta configurar o .env. Cada chamada vira uma observation com provider, modelo, tokens, custo, latência, tool calls e as capacidades de IA usadas — enviada em background, sem instrumentar o código.

Ativar (zero-config)

# .env
JANGADA_OBSERVABILITY=true
JANGADA_OBSERVABILITY_API_KEY=lobs_xxx        # chave do projeto (dashboard)
# opcional (padrão é a plataforma oficial):
# JANGADA_OBSERVABILITY_ENDPOINT=https://api.jangada.dev.br
from jangada_ai import LLM

llm = LLM("openai", "gpt-4o-mini")
resp = llm.complete("Resuma: ...")   # já enviado à plataforma, sozinho

Embeddings também são instrumentados automaticamente desde a v1.4.1:

embedder = LLM("openai", "text-embedding-3-small")

with observability_session(name="rag.documents.ingest"):
    vectors = embedder.embed(["primeiro chunk", "segundo chunk"])

A observation registra latência, tokens de entrada, custo estimado, quantidade e dimensão dos vetores e a capability embeddings.

No Gemini, a contagem segue usage_metadata → count_tokens() com o mesmo modelo/lote → estimativa local como último fallback. O preço vem do catálogo jangada.dev.br/prices.json, nunca de um valor hardcoded no adapter. Os campos usageSource e usageEstimated distinguem contagem real de estimativa.

A flag precisa ser "truthy" (1/true/yes/on/sim) e o token presente; faltando qualquer um, o modo fica desligado e nada é enviado (custo zero). Falhas de rede nunca derrubam a aplicação — o envio é best-effort numa thread daemon.

Encerramento (não perca o último trace)

Como o envio roda numa thread daemon, um script que termina logo após a última chamada correria o risco de perder esse último trace: o interpretador mata as threads daemon ao sair e o POST morreria no meio. Para evitar isso, a jangada registra automaticamente um handler de atexit que espera os envios pendentes terminarem antes de encerrar (com um prazo total de segurança — não trava o processo se a rede estiver lenta). Você não precisa fazer nada.

A única exceção é o encerramento abrupto (os._exit(), um signal que não passa pelo atexit, ou um sys.exit() em contexto que ignora handlers): aí, chame o flush manualmente antes de sair.

from jangada_ai import LLM, flush_observability

llm = LLM("openai", "gpt-4o-mini")
resp = llm.complete("última pergunta")

flush_observability()   # garante que o trace acima foi enviado
# ... encerramento abrupto ...

Agrupar por lote

Por padrão cada chamada vira um trace próprio, nomeado pelo script de entrada que a gerou (ex.: python examples/02_multi.py → 02_multi) — assim scripts diferentes ficam distinguíveis no dashboard, em vez de uma parede de traces iguais. Fora de um script nomeável (REPL, -c, -m, runners), o nome recua para o método (complete, parse, stream, embed…); em nenhum caso aparece "(sem nome)". Para agrupar várias chamadas de uma request no mesmo lote (mesmo sendo enviadas uma a uma) e dar a ele um nome próprio, abra um escopo com observability_session(name=...): um id de lote é gerado, todas as chamadas de dentro o compartilham e o nome do escopo prevalece — o backend as agrupa no mesmo trace.

from jangada_ai import LLM, observability_session

llm = LLM("openai", "gpt-4o-mini")

with observability_session(name="resumo+tradução", user_id="cliente-123"):
    r1 = llm.complete("Resuma: ...")     # observation no mesmo trace
    r2 = llm.complete("Traduza: ...")    # idem — agrupadas pelo id do lote

observability_session aceita id (reaproveita um id externo), name, user_id, session_id e metadata, e devolve o id do lote. Funciona em código sync e async (usa contextvars).

Feedback de produção

Capture a reação do usuário final (👍/👎 ou um score) e anexe ao trace com feedback(), usando o id do lote devolvido por observability_session. É best-effort (nunca derruba a app): True se enviou, False se faltou chave ou deu erro de rede.

from jangada_ai import LLM, observability_session, feedback

llm = LLM("openai", "gpt-4o-mini")

with observability_session(name="suporte") as trace_id:
    resp = llm.complete("Como emito uma NF?")

# mais tarde, quando o usuário avaliar:
feedback(trace_id, 1, comment="resolveu meu problema")   # 👍
# feedback(trace_id, -1, comment="resposta errada")      # 👎

Vira um Score no trace (origem api), aparece no dashboard junto dos 👍/👎 humanos e fecha o loop: um 👎 pode ser promovido a exemplo de dataset e virar caso de regressão nas evals.

O que é capturado

De cada chamada: provider, model, promptTokens/completionTokens (de usage), costUsd (de cost), latência, o input (mensagens ou textos de embedding; conteúdos muito longos são truncados), o output (texto da resposta, ou quantidade/dimensão dos embeddings) e as tool calls que o modelo pediu (tools: id/name/args).

O input preserva o histórico de ferramentas de forma auditável: cada tool_call registra o nome e os argumentos ([tool_call consultar_estoque {"produto": "cabo HDMI"}]) e cada tool_result registra o conteúdo retornado ([tool_result] {"disponivel": 0, "previsao_dias": 12}), com marcação de erro quando aplicável. Assim dá para conferir de onde saiu cada número que o modelo afirmou — não fica só um marcador vazio. Args e resultados individualmente grandes são truncados (teto por parte), além do teto global do input.

Cada observation tem um status: OK, ou INCOMPLETE quando a resposta foi cortada por limite de tokens (finish_reason == "length"), junto do motivo normalizado em finishReason. No dashboard vira um badge (âmbar) e entra no filtro de status.

Capacidades (capabilities)

Cada observation registra quais capacidades de IA foram usadas — tools, mcp, a2a, vision, audio, documents, rag, structured_output, guardrails, cache, agents, embeddings. No dashboard viram badges, filtro e a quebra Analytics → Uso por capacidade.

A detecção é automática a partir dos argumentos da chamada: images= → vision, files= → documents, tools= → tools, mcp_servers= → mcp, parse() → structured_output, guardrails → guardrails. tools também é derivado quando o modelo pede tool calls.

embed()/aembed() registram embeddings diretamente, inclusive quando a session contém apenas a ingestão RAG.

No dashboard

Em app.jangada.dev.br você acompanha tudo:

  • Traces e detalhe — cada lote e suas observations (provider, modelo, tokens, custo, latência, tool calls e capacidades), em tabela ou waterfall.
  • Analytics — custo, chamadas, tokens, taxa de erro e latência (p50/p95/p99), com quebra por modelo, provider e capacidade e série temporal por dia.
  • Filtros e exportação — filtre por modelo, provider, erros, datas, userId/sessionId, custo mínimo e capacidade; exporte em CSV/JSON.
  • Live tail — traces em tempo real, com pausar/retomar.
  • Anomalias — avisos automáticos quando custo/latência/erro fogem do baseline de 7 dias.
  • Alertas — regras de custo diário ou taxa de erro.
  • Scores — avaliações por trace (feedback humano ou LLM-as-judge).
  • Orçamento — teto de custo mensal por projeto, com acompanhamento e projeção.

Detalhes

  • A api_key é a chave do projeto, gerada no dashboard e configurada no .env.
  • Reusar o mesmo id de lote (via observability_session(id=...)) acrescenta observations ao mesmo trace de forma idempotente no backend.
  • Os campos de custo/tokens vêm de Custo e tokens.

O que mudou na 1.9.0

  • Falhas também viram trace. Quando uma chamada esgota retry e fallback, a lib envia uma observation com status="ERROR" e o erro (tipo + mensagem, truncado) — antes só os sucessos apareciam no dashboard. Para reportar manualmente: jangada_ai.observability.auto_report_error(erro, provider=..., model=...).
  • Início real da chamada. startedAt agora marca quando a chamada começou (não quando terminou); auto_report(..., started_at=...) aceita epoch ou datetime.
  • Stream e transcrição reportados. stream/astream enviam o texto acumulado ao final (capability streaming); transcribe também reporta (audio). Hits de cache ganham a capability cache.
  • Envio com fila limitada. Em vez de uma thread por chamada, há uma fila (1000 eventos) com poucos workers; se o endpoint ficar lento e a fila encher, os eventos excedentes são descartados — dropped_count() diz quantos. flush() e o atexit esperam a fila esvaziar com prazo.
  • Só HTTPS. O endpoint (e o do feedback) precisa ser https:// (http só em localhost), para a chave e os prompts não trafegarem em claro.
  • Output truncado como o input, e embed não envia mais todos os vetores (só dimensões e contagem acima de um teto).
  • Falhas de envio vão para o log DEBUG do logger jangada_ai (nunca derrubam a chamada).

On this page