# jangada — documentação completa (llms-full.txt)

> Camada fina e adaptável sobre os SDKs oficiais de LLM (Anthropic, OpenAI,
> Groq, Gemini). Troque provider/model/api_key sem mudar o resto do código.
> Este arquivo concatena toda a documentação de docs/ para consumo em um
> único fetch. Instalação: pip install jangada-ai (importa-se como import jangada_ai).

Fonte: https://github.com/nerigleston/jangada — índice curto em llms.txt.

---

# Começando com jangada

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/nerigleston/jangada-docs/blob/main/examples/notebooks/jangada_quickstart.ipynb)

`jangada` é uma camada fina sobre os SDKs oficiais de LLM (Anthropic, OpenAI,
Groq, Gemini). O objetivo é trocar **provider / model / api_key** sem mudar o
resto do código.

> Quer só testar? Abra o **[quickstart no Colab](https://colab.research.google.com/github/nerigleston/jangada-docs/blob/main/examples/notebooks/jangada_quickstart.ipynb)** (1 clique, cole sua chave e rode).

## Instalação

```bash
pip install jangada-ai                 # nome no PyPI; importa-se como jangada_ai
pip install "jangada-ai[anthropic]"    # só Claude
pip install "jangada-ai[openai,groq]"  # OpenAI + Groq
pip install "jangada-ai[all]"          # todos os SDKs
pip install "jangada-ai[files]"        # leitura de docx/pdf/csv/xlsx
```

> O nome de distribuição é `jangada-ai` (o nome `jangada` estava ocupado no
> PyPI). O pacote é importado como `import jangada_ai` (hífen vira underscore).

Imports são preguiçosos: `import jangada_ai` funciona sem nenhum SDK instalado.

## Primeira chamada

```python
from jangada_ai import LLM

llm = LLM("anthropic", "claude-opus-4-8")
print(llm.complete("Explique {{tema}} em 2 frases.", tema="MCP").text)
```

`complete()` aceita templates `{{ }}` direto no prompt — as variáveis vêm como
keyword args. Veja [Parâmetros de geração](parameters.md) para controlar
`temperature`, `max_tokens`, etc.

## Trocar de provider

Só muda os dois primeiros argumentos; o resto do código permanece:

```python
LLM("openai", "gpt-4o-mini")
LLM("groq", "llama-3.3-70b-versatile")
LLM("gemini", "gemini-2.5-flash")
```

Chaves de API vêm de `api_key=`, da variável de ambiente do provider, ou de um
arquivo `.env` detectado na importação. Precedência:
`api_key=` explícito > variável de ambiente > `.env`.

## Próximos passos

- [Providers e chaves](providers.md)
- [Structured output](structured-output.md)
- [Documentos (docx/pdf/csv/xlsx)](documents.md)
- [Retry e fallback](retry-fallback.md)

---

# Tutorial: primeiros passos (do zero ao fallback)

Um passo a passo de ~10 minutos: instalar, fazer a primeira chamada, trocar de
provider sem mudar o código, usar templates e deixar tudo resiliente com
fallback. Cada bloco é executável.

## 1. Instalar

Instale o núcleo + o SDK do provider que você vai usar (extras opcionais):

```bash
pip install "jangada-ai[openai]"     # ou [anthropic] / [groq] / [gemini] / [all]
export OPENAI_API_KEY=sk-...
```

> `import jangada_ai` funciona sem nenhum SDK instalado — você só instala o extra
> de quem for usar.

## 2. Primeira chamada

```python
from jangada_ai import LLM

llm = LLM("openai", "gpt-4o-mini")
print(llm.complete("Explique o que é uma jangada em uma frase.").text)
```

`complete()` devolve um `Completion`. Além de `.text`, ele traz `.usage` (tokens),
`.cost` (custo estimado), `.provider`, `.model` e o objeto nativo em `.raw`.

## 3. Trocar de provider — o pitch

A mesma chamada vale para os quatro. Só muda o par `(provider, modelo)`:

```python
LLM("anthropic", "claude-opus-4-8").complete("Oi!")
LLM("groq", "llama-3.3-70b-versatile").complete("Oi!")
LLM("gemini", "gemini-2.5-flash").complete("Oi!")
```

Você **não** muda mais nada — params, erros e custo são normalizados por baixo.
Veja [Providers e chaves](providers.md).

## 4. Templates `{{ }}`

Em vez de montar strings na mão, passe variáveis como kwargs:

```python
llm.complete("Traduza para {{idioma}}: {{frase}}",
             idioma="francês", frase="Bom dia")
```

## 5. Async

Todo método tem versão `a*` equivalente:

```python
import asyncio

async def main():
    comp = await llm.acomplete("Diga olá.")
    print(comp.text)

asyncio.run(main())
```

## 6. Resiliência: retry + fallback

Em produção, um provider pode dar rate limit ou 5xx. Defina um reserva:

```python
primario = LLM("openai", "gpt-4o-mini")
reserva  = LLM("anthropic", "claude-haiku-4-5-20251001")

llm = primario.with_fallback(reserva)
comp = llm.complete("...")      # tenta o primário (com retries); se falhar, vai pro reserva
print(comp.provider, comp.model, comp.cost)
```

A jangada tenta o primário com **backoff** e só cai pro reserva em erros
"failover-able" (rate limit, timeout, 5xx, 404). Detalhes em
[Retry e fallback](retry-fallback.md) e [Custo e tokens](cost.md).

## Próximos passos

- [Extração estruturada](tutorial-extracao-estruturada.md) — texto/imagem → Pydantic.
- [RAG do zero](tutorial-rag.md) — responder com base nos seus documentos.
- [Agente MCP do zero](tutorial-agente-mcp.md) — o modelo usando ferramentas sozinho.
- Receitas completas em [`examples/cookbook/`](https://github.com/nerigleston/jangada/tree/master/examples/cookbook).

---

# Tutorial: extração estruturada (texto e imagem → Pydantic)

Objetivo: transformar texto solto ou uma **foto** num objeto Pydantic validado,
com uma só chamada `parse()`. Mesmo código em qualquer provider — a jangada cuida
do mecanismo (OpenAI `.parse`, Groq json_schema, Gemini `response_schema`,
Anthropic tool-forcing).

## 1. Defina o que você quer extrair

O schema é um modelo Pydantic. Use `Field(description=...)` para guiar o modelo:

```python
from pydantic import BaseModel, Field

class Item(BaseModel):
    descricao: str
    quantidade: float
    valor_total: float

class NotaFiscal(BaseModel):
    estabelecimento: str
    cnpj: str | None = Field(default=None, description="CNPJ como aparece")
    itens: list[Item]
    valor_a_pagar: float
```

## 2. Extrair de texto

```python
from jangada_ai import LLM

llm = LLM("openai", "gpt-4o-mini")
nota = llm.parse("Padaria do Zé — 2 pães R$8, 1 leite R$6. Total R$14.", NotaFiscal).parsed
print(nota.estabelecimento, nota.valor_a_pagar)   # Padaria do Zé 14.0
```

`parse()` devolve um `Completion`; o objeto validado fica em `.parsed`.

## 3. Extrair de uma imagem (vision + structured)

A mesma chamada aceita `images=` — vision e structured output juntos:

```python
nota = llm.parse(
    "Extraia os dados desta nota fiscal. Transcreva exatamente; não invente.",
    NotaFiscal,
    images=["nota.jpg"],     # caminho, bytes ou base64
).parsed

for i in nota.itens:
    print(f"{i.descricao}: {i.quantidade:g} = {i.valor_total:.2f}")
```

Use um modelo com visão (gpt-4o-mini, gemini-2.5-flash, claude-haiku-4-5, ou um
multimodal do Groq como `meta-llama/llama-4-scout-17b-16e-instruct`).

## 4. Funciona em qualquer provider

```python
for provider, modelo in [("openai", "gpt-4o-mini"),
                         ("gemini", "gemini-2.5-flash"),
                         ("anthropic", "claude-haiku-4-5-20251001")]:
    p = LLM(provider, modelo).parse("João tem 30 anos.", NotaFiscal)  # exemplo simplificado
```

> **Groq:** modelos sem `json_schema` (ex.: `llama-3.3-70b`) caem automaticamente
> para JSON Object mode — `parse()` funciona mesmo assim.

## Próximos passos

- Receita completa: [`examples/cookbook/01_extracao_nota_fiscal.py`](https://github.com/nerigleston/jangada/blob/master/examples/cookbook/01_extracao_nota_fiscal.py).
- Referência: [Structured output](structured-output.md) e [Vision](vision.md).
- Para documentos (docx/pdf/xlsx) sem vision, veja [Documentos](documents.md).

---

# Tutorial: RAG do zero

Objetivo: responder perguntas com base nos **seus** textos. A jangada faz
embeddings + busca **híbrida** (BM25 lexical + vetorial, fundidos por RRF) +
geração da resposta. Comece em memória (sem banco) e troque por um vector store
real depois — sem mudar o resto.

Instale o extra:

```bash
pip install "jangada-ai[openai,rag]"
export OPENAI_API_KEY=sk-...
```

## 1. Montar o RAG

Você precisa de um **embedder** (gera vetores), um **store** (guarda) e um
**chat** (responde):

```python
from jangada_ai import LLM
from jangada_ai.rag import RAG, InMemoryVectorStore

embedder = LLM("openai", "text-embedding-3-small")
chat     = LLM("openai", "gpt-4o-mini")

rag = RAG(embedder, InMemoryVectorStore(), chat=chat, k=3, alpha=0.5)
```

- `k` — quantos trechos recuperar.
- `alpha` — balanço da busca: `0` = só BM25 (palavras), `1` = só vetorial
  (semântica), `0.5` = equilíbrio.

## 2. Indexar conteúdo

```python
rag.add_texts([
    "A jangada troca de provider mudando só LLM('provider', 'modelo').",
    "O fallback é acionado em rate limit, 5xx ou timeout.",
])

# ou a partir de arquivos (docx/pdf/csv/xlsx/txt):
rag.add_document("manual.pdf")
```

## 3. Perguntar

```python
resp = rag.ask("Como troco de provider?")
print(resp.text)                 # resposta com base no contexto recuperado
print(len(resp.sources), "trechos usados")
```

`ask()` recupera os trechos mais relevantes, monta o contexto e gera a resposta.
`resp.sources` traz os trechos (com score) que embasaram.

## 4. Indo para produção: vector store real

Troque só o store — pgvector ou Mongo, detectado pela string de conexão:

```python
from jangada_ai.rag import vector_store

store = vector_store("postgresql://user:pass@host/db")   # ou "mongodb+srv://..."
rag = RAG(embedder, store, chat=chat)
```

O resto do código continua igual. Ajustes finos (`min_score`, `filter`, `weights`,
`max_context_chars`) estão em [RAG](rag.md).

## Próximos passos

- Receita completa: [`examples/cookbook/03_chatbot_rag.py`](https://github.com/nerigleston/jangada/blob/master/examples/cookbook/03_chatbot_rag.py).
- Referência detalhada: [RAG](rag.md).

---

# Tutorial: agente MCP do zero

Objetivo: conectar num servidor **MCP** (Model Context Protocol) e deixar o modelo
**usar as ferramentas sozinho** — ele decide qual tool chamar, a jangada executa e
reenvia, até a resposta final. Funciona em qualquer provider (não só os com MCP
nativo), porque usa o tool calling comum.

Instale o extra (traz o pacote oficial `mcp`):

```bash
pip install "jangada-ai[openai,mcp]"
export OPENAI_API_KEY=sk-...
```

## 1. Conectar num servidor MCP

`MCPClient` conecta por **URL** (streamable-http) ou **stdio** (subprocesso):

```python
import asyncio
from jangada_ai.mcp import MCPClient

async def main():
    async with MCPClient("https://seu-mcp/mcp/") as mcp:     # remoto
        tools = await mcp.list_tools()
        print([t.name for t in tools])

asyncio.run(main())

# stdio (servidor local como subprocesso):
# async with MCPClient(command="python", args=["server.py"]) as mcp: ...
```

## 2. Rodar o agente

`run_agent` faz o loop completo: lista as tools, manda pro modelo, executa as
chamadas e reenvia, até a resposta final.

```python
from jangada_ai import LLM
from jangada_ai.mcp import MCPClient, run_agent

async def main():
    llm = LLM("openai", "gpt-4o-mini")
    async with MCPClient("https://seu-mcp/mcp/") as mcp:
        comp = await run_agent(llm, "Quais são os dados da empresa?", client=mcp)
        print(comp.text)

asyncio.run(main())
```

O modelo escolhe a tool certa (ex.: `obter_dados_empresa`), a jangada executa via
MCP e devolve o resultado pro modelo, que responde em linguagem natural.

## 3. Além de tools: resources e prompts

O `MCPClient` cobre todos os primitivos do MCP:

```python
async with MCPClient("https://seu-mcp/mcp/") as mcp:
    texto = await mcp.resource_text("file:///guia.md")        # dados/contexto do server
    msgs  = await mcp.prompt_messages("revisar", {"x": "..."}) # template do server -> list[Message]
    resp  = await llm.acomplete(None, history=msgs)
```

E recursos de **cliente** (o server chama de volta): `roots=[...]`,
`sampling_llm=LLM(...)` (o server pede uma geração ao **seu** LLM),
`elicitation_callback`, `logging_callback`.

## Próximos passos

- Receita completa: [`examples/cookbook/02_agente_mcp.py`](https://github.com/nerigleston/jangada/blob/master/examples/cookbook/02_agente_mcp.py).
- Referência detalhada: [MCP](mcp.md) e [Tools](tools.md).

---

# Providers e chaves de API

A jangada suporta seis providers, cada um isolado em um *adapter* que traduz
os tipos normalizados (`Message`/`Completion`) para o SDK nativo.

| Provider    | `provider=`  | Variável de ambiente | Extra para instalar         |
|-------------|--------------|----------------------|-----------------------------|
| Anthropic   | `anthropic`  | `ANTHROPIC_API_KEY`  | `jangada-ai[anthropic]`     |
| OpenAI      | `openai`     | `OPENAI_API_KEY`     | `jangada-ai[openai]`        |
| Groq        | `groq`       | `GROQ_API_KEY`       | `jangada-ai[groq]`          |
| Gemini      | `gemini`     | `GEMINI_API_KEY`     | `jangada-ai[gemini]`        |
| Mistral     | `mistral`    | `MISTRAL_API_KEY`    | `jangada-ai[mistral]`       |
| OpenRouter  | `openrouter` | `OPENROUTER_API_KEY` | `jangada-ai[openai]`        |

## OpenRouter (gateway para centenas de modelos)

[OpenRouter](https://openrouter.ai) é um *gateway* compatível com o dialeto
`chat.completions` da OpenAI — por isso reusa o **mesmo SDK `openai`** (extra
`jangada-ai[openai]`), só apontando para outro `base_url`. Dá acesso a centenas
de modelos de vários provedores com uma única chave.

```python
LLM("openrouter", "openai/gpt-4o")                       # usa OPENROUTER_API_KEY
LLM("openrouter", "anthropic/claude-sonnet-4.6")
LLM("openrouter", "google/gemini-2.5-flash", api_key="sk-or-...")
```

O modelo é **qualificado por provedor** (`provedor/modelo`). Cabeçalhos opcionais
de ranking vão pelo cliente; argumentos exclusivos do OpenRouter (ex.: `models`
para roteamento com fallback) vão por `extra=`:

```python
LLM(
    "openrouter", "openai/gpt-4o",
    default_headers={"HTTP-Referer": "https://meusite.com", "X-Title": "Meu App"},
    extra={"models": ["openai/gpt-4o", "anthropic/claude-sonnet-4.6"]},
)
```

Suporta texto, streaming, structured output (json_schema com fallback automático
para JSON Object mode), vision, tools/function calling, **transcrição de áudio**
(modelos `openai/whisper-*`, `openai/gpt-4o-transcribe`, ...) e **embeddings**
(`openai/text-embedding-3-*`, `google/gemini-embedding-001`, `qwen/qwen3-embedding-*`,
...). O único recurso não suportado é **MCP server-side**: ele depende da Responses
API da OpenAI, que o OpenRouter não expõe — esse caminho levanta `UnsupportedError`.

## Resolução da chave

```python
LLM("openai", "gpt-4o-mini", api_key="sk-...")   # explícito
LLM("openai", "gpt-4o-mini")                       # usa OPENAI_API_KEY ou .env
```

Precedência: **`api_key=` explícito > variável de ambiente > arquivo `.env`**.
O `.env` é detectado de forma não-destrutiva na importação (desative com
`JANGADA_NO_DOTENV=1`).

## Como cada adapter trata structured output

- **OpenAI**: `chat.completions.parse(response_format=Modelo)` → `.message.parsed`
- **Groq**: `response_format={"type":"json_schema",...}` + `model_validate_json`
- **Gemini**: `config.response_schema=Modelo` → `resp.parsed`
- **Anthropic**: tool-forcing (`tool_choice` fixo) → valida `tool_use.input`
- **Mistral**: helper nativo `chat.parse(response_format=Modelo)` → `.message.parsed`
- **OpenRouter**: igual ao Groq (json_schema com fallback p/ JSON Object mode)

Veja [Structured output](structured-output.md) para o uso uniforme.

## Adicionando um provider novo

Se ele falar o dialeto `chat.completions` da OpenAI, herde de
`_OpenAICompatible` e ajuste `sdk_module`/`sync_class`/`async_class`. Caso
contrário, implemente os 6 métodos do contrato `Provider`. Detalhes em
[Estendendo](extending.md).

---

# Matriz de capacidades por provider

O que cada provider suporta na jangada. Os recursos da API pública são os
mesmos (`complete`, `parse`, `stream`, `transcribe`, ...); o que muda é o que
cada provider consegue fazer por baixo.

| Recurso                         | OpenAI | Groq | Gemini | Anthropic | Mistral |
|---------------------------------|:------:|:----:|:------:|:---------:|:-------:|
| Texto (`complete`/`acomplete`)  | ✅     | ✅   | ✅     | ✅        | ✅      |
| Structured output (`parse`)     | ✅     | ✅   | ✅     | ✅        | ✅      |
| Tools / function calling        | ✅     | ✅   | ✅     | ✅        | ✅      |
| MCP (`mcp_servers=`)            | ✅ URL | ✅ URL | ✅ sessão⁴ | ✅ URL | ❌      |
| Embeddings (`embed`)            | ✅     | ❌   | ✅     | ❌        | ✅      |
| Streaming (`stream`/`astream`)  | ✅     | ✅   | ✅     | ✅        | ✅      |
| Vision / imagens (`images=`)    | ✅     | ⚠️¹  | ✅     | ✅        | ⚠️¹     |
| Documentos (`files=`)²          | ✅     | ✅   | ✅     | ✅        | ✅      |
| Detecção de objetos             | ✅     | ⚠️¹  | ✅³    | ⚠️        | ⚠️      |
| Transcrição de áudio (`transcribe`) | ✅ | ✅   | ✅     | ❌        | ✅ (Voxtral) |
| OCR / Document AI (`ocr`)       | ❌     | ❌   | ❌     | ❌        | ✅      |
| Param `top_k`                   | ❌     | ❌   | ✅     | ✅        | ❌      |
| Param `seed`                    | ✅     | ✅   | ✅     | ❌        | ✅ (`random_seed`) |
| Param `stop`                    | ✅     | ✅   | ✅ (`stop_sequences`) | ✅ (`stop_sequences`) | ✅ |

¹ Depende do modelo: vision no Groq exige um modelo com visão (ex.: família
Llama vision); modelos de texto puro não aceitam imagem.
² `files=` extrai texto **localmente** (docx/pdf/csv/xlsx) e envia como texto —
por isso funciona em todos. Veja [Documentos](documents.md).
³ A convenção de bounding box (0–1000) é nativa do Gemini, que é o mais preciso.
⁴ MCP no Gemini é **client-side por sessão** e só no async (`acomplete`); os
demais (exceto Mistral) são **remoto por URL** (server-side). Veja [MCP](mcp.md).

## Como cada um implementa o structured output

| Provider  | Mecanismo                                                |
|-----------|----------------------------------------------------------|
| OpenAI    | `chat.completions.parse(response_format=Modelo)`         |
| Groq      | `response_format={"type":"json_schema",...}` + validação |
| Gemini    | `config.response_schema=Modelo` → `resp.parsed`          |
| Anthropic | tool-forcing (`tool_choice` fixo) → valida `tool_use`    |
| Mistral   | helper nativo `chat.parse(response_format=Modelo)` → `.message.parsed` |

## Detalhe por provider

- [OpenAI](llm-openai.md)
- [Groq](llm-groq.md)
- [Gemini](llm-gemini.md)
- [Anthropic](llm-anthropic.md)
- [Mistral](llm-mistral.md)

Os parâmetros canônicos e os perfis por modelo (gpt-5, gemini-3.x) estão em
[Parâmetros e perfis](parameters.md).

---

# OpenAI

Provider `openai`. Adapter sobre o SDK `openai` (dialeto `chat.completions`).

```bash
pip install "jangada-ai[openai]"
```

- **`provider=`**: `"openai"`
- **Variável de ambiente**: `OPENAI_API_KEY`
- **Base do adapter**: `_OpenAICompatible` (compartilhado com Groq)

```python
from jangada_ai import LLM
llm = LLM("openai", "gpt-4o-mini")
```

## O que faz

- **Texto** (`complete`/`acomplete`) e **streaming** (`stream`/`astream`).
- **Structured output** (`parse`): usa o helper nativo
  `chat.completions.parse(response_format=Modelo)` → `.message.parsed`.
- **Vision** (`images=`): imagens viram `image_url` com data URI.
- **Documentos** (`files=`): extração de texto local (comum a todos).
- **Detecção de objetos** (`detect_objects`): via vision + structured.
- **Transcrição de áudio** (`transcribe`): endpoint dedicado
  `audio.transcriptions.create`. Modelos: `gpt-4o-transcribe`,
  `gpt-4o-mini-transcribe`, `whisper-1`.

## Estrutura e quirks

- **Parâmetros**: aceita `temperature`, `max_tokens`, `top_p`, `stop`, `seed`.
  **Não** tem `top_k` (é descartado).
- **Perfil de modelo** (`profiles.py`): a família `gpt-5` rejeita `temperature`
  e usa `max_completion_tokens` em vez de `max_tokens` — a jangada normaliza
  isso automaticamente. Veja [Parâmetros e perfis](parameters.md).
- **Resposta** (`Completion`): `text`, `usage` (`input_tokens`/`output_tokens`
  derivados de `prompt_tokens`/`completion_tokens`), `raw` com o objeto nativo.
- **Erros**: traduzidos por `errors.classify()` para a hierarquia normalizada.

## Exemplo de structured

```python
from pydantic import BaseModel
class Pessoa(BaseModel):
    nome: str; idade: int

llm.parse("Extraia: João, 30 anos.", Pessoa).parsed   # Pessoa(nome='João', idade=30)
```

Relacionado: [Matriz de capacidades](capabilities.md), [Groq](llm-groq.md)
(mesmo dialeto), [Transcrição de áudio](audio.md).

---

# Groq

Provider `groq`. Adapter sobre o SDK `groq`, que fala o mesmo dialeto
`chat.completions` da OpenAI — por isso herda de `_OpenAICompatible`.

```bash
pip install "jangada-ai[groq]"
```

- **`provider=`**: `"groq"`
- **Variável de ambiente**: `GROQ_API_KEY`
- **Diferença para a OpenAI**: `supports_parse_helper = False` (não tem
  `.parse()` nativo).

```python
from jangada_ai import LLM
llm = LLM("groq", "llama-3.3-70b-versatile")
```

## O que faz

- **Texto** e **streaming** — foco em latência muito baixa.
- **Structured output** (`parse`): como não há helper `.parse`, usa
  `response_format={"type":"json_schema",...}` e valida com
  `model_validate_json`.
- **Vision** (`images=`): suportado **apenas em modelos com visão** (ex.:
  família Llama vision). Modelos de texto puro recusam imagem.
- **Documentos** (`files=`): extração de texto local.
- **Transcrição de áudio** (`transcribe`): endpoint dedicado (compatível com o
  da OpenAI). Modelos: `whisper-large-v3`, `whisper-large-v3-turbo` (rápido).

## Estrutura e quirks

- **Parâmetros**: `temperature`, `max_tokens`, `top_p`, `stop`, `seed`.
  Sem `top_k`.
- **Mesma base da OpenAI**: a tradução de mensagens, structured e áudio
  reaproveitam `_OpenAICompatible`; só mudam `sdk_module`/`sync_class`/
  `async_class`/`supports_parse_helper`.
- **Resposta** (`Completion`): igual à OpenAI (`text`, `usage`, `raw`).

## Quando escolher Groq

Velocidade e custo de inferência baixos — ótimo para transcrição em lote
(`whisper-large-v3-turbo`) e respostas de baixa latência. Combine como
**fallback** ou **primário** com OpenAI (mesmo dialeto). Veja
[Transcrição de áudio](audio.md) e [Retry e fallback](retry-fallback.md).

---

# Gemini

Provider `gemini`. Adapter sobre o SDK `google-genai`. Tem um único `Client`; o
async fica em `client.aio`.

```bash
pip install "jangada-ai[gemini]"
```

- **`provider=`**: `"gemini"`
- **Variável de ambiente**: `GEMINI_API_KEY` (ou `GOOGLE_API_KEY`)

```python
from jangada_ai import LLM
llm = LLM("gemini", "gemini-2.5-flash")
```

## O que faz

- **Texto** e **streaming** (`generate_content` / `generate_content_stream`).
- **Structured output** (`parse`): `config.response_schema=Modelo` +
  `response_mime_type="application/json"` → `resp.parsed`.
- **Vision** (`images=`): imagens viram `types.Part.from_bytes`.
- **Documentos** (`files=`): extração de texto local.
- **Detecção de objetos** (`detect_objects`): **o mais preciso** — o formato de
  bounding box 0–1000 é nativo do treino do Gemini.
- **Transcrição de áudio** (`transcribe`): multimodal — o áudio entra como
  `Part.from_bytes` junto de uma instrução; **não** é endpoint dedicado.

## Estrutura e quirks

- **Mensagens**: o papel `system` vira `system_instruction` no
  `GenerateContentConfig` (não é uma mensagem comum); `assistant` vira `model`.
- **Parâmetros canônicos → config**: `max_tokens`→`max_output_tokens`,
  `stop`→`stop_sequences`; `temperature`/`top_p`/`top_k`/`seed` passam direto.
  É o único provider com `top_k`.
- **Perfil de modelo** (`profiles.py`): `gemini-3.x` descarta sampling
  (`temperature`/`top_p`/`top_k`). Veja [Parâmetros e perfis](parameters.md).
- **Function calling multi-turno no 3.x — *thought signatures* (resolvido pela
  lib)**: o Gemini 3.x anexa um `thought_signature` opaco a cada function call e
  o exige de volta, inalterado, ao reenviar o histórico. A jangada preserva isso
  automaticamente — o `thought_signature` viaja no campo opaco
  `metadata: dict` de `ToolCall`/`ToolCallPart` (dados do provider que a lib só
  guarda, não interpreta), e o adapter o reanexa na `Part` ao remontar o
  histórico. Ou seja, `tools=`, `Agent` e `MCPClient` funcionam em multi-turno
  sem você tocar em nada. `gemini-2.5` não usa esse campo. (corrigido em 1.3.1)
- **Resposta** (`Completion`): `usage` vem de `usage_metadata`
  (`prompt_token_count`/`candidates_token_count`).

## Thinking (raciocínio) — você passa só `thinking_budget`

O Gemini tem duas convenções incompatíveis: o **2.5** só aceita `thinking_budget`
(em tokens) e o **3.x** só aceita `thinking_level` (`LOW`/`HIGH`) — misturar dá
HTTP 400. A jangada resolve por você: passe **`thinking_budget`** (ou
`thinking_level`) e a lib adapta ao modelo, empacotando no `thinking_config`
nativo por baixo dos panos.

```python
# Funciona igual nas duas versões — você não muda nada:
LLM("gemini", "gemini-2.5-flash").complete("...", params={"thinking_budget": 1024})
LLM("gemini", "gemini-3-pro").complete("...",  params={"thinking_budget": 1024})
# no 2.5 vira thinking_config(thinking_budget=1024);
# no 3.x o perfil converte para thinking_config(thinking_level="HIGH").
```

- `thinking_budget`: `0` desliga (quando o modelo permite), `-1` é automático.
- No 3.x o budget é aproximado para um nível válido (`LOW` se ≤0, senão `HIGH`);
  no 2.5 um `thinking_level` é convertido para budget. Um `thinking_config` pronto
  é respeitado como veio.
- **Níveis aceitos**: `thinking_level` reconhece `MINIMAL`/`LOW`/`MEDIUM`/`HIGH`.
  Como **entrada no 2.5** os quatro são convertidos para um `thinking_budget`
  (`0`/`1024`/`8192`/`24576`). Já **direto no 3.x**, o Gemini só aceita `LOW` e
  `HIGH` de forma universal (`MINIMAL` é só Flash/Lite e `MEDIUM` só no 3.0 Flash;
  passá-los como nível em outros 3.x dá HTTP 400) — por isso a conversão
  budget→level da lib usa apenas `LOW`/`HIGH`.

## Por que é o "canivete suíço" aqui

É o único que cobre vision, detecção e áudio **sem endpoints separados** —
tudo via `generateContent`. Veja [Matriz de capacidades](capabilities.md).

---

# Anthropic (Claude)

Provider `anthropic`. Adapter sobre o SDK `anthropic`.

```bash
pip install "jangada-ai[anthropic]"
```

- **`provider=`**: `"anthropic"`
- **Variável de ambiente**: `ANTHROPIC_API_KEY`

```python
from jangada_ai import LLM
llm = LLM("anthropic", "claude-opus-4-8")
```

## O que faz

- **Texto** e **streaming**.
- **Structured output** (`parse`): por **tool-forcing** — define uma ferramenta
  com o schema e fixa `tool_choice`, depois valida o `tool_use.input` contra o
  modelo Pydantic. (Claude não tem um `response_format` como a OpenAI.)
- **Vision** (`images=`): imagens viram bloco `image` com `source` base64.
- **Documentos** (`files=`): extração de texto local.

## O que NÃO faz

- **Transcrição de áudio**: a API do Claude **não aceita áudio** — `transcribe()`
  levanta `UnsupportedError`. Use OpenAI, Groq ou Gemini para áudio (e, se
  quiser, mantenha o Claude como provider de texto com fallback de áudio em
  outro). Veja [Transcrição de áudio](audio.md).
- **Detecção de objetos**: funciona mecanicamente (vision + structured), mas a
  precisão das coordenadas é menor que a do Gemini.

## Estrutura e quirks

- **Parâmetros**: `temperature`, `max_tokens`, `top_p`, `top_k`, `stop`
  (→ `stop_sequences`). **Não** tem `seed` (é descartado).
- **`max_tokens` é obrigatório** na API do Claude — a jangada já usa um default
  alto (`8192`) quando você não informa.
- **System**: vai no campo `system` da requisição (não como mensagem).
- **Resposta** (`Completion`): `usage` de `input_tokens`/`output_tokens` nativos;
  `raw` com o objeto da SDK.
- **Erros**: 529 (overloaded) vira `OverloadedError` (subtipo de `ServerError`),
  elegível a retry/fallback. Veja [Erros](errors.md).

Relacionado: [Matriz de capacidades](capabilities.md),
[Structured output](structured-output.md).

---

# Parâmetros de geração e perfis por modelo

A jangada aceita **nomes canônicos** de parâmetros e cada adapter os traduz para
o nome nativo do SDK, descartando os não suportados.

## Parâmetros canônicos

| Canônico      | OpenAI/Groq            | Anthropic        | Gemini                |
|---------------|------------------------|------------------|-----------------------|
| `temperature` | `temperature`          | `temperature`    | `temperature`         |
| `max_tokens`  | `max_tokens`           | `max_tokens`     | `max_output_tokens`   |
| `top_p`       | `top_p`                | `top_p`          | `top_p`               |
| `top_k`       | *(descartado)*         | `top_k`          | `top_k`               |
| `stop`        | `stop`                 | `stop_sequences` | `stop_sequences`      |
| `seed`        | `seed`                 | *(descartado)*   | `seed`                |

Por padrão, `LLM` usa `max_tokens=8192` em todos os providers. Esse valor pode
ser alterado no construtor ou sobrescrito em uma chamada específica:

```python
llm = LLM("anthropic", "claude-opus-4-8")  # max_tokens=8192

# override por chamada
llm.complete("...", params={"temperature": 0.9, "max_tokens": 16384})

# clone com novos defaults
criativo = llm.with_params(temperature=1.0)
```

Parâmetros específicos de um SDK que não têm nome canônico vão via `extra=`.

## Perfis automáticos (quirks por modelo)

Modelos do mesmo provider às vezes têm contratos diferentes. A jangada normaliza
isso em `profiles.py`, **por modelo**, sem você precisar saber:

- `gpt-5` rejeita `temperature` (HTTP 400) e exige `max_completion_tokens`.
- `gemini-3.x` descarta `temperature`/`top_p`/`top_k`.
- **Thinking do Gemini**: você passa só `thinking_budget` (ou `thinking_level`) e
  a lib adapta — no 2.5 vira `thinking_budget`, no 3.x vira `thinking_level` —,
  empacotando no `thinking_config` nativo. Ver [Gemini](llm-gemini.md).

`thinking_budget`/`thinking_level` **não são** kwargs do construtor: um kwarg solto
cairia em `client_kwargs` (vai para o `genai.Client`, lugar errado). Passe por
`extra=` no construtor **ou** por `params=` na chamada:

```python
# no construtor (vale para todas as chamadas deste LLM)
llm = LLM("gemini", "gemini-3.5", extra={"thinking_level": "LOW"})

# por chamada (sobrescreve só nesta)
llm.complete("...", params={"thinking_budget": 1024})
```

Não misture as duas convenções no mesmo valor (ex.: `thinking_level=500` é erro —
`thinking_level` é enum `LOW`/`HIGH`; `500` seria um `thinking_budget`). É
justamente o que a lib evita: escolha uma e ela traduz para o que o modelo aceita.

Ordem aplicada no adapter: `_translate()` (canônico → nativo) →
`apply_profile()` (quirks de modelo) → empacota `thinking_config`. Ao suportar um
modelo novo com contrato diferente, adicione uma regra em `profiles.py` em vez de
espalhar `if`s.

Veja [Providers](providers.md) e [Estendendo](extending.md).

## Exemplo

[`examples/model_profiles_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/model_profiles_example.py) — script executável.

---

# Structured output (Pydantic)

Uma só chamada `parse()` devolve uma instância Pydantic validada, independente
de como cada provider implementa isso por baixo.

```python
from pydantic import BaseModel
from jangada_ai import LLM

class Pessoa(BaseModel):
    nome: str
    idade: int

llm = LLM("openai", "gpt-4o-mini")
comp = llm.parse("Extraia: João tem 30 anos.", Pessoa)
print(comp.parsed.nome, comp.parsed.idade)   # João 30
```

- `comp.parsed` → a instância Pydantic. **Confiável**: se algum SDK devolver
  `parsed=None` com JSON válido em `.text`, a jangada valida o texto pelo schema
  automaticamente (você não precisa fazer `model_validate_json` à mão).
- `comp.text` → o JSON bruto retornado.
- `comp.usage` / `comp.cost` → tokens e custo estimado.

> 🔁 **JSON fora do schema → failover.** Se a saída não casar com o modelo, a
> jangada levanta `errors.OutputValidationError` e **tenta o próximo modelo** do
> `with_fallback` (não repete o mesmo — daria o mesmo JSON). Assim o fallback
> cobre tanto erro de API quanto resposta malformada.

> `max_tokens` tem default de **8192**. Em listas especialmente grandes a resposta pode ser
> cortada; nesse caso a jangada levanta `errors.TruncatedError` ("aumente
> max_tokens") **antes** de validar — em vez de um erro confuso de JSON cortado do
> Pydantic. A correção é subir `max_tokens` (ver [Parâmetros](parameters.md)).

## Async

```python
comp = await llm.aparse("Extraia: ...", Pessoa)
```

## Como cada provider resolve

| Provider  | Mecanismo                                                |
|-----------|----------------------------------------------------------|
| OpenAI    | `chat.completions.parse(response_format=Modelo)`         |
| Groq      | `json_schema` quando o modelo suporta; senão **JSON Object mode** |
| Gemini    | `config.response_schema=Modelo` → `resp.parsed`          |
| Anthropic | tool-forcing (`tool_choice` fixo) → valida `tool_use`    |

Você não precisa saber qual é qual — `parse()`/`aparse()` cuidam disso. Use
Pydantic v2 (`model_json_schema()`, `model_validate`).

> **Groq — funciona em qualquer modelo.** Só alguns modelos do Groq aceitam
> `json_schema` (ex.: `openai/gpt-oss-*`, `llama-4-scout`). Para os demais (ex.:
> `llama-3.3-70b-versatile`), a jangada **cai automaticamente para JSON Object
> mode**: injeta o schema na instrução, pede um objeto JSON e valida com Pydantic.
> Você chama `parse()` igual — sem saber o que o modelo suporta.

Funciona junto com [Vision](vision.md) e [Documentos](documents.md): passe
`images=` ou `files=` na mesma chamada `parse()`.

## Exemplo

[`examples/structured_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/structured_example.py) — script executável.

---

# Tools (function calling)

O modelo pode pedir para chamar **ferramentas** (funções). A API é de **baixo
nível**: o `complete()` devolve as chamadas pedidas em `Completion.tool_calls`,
**você executa** e reenvia o resultado. Suportado em **OpenAI, Groq, Anthropic
e Gemini** — a mesma interface nos quatro.

```python
from jangada_ai import LLM, Message

def get_weather(city: str, units: str = "metric") -> str:
    """Retorna o clima atual de uma cidade."""
    return "25°C, ensolarado"

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

# 1) o modelo decide chamar a ferramenta
comp = llm.complete("Como está o tempo em Recife?", tools=[get_weather])

# 2) você executa cada chamada e monta os resultados
results = []
for call in comp.tool_calls:        # call.name, call.args (dict)
    saida = get_weather(**call.args)
    results.append(call.result(saida))

# 3) reenvia: histórico = pergunta + resposta-com-tool-calls + resultados
comp2 = llm.complete(
    "Como está o tempo em Recife?",
    history=[comp.assistant_message(), Message.tool_results(*results)],
    tools=[get_weather],
)
print(comp2.text)
```

## Definindo ferramentas

`tools=[...]` aceita:

- **função Python** — o schema sai da assinatura (type hints) + docstring;
- **modelo Pydantic** — vira o schema dos argumentos;
- **dict** `{"name", "description", "parameters"}` (JSON Schema) já pronto;
- um **`Tool`** (via `to_tool(...)`).

`tool_choice` controla a escolha: `"auto"` (padrão), `"none"`, `"required"`, ou
o **nome** de uma ferramenta para forçá-la.

## Peças

- `Completion.tool_calls`: lista de `ToolCall(id, name, args)`. Cada call também
  carrega um `metadata: dict` opaco — dados específicos do provider que a lib
  apenas preserva (não interpreta) e propaga de volta no histórico. É o que
  mantém, por exemplo, o `thought_signature` do Gemini 3.x vivo entre rodadas;
  você normalmente não precisa tocar nele.
- `comp.assistant_message()`: reconstrói a mensagem do assistant (texto + tool
  calls, incluindo o `metadata` de cada call) para o histórico.
- `call.result(saida)`: cria o `ToolResultPart` correspondente.
- `Message.tool_results(*parts)`: empacota os resultados numa mensagem.

## Ferramentas pré-prontas

A jangada traz tools prontas em `jangada_ai.prebuilt`:

```python
from jangada_ai.prebuilt import tavily_search   # busca na web (Tavily)

llm.complete("Qual a cotação do dólar hoje?", tools=[tavily_search])
# execute: tavily_search(**call.args)  (precisa de TAVILY_API_KEY no ambiente)
```

**Sem dependência e sem chave:**

| Tool | O que faz |
|------|-----------|
| `calculator` | avalia expressões aritméticas (seguro, via `ast`) |
| `current_datetime` | data/hora atuais (fuso IANA) |
| `fetch_url` | baixa uma página e devolve o texto legível |
| `wikipedia_search` | resumo da Wikipedia (sem chave) |
| `http_request` | requisição HTTP genérica (GET/POST/...) |

**Com chave (lê do ambiente, ou passe `api_key=`):**

| Tool | Chave |
|------|-------|
| `tavily_search` | `TAVILY_API_KEY` (ou `tavily_tool(api_key=...)`) |
| `brave_search` | `BRAVE_API_KEY` |
| `openweather` | `OPENWEATHER_API_KEY` |

> Parâmetros **keyword-only** (após `*`, como `api_key`/`timeout`) são config de
> runtime e **não** aparecem no schema que o modelo vê.

As tools que chamam API externa tratam **todos os casos de resposta**: rate
limit (429, respeitando `Retry-After`), auth (401/403), 404, 5xx, timeout/
conexão e corpo não-JSON. Erros transitórios (429/5xx/timeout) têm **retry leve
com backoff**; ao esgotar, a tool **devolve uma mensagem de erro como texto**
(em vez de levantar exceção), para o modelo decidir o que fazer.

Veja também [Structured output](structured-output.md) (que no Anthropic já usa
tool-forcing por baixo) e [Observabilidade](observability.md) (as tool calls
aparecem no trace).

## Exemplo

[`examples/tools_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/tools_example.py) — script executável.

---

# MCP (Model Context Protocol)

Conecte servidores MCP às chamadas via `mcp_servers=[...]`. **Todos os providers
suportam MCP**, mas de formas diferentes — e a jangada usa o jeito nativo de cada
SDK.

## Dois modelos (importante)

- **Remoto (URL)** — `MCPServer(url=...)`: o **provider** conecta no servidor MCP
  e executa as tools (server-side). Você não roda nada. → **Anthropic, OpenAI, Groq**.
- **Client-side (sessão)** — você passa uma `ClientSession` do pacote `mcp`: o
  **SDK** chama as tools localmente (automatic function calling). → **Gemini**.

| Provider | Modelo | Como | Observação |
|----------|--------|------|------------|
| Anthropic | remoto (URL) | Messages API (`mcp_servers` + `MCPToolset`, header beta) | **beta** (`mcp-client-2025-11-20`) |
| OpenAI | remoto (URL) | **Responses API** (`tools=[{type:"mcp"}]`) | usa a Responses API, não `chat.completions` |
| Groq | remoto (URL) | Responses API (compatível com a OpenAI) | beta — a jangada usa o **client OpenAI no `base_url` do Groq** por baixo (precisa do pacote `openai` instalado) |
| Gemini | client-side (sessão) | `tools=[session]` (automatic function calling) | **só no async** (`acomplete`) — a sessão é assíncrona |

## Remoto (Anthropic / OpenAI / Groq)

```python
from jangada_ai import LLM, MCPServer

llm = LLM("anthropic", "claude-opus-4-8")   # ou ("openai", "gpt-4o"), ("groq", ...)
comp = llm.complete(
    "Liste as issues abertas do repositório.",
    mcp_servers=[MCPServer(
        url="https://mcp.exemplo.com/sse",
        name="github",
        authorization_token="TOKEN",     # opcional (OAuth/Bearer)
        allowed_tools=["list_issues"],   # opcional (restringe as tools)
    )],
)
print(comp.text)   # o provider já executou as tools do MCP
```

## Client-side (Gemini, async)

A sessão MCP é assíncrona, então use **`acomplete`**:

```python
from mcp import ClientSession
from mcp.client.stdio import stdio_client, StdioServerParameters
from jangada_ai import LLM

llm = LLM("gemini", "gemini-2.5-flash")
params = StdioServerParameters(command="npx", args=["-y", "@exemplo/mcp"])

async with stdio_client(params) as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()
        comp = await llm.acomplete("Use a ferramenta X", mcp_servers=[session])
        print(comp.text)
```

> No Gemini, `complete()` (sync) com sessão levanta `UnsupportedError` pedindo
> `acomplete()`. Passar uma `MCPServer(url=...)` no Gemini também levanta — ele é
> client-side. E passar uma sessão no Anthropic/OpenAI/Groq levanta — eles são
> remotos por URL.

## Cliente MCP próprio + agente (portável, qualquer provider)

A jangada traz seu **próprio cliente MCP** (`MCPClient`) e um **loop de agente**
(`run_agent`) que conecta no servidor, lista as tools, e roda o ciclo
(modelo pede → executa → reenvia) **sozinho** — em **qualquer provider** (usa
`tools=`, suportado nos 4), independente do MCP nativo de cada SDK.

```bash
pip install "jangada-ai[mcp]"   # cliente MCP (pacote oficial `mcp`)
```

```python
from jangada_ai import LLM
from jangada_ai.mcp import MCPClient, run_agent

llm = LLM("openai", "gpt-4o-mini")   # ou anthropic/groq/gemini

async with MCPClient("https://meu-mcp/mcp/") as mcp:      # ou command=/args= (stdio)
    ans = await run_agent(llm, "Role uns dados", client=mcp)
    print(ans.text)
```

Por baixo: `await mcp.list_tools()` vira `tools=[...]`, e cada `tool_call` do
modelo é executado com `await mcp.call_tool(...)` e reenviado via
`Message.tool_results(...)` — o mesmo [tool calling](tools.md) de sempre, no
automático. Quer controle total? Use `MCPClient` + `tools=` na mão.

## Primitivos completos do MCP (no `MCPClient`)

Além de **tools**, o `MCPClient` cobre o resto do protocolo:

```python
async with MCPClient("https://meu-mcp/mcp/") as mcp:
    # Resources — dados/contexto que o server expõe
    recursos = await mcp.list_resources()
    texto    = await mcp.resource_text("file:///guia.md")

    # Prompts — templates reutilizáveis do server -> vira list[Message]
    msgs = await mcp.prompt_messages("revisar", {"texto": "..."})
    resp = await llm.acomplete(None, history=msgs)
```

E os **recursos de cliente** (o server chama o cliente de volta), configurados no
construtor:

```python
from jangada_ai import LLM

async with MCPClient(
    command="python", args=["server.py"],
    roots=["./workspace"],                       # escopo de filesystem (file://)
    sampling_llm=LLM("openai", "gpt-4o-mini"),   # o server pede geração ao SEU LLM
    elicitation_callback=meu_handler,            # o server pede input ao usuário
    logging_callback=meu_logger,                 # logs do server
) as mcp:
    ...
```

- **Roots**: o server pergunta quais diretórios pode usar; o cliente responde a lista.
- **Sampling**: o server pede uma geração de LLM (`sampling/createMessage`) e a
  jangada executa com o seu `LLM` — o server fica model-independent e **você**
  controla custo/permissões.
- **Elicitation / Logging**: você passa um callback (`async`) que o SDK chama.

| Primitivo | Métodos / config |
|---|---|
| Tools | `list_tools` / `call_tool` |
| Resources | `list_resources` / `read_resource` / `resource_text` |
| Prompts | `list_prompts` / `get_prompt` / `prompt_messages` |
| Roots | `roots=[...]` |
| Sampling | `sampling_llm=LLM(...)` |
| Elicitation | `elicitation_callback=...` |
| Logging | `logging_callback=...` / `set_logging_level(...)` |

## Ser um servidor MCP (expor suas tools/Agent)

Além de **consumir** MCP (acima), o jangada pode **ser** um servidor MCP — expor
suas funções/`Agent` para clientes (Claude Desktop, Cursor, outro agente)
chamarem. É construído sobre o `Server` **low-level** do SDK (o protocolo), não
sobre o FastMCP — e reaproveita o `tools.py` (o schema sai da assinatura da função).

```python
from jangada_ai import serve_mcp

def somar(a: float, b: float) -> float:
    "Soma dois números."
    return a + b

serve_mcp("minha-calc", tools=[somar])          # transporte stdio (Claude Desktop/Cursor)
```

Expor um **Agent** inteiro (vira a tool `ask`):

```python
from jangada_ai import Agent, LLM, serve_mcp
agente = Agent(LLM("openai", "gpt-4o-mini"), role="suporte", tools=[somar])
serve_mcp("suporte", agent=agente)
```

### Por HTTP (streamable-http)

```python
serve_mcp("minha-calc", tools=[somar], transport="streamable-http", port=8000)
```

Ou monte o app ASGI no seu servidor (FastAPI/Starlette):

```python
from jangada_ai import build_mcp_app
app = build_mcp_app("minha-calc", tools=[somar], path="/mcp")   # Starlette ASGI
```

Requer o extra `jangada-ai[mcp]`; para HTTP, também `starlette` + um servidor ASGI
(`uvicorn`). O **stdio** é o transporte usado por Claude Desktop/Cursor.

> `serve_mcp`/`build_mcp_app`/`build_mcp_server` = **lado servidor**. O `MCPClient`
> (acima) é o **lado cliente**. São complementares.

**Exemplo pronto:** [`jangada-docs-mcp`](https://github.com/nerigleston/jangada-docs-mcp)
é um servidor MCP (feito com `serve_mcp`) que expõe toda a documentação do jangada
ao seu editor — rode sem clonar: `uvx --from git+https://github.com/nerigleston/jangada-docs-mcp jangada-docs-mcp`.

## Exemplo

- [`examples/mcp_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/mcp_example.py) — cliente MCP.
- [`examples/mcp_server_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/mcp_server_example.py) — **servidor** MCP (stdio).

---

# Vision (imagens)

Imagens entram como `ImagePart` (bytes + mime) e são traduzidas para o formato
nativo de cada SDK. Use sempre um modelo com visão.

```python
from jangada_ai import LLM, Image

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

# por caminho
llm.complete("O que aparece aqui?", images=["foto.jpg"])

# por bytes ou base64
img = Image.from_bytes(upload_bytes, "image/png")   # ou Image.from_base64
recibo = llm.parse("Extraia o total.", Recibo, images=[img]).parsed

# rotular cada imagem: tuplas (rótulo, imagem) -> TextPart(rótulo) antes de cada uma
llm.parse("Compare.", Comparacao, images=[("frente", img), ("verso", img2)])
```

Para controle total da ordem (vários blocos texto/imagem, multi-turno), monte
`history=[Message("user", [TextPart(...), ImagePart(...), ...])]` com `prompt=None`.

## Tradução por provider

| Provider     | Formato nativo                          |
|--------------|-----------------------------------------|
| OpenAI/Groq  | `image_url` com data URI                |
| Anthropic    | bloco `image` / `source` base64         |
| Gemini       | `types.Part.from_bytes`                 |

Apenas bytes circulam (use `Image.from_path/bytes/base64`). Combine com
[Structured output](structured-output.md) passando `images=` no `parse()`.

## Vision x Documentos

Para **docx/pdf/csv/xlsx**, prefira [Documentos](documents.md): por padrão a
jangada extrai o texto localmente (mais barato, funciona em modelo sem visão) e
só usa vision quando você força `mode="vision"` ou quando o PDF é escaneado.

## Exemplo

[`examples/vision_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/vision_example.py) — script executável.

---

# Detecção de objetos

`detect_objects()` detecta objetos em uma imagem e devolve as **caixas
delimitadoras em pixels absolutos**. É vision + structured output, então
**funciona em qualquer provider com visão** — não é exclusivo do Gemini.

```python
from jangada_ai import LLM, detect_objects

llm = LLM("gemini", "gemini-2.5-flash")
dets = detect_objects(llm, "foto.png")

for d in dets:
    print(d.label, d.box)   # box = [x1, y1, x2, y2] em pixels
```

Cada `Detection` tem:

- `label` — nome do objeto.
- `box` — caixa em **pixels absolutos**, `[x1, y1, x2, y2]` (canto superior
  esquerdo e inferior direito), já convertida ao tamanho real da imagem.
- `box_2d` — caixa crua do modelo, `[ymin, xmin, ymax, xmax]` normalizada 0–1000.

## Parâmetros

```python
detect_objects(
    llm,
    image,                      # caminho, ImagePart ou bytes via Image.from_bytes
    target="todos os gatos",   # restringe o que procurar (opcional)
    max_objects=10,             # limita a quantidade (opcional)
    instructions="Ignore objetos desfocados; rotule em inglês.",  # ACRESCENTA ao prompt padrão
    prompt=None,                # sobrescreve a instrução inteira (opcional)
    image_size=(800, 600),      # informe se o formato não for detectável
)
```

- `instructions` **soma** ao prompt padrão — útil para dar contexto da cena,
  regras de rotulagem ou o que ignorar, sem perder o formato garantido.
- `prompt` **substitui** a instrução inteira (o schema ainda garante a saída
  JSON). Pode combinar os dois: `prompt=` define a base e `instructions=` agrega.

Versão async: `await adetect_objects(llm, image, ...)`.

## Funciona em todos os providers?

**Sim, mecanicamente.** A convenção `box_2d [ymin,xmin,ymax,xmax]` em escala
0–1000 é a do **Gemini** (instruída via prompt e validada por schema Pydantic),
então:

- **Gemini** — mais preciso (formato nativo de treino).
- **OpenAI (gpt-4o, ...)** — funciona bem.
- **Anthropic (Claude vision)** — detecta, mas a precisão das coordenadas varia.

Use sempre um modelo com visão. Veja também [Vision](vision.md) e
[Structured output](structured-output.md).

## Robustez

A leitura é **tolerante**: se o modelo localizar a chave (ex.: devolver `objetos`
em vez de `objects`) ou cortar um `box_2d` (≠ 4 números), o `detect_objects`
ainda extrai o que dá e **descarta as caixas inválidas** em vez de retornar vazio.

## Dimensões da imagem

As dimensões são lidas direto dos bytes (PNG, JPEG, GIF, BMP, WEBP) sem
dependência externa. Para outros formatos, passe `image_size=(largura, altura)`.

## Exemplo

[`examples/detect_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/detect_example.py) — script executável.

---

# Step-back prompting

`step_back()` gera, a partir de uma pergunta específica, **uma pergunta
conceitualmente mais ampla**. É uma técnica de transformação de query para RAG:
a pergunta mais genérica recupera documentos de **contexto amplo** (princípios,
categorias, fundamentos) que a busca pela pergunta original sozinha costuma
perder. O padrão é buscar com **as duas** e juntar os trechos.

```python
from jangada_ai import LLM, step_back

llm = LLM("openai", "gpt-4o-mini")
ampla = step_back(llm, "Quais são as opções de tratamento para catarata?")
print(ampla)
# "Quais são as abordagens cirúrgicas e farmacológicas para o manejo da
#  opacidade do cristalino?"
```

É só LLM + structured output, então **funciona em qualquer provider** — não
depende de visão nem do extra `[rag]`.

## Uso típico com RAG

Busque o contexto com a pergunta original **e** com a step-back, e una os
resultados antes de responder:

```python
from jangada_ai import LLM, step_back
from jangada_ai.rag import RAG, vector_store

llm = LLM("openai", "gpt-4o-mini")
emb = LLM("openai", "text-embedding-3-small")
rag = RAG(emb, vector_store("postgresql://..."), chat=llm)

pergunta = "Quais são as opções de tratamento para catarata?"
ampla = step_back(llm, pergunta)

especificos = rag.search(pergunta)
gerais = rag.search(ampla)
# combine `especificos` + `gerais` (deduplique) e responda com o contexto unido.
```

## Parâmetros

```python
step_back(
    llm,
    query,                          # a pergunta específica
    instructions="Foco em medicina; responda em pt-BR.",  # ACRESCENTA ao prompt padrão
    prompt=None,                    # sobrescreve a instrução inteira (opcional)
    params={"temperature": 0.2},   # params de geração (opcional)
)
```

- `instructions` **soma** ao prompt padrão — útil para fixar domínio, idioma ou
  o que enfatizar, sem perder o formato garantido pelo schema.
- `prompt` **substitui** a instrução inteira (o schema ainda garante a saída).
  Ao sobrescrever, inclua a pergunta no seu texto — o `{query}` só é interpolado
  no prompt padrão.

Versão async: `await astep_back(llm, query, ...)`.

## Robustez

A leitura é **tolerante**: usa o structured output quando vem válido, cai no
texto cru se o modelo ignorar o schema e, em último caso, devolve a própria
`query` — **nunca** retorna vazio.

## Exemplo

[`examples/stepback_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/stepback_example.py) — script executável.

Veja também [RAG](rag.md) e [Structured output](structured-output.md).

---

# Transcrição de áudio (speech-to-text)

`LLM.transcribe()` converte áudio em texto. **Não é suportado por todos os
providers** — depende de a API do provider aceitar áudio:

| Provider  | Suporta? | Como         | Modelos típicos                                  |
|-----------|----------|--------------|--------------------------------------------------|
| OpenAI    | ✅       | endpoint dedicado | `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, `whisper-1` |
| Groq      | ✅       | endpoint dedicado | `whisper-large-v3`, `whisper-large-v3-turbo`     |
| Gemini    | ✅       | multimodal (`generateContent`) | `gemini-2.5-flash`, `gemini-2.5-pro` |
| Anthropic | ❌       | —            | a API do Claude não aceita áudio                 |

> Tentar transcrever no Anthropic levanta `UnsupportedError`. O "voice" do
> Claude é recurso de produto (app/Claude Code), não da API do modelo.

## Uso

```python
from jangada_ai import LLM, Audio

# OpenAI
llm = LLM("openai", "gpt-4o-transcribe")
print(llm.transcribe("entrevista.mp3").text)

# Groq (mais rápido/barato)
llm = LLM("groq", "whisper-large-v3-turbo")
print(llm.transcribe("entrevista.mp3").text)

# Gemini (multimodal)
llm = LLM("gemini", "gemini-2.5-flash")
print(llm.transcribe("entrevista.mp3").text)
```

Entradas aceitas: caminho, `AudioPart` ou bytes via `Audio.from_bytes`:

```python
audio = Audio.from_bytes(blob, "audio/wav", name="fala.wav")
llm.transcribe(audio)
```

Async: `await llm.atranscribe(audio)`.

## Opções por provider

Os kwargs extras vão direto ao provider quando ele os aceita:

```python
# OpenAI/Groq aceitam language, prompt, response_format, temperature, ...
llm.transcribe("audio.mp3", language="pt", response_format="text")

# Gemini aceita prompt= como instrução (ex.: incluir timestamps)
llm.transcribe("audio.mp3", prompt="Transcreva com marcações de tempo.")
```

## Fallback entre providers de áudio

Como qualquer chamada da jangada, `transcribe()` honra retry e fallback:

```python
from jangada_ai import LLM

primario = LLM("groq", "whisper-large-v3-turbo")
reserva  = LLM("openai", "gpt-4o-transcribe")
stt = primario.with_fallback(reserva)

stt.transcribe("audio.mp3")   # tenta Groq; se falhar (5xx/timeout), vai pro OpenAI
```

> `UnsupportedError` **não** entra no failover padrão — é erro de configuração
> (provider sem áudio), não falha transitória. Veja [Erros](errors.md) e
> [Retry e fallback](retry-fallback.md).

## Formatos e limites

- OpenAI/Groq: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, webm — até ~25 MB.
- Gemini: áudio inline até ~20 MB no total do request (use a Files API do SDK
  para arquivos maiores).

## Exemplo

[`examples/transcribe_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/transcribe_example.py) — script executável.

---

# Documentos (docx, pdf, csv, xlsx)

Anexe arquivos a qualquer chamada com `files=`. Por padrão a jangada **extrai o
texto** do arquivo localmente em vez de usar vision — é mais barato e funciona
em qualquer modelo, inclusive os sem visão.

```bash
pip install "jangada-ai[files]"   # pypdf, python-docx, openpyxl
```

```python
from jangada_ai import LLM, Document

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

# caminhos: tipo detectado pela extensão
llm.complete("Resuma:", files=["relatorio.pdf", "contrato.docx"])

# xlsx: TODAS as abas entram, cada uma rotulada (## Aba: ...)
llm.complete("Maior total?", files=[Document("vendas.xlsx", max_rows=200)])

# bytes em memória (upload/fila) — informe o nome para detectar o tipo
llm.parse("Há duplicadas?", Relatorio, files=[Document(blob, name="x.csv")])

# forçar vision (PDF escaneado / quando o layout importa)
llm.complete("Transcreva:", files=[Document("scan.pdf", mode="vision")])
```

## Regra de `mode`

| `mode`     | Comportamento                                                     |
|------------|------------------------------------------------------------------|
| `"auto"`   | (padrão) extrai texto de csv/xlsx/docx/pdf-com-texto; imagem → vision |
| `"text"`   | força extração de texto (erro se o formato não tiver texto)      |
| `"vision"` | força o caminho de imagem                                        |

## Por que não usar vision para tudo

- **Mais barato**: texto custa muito menos que tokens de imagem.
- **Funciona em qualquer modelo**, inclusive sem visão.
- **Preserva tabelas** como markdown.

Um PDF **sem** camada de texto (escaneado) levanta `DocumentError` sugerindo
`mode="vision"` — nunca devolve um bloco vazio em silêncio.

## Detalhes

- `files=` existe em `complete/parse/stream` (sync e async) e convive com `images=`.
- A conversão acontece na fronteira do client (`files.py` → `to_part()`): cada
  arquivo vira `TextPart` ou `ImagePart`, então os adapters nunca veem formato
  de documento.
- Formatos: `.csv`, `.tsv`, `.xlsx`, `.xlsm`, `.docx`, `.pdf`, além de texto
  puro (`.txt`, `.md`, `.json`, ...).

Relacionado: [Vision](vision.md), [Structured output](structured-output.md).

## Exemplo

[`examples/files_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/files_example.py) — script executável.

---

# RAG (embeddings + busca vetorial/híbrida)

A jangada cobre as partes "de LLM" do RAG (embeddings + montar o contexto) e traz
um módulo `jangada_ai.rag` opcional com chunking, vector store (pgvector/Mongo)
e busca híbrida.

```bash
pip install "jangada-ai[rag]"   # psycopg (pgvector) + pymongo (Mongo)
```

## Embeddings (`embed`)

Capacidade opcional, igual ao áudio: **OpenAI e Gemini** suportam; **Anthropic e
Groq** levantam `UnsupportedError`.

```python
from jangada_ai import LLM

emb = LLM("openai", "text-embedding-3-small")   # ou ("gemini", "gemini-embedding-001")
emb.embed("uma frase")            # -> vetor (list[float])
emb.embed(["a", "b"])             # -> lista de vetores
emb.embed(textos, task="document")  # task: "document" ao indexar, "query" ao buscar
emb.embed(textos, batch_size=50)    # Gemini: textos por requisição (1..100, padrão 100)
```

`task` vira `task_type` no Gemini (`RETRIEVAL_DOCUMENT`/`RETRIEVAL_QUERY`); a
OpenAI ignora. `dimensions` (OpenAI) / `output_dimensionality` (Gemini) vão via `**opts`.

`batch_size` (1..100, padrão 100) controla quantos textos vão por requisição: a
API do Gemini aceita no máximo 100 por `BatchEmbedContents`, então listas maiores
são fatiadas nesse tamanho. Só o Gemini usa — OpenAI/Azure/OpenRouter não têm esse
limite e ignoram o parâmetro. Valores fora de 1..100 levantam `ValueError`.

| Provider | Embeddings? | Modelo típico |
|----------|:-----------:|---------------|
| OpenAI   | ✅ | `text-embedding-3-small` / `-large` |
| Gemini   | ✅ | `gemini-embedding-001` / `gemini-embedding-2` |
| Anthropic| ❌ | — (use Voyage/Cohere por fora) |
| Groq     | ❌ | — |

Com a observabilidade automática ativada, cada `embed()`/`aembed()` registra
latência, tokens, custo estimado, capability `embeddings` e o array completo dos
vetores em `output`. Embeddings executados dentro de
`observability_session(name="rag.documents.ingest")` entram no mesmo trace da
ingestão.

Se o Gemini não devolver tokens na resposta de embedding, a Jangada usa
`count_tokens()` com o mesmo lote. O custo combina essa contagem com o preço do
catálogo `jangada.dev.br/prices.json`; uma aproximação local só é usada se a
contagem do provider também falhar.

## Pipeline completo (`RAG`)

```python
from jangada_ai import LLM
from jangada_ai.rag import RAG, vector_store

emb  = LLM("openai", "text-embedding-3-small")
chat = LLM("openai", "gpt-4o-mini")

# o store é escolhido pela STRING DE CONEXÃO (só passe a sua DATABASE_URL_VECTOR)
store = vector_store("postgresql://user:senha@host:5432/db")   # ou "mongodb+srv://..."
rag = RAG(emb, store, chat=chat, k=5)   # k = nº de trechos no contexto (ajustável)

rag.add_document("manual.pdf", metadata={"fonte": "manual"})   # extrai -> chunk -> embed -> grava
resposta = rag.ask("Como faço backup?", mode="hybrid")          # usa o k do RAG
mais = rag.ask("Como faço backup?", k=10, mode="hybrid")        # override por chamada
print(resposta.text)
for s in resposta.sources:
    print(s.score, s.chunk.content[:80])
```

## Vector store por string de conexão

`vector_store(url)` detecta o adapter pelo esquema:

| URL | Adapter | Busca |
|-----|---------|-------|
| `postgresql://` / `postgres://` | pgvector (Postgres) | cosseno (`<=>`) + full-text (`tsvector`) |
| `mongodb://` / `mongodb+srv://` | MongoDB | Atlas `$vectorSearch` + `$text` (fallback cosseno client-side) |
| `memory` / `None` | em memória | cosseno + keyword (sem deps) |

As tabelas/coleções e índices são criados sozinhos no primeiro uso (`setup`).

### Busca textual em português (pgvector)

O `PgVectorStore` usa a configuração `'simple'` do Postgres por padrão (sem
stemming nem stopwords). Para conteúdo em **português**, passe
`text_config="portuguese"` — melhora a parte lexical da busca híbrida (stemming PT
+ stopwords):

```python
from jangada_ai.rag import vector_store

store = vector_store("postgresql://...", text_config="portuguese")
```

A config é fixada na **criação** da tabela (a coluna `tsv` é GENERATED), então
defina-a antes do primeiro `setup`. Vale qualquer `regconfig` do Postgres
(`english`, `spanish`, ...).

## Modos de busca

`mode="vector" | "text" | "hybrid"`:

- **vector** — só similaridade do embedding.
- **text** — lexical: **BM25** no store em memória (com `rank_bm25`; cai para
  contagem de termos sem ele) e full-text nativo no pgvector (`tsvector`) / Mongo (`$text`).
- **hybrid** — combina os dois por **Reciprocal Rank Fusion (RRF)**.

O balanço vetorial × lexical sai de `weights=(vetorial, texto)` ou do atalho
**`alpha`** (0 = só BM25/texto, 1 = só vetorial; `alpha` vira `weights=(alpha, 1-alpha)`):

```python
RAG(emb, store, chat=chat, alpha=0.5)   # equilíbrio; 0.0 = só BM25, 1.0 = só vetorial
```

```python
rag.search("backup incremental", k=5, mode="vector")   # só vetorial
rag.search("backup incremental", k=5, mode="hybrid")   # vetorial + texto (RRF)
```

## Reranking (maior salto de qualidade)

O retriever traz candidatos (bom *recall*), mas a ordem nem sempre é a melhor.
Um **reranker** — modelo treinado em "este trecho responde a esta pergunta?" —
reordena os candidatos e fica com os melhores. Em RAG é o maior ganho de
qualidade por esforço.

Com `reranker=`, o `RAG` busca **mais** candidatos (`fetch_k`, padrão `k*4`) e
reordena, devolvendo os `k` melhores:

```python
from jangada_ai.rag import RAG, Reranker, vector_store

rag = RAG(emb, vector_store("memory"), chat=chat, reranker=Reranker.cohere())
rag.ask("Como faço backup incremental?")   # busca k*4 e reordena para os k melhores
```

Construtores do `Reranker`:

- `Reranker.cohere(model="rerank-v3.5")` — Cohere Rerank (precisa de `cohere` e
  `COHERE_API_KEY`).
- `Reranker.voyage(model="rerank-2.5")` — Voyage Rerank (precisa de `voyageai` e
  `VOYAGE_API_KEY`).
- `Reranker.fn(lambda query, docs: [scores])` — qualquer scorer (ex.: um
  cross-encoder local ou um LLM seu).

Extra opcional: `pip install "jangada-ai[rerank]"`. Controle fino:
`RAG(..., reranker=..., fetch_k=40)`; por chamada, `rag.search(q, rerank=False)`
desliga e `rerank=OutroReranker` sobrescreve.

## Parâmetros ajustáveis

Definidos no `RAG(...)` (padrão) e/ou por chamada:

```python
rag = RAG(
    emb, store, chat=chat,
    k=5,                      # nº de trechos no contexto
    min_score=0.25,           # descarta trechos abaixo dessa similaridade
    max_context_chars=6000,   # orçamento do contexto (trunca o excedente)
    chunker=meu_chunker,      # função(text)->list[str] (troca o chunking padrão)
    rrf_k=60,                 # constante do RRF (híbrido)
    weights=(1.0, 0.5),       # pesos (vetorial, texto) no híbrido
    chunk_size=1000, overlap=200,
)

# filtro por metadata (escopar por documento/fonte/tenant) + override por chamada
rag.ask("backup?", k=8, filter={"fonte": "manual"}, min_score=0.3, mode="hybrid")
rag.search("backup?", filter={"tenant": "acme"}, mode="vector")
```

- **`filter`** vira `metadata @> ...` no pgvector e `$match` em `metadata.<chave>` no Mongo.
- **`min_score`** usa a similaridade real (cosseno no vetorial, rank no texto).
- **`max_context_chars`** corta os trechos que não couberem (mantém ao menos um).
- **`weights=(v, t)`** pondera os rankings vetorial e de texto na fusão RRF.

## Estratégias avançadas de retrieval (opt-in)

Passe `strategy=` no `search`/`ask`. Padrão: busca simples (continua igual).

- **multi-query** — o LLM gera variações da pergunta, busca todas e funde por RRF
  (pega sinônimos/ângulos). `rag.ask(q, strategy="multi_query")`.
- **parent-document** — indexe filhos pequenos (precisos) guardando o trecho-pai
  grande; a busca devolve o pai (melhor contexto):
  ```python
  rag.add_document("manual.pdf", parent_chunk_size=4000)   # filhos + pai
  rag.ask("Como faço X?", strategy="parent_document")
  ```
- **contextual compression** — após buscar, o LLM extrai de cada trecho só o
  relevante (encolhe o contexto/custo): `rag.ask(q, compress=True)`.

Combinam entre si e com o `reranker=`.

## Indexação incremental

`sync_document`/`sync_texts` reindexam **só o que mudou** (dedup por hash do
conteúdo): embeda os chunks novos e remove os que sumiram. Requer `document_id`
e um store com suporte: memória ou PostgreSQL/pgvector. MongoDB ainda não oferece
as primitivas incrementais.

```python
rag.sync_document("manual.pdf", name="manual.pdf", document_id="manual")
# {'added': 3, 'removed': 1, 'unchanged': 42}  -> só os 3 novos foram embedados
```

No pgvector, a primeira sincronização migra a tabela de forma idempotente:
adiciona/backfill a coluna `hash`, remove duplicatas exatas por
`(document_id, hash)` e cria um índice único parcial. Linhas antigas sem
`document_id` são preservadas. Cada atualização roda numa transação protegida por
advisory lock do `document_id`, portanto sincronizações concorrentes não misturam
versões. Mudanças apenas de metadata/posição são persistidas sem reembedding; se
extração, chunking, embedding ou gravação falhar, a versão anterior continua
disponível.

## Qual recurso usar? (resumo)

Todos são **opt-in** — a lib funciona sem nenhum. Some-os conforme a necessidade:

| Quero… | Use | Custo extra |
|--------|-----|-------------|
| Melhorar muito a ordem dos trechos | `reranker=Reranker.cohere()` | 1 chamada de rerank |
| Pegar sinônimos/perguntas vagas | `strategy="multi_query"` | N buscas + 1 LLM |
| Contexto melhor sem perder precisão | `parent_chunk_size=` + `strategy="parent_document"` | — |
| Reduzir tokens de contexto | `compress=True` | 1 LLM por trecho |
| Chunks que não cortam ideias | `chunker=semantic_chunker(emb)` | embeddings no índice |
| Reindexar barato (só o que mudou) | `sync_document(...)` | — |

Receita recomendada para produção: **semantic chunking** (índice) +
**reranker** (busca). Acrescente **multi-query** se as perguntas forem vagas.

Exemplo combinando tudo:

```python
from jangada_ai import LLM
from jangada_ai.rag import RAG, Reranker, semantic_chunker, vector_store

emb = LLM("openai", "text-embedding-3-small")
rag = RAG(
    emb, vector_store("postgresql://..."), chat=LLM("openai", "gpt-4o-mini"),
    chunker=semantic_chunker(emb),     # índice por assunto
    reranker=Reranker.cohere(),        # ordem por relevância
)
rag.sync_document("manual.pdf", name="manual")          # incremental
ans = rag.ask("Como faço backup?", strategy="multi_query", compress=True)
```

POC executável com todos os recursos lado a lado:
[`pocs/rag-advanced-poc`](https://github.com/nerigleston/jangada).

## Chunking

```python
from jangada_ai.rag import chunk_text, semantic_chunker
chunk_text(texto, size=1000, overlap=200)        # padrão: tamanho fixo, sem cortar palavras

# semantic chunking (opt-in): quebra por similaridade, não por tamanho
RAG(emb, store, chunker=semantic_chunker(emb))   # frases do mesmo assunto ficam juntas
```

Relacionado: [Documentos](documents.md) (extração de texto reaproveitada no RAG)
e [Observabilidade](observability.md).

## Exemplo

[`examples/rag_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/rag_example.py) — script executável.

---

# Streaming

Receba tokens incrementais com `stream()` (sync) ou `astream()` (async).

```python
for token in llm.stream("Conte sobre {{x}}", x="João Pessoa"):
    print(token, end="")
```

```python
async for token in llm.astream("..."):   # ex.: FastAPI StreamingResponse
    ...
```

## Retry e fallback no streaming

O retry e o fallback acontecem **antes do primeiro token**: se a abertura do
stream falhar com erro transitório, a jangada tenta de novo (backoff) e, se
preciso, cai para o próximo candidato — tudo antes de você receber qualquer
conteúdo. Depois que o primeiro token sai, o stream segue até o fim.

Veja [Retry e fallback](retry-fallback.md) para a política completa.

## Observações

- `stream()`/`astream()` aceitam os mesmos `system=`, `history=`, `images=`,
  `files=` e `params=` das outras chamadas.
- Para custo e tokens use as chamadas não-stream (`complete`/`parse`), que
  retornam `usage`/`cost` na resposta — veja [Custo e tokens](cost.md).

## Exemplo

[`examples/async_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/async_example.py) — script executável.

---

# Retry e fallback

A jangada combina duas defesas contra falhas de API: **retry com backoff** no
mesmo candidato e **fallback** para outro modelo/provider.

```python
from jangada_ai import LLM

primario = LLM("openai", "gpt-4o-mini")
reserva  = LLM("anthropic", "claude-haiku-4-5-20251001")

llm = primario.with_fallback(reserva)
llm.complete("...")   # tenta o primário (com retries); se falhar, vai pro reserva
```

## Como a ordem funciona

Por candidato, o cliente tenta `max_retries + 1` vezes com backoff exponencial
(com jitter) antes de cair para o próximo candidato:

```
[primário] tenta → retry → retry → falhou ─▶ [reserva] tenta → retry → ...
```

- **Retry** acontece em erros transitórios (`backoff_on`, padrão
  `errors.TRANSIENT`: rate limit, timeout, conexão, 5xx).
- **Fallback** acontece nos erros de `retry_on` (padrão `DEFAULT_FAILOVER`:
  rate limit, timeout, conexão, 5xx, **404**).
- `NotFoundError` (404) **não** repete no mesmo candidato, mas **faz** fallback.
- `auth` e `bad_request` **não** entram no failover padrão — falham de imediato.

## Parâmetros

```python
LLM(
    "openai", "gpt-4o-mini",
    max_retries=2,          # tentativas extras por candidato
    backoff_base=0.5,       # segundos
    backoff_max=8.0,
    jitter=True,
    retry_on=None,          # default: errors.DEFAULT_FAILOVER
    backoff_on=None,        # default: errors.TRANSIENT
    fallbacks=[reserva],
)
```

Os erros são normalizados (com `status_code`) — veja [Erros](errors.md). Para o
custo agregado entre candidatos, veja [Custo e tokens](cost.md).

## Exemplo

[`examples/fallback_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/fallback_example.py) — script executável.

---

# Custo e tokens

Toda resposta bem-sucedida volta com `usage` (tokens) e `cost` (USD estimado).

```python
comp = llm.complete("...")
print(comp.usage)   # {"input_tokens": ..., "output_tokens": ...}
print(comp.cost)    # ex.: 0.000123  (USD, aproximado)
```

O cliente chama `pricing.compute_cost()` após cada sucesso e seta
`Completion.cost`. `FlowResult` e `GraphResult` **agregam** `usage`/`cost` ao
longo da cadeia.

## Tabela de preços

Os preços vêm de um catálogo JSON (aproximados, por 1M de tokens). Você pode
registrar/ajustar em runtime:

```python
from jangada_ai import register_price, price_for

register_price("meu-modelo", 0.5, 1.5)   # USD por 1M tokens (input, output)
print(price_for("gpt-4o-mini"))
```

> ⚠️ Os valores são **aproximados** e servem para estimativa/observabilidade —
> não trate como fonte de billing.

## Preços sempre atualizados (sem atualizar a lib)

Os preços moram num **catálogo JSON** (não mais hardcoded). A cópia embutida no
pacote (`prices.json`) é só o **fallback offline**; como preços de API mudam toda
hora, a jangada publica o catálogo em `jangada.dev.br/prices.json` e o aplica
**sozinha, sem você escrever nada** — e **sem precisar dar `pip install -U`**.

**Automático (padrão).** Na primeira vez que um custo é calculado (logo abaixo de
cada chamada de provider, em `compute_cost`), a lib dispara **em background** um
refresh do catálogo, cacheado por **1 dia**. Você só usa o LLM normalmente:

```python
llm = jangada_ai.LLM(provider="openai", model="gpt-4o-mini")
comp = llm.complete("...")
print(comp.cost)   # já tende a usar os preços do dia (refresh roda em background)
```

- **Não bloqueia**: a busca roda numa thread daemon; o `import` nunca toca a rede e
  a chamada não trava. As primeiras chamadas usam o embutido até o refresh terminar.
- **No máximo 1x/dia**: cache em `~/.cache/jangada/prices.json` (e 1x por processo).
- **Resiliente**: rede falhou? fica no cache/embutido — nunca levanta.
- **Desligar**: env `JANGADA_NO_PRICE_REFRESH=1`.

**Manual (`refresh_prices`).** Para controle explícito/síncrono — garantir preços
frescos no boot, forçar agora, ou apontar outra URL:

```python
jangada_ai.refresh_prices()                          # síncrono, respeita o cache
jangada_ai.refresh_prices(ttl=3600, force=True)      # revalida/ força agora
jangada_ai.refresh_prices(url="https://meu-espelho/prices.json")
```

Default da URL: `https://jangada.dev.br/prices.json` (override por `url=` ou env
`JANGADA_PRICES_URL`; não precisa de `.env`).

O override manual (`register_price`) tem prioridade sobre tudo — use para fixar um
número exato de um modelo específico.

## Custo multimodal

- **Imagem (vision)**: não há preço separado — os providers já contam os tokens da
  imagem dentro de `input_tokens`. O custo da imagem já sai pela tabela normal.
- **Áudio (transcrição)**: cobrado **por minuto**, não por token. O custo só sai
  quando o `usage` traz a duração (`audio_seconds`): passe
  `response_format="verbose_json"` na transcrição **ou** informe a duração em
  `Audio.from_bytes(dados, mime, duration=...)`. Registre/ajuste com
  `register_audio_price("whisper-1", 0.006)` (USD por minuto).
- **Detecção**: `detect_objects`/`adetect_objects` devolvem só `list[Detection]`
  (sem custo). Para o custo, use `detect_objects_full`/`adetect_objects_full`, que
  devolvem um `DetectionResult` com `.detections` **e** `.completion`/`.cost`/`.usage`.

## Onde isso aparece

- `Completion.cost` / `Completion.usage` em cada chamada.
- Totais agregados em [Fluxos e Graph](flows.md).
- No [Debug passo a passo](debug.md), o custo de cada etapa é exibido no trace.

## Exemplo

[`examples/retry_cost_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/retry_cost_example.py) — script executável.

[`examples/pricing_refresh_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/pricing_refresh_example.py) — preços dinâmicos com `refresh_prices()`.

---

# Cache de respostas

A jangada pode **cachear respostas do LLM** para economizar tokens e latência —
plugado via `LLM(..., cache=...)`. O cliente consulta o cache **antes** de chamar
o provider e o popula **depois** de uma resposta bem-sucedida. Dois modos:
**exato** e **semântico**.

## Cache exato

Acerta só quando a requisição é **idêntica** (mesmo método, escopo
`provider`/`model`/`params` e mensagens). LRU com `max_size` e `ttl` opcionais,
sem custo de embedding.

```python
from jangada_ai import LLM, ExactCache

llm = LLM("openai", "gpt-4o-mini", cache=ExactCache(max_size=512, ttl=3600))
llm.complete("Resuma a teoria da relatividade.")   # chama o provider
llm.complete("Resuma a teoria da relatividade.")   # vem do cache (idêntico)
```

## Cache semântico

Acerta quando a pergunta é **suficientemente parecida** com uma anterior do mesmo
escopo (similaridade de cosseno ≥ `threshold`). Reusa `LLM.embed` + um
`vector_store` do RAG. Após o acerto semântico, aplica um **filtro exato de
escopo** (provider/model/params/método) para alta precisão.

```python
from jangada_ai import LLM, SemanticCache

embedder = LLM("openai", "text-embedding-3-small")
cache = SemanticCache(embedder, threshold=0.85)

llm = LLM("openai", "gpt-4o-mini", cache=cache)
llm.complete("Qual a capital da França?")
llm.complete("Me diga a capital francesa.")   # paráfrase → acerta o cache
```

### Calibre o `threshold` por modelo de embedding

A escala de cosseno **varia muito entre modelos** — não existe um número mágico:

| Modelo de embedding | Paráfrase (≈) | Pergunta distinta (≈) |
|---|---|---|
| `text-embedding-3-small` (OpenAI) | 0.62 | 0.11 |
| `gemini-embedding-001` (Gemini) | 0.90 | 0.49 |

O padrão é `0.85` (meio-termo). **Meça paráfrases vs. perguntas distintas no seu
modelo** e escolha um corte entre as duas distribuições. Threshold alto demais =
cache morto; baixo demais = respostas erradas (falso-positivo). Cache semântico
sem avaliação de falso-positivos é um tiro no pé — amostre alguns acertos e
confira.

## O que **não** é cacheado

- Chamadas com **tools** ou **MCP** (`tools=`/`mcp_servers=`): as tool-calls
  variam e têm efeito colateral.
- **Streaming** (`stream`/`astream`).
- Respostas vindas de **fallback** (só o candidato primário popula o cache).
- Recusas de **guardrail** (o cache fica depois do guard de saída).

`complete`/`parse` e suas versões async compartilham a mesma chave (a `Completion`
cacheada preserva `parsed`, `usage` e `cost`). O cache é **local ao processo** (a
`Completion` é mantida em memória; o vector store é usado só para a similaridade).

---

# 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)

```bash
# .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
```

```python
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. Isso inclui os embeddings
gerados por RAG, cache semântico e indexação em lote:

```python
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, a capability
`embeddings` e o array completo de vetores em `output`. O output observado mantém
sempre o formato de lote (`[[...], [...]]`), inclusive quando `embed()` recebe uma
única string. O retorno público não muda: continua sendo um vetor para `str` ou uma
lista de vetores para uma lista de textos.

No Gemini, a contagem segue esta ordem: `usage_metadata` da própria resposta;
`count_tokens()` com o mesmo modelo e lote quando o usage estiver ausente; e,
somente se ambos falharem, estimativa local de aproximadamente 4 caracteres por
token. O preço nunca fica no adapter: `compute_cost()` resolve o modelo pelo
catálogo atualizado de `jangada.dev.br/prices.json`. As observations indicam a
origem em `usageSource` (`provider`, `count_tokens` ou `estimated`) e marcam
`usageEstimated=true` apenas no último fallback.

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()`, `signal` que não passa
pelo `atexit`, ou um `sys.exit()` dentro de contexto que ignora handlers): aí,
chame o flush manualmente antes de sair.

```python
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. Para agrupar várias chamadas de
uma request no **mesmo lote** (mesmo sendo enviadas uma a uma), abra um escopo
com `observability_session`: um id de lote é gerado e todas as chamadas de dentro
o compartilham — o backend as agrupa no mesmo trace.

```python
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()`. Use o **id do lote** devolvido por `observability_session`. É
**best-effort** (nunca derruba a app): devolve `True` se enviou, `False` se faltou
chave ou deu erro de rede.

```python
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 a resposta:
feedback(trace_id, 1, comment="resolveu meu problema")   # 👍
# feedback(trace_id, -1, comment="resposta errada")      # 👎
```

O feedback 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
(pelo dashboard) e virar caso de regressão nas [evals](eval.md).

## 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).

Cada observation tem um **status**: `OK`, ou **`INCOMPLETE`** quando a resposta
foi cortada por limite de tokens (`finish_reason == "length"`). O motivo de parada
normalizado também é enviado em **`finishReason`**. No dashboard isso vira um badge
(âmbar para incompleto) 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. Assim, um
`observability_session` que só contenha ingestão RAG deixa de ser um escopo vazio
e aparece normalmente no dashboard.

## No dashboard

Em [app.jangada.dev.br](https://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](cost.md).

---

# Erros normalizados

Cada SDK levanta exceções diferentes. A jangada traduz tudo para uma hierarquia
única via `errors.classify()`, com `status_code` quando disponível. Nenhum erro
nativo de SDK escapa da fronteira dos adapters.

```python
from jangada_ai import LLM, errors

try:
    LLM("openai", "modelo-inexistente").complete("oi")
except errors.NotFoundError as e:
    print(e.status_code)   # 404
except errors.LLMError as e:
    print("falha genérica:", e)
```

## Hierarquia (resumo)

Todas herdam de `errors.LLMError`. As principais categorias:

| Erro                 | Origem típica                      | Failover padrão? |
|----------------------|------------------------------------|------------------|
| `RateLimitError`     | 429                                | sim              |
| `TimeoutError`       | timeout de rede                    | sim              |
| `ConnectionError`    | falha de conexão                   | sim              |
| `ServerError`        | 5xx                                | sim              |
| `NotFoundError`      | 404 (modelo/endpoint)              | sim (sem retry)  |
| `OutputValidationError` | `parse()` com JSON fora do schema | sim (sem retry)  |
| `AuthError`          | 401/403                            | **não**          |
| `BadRequestError`    | 400 (params inválidos)             | **não**          |
| `TruncatedError`     | resposta cortada (`max_tokens`)    | **não** (suba `max_tokens`) |

## Conjuntos usados pela política

- `errors.TRANSIENT` — o que dispara **retry com backoff** (rate limit, timeout,
  conexão, 5xx).
- `errors.DEFAULT_FAILOVER` — o que dispara **fallback** (os transitórios + 404 +
  `OutputValidationError`). Saída fora do schema tenta o **próximo modelo** (sem
  repetir o mesmo, que daria o mesmo JSON).
  Não inclui `auth` nem `bad_request` por padrão.

Você pode customizar `retry_on=` e `backoff_on=` por `LLM` — veja
[Retry e fallback](retry-fallback.md).

---

# Fluxos e orquestração (Flow e Graph)

A jangada traz duas formas de encadear chamadas, ambas agregando `usage`/`cost`.

## Flow — sequencial

`Flow` encadeia `Step`s: a saída de um vira entrada do próximo.

```python
from jangada_ai import LLM, Flow, Step

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

flow = Flow([
    Step("rascunho", "Escreva um parágrafo sobre {{tema}}."),
    Step("revisao",  "Revise e melhore:\n{{rascunho}}"),
])

resultado = flow.run(llm, tema="jangadas do Nordeste")
print(resultado.output)     # saída do último step
print(resultado.cost)       # custo agregado de toda a cadeia
```

Cada `Step` referencia as saídas anteriores pelo nome via template `{{ }}`.

## Graph — roteamento condicional + paralelo

`Graph` permite ramificar (roteamento condicional) e executar nós em paralelo
(core async), juntando os resultados.

```python
from jangada_ai import Graph

# roteamento condicional: escolhe o próximo nó conforme a saída
# paralelo + junção: dispara vários nós e combina as respostas
g = Graph()
# ... defina nós, arestas condicionais e junções ...
res = g.run(...)        # GraphResult agrega usage/cost
```

Veja os exemplos executáveis em
[`examples/graph_example.py`](https://github.com/nerigleston/jangada/blob/master/examples/graph_example.py).

### Fan-out robusto e observável (`parallel`)

`parallel` aceita opções para produção:

```python
g.parallel(
    "analises", branches, join="sintese",
    on_error="skip",        # um ramo que falha não derruba o run (default: "raise")
    max_concurrency=4,      # teto de ramos simultâneos (evita rate limit)
    summarize=True,         # cada ramo resume a própria saída antes do join
)
```

- **`on_error="skip"`**: o ramo que falhar fica ausente do join (string vazia no
  contexto) e vai para `GraphResult.failures` (`{ramo: erro}`). O fan-out entrega o
  parcial em vez de perder o trabalho já pago dos outros ramos. Com `"raise"`
  (padrão) a primeira exceção derruba o run.
- **`max_concurrency=N`**: semáforo — no máximo N ramos rodam ao mesmo tempo.
- **`summarize`**: `True` (instrução padrão) ou uma instrução `str`. Corta o input
  do join (que lê todos os ramos e cresce O(N)); a chamada de resumo entra no
  `usage`/`cost`.

`GraphResult` também expõe **`durations`** (`{nó/ramo: segundos}`) — dá para medir
`max(ramos)` vs `t_join` diretamente, o diagnóstico que importa num fan-out.

## Custo agregado

`FlowResult` e `GraphResult` somam `usage` e `cost` de todas as etapas — útil
para observabilidade. Detalhes em [Custo e tokens](cost.md) e, para inspecionar
passo a passo, [Debug](debug.md).

## Exemplo

[`examples/graph_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/graph_example.py) — script executável.

---

# Agentes e times

A jangada traz uma camada leve de **orquestração multi-agente** — `Agent` e
`Squad` — construída sobre o que já existe (tool calling, MCP, RAG). Sem
dependência nova: é Python puro compondo a própria lib.

## Agent — um agente com papel e ferramentas

Um `Agent` é um LLM com **papel/objetivo**, opcionalmente com **tools** (funções
que ele executa) e **memória**. Ele roda o loop de tool calling sozinho até a
resposta final.

```python
from jangada_ai import LLM, Agent

def clima(cidade: str) -> str:
    "Retorna o clima de uma cidade."
    return f"ensolarado em {cidade}, 28°C"

meteoro = Agent(
    LLM("openai", "gpt-4o-mini"),
    role="Meteorologista",
    goal="informar o clima de forma clara",
    tools=[clima],
)

res = meteoro.run("Como está o clima em Recife?")
print(res.text)          # o modelo chamou clima("Recife") e respondeu
print(res.cost, res.usage, res.iterations)
```

- `tools=` são **callables** — a função é executada localmente quando o modelo a
  chama, e o resultado volta pro modelo. Podem ser **síncronas** (`def`) ou
  **assíncronas** (`async def`): tools `async def` são aguardadas no laço de
  `arun`. No `run` (síncrono) só use tools síncronas — uma tool async vira um
  resultado de erro orientando a usar `arun`.
- Para um servidor **MCP**, passe `mcp_client=MCPClient(...)` e use **`arun`**
  (async): o agente lista as tools do servidor e as usa junto das suas.

```python
async with MCPClient("https://seu-mcp/mcp/") as mcp:
    agente = Agent(llm, role="Operador", mcp_client=mcp)
    print((await agente.arun("Liste os produtos")).text)
```

## Conversa multi-turno (`history=`)

`run`/`arun` aceitam um `history` de turnos anteriores (`list[Message]`) — eles
entram **antes** da nova tarefa, dando continuidade fiel ao diálogo. O
`AgentResult.messages` devolve o histórico completo daquele turno, que você pode
**persistir** (ex.: numa tabela por `conversation_id`) e reinjetar no próximo:

```python
from jangada_ai.message import Message

historico = [
    Message("user", "Quanto gastei em maio?"),
    Message("assistant", "R$ 3.200 em maio."),
]
res = await agente.arun("E no mês anterior?", history=historico)
# guarde res.messages (ou só os pares user/assistant) para o próximo turno
```

> Diferente de um *checkpointer* que serializa o estado inteiro do grafo, aqui o
> histórico é **explícito**: você decide o que persistir e reinjetar. Para
> recall semântico (e não turn-by-turn), use `RAGMemory` (abaixo) — os dois
> compõem.

## Streaming da resposta (`astream`)

`astream` emite a **resposta final** token-a-token. As tool-calls (function tools
e MCP) são resolvidas internamente antes — o protocolo de stream não expõe
`tool_calls`, então não há streaming *durante* a fase de ferramentas; quando o
agente chega à resposta final, ela sai incremental. Sem tools, streama direto.

```python
async for token in agente.astream("Resuma meus gastos do mês"):
    print(token, end="", flush=True)
```

## Agent Card (descoberta / A2A)

`card()` devolve metadados descobríveis do agente no vocabulário do **Agent Card**
do protocolo [A2A](https://a2a-protocol.org) (`name`, `description`, `version`,
`url`, `capabilities`, `skills`):

```python
sofia = Agent(llm, role="Sofia", goal="assistente financeira", tools=[buscar])
sofia.card(url="https://app.exemplo/agents/sofia", version="1.0.0")
# {"name": "Sofia", "capabilities": {"streaming": True, ...},
#  "skills": [{"id": "buscar", "description": "...", "parameters": {...}}], ...}
```

## Servidor A2A (transporte HTTP/JSON-RPC) — `jangada[a2a]`

O extra `jangada[a2a]` expõe um (ou vários) `Agent` como um **servidor A2A** de
verdade, falando o binding JSON-RPC do protocolo: descoberta do Agent Card,
`message/send` (síncrono) e `message/stream` (SSE), com **continuidade por
`contextId`** (o histórico de cada conversa é mantido e reinjetado).

```python
from jangada_ai import LLM, Agent
from jangada_ai.a2a import A2AHandler, build_a2a_app

sofia = Agent(LLM("openai", "gpt-4o-mini"), role="Sofia", goal="finanças", tools=[buscar])
app = build_a2a_app(A2AHandler(sofia, url="https://app.exemplo/a2a"))
# app é um ASGI Starlette — sirva com uvicorn:  uvicorn modulo:app
```

Rotas servidas:

| Rota | O quê |
|------|-------|
| `GET /.well-known/agent.json` | Agent Card do agente principal |
| `GET /agents` | catálogo (lista de Agent Cards) |
| `POST /` | JSON-RPC `message/send` (JSON), `message/stream` (SSE), `tasks/get`, `tasks/cancel` |
| `POST /agents/{name}` | idem, para um agente específico do catálogo |

Para um time descobrível, passe uma lista: `build_a2a_app([sofia_h, orcamento_h])`.
A **lógica do protocolo** vive em `A2AHandler` (Python puro, testável sem rede); o
servidor ASGI importa Starlette só aqui. O histórico fica em memória por padrão —
passe `A2AHandler(agent, store=meu_dict)` para plugar persistência.

### Multi-tenant: agente resolvido por request

Em vez de um agente fixo, passe um **`resolver`** que recebe o **contexto da
requisição** (headers/auth) e devolve o `Agent` certo para aquele request — útil
quando o agente é montado por tenant (ex.: tools com closure no `tenant_id` do
JWT). Com `tenant_key`, o histórico fica **isolado por tenant**.

```python
def resolver(ctx):                       # ctx = {"headers": {...}, "auth": "Bearer ..."}
    tenant = (ctx.get("auth") or "").removeprefix("Bearer ")
    return build_sofia(tenant_id=tenant)

handler = A2AHandler(resolver=resolver, name="Sofia",
                     tenant_key=lambda c: c.get("auth", ""))
app = build_a2a_app(handler)             # extrai o contexto do request por padrão
```

Tudo é opcional (o modo fixo `A2AHandler(agent)` segue igual). Os parâmetros:
`resolver` (exclui `agent`), `name` (obrigatório com `resolver`), `description`,
`tenant_key` (isola histórico), e `context_factory=` no `build_a2a_app` (como
montar o contexto a partir do `Request` — troque para validar/decodificar o JWT).
A lib **não** decodifica JWT: o contexto traz os headers crus e o `Authorization`.

## Memória de longo prazo (RAG)

`RAGMemory` dá ao agente memória persistente sobre um `RAG`: antes de responder
ele **recupera** o que é relevante; depois, **guarda** o que aconteceu.

```python
from jangada_ai import LLM, Agent, RAGMemory
from jangada_ai.rag import RAG, InMemoryVectorStore

rag = RAG(LLM("openai", "text-embedding-3-small"), InMemoryVectorStore())
agente = Agent(llm, role="Suporte", memory=RAGMemory(rag, k=3))
```

## Squad — vários agentes colaborando

`Squad` orquestra um time de agentes. Dois processos:

### Sequencial (handoff)

Cada agente roda em ordem e recebe, por padrão, **apenas a saída do agente
anterior** como contexto (não o transcript acumulado):

```python
from jangada_ai import LLM, Agent, Squad

llm = LLM("openai", "gpt-4o-mini")
pesquisador = Agent(llm, role="Pesquisador", goal="levantar fatos")
escritor    = Agent(llm, role="Escritor", goal="escrever um texto claro")

squad = Squad([pesquisador, escritor])
res = squad.run("Escreva um parágrafo sobre jangadas nordestinas.")
print(res.text)            # saída do último agente
print(res.outputs)         # {"Pesquisador": "...", "Escritor": "..."}
```

**Semântica do contexto** (`context=`):

- **`"last"`** (padrão): cada agente recebe só a saída imediatamente anterior. O
  input por salto é ~constante — o custo da cadeia cresce **O(N)**, não O(N²).
- **`"full"`**: cada agente recebe o **transcript acumulado** (todas as saídas
  anteriores, rotuladas por papel). Mais contexto, custo **O(N²)** em cadeias longas.

```python
Squad([pesquisador, escritor], context="full")   # transcript inteiro a cada salto
```

**Observabilidade por agente**: `res.steps` traz uma entrada por agente com
`(role, usage, cost, cost_complete, dt)` — dá para ver o input/custo/latência
crescer (ou não) a cada salto sem desmontar o `Squad` na mão.

### Hierárquico (delegação)

Um agente **gerente** recebe ferramentas `delegar_para_<papel>` geradas
automaticamente a partir dos membros e decide a quem delegar cada subtarefa:

```python
gerente = Agent(llm, role="Gerente", goal="coordenar o time")
squad = Squad([pesquisador, escritor], manager=gerente)
res = squad.run("Produza um resumo sobre o tema X.")
```

Tanto `run` quanto `arun` agregam `usage`/`cost` de todo o time.

## Planejamento

`plan()` decompõe um objetivo numa lista ordenada de tarefas (structured output):

```python
from jangada_ai import plan

for tarefa in plan(llm, "Lançar uma newsletter sobre IA", max_tasks=5):
    print("-", tarefa)
```

## Como se relaciona com o resto

Não há mágica nem infra nova: `Agent` é o loop de tool calling (como o
[`run_agent`](mcp.md)); a memória é o [RAG](rag.md); o `Squad` hierárquico usa
delegação por tools ([Tools](tools.md)). Você pode trocar o provider de qualquer
agente sem mudar mais nada — a tese da jangada vale também aqui.

---

# Guardrails de escopo

Mantêm a LLM **dentro de um domínio** — para que ela não vire um assistente que
responde qualquer coisa — e **barram falas** indesejadas. É uma camada fina de
composição (Python puro, sem dep nova): reusa `Message`/`Completion` e o próprio
`parse` da lib.

Um guardrail intercepta a chamada em dois pontos:

- **input** — antes de chamar o modelo principal (valida o pedido do usuário);
- **output** — depois da resposta (valida o que o modelo respondeu).

Quando barra, o cliente **curto-circuita** e devolve um `Completion` com a
mensagem de recusa (`message=`) — no caso de input, nem chega a gastar o modelo
principal. Com `raise_on_block=True`, levanta `GuardrailError` no lugar.

## `ScopeGuard`

Combina dois mecanismos, do barato ao robusto:

1. **blocklist** (regex/termos) — barra na hora, sem custo nem LLM;
2. **classificador de escopo** (LLM-as-judge) — um `judge=LLM(...)` barato
   decide, via structured output, se o texto pertence ao escopo descrito.

```python
from jangada_ai import LLM, ScopeGuard

guard = ScopeGuard(
    scope=(
        "Suporte do sistema e-Gestor: notas fiscais, financeiro, cadastros. "
        "NÃO responde sobre outros assuntos (receitas, política, código, etc.)."
    ),
    judge=LLM("groq", "llama-3.1-8b"),   # modelo barato/rápido só pra classificar
    block=[r"\bsenha\b", "ignore as instruções"],  # barra na hora, sem LLM
    message="Desculpe, só posso ajudar com assuntos do e-Gestor.",
    check="both",                         # "input" (padrão), "output" ou "both"
)

llm = LLM("openai", "gpt-4o", guardrails=[guard])

llm.complete("como emito uma NF-e?")     # dentro do escopo -> responde normal
llm.complete("me ensina a fazer um bolo")  # fora -> Completion com a recusa
```

A recusa vem como um `Completion` normal (`comp.text == message`), com
`comp.cost is None` e `comp.raw == {"guardrail": "<motivo>"}` para inspeção.

## Parâmetros

| Parâmetro | Função |
|---|---|
| `scope` | Descrição em texto do que é permitido (usada pelo judge). |
| `judge` | `LLM` barato que classifica o escopo. Se omitido, usa o modelo principal. |
| `block` | Lista de regex/termos que barram na hora, sem chamar LLM. |
| `message` | Texto da recusa devolvido quando barra. |
| `check` | `"input"` (padrão), `"output"` ou `"both"`. |
| `instruction` | Sobrescreve a instrução do classificador. |
| `raise_on_block` | `True` levanta `GuardrailError` em vez de recusar. |
| `fail_closed` | Se o judge falhar/for inconclusivo: `True` barra (seguro), `False` (padrão) libera (disponibilidade). |

## Recomendações

- **Use um `judge` separado e barato** (ex.: `llama-3.1-8b`, `gpt-4.1-nano`,
  `gemini-2.5-flash-lite`): a classificação é por chamada, então um modelo pequeno
  derruba o custo. Sem `judge`, o próprio modelo principal classifica (a recursão
  é evitada internamente, mas fica mais caro).
- **A blocklist é grátis**: ponha nela os termos/frases óbvios (vazamento de
  segredo, prompt injection conhecido) e deixe o judge para o julgamento de tema.
- **Custo**: input barrado **não** gasta o modelo principal. Output barrado já
  pagou a geração (a recusa só substitui o texto).

## Onde se aplica

- `complete`/`acomplete` e `parse`/`aparse`: input **e** output.
- `stream`/`astream`: apenas **input** (o guard de output exigiria bufferizar o
  stream inteiro). Se barrado, o stream emite só a mensagem de recusa.

## Guardrail customizado

`ScopeGuard` cobre o caso comum, mas você pode escrever o seu herdando de
`Guardrail` e sobrescrevendo `check_input`/`check_output` (e as versões `a*`),
retornando `GuardResult(ok, reason)`:

```python
from jangada_ai import Guardrail, GuardResult

class MaxLength(Guardrail):
    message = "Mensagem longa demais."
    def __init__(self, limite): self.limite = limite
    def check_input(self, messages, judge):
        texto = " ".join(m.content for m in messages if isinstance(m.content, str))
        return GuardResult(len(texto) <= self.limite, "excedeu o limite")
```

---

# Debug passo a passo

Ative `debug=True` para um trace de cada chamada: provider/modelo, params,
retries, fallback, tokens, custo e duração — por agente.

```python
from jangada_ai import LLM

llm = LLM("openai", "gpt-4o-mini", debug=True, name="extrator")
llm.complete("...")
```

O `Debugger` registra os eventos da cadeia:

- `start` — provider, modelo e params da tentativa
- `retry` — erro, número da tentativa e atraso do backoff
- `fallback` — para qual provider/modelo caiu
- `end` — `Completion` resultante e duração em ms
- `error` — erro normalizado quando o candidato esgota as tentativas

O parâmetro `name=` rotula o agente no trace, útil quando há vários `LLM`
diferentes numa mesma orquestração ([Flow/Graph](flows.md)).

Relacionado: [Retry e fallback](retry-fallback.md), [Custo e tokens](cost.md),
[Erros](errors.md).

## Exemplo

[`examples/debug_params_example.py`](https://raw.githubusercontent.com/nerigleston/jangada/master/examples/debug_params_example.py) — script executável.

---

# Estendendo: adicionar um provider

Cada provider é um *adapter* que herda de `Provider` e traduz os tipos
normalizados para o SDK nativo.

## Passos

1. Crie `jangada/providers/<nome>.py` com uma classe que herda de `Provider` e
   implementa os 6 métodos + `_build_client` / `_build_async_client`.
2. Defina `name` e `env_key`.
3. Importe o SDK **só dentro** dos métodos (imports preguiçosos — invariante).
4. Registre em `registry.py` com um loader preguiçoso.
5. Adicione o extra em `pyproject.toml`.

## Contrato (`Provider`)

```python
class Provider:
    name: str
    env_key: str | None

    def _build_client(self) -> Any: ...
    def _build_async_client(self) -> Any: ...
    def complete(self, messages, **opts) -> Completion: ...
    async def acomplete(self, messages, **opts) -> Completion: ...
    def parse(self, messages, schema, **opts) -> Completion: ...
    async def aparse(self, messages, schema, **opts) -> Completion: ...
    def stream(self, messages, **opts) -> Iterator[str]: ...
    def astream(self, messages, **opts) -> AsyncIterator[str]: ...
```

## Atalho para dialeto OpenAI

Se o provider falar `chat.completions` (estilo OpenAI), herde de
`_OpenAICompatible` e só ajuste os atributos:

```python
class MeuProvider(_OpenAICompatible):
    name = "meu"
    env_key = "MEU_API_KEY"
    sdk_module = "meu_sdk"
    sync_class = "Client"
    async_class = "AsyncClient"
    supports_parse_helper = False   # True se tiver .parse() nativo
```

## Invariantes a respeitar

- **Imports preguiçosos**: `import jangada_ai` deve funcionar sem o SDK.
- **Tipos normalizados na fronteira**: fora dos adapters só circula
  `Message`/`Completion`; objetos nativos ficam em `Completion.raw`.
- **Tradução de erro sempre**: envolva chamadas de SDK em `try/except` e
  re-levante via `classify(e, self.name)` — veja [Erros](errors.md).
- **Paridade sync/async**: todo método tem versão `a*`.
- **Quirks por modelo** vão em `profiles.py` — veja [Parâmetros](parameters.md).

## Testes

Use o padrão de `FakeProvider` registrado em runtime; veja
[`tests/conftest.py`](https://github.com/nerigleston/jangada/blob/master/tests/conftest.py).

---

# Melhores práticas

Um apanhado de recomendações para tirar o máximo da jangada em produção. Cada
item aponta para o guia detalhado da capacidade correspondente.

## Provider e modelo

- **Troque por configuração, não por código.** Mantenha `provider`, `model` e
  `api_key` em variáveis de ambiente. A promessa da lib é trocar o provider sem
  mexer no resto — aproveite isso para alternar entre ambientes (dev/prod) e
  fazer testes A/B de modelo.
- **Use o modelo certo para cada tarefa.** Um modelo forte para raciocínio/escrita
  e um modelo barato (ex.: `llama-3.1-8b-instant`) para classificação, triagem e
  judges de guardrail. Não pague por capacidade que a tarefa não exige.
- **Deixe os perfis normalizarem os params.** Não escreva `if model == ...` para
  ajustar `temperature`/`max_tokens`: a camada de perfis já adapta o payload por
  modelo (ver [Parâmetros](parameters.md)).

> **Nota:** o padrão global é `max_tokens=8192`; defina-o explicitamente quando
> o modelo ou a extração exigir um limite diferente.
> Modelos com *thinking* (ex.: Gemini 2.5) podem consumir o orçamento de saída e
> truncar o JSON — um `max_tokens` folgado evita respostas cortadas.

## Structured output

- **Valide sempre contra o schema.** Use `parse()`/`aparse()` com um modelo
  Pydantic; não confie em parsing manual de texto.
- **`comp.parsed` é confiável** — a jangada já faz a coerção: se algum SDK
  devolver `parsed=None` com JSON válido em `.text`, a lib valida o texto pelo
  schema automaticamente (você não precisa do `model_validate_json` manual).
- **JSON fora do schema entra no failover.** Se a saída não casar com o modelo,
  a jangada levanta `errors.OutputValidationError` e **tenta o próximo modelo**
  do `with_fallback` (outro pode acertar). Não repete o mesmo modelo (seria o
  mesmo JSON). Ou seja: o fallback cobre tanto erro de API quanto saída malformada.
- Campos opcionais com `default=None` evitam que o modelo invente valores quando
  o dado não existe. Veja [Structured output](structured-output.md).

## Guardrails

- **Cheque o escopo na entrada, uma vez.** Em agentes com loop de ferramentas,
  não coloque o `ScopeGuard` no LLM que itera — a cada passo o histórico cresce e
  a reavaliação pode recusar uma fala válida. Faça a checagem com um LLM
  "porteiro" antes de iniciar o agente.
- **Use `raise_on_block=True`** quando quiser tratar a recusa no seu fluxo (ex.:
  responder com uma mensagem própria) em vez de devolver a `Completion` de recusa.
- **`fail_closed=True`** em domínios sensíveis: se o judge falhar, barra (seguro).
  Deixe `False` quando disponibilidade importa mais que rigor.
- Reserve a **blocklist** para termos óbvios (regex, custo zero) e deixe o
  **judge** decidir o escopo semântico. Veja [Guardrails](guardrails.md).

## RAG

- **Comece com busca híbrida** (`mode="hybrid"`): combina vetorial e BM25 por RRF
  e costuma superar só vetorial em perguntas com termos exatos.
- **Use `task="document"` ao indexar e `task="query"` ao buscar** — alguns
  providers (Gemini) diferenciam, e o orquestrador `RAG` já faz isso por você.
- **Amplie a recuperação com step-back.** Para perguntas muito específicas, gere
  uma pergunta mais ampla com [`step_back()`](stepback.md) e busque com as duas —
  recupera contexto de fundo que a query original sozinha perderia.
- **Deixe um agente decidir quando recuperar.** Nem toda mensagem precisa de RAG:
  exponha a busca como uma ferramenta e deixe o modelo chamá-la só quando a
  pergunta exigir contexto — saudações e conversa não devem disparar a base.
- Ajuste `k`, `min_score` e `chunk` ao seu conteúdo. Veja [RAG](rag.md).

## Retry e fallback

- **Configure retry para erros transitórios** (429, 5xx, timeouts) com backoff e
  `jitter=True`. Não faça failover em erros de auth (401/403) ou bad request
  (400/422) — trocar de provider não resolve.
- **Encadeie um fallback barato → forte → alternativo** com `with_fallback`. O
  failover acontece antes do primeiro token, inclusive em streaming.
- Veja [Retry e fallback](retry-fallback.md) e [Erros](errors.md).

## Custo e observabilidade

- **Leia `usage` e `cost`** em cada resposta e agregue por fluxo (`Flow`/`Graph`
  somam automaticamente). Sobrescreva preços com `register_price` conforme seu
  contrato.
- **Agrupe chamadas num `Trace`.** Uma requisição do seu serviço = um lote de
  observações (detecção, extração, conferência...), facilitando auditoria e
  diagnóstico. Veja [Custo](cost.md) e [Observabilidade](observability.md).

## Agentes e ferramentas

- **Descreva bem cada ferramenta.** O docstring é o que o modelo lê para decidir
  quando chamá-la — diga o que faz e *quando (não) usar*.
- **Force aritmética via tool.** Para somas/contas, use `tool_choice="required"`
  numa ferramenta de cálculo em vez de confiar na conta "de cabeça" do modelo.
- **Limite `max_iterations`** em agentes para evitar loops longos, e prefira
  `Squad` sequencial quando os papéis são claros (pesquisar → analisar → redigir).
- Veja [Tools](tools.md) e [Agentes](agents.md).

## Transcrição (áudio e vídeo)

- **O Whisper aceita vídeo, mas tem teto de tamanho.** Os endpoints de STT
  (OpenAI/Groq) transcrevem `mp4` direto — extraem a faixa de áudio sozinhos —,
  porém há um limite de **~25 MB** por arquivo. Um vídeo de reunião estoura isso
  com facilidade.
- **Pré-processe a mídia no seu serviço, não na lib.** Antes de transcrever,
  extraia só o áudio e normalize para algo leve (mono, 16 kHz, comprimido) com
  `ffmpeg`. Isso cabe no limite, acelera o upload e padroniza o container.
  Mantenha essa etapa no **seu backend**: a jangada é fina de propósito e não
  embute dependências de sistema (como `ffmpeg`) — ela só repassa os bytes ao
  provider via `Audio.from_bytes(dados, mime, name=...)`.
- **Preserve a extensão no `name`.** O Whisper usa o nome do arquivo para inferir
  o container; ao reprocessar, devolva algo como `reuniao.mp3`.
- **Tenha um fallback.** Se o `ffmpeg` não estiver disponível (ou falhar num
  codec exótico), mande o arquivo original — um `mp4` pequeno ainda transcreve.
- Veja [Transcrição de áudio](audio.md).

## Async e FastAPI

- Use `acomplete`/`aparse`/`astream` em handlers async. Para o pipeline síncrono
  (parsing de arquivos, chamadas em lote), rode em thread
  (`anyio.to_thread.run_sync`) para não bloquear o event loop.
- Em streaming, devolva um `StreamingResponse` consumindo o `astream`. Veja
  [Streaming](streaming.md).

> **Atenção:** não exponha suas chaves no front-end. O navegador deve falar com o
> **seu** backend; é o backend que detém as `api_key` dos providers.
