Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions lib/motores/PROVENIENCIA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Proveniência do motor vendorado

`crystalball_llm.py` neste diretório é **cópia byte-idêntica** de
`webapp/crystalball_llm.py` do repositório `sultowskigus/mk-crystalball`, no ref
`13a55d5`.

| | |
|---|---|
| origem | `sultowskigus/mk-crystalball`, `webapp/crystalball_llm.py` |
| ref | `13a55d5` |
| blob git | `22fbdd017aa948cd54ea3982568edd5f9fccee8c` |
| sha256 | `da73ba55d92dbff7af4c08670ff0776c71dd6f419cc7c5c3e8a70d212812d529` |
| tamanho | 161.252 bytes |
| copiado em | 2026-08-23 |

## Por que uma cópia, e não um checkout

O repositório de origem **não é da org `metaKosmos`**, então nem o CI deste repo
nem a outra estação alcançam o fonte sem credencial de terceiro. Um checkout
apontado por variável de ambiente é escopo de máquina: não viaja pelo git, e a
outra estação precisaria do próprio clone no ref certo. A cópia paga um arquivo
com dois donos para que o CI consiga testar o adaptador.

## Não edite este arquivo

Nenhuma linha. A checagem de proveniência é por blob, então um `ruff format`
sozinho quebraria a conferência sem mudar comportamento. É por isso que
`pyproject.toml` exclui `lib/motores/crystalball_llm.py` do relatório de estilo
mas **mantém** `E9` e `F`, que são erro de verdade.

Adaptação vive em `lib/motores/carga.py` e em `lib/reason_engines.py`.

## Como conferir que não derivou

```sh
test "$(git hash-object lib/motores/crystalball_llm.py)" \
= "$(gh api '/repos/sultowskigus/mk-crystalball/contents/webapp/crystalball_llm.py?ref=13a55d5' --jq .sha)" \
&& echo "em dia com 13a55d5"
```

Isso detecta deriva contra o ref fixado, não contra o `HEAD` de origem. Para
saber se o upstream andou:

```sh
gh api '/repos/sultowskigus/mk-crystalball/commits?path=webapp/crystalball_llm.py&per_page=1' --jq '.[0].sha'
```

Se o sha voltar diferente de `13a55d5`, alguém mexeu no motor. Quem sincroniza é
uma pessoa: **o Gustavo Sultowski é dono do conteúdo e do prompt.** O que segura
a sincronização automaticamente é o teste de forma em
`tests/test_reason_engines.py`, que falha se o contrato de retorno de qualquer
das quatro funções da lista fechada mudar.

## O que ficou de fora, de propósito

`webapp/store.py`, `webapp/app.py`, `webapp/config.py`, os `.bat`, a fila de
`threading` e o caminho de visão com `opencv`. O motor lê exatamente três tuplas
de `store.py` (`SUGESTOES_NARRATIVE`, `SUGESTOES_EMOTION`, `SUGESTOES_AUDIO`, 17
strings ao todo), e elas estão inlinadas em `carga.py`.
6 changes: 6 additions & 0 deletions lib/motores/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Motores de raciocínio vendorados, carregados sob demanda.

Nada aqui é importado na carga do pacote: o motor do Crystal Ball só entra em
memória quando um passo `kind: reason` com `motor: crystalball` roda. Ver
`carga.py` e `PROVENIENCIA.md`.
"""
157 changes: 157 additions & 0 deletions lib/motores/carga.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""Carrega o motor vendorado do Crystal Ball sem tocar `sys.path`.

Duas coisas o motor espera do mundo, e nenhuma delas pode virar dependência do
StudioLocal:

1. **`import store`** (`crystalball_llm.py:182`), de onde ele lê três tuplas de
vocabulário sugerido. Importar `webapp/store.py` de verdade arrastaria
`psycopg`, um `init_store()` que escreve `crystalball.db` ao lado do fonte e a
taxonomia inteira do blueprint, tudo por 123 caracteres de string. As três
tuplas entram aqui, copiadas de `webapp/store.py:69-71` no ref `13a55d5`.

2. **A chave do Gemini na variável `GEMINI_API_KEY`, no momento da CHAMADA.**
Isto é contraintuitivo e foi medido, não lido: `API_KEY, MODEL` da linha 37 do
motor são calculados na carga do módulo e **nunca consumidos**. Quem lê a
chave é `_load_config()`, chamado fresco dentro de `_call` (`:881`) e de
`generate_image` (`:1433`). Ou seja, definir a chave antes do import não faz
nada, e reatribuir `mod.API_KEY` também não. O que funciona é a variável estar
no ambiente enquanto a função roda, e é por isso que a chave entra por
`chave_ligada()`, um contexto, em vez de ficar largada no ambiente do processo.

`webapp/` nunca entra no `sys.path`: o módulo sintético é registrado direto em
`sys.modules`. E o motor é carregado com o nome `_crystalball_motor`, não
`crystalball_llm`, para que um `import crystalball_llm` acidental em qualquer
outro lugar do processo falhe em vez de pegar este por acaso.
"""

from __future__ import annotations

import contextlib
import importlib.util
import os
import sys
from pathlib import Path
from types import ModuleType

_AQUI = Path(__file__).resolve().parent
_FONTE = _AQUI / "crystalball_llm.py"
_NOME_INTERNO = "_crystalball_motor"

# Copiadas de webapp/store.py:69-71 no ref 13a55d5. Se o upstream mudar o
# vocabulário, o motor passa a sugerir termos diferentes dos que estão aqui: é
# deriva silenciosa, e é por isso que PROVENIENCIA.md manda conferir o blob.
_SUGESTOES = {
"SUGESTOES_NARRATIVE": ("linear", "loop", "reveal", "montagem", "demo", "depoimento"),
"SUGESTOES_EMOTION": ("humor", "surpresa", "desejo", "urgencia", "empatia", "orgulho"),
"SUGESTOES_AUDIO": ("locucao", "trilha", "som_ambiente", "silencio", "trend_audio"),
}

_MARCA = "_studiolocal_store_sintetico"


class MotorError(RuntimeError):
"""Falha ao carregar ou configurar o motor vendorado."""


def _store_sintetico() -> ModuleType:
mod = ModuleType("store")
mod.__doc__ = (
"Substituto mínimo de webapp/store.py, com as três tuplas de vocabulário "
"que crystalball_llm.py:182 lê. Ver lib/motores/carga.py."
)
for nome, valor in _SUGESTOES.items():
setattr(mod, nome, valor)
setattr(mod, _MARCA, True)
return mod


def _registrar_store() -> None:
"""Registra o `store` sintético, e falha alto se outro já ocupou o nome.

Sobrescrever um `store` de terceiro em silêncio trocaria o vocabulário do
motor por outro sem que nada aparecesse na saída, e `store` é um nome
genérico o bastante para colidir de verdade.
"""
existente = sys.modules.get("store")
if existente is not None:
if getattr(existente, _MARCA, False):
return
raise MotorError(
"sys.modules já tem um módulo 'store' que não é o sintético do "
f"StudioLocal ({getattr(existente, '__file__', '?')}). O motor do "
"Crystal Ball leria o vocabulário errado, então a carga para aqui."
)
sys.modules["store"] = _store_sintetico()


def motor() -> ModuleType:
"""Devolve o módulo do motor, carregado uma vez por processo.

Não recebe chave de propósito: a chave não é lida na carga (ver o docstring
do módulo). Para chamar qualquer função do motor, embrulhe em
`chave_ligada()`.
"""
if not _FONTE.exists():
raise MotorError(
f"motor vendorado ausente em {_FONTE}. Ver lib/motores/PROVENIENCIA.md."
)

_registrar_store()

mod = sys.modules.get(_NOME_INTERNO)
if mod is not None:
return mod

spec = importlib.util.spec_from_file_location(_NOME_INTERNO, _FONTE)
if spec is None or spec.loader is None:
raise MotorError(f"não foi possível montar o spec de {_FONTE}")
mod = importlib.util.module_from_spec(spec)
sys.modules[_NOME_INTERNO] = mod
try:
spec.loader.exec_module(mod)
except Exception:
sys.modules.pop(_NOME_INTERNO, None)
raise
return mod


@contextlib.contextmanager
def chave_ligada(gemini_key: str, modelo: str | None = None):
"""Põe a chave no ambiente pelo tempo da chamada, e a tira depois.

O motor relê o ambiente a cada chamada, então este é o único ponto de
entrada honesto. Sai restaurando o valor anterior, inclusive em exceção:
deixar credencial largada no ambiente de um processo que também roda outros
providers é vazamento lateral, mesmo que ninguém a leia hoje.

`modelo` fixa `CRYSTALBALL_MODEL`. Sem ele, o motor cai no default dele
(`gemini-2.5-flash`) ou no que um `webapp/config.py` importável disser, e o
segundo caso seria configuração vindo de fora sem ninguém pedir.
"""
if not (gemini_key or "").strip():
raise MotorError(
"chave do Gemini vazia. Sem ela `_call` levanta LLMError falando de "
"webapp/config.py, que não existe nesta instalação, e a mensagem "
"manda o operador para o lugar errado."
)
guardado: dict[str, str | None] = {}
for nome, valor in (("GEMINI_API_KEY", gemini_key), ("CRYSTALBALL_MODEL", modelo)):
if valor is None:
continue
guardado[nome] = os.environ.get(nome)
os.environ[nome] = valor
try:
mod = motor()
if not mod.has_key():
raise MotorError(
"o motor não vê a chave mesmo com GEMINI_API_KEY no ambiente, o "
"que significa que a forma de `_load_config` mudou no upstream. "
"Confira o blob contra o ref em lib/motores/PROVENIENCIA.md."
)
yield mod
finally:
for nome, anterior in guardado.items():
if anterior is None:
os.environ.pop(nome, None)
else:
os.environ[nome] = anterior
Loading
Loading