# 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). Pacote no PyPI: https://pypi.org/project/jangada-ai/ — í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-docs/tree/main/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-docs/blob/main/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-docs/blob/main/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-docs/blob/main/examples/cookbook/02_agente_mcp.py). - Referência detalhada: [MCP](mcp.md) e [Tools](tools.md). --- # Providers e chaves de API A jangada suporta cinco 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]` | | 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` - **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 | |---------------------------------|:------:|:----:|:------:|:---------:| | 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`) | ✅ | ✅ | ✅ | ❌ | | Param `top_k` | ❌ | ❌ | ✅ | ✅ | | Param `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 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` | ## Detalhe por provider - [OpenAI](llm-openai.md) - [Groq](llm-groq.md) - [Gemini](llm-gemini.md) - [Anthropic](llm-anthropic.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`) e usa `thinking_level` no lugar de `thinking_budget`. Function calling multi-turn no 3.x exige preservar as *thought signatures* (integridade do histórico). Veja [Parâmetros e perfis](parameters.md). - **Resposta** (`Completion`): `usage` vem de `usage_metadata` (`prompt_token_count`/`candidates_token_count`). ## 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 (`1024`) 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` | ```python llm = LLM("anthropic", "claude-opus-4-8", temperature=0.2, max_tokens=512) # override por chamada llm.complete("...", params={"temperature": 0.9}) # 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` e troca `thinking_budget` por `thinking_level`. Ordem aplicada no adapter: `_translate()` (canônico → nativo) → `apply_profile()` (quirks de modelo). 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-docs/main/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. - `comp.text` → o JSON bruto retornado. - `comp.usage` / `comp.cost` → tokens e custo estimado. ## 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-docs/main/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)`. - `comp.assistant_message()`: reconstrói a mensagem do assistant (texto + tool calls) 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-docs/main/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(...)` | ## Exemplo [`examples/mcp_example.py`](https://raw.githubusercontent.com/nerigleston/jangada-docs/main/examples/mcp_example.py) — script executável. --- # 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 ``` ## 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-docs/main/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-docs/main/examples/detect_example.py) — script executável. --- # 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-docs/main/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-docs/main/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 ``` `task` vira `task_type` no Gemini (`RETRIEVAL_DOCUMENT`/`RETRIEVAL_QUERY`); a OpenAI ignora. `dimensions` (OpenAI) / `output_dimensionality` (Gemini) vão via `**opts`. | Provider | Embeddings? | Modelo típico | |----------|:-----------:|---------------| | OpenAI | ✅ | `text-embedding-3-small` / `-large` | | Gemini | ✅ | `gemini-embedding-001` | | Anthropic| ❌ | — (use Voyage/Cohere por fora) | | Groq | ❌ | — | ## 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`). ## 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) ``` ## 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.` 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. ## Chunking ```python from jangada_ai.rag import chunk_text chunk_text(texto, size=1000, overlap=200) # quebra sem cortar palavras ``` 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-docs/main/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-docs/main/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-docs/main/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 ficam em `pricing.py` (aproximados, por 1M de tokens). Você pode registrar/ajustar: ```python from jangada_ai import register_price, price_for register_price("meu-modelo", input=0.5, output=1.5) # USD por 1M tokens print(price_for("gpt-4o-mini")) ``` > ⚠️ Os valores são **aproximados** e servem para estimativa/observabilidade — > não trate como fonte de billing. ## 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-docs/main/examples/retry_cost_example.py) — script executável. --- # Observabilidade (envio de traces) `Observability`/`Trace` enviam as chamadas de uma request, agrupadas em um **lote** (trace), para uma plataforma de observabilidade. Numa request que faz N chamadas de IA, abre-se 1 trace e registram-se N observations — um único POST ao final. ```python import os from jangada_ai import LLM, Observability obs = Observability(api_key=os.environ["LOBS_API_KEY"], endpoint="https://sua-api") llm = LLM("openai", "gpt-4o-mini") with obs.trace(name="resumo+tradução") as t: r1 = llm.complete("Resuma: ...") t.log(r1) r2 = llm.complete("Traduza: ...") t.log(r2) # flush automático -> 1 POST com as 2 observations ``` ## O que é capturado `t.log(completion)` extrai do `Completion`: `provider`, `model`, `promptTokens`/`completionTokens` (de `usage`), `costUsd` (de `cost`), o texto de saída e as **tool calls** que o modelo pediu (`tools`: id/name/args). Você pode complementar: ```python t.log(r1, name="extração", input=prompt, latency_ms=820) t.log(error="timeout ao chamar o modelo") # registra falha # tool calls + o resultado que VOCÊ executou (casa por id, senão por name): t.log(comp, tool_results={"get_weather": "25°C", "somar": 3}) ``` ## Detalhes - `trace(id=...)`: informe um id externo para **acrescentar** observations ao mesmo lote em chamadas separadas (idempotente no backend). - `user_id` / `session_id` / `metadata`: contexto do usuário final do seu app. - Falhas de rede **não derrubam** sua aplicação (`raise_on_error=False` por padrão); o erro vai para o stderr. - A `api_key` é a chave do projeto, configurada no `.env` do seu projeto. 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) | | `AuthError` | 401/403 | **não** | | `BadRequestError` | 400 (params inválidos) | **não** | ## 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). 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-docs/blob/main/examples/graph_example.py). ## 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-docs/main/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. - 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) ``` ## 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 a saída do anterior como contexto: ```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": "..."} ``` ### Hierárquico (delegação) Um agente **gerente** recebe ferramentas `delegar_para_` 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": ""}` 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-docs/main/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/.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-docs/blob/main/tests/conftest.py).