Metadata-Version: 2.5
Name: jevaas
Version: 0.3.3
Summary: SDK Python oficial do JEVaaS (jevaas.com.br) — decisão tipada com contrato versionado, rota por confiança e recibo auditável
Project-URL: Homepage, https://jevaas.com.br/developers
Project-URL: Documentation, https://jevaas.com.br/docs
Project-URL: Repository, https://github.com/vertikon/jev.vertikon.com.br
Author: Vertikon
License-Expression: MIT
Keywords: auditoria,brasil,confianca,contrato,decisao,julgamento,lgpd,llm,roteamento,sdk
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# jevaas — SDK Python

SDK oficial do **JEVaaS** — decisão tipada com contrato versionado, rota por confiança e recibo auditável. Zero dependências (só stdlib).

```bash
pip install jevaas
```

```python
import os
from jevaas import Jevaas, JevaasError, describe_route, is_actionable

jev = Jevaas(api_key=os.environ["JEVAAAS_API_KEY"])   # chave jev_sk_* do inquilino

# julga com o contrato publicado (versão vigente, ou fixe com contract_version=3)
r = jev.judge("ticket-router", {"ticket": {"messages": [{"author": "customer", "text": "..."}]}})

print(r["route"], r["selected"]["choice"], r["decisive_confidence"])
print(describe_route(r["route"], r["status"]))

# o JEVaaS NUNCA executa nada: `allow` é token opaco e quem o mapeia para
# permissão é o motor de política do consumidor.
if is_actionable(r):
    rotear_para(r["allow"])

# sem contrato publicado, perguntas soltas (sem rotas, sem barra):
a = jev.fanout("texto do ticket", {"is_urgent": {"type": "noul", "instructions": "É urgente?"}})
print(a["answers"][0]["noul"])

# modo sombra: o julgamento vem, mas não autoriza agir. Devolva o desfecho real.
jev.receipt_outcome(r["receipt_id"], "aberto como incidente", action_taken=True)
jev.receipt_label(r["receipt_id"], "suporte")
```

## Rotas de decisão

| `route` | O que o consumidor faz |
| --- | --- |
| `auto` | Age, dentro do `allow` — só com `enforced: true` |
| `collect_evidence` | Busca **evidência nova** e rejulga; não age |
| `human_review` | Enfileira para uma pessoa |
| `abstain` | Corrige o **menu**, não a pergunta |

`is_actionable(r)` responde a única pergunta que autoriza agir: `route == "auto"` **e** `enforced is True`. Em `mode: shadow` o julgamento é real e a autorização não existe — agir com ele é exatamente o que o modo sombra existe para impedir.

## Robustez (produção)

- **Retry/backoff** exponencial com jitter em `429/502/503/504` e falhas de rede (`status 0`); respeita `Retry-After`. Configurável via `max_retries` / `timeout`.
- **Destino (SSRF):** o construtor recusa `base_url` que não seja `http`/`https` **e** host público — loopback, faixas privadas, link-local, reservadas e multicast (IPv4 e IPv6, inclusive `::ffff:127.0.0.1` e CGNAT `100.64/10`) e os nomes `localhost`, `*.localhost`, `*.local`, `*.internal`. Nome DNS que não seja IP literal passa: resolver está fora do escopo do SDK. Para um servidor local, `permitir_endpoint_privado=True` — nunca em produção.
- **Erros tipados:** `JevaasError` expõe `.status`, `.message`, `.request_id` (header `X-Request-Id` — cite no suporte), `.body` e `.retryable`.
- **Observabilidade:** `on_request` recebe `{"method","path","status","request_id","attempt"}` a cada requisição terminada, inclusive nas tentativas que falharam.

```python
try:
    r = jev.judge("ticket-router", state)
except JevaasError as e:
    print(e.status, e.message, "request:", e.request_id, "retryable:", e.retryable)
```

`endpoint_publico(url)` aplica a mesma guarda sem levantar — útil para conferir a configuração antes de construir (e antes de subir):

```python
from jevaas import endpoint_publico

if not endpoint_publico(url_do_painel):
    raise SystemExit("destino não público: só http(s) de host público")
```

## Ambiente

| Variável | Papel |
| --- | --- |
| `JEVAAAS_URL` | Base da API (padrão `https://jev.api.br/v1`) |
| `JEVAAAS_API_KEY` | Chave `jev_sk_*`; o texto claro aparece uma única vez, na emissão |
| `JEVAAAS_TIMEOUT_S` | Timeout em segundos (padrão `30`) |

O SDK não lê o ambiente sozinho: passe os valores no construtor.

## Superfície

`judge` · `fanout` · `contracts` · `contract` · `contract_create` · `contract_update` · `contract_versions` ·
`contract_promote` · `contract_set_mode` · `receipts` · `receipt` · `receipt_label` · `receipt_outcome` ·
`calibration` · `run_eval` · `me` · `quota` · `usage` · `is_actionable` · `describe_route` · `endpoint_publico`.

Especificação completa (OpenAPI 3.1): <https://jev.api.br/v1/openapi.json> · docs: <https://jevaas.com.br/docs>

Contrato de borda versionado: [`devkit/API-CONTRACT.md`](../../devkit/API-CONTRACT.md). Se o SDK divergir dele, o SDK está errado.

## Escolha do modelo (0.3.3)

O JEVaaS oferece o **Jev** (TypeSafe) e o **Drex** (nace.ai), com o mesmo preço por token.
`models()` lista o que está disponível; escolha com `model` no `fanout` ou no contrato.

```python
print(jevaas.models())   # {"default": "typesafe/jev-1.13.0", "models": [...]}
r = jevaas.fanout({"texto": "parem de me mandar promoção"},
                  {"pedido": {"type": "choice", "instructions": "O que o cliente pede?",
                              "criteria": {"parar_marketing": "Parar promoções", "sem_pedido": "Nada",
                                           "indeterminado": "Ambíguo"}}},
                  model="nace/drex-latest")
```

Perfis medidos em português: o Jev é mais sensível; o Drex é conservador (quase não age no que é
inofensivo) e gasta cerca de 5× menos tokens por decisão. No Drex, instrução e critérios precisam
ser texto. Comparação: <https://jevaas.com.br/model#escolha>.

## Mudanças na 0.2.0

- **`judge()` gera `decision_id`** quando você não passa — uma vez por chamada, então o retry
  automático reenvia a mesma chave e o serviço devolve o recibo em vez de julgar e cobrar de
  novo. Continue passando o id de negócio (`tkt_…`) quando tiver: ele é preservado.
- **`fanout()` aceita `decision_id`** e **não repete mais em falha ambígua** (rede/timeout, 504):
  o serviço não deduplica fanout. Trate status `0`/`504` como "pode ter sido cobrado".