Metadata-Version: 2.5
Name: prompt-manager-client-dl
Version: 0.6.0
Summary: Live-updating client for Prompt Manager
Requires-Python: >=3.10
Requires-Dist: cachetools<7,>=5
Requires-Dist: httpx<1,>=0.27
Requires-Dist: requests<3,>=2.31
Description-Content-Type: text/markdown

# Prompt Manager — Cliente Python

`prompt-manager-client` é a biblioteca que as aplicações de IA importam para buscar seus prompts na API do Prompt Manager em tempo de execução. Os prompts se atualizam ao vivo: quando alguém commita e publica uma nova versão na UI, a aplicação passa a usá-la em segundos, sem redeploy.

Ela é deliberadamente pequena — três coisas públicas:

- `PromptManager` — cliente síncrono (`requests`)
- `AsyncPromptManager` — cliente assíncrono (`httpx`), mesma interface com `await`
- `Prompt` — o prompt imutável e totalmente resolvido que uma busca retorna

## Instalação

O pacote está publicado no PyPI como [`prompt-manager-client-dl`](https://pypi.org/project/prompt-manager-client-dl/):

```bash
uv add prompt-manager-client-dl
# ou: pip install prompt-manager-client-dl
```

O nome de import continua sendo `prompt_manager`. O código-fonte vive neste repositório, em `client/` — para desenvolver contra ele, instale como dependência de path/git:

```bash
uv add "prompt-manager-client-dl @ git+ssh://git@bitbucket.org/avisourgente/prompt-manager.git#subdirectory=client"
```

Requer Python ≥ 3.10. Dependências: `requests`, `httpx`, `cachetools`.

## Começo rápido

Crie **um cliente por processo** na inicialização (ele guarda o cache e a sessão HTTP) e reutilize-o:

```python
from prompt_manager import PromptManager

pm = PromptManager(
    application="superchat",                 # o nome da sua aplicação no manager
    environment="prod",                      # qual deployment seguir
    fallback_dir="prompts_fallback",         # opcional: snapshots offline (veja abaixo)
)

# Uma chamada: busca (com cache) + preenche variáveis + relata o uso
text = pm.render(
    "resposta_juridica",
    variables={"pergunta": pergunta, "contexto": contexto},
    request_id=request_id,        # campos opcionais de observabilidade —
    user=user_email,              # eles vinculam o caso real ao prompt
    metadata={"tenant": tenant},  # na tela de "Registros" do manager
)
```

`render()` retorna a string final pronta para enviar ao modelo — e levanta `PromptValidationError` se o prompt define mensagens de chat. Para esses (ou para padronizar tudo em mensagens), use `render_messages()`, que aceita os mesmos argumentos e retorna uma lista `[{"role", "content"}]` no formato OpenAI:

```python
messages = pm.render_messages(
    "resposta_juridica",
    variables={"pergunta": pergunta, "contexto": contexto},
)
# → [{"role": "system", "content": "..."}, {"role": "user", "content": "..."}]

# pronto para qualquer SDK no formato OpenAI (openai, litellm, ...):
resposta = client.chat.completions.create(model=..., messages=messages)

# LangChain aceita esses dicts diretamente:
resposta = chat_model.invoke(messages)
```

Um prompt sem marcadores de papel vira uma única mensagem `system` — então `render_messages()` pode ser o único caminho da sua aplicação, sem ramificar pelo formato do prompt.

### Onde o cliente se conecta (`base_url`)

O endereço da API é resolvido nesta ordem:

1. o argumento `base_url` do construtor, se informado;
2. a variável de ambiente **`PROMPT_MANAGER_URL`**;
3. o padrão de produção: **`http://promptmanager-prod.datalawyer.local`**.

Ou seja: em produção você não configura nada, e deployments de staging/dev redirecionam todos os clientes com uma única variável de ambiente:

```bash
export PROMPT_MANAGER_URL=http://localhost:8000   # ex.: stack de dev local
```

### Async

```python
from prompt_manager import AsyncPromptManager

pm = AsyncPromptManager(application="superchat", environment="prod")

async def handle(question: str) -> str:
    return await pm.render("resposta_juridica", variables={"pergunta": question})
```

Use-o como async context manager (`async with AsyncPromptManager(...) as pm:`) ou mantenha-o pela vida do processo; ao sair, ele descarrega os relatos de uso pendentes e fecha seu cliente `httpx` (a menos que você tenha passado o seu próprio).

## Trabalhando com o objeto `Prompt`

`get_prompt()` retorna o prompt resolvido sem renderizá-lo:

```python
prompt = pm.get_prompt("resposta_juridica")

prompt.template          # texto resolvido completo (fragmentos e exemplos já expandidos)
prompt.variables         # as variáveis de runtime que o template ainda espera
prompt.version           # número da versão commitada que esta resolução usou
prompt.hash              # hash do snapshot — registre-o para rastrear qualquer chamada de LLM até o prompt exato
prompt.suggested_model   # modelo + parâmetros sugeridos pelo autor do prompt
prompt.suggested_params
prompt.fragment_versions # quais versões de fragmentos foram embutidas
prompt.fewshots          # quais bancos de exemplos foram injetados, e quantos exemplos cada um contribuiu

prompt.messages          # o template já dividido em mensagens de chat pelo servidor
prompt.is_chat           # True se o template usa marcadores de papel

rendered = prompt.render(pergunta="...", contexto="...")            # prompts de texto
messages = prompt.render_messages(pergunta="...", contexto="...")   # qualquer prompt
```

`render` e `render_messages` (tanto no cliente quanto no `Prompt`) validam os valores: nomes de variáveis desconhecidos, variáveis obrigatórias faltando e `{{placeholders}}` restantes levantam `PromptValidationError`. Valores dict/list são codificados em JSON automaticamente.

## Fragmentos

Fragmentos (blocos reutilizáveis: modelos de documento, seções de instrução, personas) são servidos pelo mesmo `/fetch` que os prompts — `get_prompt()` funciona para ambos e `prompt.kind` distingue (`"prompt"` ou `"fragment"`). Para o caso comum — a aplicação quer só o texto do fragmento, mantendo os `{{placeholders}}` para preencher depois — use `get_fragment_text()`:

```python
modelo = pm.get_fragment_text("modelo-contestacao")             # placeholders intactos
modelo = pm.get_fragment_text("modelo-contestacao", values={"esfera": "civel"})
```

Sub-fragmentos e bancos de exemplos chegam expandidos pelo servidor; ramos `{% if %}` são resolvidos localmente com os `values` informados (os defaults registrados preenchem os omitidos); marcadores de papel são descartados e o texto vem inteiro. Diferente de `render()`, nada é obrigatório — placeholders restantes são devolvidos como estão. Cache, revalidação por ETag e fallback offline funcionam exatamente como em `get_prompt()`; fragmentos compartilhados vivem na pseudo-aplicação `shared` (`PromptManager(application="shared", ...)`). Disponível também no `AsyncPromptManager` (`await pm.get_fragment_text(...)`).

### Mensagens de chat

Um template pode conter linhas com marcadores de papel `{% system %}` / `{% user %}` / `{% assistant %}` — uma nova mensagem começa em cada marcador, e o conteúdo antes do primeiro pertence a `system`. O servidor já entrega o template dividido (`prompt.messages`), e `render_messages()` preenche as variáveis mantendo os papéis. `render()` recusa prompts de chat (`PromptValidationError`) em vez de devolver texto com marcadores embutidos; se você realmente quiser o texto plano, junte os conteúdos de `render_messages()` deliberadamente. Snapshots de fallback antigos (sem o campo `messages`) continuam funcionando: o cliente divide o template localmente com a mesma regra do servidor.

## Ambientes

O `environment` informado na construção é o padrão; qualquer chamada pode sobrescrevê-lo:

```python
pm.get_prompt("resposta_juridica", environment="staging")
```

`"default"` é especial: ele sempre segue a **versão commitada mais recente**, sem precisar de deployment explícito. Ambientes nomeados (`prod`, `staging`, …) servem a versão que foi fixada neles pela UI — e caem para a versão mais recente se o prompt nunca foi fixado ali.

## Fluxo (`{% if %}` no template)

Não existe uma classe separada de variáveis de "controle": as booleanas e categóricas que decidem os ramos `{% if %}` são variáveis comuns, passadas em `variables` junto com as de texto. O servidor entrega o template com as tags `{% if %}` intactas e a biblioteca resolve os ramos localmente a cada render; a mesma variável também pode ser impressa como `{{placeholder}}` (booleanas viram `true`/`false`). Uma busca e uma entrada de cache cobrem todas as combinações.

```python
messages = pm.render_messages(
    "chitchat",
    variables={
        "agente_customizado": True,        # booleana → decide o ramo
        "agent_description": descricao,    # variável de texto → preenche {{...}}
        "query": pergunta,
        ...
    },
)
```

Uma booleana/categórica omitida usa o valor padrão registrado no manager — a mesma regra do servidor, então o texto resultante é byte a byte o que o playground mostra para os mesmos valores. Variáveis cujo único uso está em um ramo desligado deixam de ser obrigatórias (continuam aceitas, apenas não são usadas). Snapshots de fallback gerados pelo `prompt-manager pull` preservam as tags, então o fallback offline também cobre todas as combinações.

## Ablação de few-shots (A/B sem exemplos)

`fewshots=False` resolve o mesmo prompt com todos os bancos de exemplos desligados — é assim que uma aplicação mede o que os exemplos realmente valem:

```python
com_exemplos = pm.render("classificador", variables=v)                    # normal
sem_exemplos = pm.render("classificador", variables=v, fewshots=False)   # ablação
```

As duas resoluções têm cache e ETag separados.

## Cache, resiliência e fallback offline

O cliente é construído para que a API de prompts nunca seja um ponto único de falha:

1. **Cache com TTL** (padrão 45 s, `ttl=` no construtor): buscas repetidas dentro da janela não custam nada.
2. **Revalidação por ETag**: expirado o TTL, o cliente revalida com `If-None-Match`; um prompt inalterado custa um 304, não um novo download.
3. **Último valor conhecido**: se a API estiver inacessível, o cliente registra um warning e continua servindo o último prompt buscado com sucesso (por chave nome/ambiente/fewshots), pela vida do processo.
4. **Arquivos de fallback locais**: se a API estiver inacessível *e* nada foi buscado ainda (ex.: logo após um cold start), o cliente carrega `<fallback_dir>/<nome>.<ambiente>.json`, depois `<fallback_dir>/<nome>.json` e, em último caso, `<fallback_dir>/<nome>.default.json` — assim um bundle com apenas snapshots `default` continua útil em `prod`/`staging`.

Só quando os quatro falham ele levanta `PromptUnavailableError`. O resultado obsoleto/de fallback também entra no cache com TTL, então uma indisponibilidade custa no máximo um timeout por janela de TTL, não um por chamada.

Gere os snapshots de fallback com a CLI incluída (commite-os no seu repositório ou embuta-os na sua imagem, atualizando a cada deploy):

```bash
uv run prompt-manager pull --app superchat --env prod --out prompts_fallback/
```

(`--url` sobrescreve o endereço da API; caso contrário valem `PROMPT_MANAGER_URL` / o padrão de produção, igual à biblioteca.)

## Relato de uso

Cada `render()`/`render_messages()` dispara (fire-and-forget) um relato para `POST /api/v1/usage` com a identidade do prompt (nome, versão, hash do snapshot) e os valores de variáveis com que foi preenchido. A UI do manager os lê de volta para que os prompts possam ser testados contra **casos reais de produção** em vez de casos inventados.

- Nunca bloqueia nem quebra um render — falhas são logadas em nível debug e descartadas.
- Passe `request_id`, `user` e `metadata` para tornar os casos gravados filtráveis.
- Desabilite completamente com `report_usage=False` (ex.: em testes), ou no servidor deixando `OPENSEARCH_HOST` vazio.

## Referência do construtor

| Parâmetro | Padrão | Significado |
|-----------|--------|-------------|
| `application` | *(obrigatório)* | Nome da aplicação registrada no manager |
| `base_url` | `$PROMPT_MANAGER_URL`, senão `http://promptmanager-prod.datalawyer.local` | Raiz da API do Prompt Manager |
| `environment` | `"default"` | Ambiente padrão de todas as chamadas |
| `ttl` | `45` | Segundos que um prompt buscado é servido sem revalidação |
| `fallback_dir` | `None` | Diretório com snapshots offline do `prompt-manager pull` |
| `timeout` | `10` | Timeout HTTP em segundos |
| `session` / `client` | `None` | Traga seu próprio `requests.Session` / `httpx.AsyncClient` |
| `report_usage` | `True` | Relata as variáveis de cada render para a tela de Registros |
