jangada-ai 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jangada_ai-0.6.0/.claude/settings.local.json +29 -0
- jangada_ai-0.6.0/.gitignore +89 -0
- jangada_ai-0.6.0/CLAUDE.md +156 -0
- jangada_ai-0.6.0/LICENSE +21 -0
- jangada_ai-0.6.0/PKG-INFO +397 -0
- jangada_ai-0.6.0/README.md +346 -0
- jangada_ai-0.6.0/examples/async_example.py +32 -0
- jangada_ai-0.6.0/examples/debug_params_example.py +38 -0
- jangada_ai-0.6.0/examples/fallback_example.py +32 -0
- jangada_ai-0.6.0/examples/files_example.py +40 -0
- jangada_ai-0.6.0/examples/graph_example.py +48 -0
- jangada_ai-0.6.0/examples/model_profiles_example.py +37 -0
- jangada_ai-0.6.0/examples/retry_cost_example.py +29 -0
- jangada_ai-0.6.0/examples/structured_example.py +27 -0
- jangada_ai-0.6.0/examples/vision_example.py +31 -0
- jangada_ai-0.6.0/jangada/__init__.py +50 -0
- jangada_ai-0.6.0/jangada/client.py +295 -0
- jangada_ai-0.6.0/jangada/debug.py +75 -0
- jangada_ai-0.6.0/jangada/env.py +61 -0
- jangada_ai-0.6.0/jangada/errors.py +136 -0
- jangada_ai-0.6.0/jangada/files.py +222 -0
- jangada_ai-0.6.0/jangada/flow.py +81 -0
- jangada_ai-0.6.0/jangada/graph.py +159 -0
- jangada_ai-0.6.0/jangada/message.py +75 -0
- jangada_ai-0.6.0/jangada/pricing.py +52 -0
- jangada_ai-0.6.0/jangada/profiles.py +91 -0
- jangada_ai-0.6.0/jangada/providers/__init__.py +5 -0
- jangada_ai-0.6.0/jangada/providers/anthropic.py +166 -0
- jangada_ai-0.6.0/jangada/providers/base.py +70 -0
- jangada_ai-0.6.0/jangada/providers/gemini.py +171 -0
- jangada_ai-0.6.0/jangada/providers/openai_compat.py +186 -0
- jangada_ai-0.6.0/jangada/providers/registry.py +55 -0
- jangada_ai-0.6.0/jangada/template.py +41 -0
- jangada_ai-0.6.0/pyproject.toml +55 -0
- jangada_ai-0.6.0/tests/__init__.py +0 -0
- jangada_ai-0.6.0/tests/conftest.py +93 -0
- jangada_ai-0.6.0/tests/test_client_files.py +48 -0
- jangada_ai-0.6.0/tests/test_files.py +105 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(git -C /Volumes/SSD_EXT/lib-ai/jangada status)",
|
|
5
|
+
"Bash(gh auth *)",
|
|
6
|
+
"Bash(dot_clean .)",
|
|
7
|
+
"Bash(git init *)",
|
|
8
|
+
"Bash(git add *)",
|
|
9
|
+
"Bash(git commit -q -m 'Initial commit: jangada — camada adaptável sobre SDKs de LLM *)",
|
|
10
|
+
"Bash(gh repo *)",
|
|
11
|
+
"Bash(python3 *)",
|
|
12
|
+
"Bash(.venv/bin/python -m pip install -q --upgrade pip)",
|
|
13
|
+
"Bash(.venv/bin/python -m pip install -q pytest pytest-asyncio openpyxl python-docx pypdf pydantic)",
|
|
14
|
+
"Bash(.venv/bin/python -m pytest --version)",
|
|
15
|
+
"Bash(.venv/bin/python -m pytest -q)",
|
|
16
|
+
"Bash(.venv/bin/python -m pip install -q reportlab)",
|
|
17
|
+
"Bash(git check-ignore *)",
|
|
18
|
+
"Bash(git commit -q -m 'Estrutura do pacote + suporte a documentos \\(docx/pdf/csv/xlsx\\) *)",
|
|
19
|
+
"Bash(git push *)",
|
|
20
|
+
"Bash(git commit -q -m 'docs: README com seção de Documentos \\(docx/pdf/csv/xlsx\\) e extras [files]/[dev] *)",
|
|
21
|
+
"Bash(curl -s -o /dev/null -w \"%{http_code}\" https://pypi.org/pypi/jangada/json)",
|
|
22
|
+
"Bash(curl -s https://pypi.org/pypi/jangada/json)",
|
|
23
|
+
"Bash(rm -rf dist build *.egg-info)",
|
|
24
|
+
"Bash(.venv/bin/python -m pip install -q build twine)",
|
|
25
|
+
"Bash(.venv/bin/python -m build)",
|
|
26
|
+
"Bash(.venv/bin/python -m pip install -q --upgrade pip build twine)"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Byte-compiled / optimizados
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
|
|
7
|
+
# Distribuição / empacotamento
|
|
8
|
+
.Python
|
|
9
|
+
build/
|
|
10
|
+
develop-eggs/
|
|
11
|
+
dist/
|
|
12
|
+
downloads/
|
|
13
|
+
eggs/
|
|
14
|
+
.eggs/
|
|
15
|
+
lib/
|
|
16
|
+
lib64/
|
|
17
|
+
parts/
|
|
18
|
+
sdist/
|
|
19
|
+
var/
|
|
20
|
+
wheels/
|
|
21
|
+
share/python-wheels/
|
|
22
|
+
*.egg-info/
|
|
23
|
+
.installed.cfg
|
|
24
|
+
*.egg
|
|
25
|
+
MANIFEST
|
|
26
|
+
|
|
27
|
+
# Ambientes virtuais
|
|
28
|
+
.venv/
|
|
29
|
+
venv/
|
|
30
|
+
env/
|
|
31
|
+
ENV/
|
|
32
|
+
.env
|
|
33
|
+
.env.*
|
|
34
|
+
|
|
35
|
+
# Testes / cobertura
|
|
36
|
+
.pytest_cache/
|
|
37
|
+
.coverage
|
|
38
|
+
.coverage.*
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.nox/
|
|
42
|
+
.cache
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
|
|
46
|
+
# Type checkers / linters
|
|
47
|
+
.mypy_cache/
|
|
48
|
+
.dmypy.json
|
|
49
|
+
dmypy.json
|
|
50
|
+
.ruff_cache/
|
|
51
|
+
.pyre/
|
|
52
|
+
.pytype/
|
|
53
|
+
|
|
54
|
+
# IDEs / editores
|
|
55
|
+
.idea/
|
|
56
|
+
.vscode/
|
|
57
|
+
*.swp
|
|
58
|
+
*.swo
|
|
59
|
+
*~
|
|
60
|
+
|
|
61
|
+
# Sistema operacional
|
|
62
|
+
.DS_Store
|
|
63
|
+
.AppleDouble
|
|
64
|
+
.LSOverride
|
|
65
|
+
._*
|
|
66
|
+
.Spotlight-V100
|
|
67
|
+
.Trashes
|
|
68
|
+
Thumbs.db
|
|
69
|
+
ehthumbs.db
|
|
70
|
+
Desktop.ini
|
|
71
|
+
|
|
72
|
+
# Gerenciadores de pacote / lock
|
|
73
|
+
uv.lock
|
|
74
|
+
.pdm-python
|
|
75
|
+
__pypackages__/
|
|
76
|
+
pip-log.txt
|
|
77
|
+
pip-delete-this-directory.txt
|
|
78
|
+
|
|
79
|
+
# Logs / temporários
|
|
80
|
+
*.log
|
|
81
|
+
*.tmp
|
|
82
|
+
*.bak
|
|
83
|
+
|
|
84
|
+
# Jupyter
|
|
85
|
+
.ipynb_checkpoints/
|
|
86
|
+
|
|
87
|
+
# Chaves / segredos locais
|
|
88
|
+
*.key
|
|
89
|
+
secrets.json
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Guia para o Claude Code trabalhar neste repositório. Leia antes de editar.
|
|
4
|
+
|
|
5
|
+
## O que é
|
|
6
|
+
|
|
7
|
+
`jangada` é uma camada fina e adaptável sobre os SDKs oficiais de LLM
|
|
8
|
+
(Anthropic, OpenAI, Groq, Gemini). Objetivo: **trocar provider/model/api_key
|
|
9
|
+
sem mudar o resto do código**, com templates `{{ }}`, fluxos encadeados,
|
|
10
|
+
structured output (Pydantic), vision, async e fallback por erro.
|
|
11
|
+
|
|
12
|
+
Princípio central: a complexidade de cada SDK fica isolada em um *adapter*;
|
|
13
|
+
o resto da lib só conhece os tipos normalizados (`Message`, `Completion`).
|
|
14
|
+
|
|
15
|
+
## Arquitetura
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
jangada/
|
|
19
|
+
message.py # Message, Completion, TextPart, ImagePart, Image (tipos normalizados)
|
|
20
|
+
files.py # Document/to_part: extrai TEXTO de docx/pdf/csv/xlsx (vision só opt-in)
|
|
21
|
+
template.py # motor {{ }} (regex, acesso por ponto, strict)
|
|
22
|
+
errors.py # hierarquia de erro normalizada + classify()
|
|
23
|
+
client.py # LLM: API pública (complete/parse/stream + async + retry + fallback + debug)
|
|
24
|
+
pricing.py # tabela de preços (aprox.) + compute_cost (custo na response)
|
|
25
|
+
profiles.py # normalização de params por modelo (quirks gpt-5, gemini-3.x...)
|
|
26
|
+
debug.py # Debugger: trace passo a passo da cadeia, por agente
|
|
27
|
+
flow.py # Flow/Step/FlowResult (encadeamento sequencial)
|
|
28
|
+
graph.py # Graph: roteamento condicional + paralelo (async-core)
|
|
29
|
+
env.py # detecção de .env (não-destrutiva, na importação)
|
|
30
|
+
providers/
|
|
31
|
+
base.py # ABC Provider: complete/acomplete/parse/aparse/stream/astream
|
|
32
|
+
anthropic.py # adapter Claude (structured = tool-forcing)
|
|
33
|
+
openai_compat.py # adapters OpenAI e Groq (compartilham chat.completions)
|
|
34
|
+
gemini.py # adapter Gemini (async via client.aio)
|
|
35
|
+
registry.py # get_provider/register com loaders preguiçosos
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Fluxo de uma chamada: `LLM.complete()` → renderiza template → monta
|
|
39
|
+
`list[Message]` → `_run()` percorre `[self, *fallbacks]` → chama
|
|
40
|
+
`provider.complete(messages)` → o adapter traduz para o SDK nativo, executa,
|
|
41
|
+
e devolve `Completion`. Erros do SDK passam por `errors.classify()` e viram
|
|
42
|
+
`LLMError` normalizado, que a política de fallback usa para decidir failover.
|
|
43
|
+
|
|
44
|
+
## Invariantes — NÃO QUEBRE
|
|
45
|
+
|
|
46
|
+
- **Imports preguiçosos.** `import jangada` deve funcionar sem nenhum SDK
|
|
47
|
+
instalado. Os SDKs só podem ser importados dentro de `_build_client` /
|
|
48
|
+
`_build_async_client` ou dentro dos métodos do adapter (nunca no topo do módulo).
|
|
49
|
+
- **Tipos normalizados na fronteira.** Fora dos adapters, só circula
|
|
50
|
+
`Message`/`Completion`. Não vaze objetos nativos de SDK (eles ficam em
|
|
51
|
+
`Completion.raw`).
|
|
52
|
+
- **Tradução de erro sempre.** Toda chamada de SDK fica em `try/except` e
|
|
53
|
+
re-levanta via `classify(e, self.name)`. Nunca deixe erro nativo escapar.
|
|
54
|
+
- **Paridade sync/async.** Todo método tem versão `a*` equivalente. Ao mudar
|
|
55
|
+
um, mude o outro.
|
|
56
|
+
- **Params canônicos.** O `LLM` aceita params de geração comuns como nomes
|
|
57
|
+
canônicos (`temperature`, `max_tokens`, `top_p`, `top_k`, `stop`, `seed`).
|
|
58
|
+
Cada adapter implementa `_translate()` para o nome nativo e descarta os não
|
|
59
|
+
suportados (ex.: OpenAI não tem `top_k`; Anthropic não tem `seed`; Gemini usa
|
|
60
|
+
`max_output_tokens`/`stop_sequences`). Params específicos vão via `extra=`.
|
|
61
|
+
Ordem no adapter: `_translate()` (canônico→nativo) → `apply_profile()` (quirks
|
|
62
|
+
de modelo). NÃO inverta essa ordem.
|
|
63
|
+
- **Retry antes de fallback.** Por candidato, o cliente tenta `max_retries+1`
|
|
64
|
+
vezes com backoff (`backoff_on`, padrão `errors.TRANSIENT`) antes de cair pro
|
|
65
|
+
próximo candidato (`retry_on`). NotFoundError não repete, mas faz fallback.
|
|
66
|
+
- **Custo na response.** O cliente chama `pricing.compute_cost()` após cada
|
|
67
|
+
sucesso e seta `Completion.cost`. `FlowResult`/`GraphResult` agregam
|
|
68
|
+
`usage`/`cost`. Preços são aproximados — não trate como fonte de billing.
|
|
69
|
+
- **Default de failover** (`errors.DEFAULT_FAILOVER`): rate limit, timeout,
|
|
70
|
+
connection, 5xx, 404. NÃO inclua auth nem bad_request por padrão.
|
|
71
|
+
- **Normalização por modelo.** Os adapters chamam `profiles.apply_profile()`
|
|
72
|
+
ao montar o payload, para tratar quirks específicas de modelo (ex.: gpt-5
|
|
73
|
+
não aceita `temperature` e usa `max_completion_tokens`; gemini-3.x descarta
|
|
74
|
+
sampling e usa `thinking_level`). Ao suportar um modelo novo com contrato
|
|
75
|
+
diferente, adicione uma regra em `profiles.py` em vez de espalhar `if`s nos
|
|
76
|
+
adapters. Function calling multi-turn no Gemini 3.x exige preservar thought
|
|
77
|
+
signatures (não é problema de param — é integridade do histórico).
|
|
78
|
+
|
|
79
|
+
## Como adicionar um provider
|
|
80
|
+
|
|
81
|
+
1. Crie `providers/<nome>.py` com classe que herda de `Provider` e implementa
|
|
82
|
+
os 6 métodos + `_build_client`/`_build_async_client`.
|
|
83
|
+
2. Defina `name` e `env_key`.
|
|
84
|
+
3. Importe o SDK só dentro dos métodos.
|
|
85
|
+
4. Registre em `registry.py` com um loader preguiçoso.
|
|
86
|
+
5. Adicione o extra em `pyproject.toml`.
|
|
87
|
+
|
|
88
|
+
Se o provider falar o dialeto `chat.completions` da OpenAI, herde de
|
|
89
|
+
`_OpenAICompatible` e só ajuste `sdk_module`/`sync_class`/`async_class`/
|
|
90
|
+
`supports_parse_helper` (veja `GroqProvider`).
|
|
91
|
+
|
|
92
|
+
## Mapa de structured output (por provider)
|
|
93
|
+
|
|
94
|
+
- OpenAI: `chat.completions.parse(response_format=Modelo)` → `.message.parsed`.
|
|
95
|
+
- Groq: `response_format={"type":"json_schema",...}` + `model_validate_json`.
|
|
96
|
+
- Gemini: `config.response_schema=Modelo` → `resp.parsed`.
|
|
97
|
+
- Anthropic: tool-forcing (`tool_choice` fixo) → valida `tool_use.input`.
|
|
98
|
+
|
|
99
|
+
## Vision
|
|
100
|
+
|
|
101
|
+
Imagens entram como `ImagePart` (bytes + mime). Cada adapter traduz:
|
|
102
|
+
OpenAI/Groq → `image_url` data URI; Anthropic → bloco `image/source base64`;
|
|
103
|
+
Gemini → `types.Part.from_bytes`. Apenas bytes (use `Image.from_path/bytes/base64`).
|
|
104
|
+
|
|
105
|
+
## Documentos (docx, pdf, csv, xlsx)
|
|
106
|
+
|
|
107
|
+
`files=[...]` em `complete/parse/stream` (e async). `files.py` resolve cada item
|
|
108
|
+
em `to_part()` **na fronteira do client** — vira `TextPart` ou `ImagePart`, então
|
|
109
|
+
os adapters NÃO veem nenhum formato de documento (invariante de tipos preservada).
|
|
110
|
+
|
|
111
|
+
Regra padrão (`mode="auto"`): **extrai texto, não usa vision**. csv/xlsx/docx e
|
|
112
|
+
PDF com camada de texto viram markdown/texto (mais barato, funciona em modelo sem
|
|
113
|
+
vision). xlsx percorre TODAS as abas. PDF sem texto (escaneado) levanta
|
|
114
|
+
`DocumentError` sugerindo vision — nunca devolve bloco vazio. `mode="vision"`
|
|
115
|
+
força o caminho de imagem; imagens em `auto` já vão para vision.
|
|
116
|
+
|
|
117
|
+
Deps (`pypdf`, `python-docx`, `openpyxl`) são extra opcional `jangada[files]`,
|
|
118
|
+
com import preguiçoso dentro dos extractors — `import jangada` funciona sem elas.
|
|
119
|
+
|
|
120
|
+
## Testes
|
|
121
|
+
|
|
122
|
+
Os testes ficam em `tests/` (pacote: tem `__init__.py`). `tests/conftest.py`
|
|
123
|
+
expõe um `FakeProvider` (auto-registrado por fixture) e fixtures que geram
|
|
124
|
+
xlsx/docx/pdf em memória. Config do pytest (`asyncio_mode=auto`, `testpaths`)
|
|
125
|
+
está em `pyproject.toml`. Ambiente: `pip install -e ".[dev]"` numa venv (`.venv/`,
|
|
126
|
+
ignorada no git).
|
|
127
|
+
|
|
128
|
+
Os SDKs reais exigem chave e rede. Os testes cobrem a lógica com adapters
|
|
129
|
+
*mockados* (clientes nativos falsos) e o `FakeProvider` registrado em runtime.
|
|
130
|
+
Padrão ao testar:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from jangada.providers.registry import register
|
|
134
|
+
from jangada.providers.base import Provider
|
|
135
|
+
# crie um Provider fake, registre com register("fake", lambda: FakeClass), e teste LLM/Flow/fallback.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Para testar um adapter real sem rede, injete um cliente nativo falso em
|
|
139
|
+
`provider._client` (ou `._aclient`) e verifique os kwargs capturados.
|
|
140
|
+
|
|
141
|
+
Rode: `.venv/bin/python -m pytest` (ou os scripts em `examples/` com chaves reais).
|
|
142
|
+
|
|
143
|
+
## Convenções
|
|
144
|
+
|
|
145
|
+
- Python 3.10+, type hints em tudo, `from __future__ import annotations`.
|
|
146
|
+
- Pydantic v2 (`model_json_schema()`, `model_validate`/`model_validate_json`).
|
|
147
|
+
- Sem dependências pesadas no core (só `pydantic`); SDKs são extras opcionais.
|
|
148
|
+
- Mensagens de erro e docstrings em PT-BR (padrão deste projeto).
|
|
149
|
+
|
|
150
|
+
## Comandos úteis
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pip install -e ".[all]" # instala com todos os SDKs
|
|
154
|
+
pip install -e ".[anthropic,groq]" # subconjunto
|
|
155
|
+
python examples/structured_example.py
|
|
156
|
+
```
|
jangada_ai-0.6.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Neri
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|