@danhachuel/thunderbolt 0.4.79 → 0.4.81
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.
- package/README.md +8 -0
- package/app/main.py +22 -0
- package/app/modules/niche_finder/summarizer.py +6 -1
- package/app/modules/token_optimizer/__init__.py +6 -0
- package/app/modules/token_optimizer/cache_manager.py +22 -0
- package/app/modules/token_optimizer/compressor.py +137 -0
- package/app/modules/token_optimizer/config.py +36 -0
- package/app/modules/token_optimizer/metrics.py +22 -0
- package/hermes_ui/provider_routing.py +4 -1
- package/package.json +1 -1
- package/requirements.txt +1 -0
package/README.md
CHANGED
|
@@ -443,3 +443,11 @@ Durante a execução, o backlog mostra a actividade recente do helper e o tempo
|
|
|
443
443
|
A mensagem do card de vídeo deixou de mostrar apenas os primeiros 240 caracteres. A UI apresenta um resumo de erro com até 700 caracteres e disponibiliza um expander não editável com o diagnóstico completo, incluindo os caminhos do log e do manifesto quando existem. Linhas de sucesso de Pexels, sincronização do projecto, verificação do `uv` e marcadores de caminho são excluídas do resumo terminal.
|
|
444
444
|
|
|
445
445
|
A verificação `uv sync --frozen` tem agora timeout controlado, detalhe seguro do resolver em caso de falha e confirmação explícita quando termina. O estado do manifesto é marcado como `failed` mesmo quando a falha acontece antes de a CLI do MoneyPrinterTurbo iniciar.
|
|
446
|
+
|
|
447
|
+
## Optimização nativa de tokens com jusTokenMax
|
|
448
|
+
|
|
449
|
+
O Thunderbolt integra o [jusTokenMax](https://github.com/Kalmantic/jusTokenMax) como optimizador local e reversível de contexto. Payloads JSON e transcrições muito extensas podem ser compactados antes de serem enviados aos providers LLM. Prompts criativos estruturados e conteúdos pequenos são preservados integralmente para evitar alteração semântica.
|
|
450
|
+
|
|
451
|
+
A configuração encontra-se em **Configuração API > API Keys > Optimização de tokens — jusTokenMax**. É possível activar ou desactivar o optimizador, controlar os levers por tipo, verificar a versão instalada, consultar métricas e limpar apenas os artefactos derivados. Os originais permanecem em `storage/state/token_optimizer_originals/` e podem ser recuperados pelo hash através do módulo interno.
|
|
452
|
+
|
|
453
|
+
A instalação do pacote inclui `justokenmax` a partir do repositório oficial. Se o CLI não estiver disponível ou ocorrer um erro, o Thunderbolt usa o conteúdo original e regista um fallback local; a geração não fica bloqueada. API keys, passwords, tokens e blobs base64 são redigidos antes de serem guardados em artefactos derivados.
|
package/app/main.py
CHANGED
|
@@ -44,6 +44,10 @@ from app.modules.niche_finder.apify import ApifyError, DEFAULT_ACTOR_ID, abort_a
|
|
|
44
44
|
from app.modules.niche_finder.core import NicheAnalysisError, run_niche_analysis
|
|
45
45
|
from app.modules.niche_finder.data_loader import DatasetError, download_kaggle_dataset
|
|
46
46
|
from app.modules.niche_finder.summarizer import summarize_items
|
|
47
|
+
from app.modules.token_optimizer.cache_manager import clear_derived_cache
|
|
48
|
+
from app.modules.token_optimizer.compressor import check_installation
|
|
49
|
+
from app.modules.token_optimizer.config import DEFAULTS as TOKEN_OPTIMIZER_DEFAULTS
|
|
50
|
+
from app.modules.token_optimizer.metrics import get_stats as get_token_optimizer_stats
|
|
47
51
|
from app.influencers_ui import render_ai_influencer_characters, render_ai_influencer_content, render_ai_influencers_api_status, render_motion_control, render_ugc_products
|
|
48
52
|
from hermes_ui.blueprints import create_blueprint_from_link, list_branding_files, save_generated_blueprint
|
|
49
53
|
from hermes_ui.thumbnail_blueprints import generate_thumbnail_blueprint, list_thumbnail_blueprint_documents, resolve_thumbnail_blueprint, save_thumbnail_blueprint, save_thumbnail_blueprint_pair, thumbnail_blueprint_catalog, thumbnail_blueprint_for_blueprint, thumbnail_blueprint_for_channel
|
|
@@ -7133,6 +7137,24 @@ def render_settings():
|
|
|
7133
7137
|
st.subheader("API Keys")
|
|
7134
7138
|
moneyprinter_path = str(settings.get("moneyprinter_path") or "").strip()
|
|
7135
7139
|
st.caption(f"Pasta do motor de vídeo: `{moneyprinter_path or 'não configurada'}`")
|
|
7140
|
+
with st.expander("Optimização de tokens — jusTokenMax", expanded=False):
|
|
7141
|
+
st.caption("Compressão local e reversível de contextos volumosos antes das chamadas LLM. Os originais permanecem guardados no storage local.")
|
|
7142
|
+
optimizer_enabled = st.checkbox("Activar optimizador", value=bool(settings.get("token_optimizer_enabled", True)), key="token_optimizer_enabled_ui")
|
|
7143
|
+
optimizer_cols = st.columns(4)
|
|
7144
|
+
optimizer_values = {}
|
|
7145
|
+
for index, (key, label) in enumerate((("json", "JSON/API"), ("log", "Logs"), ("pdf", "PDF"), ("csv", "CSV"), ("diff", "Diffs"), ("code", "Código"))):
|
|
7146
|
+
with optimizer_cols[index % 4]:
|
|
7147
|
+
optimizer_values[f"token_optimizer_{key}_enabled"] = st.checkbox(label, value=bool(settings.get(f"token_optimizer_{key}_enabled", True)), key=f"token_optimizer_{key}_ui")
|
|
7148
|
+
if st.button("Guardar optimizador", key="save_token_optimizer", use_container_width=True):
|
|
7149
|
+
settings.update({"token_optimizer_enabled": optimizer_enabled, **optimizer_values})
|
|
7150
|
+
write_json("settings.json", settings)
|
|
7151
|
+
st.success("Configuração do jusTokenMax guardada.")
|
|
7152
|
+
status = check_installation()
|
|
7153
|
+
st.caption(f"Estado: {'instalado' if status.get('installed') else 'não instalado'}{(' · versão ' + str(status.get('version'))) if status.get('version') else ''}")
|
|
7154
|
+
stats = get_token_optimizer_stats()
|
|
7155
|
+
st.caption(f"Contextos: {stats['calls']} · redução acumulada: {stats['reduction_percent']}% · fallbacks: {stats['fallbacks']}")
|
|
7156
|
+
if st.button("Limpar cache derivado", key="clear_token_optimizer_cache"):
|
|
7157
|
+
st.info(f"{clear_derived_cache()} artefacto(s) derivado(s) removido(s). Os originais da aplicação não foram apagados.")
|
|
7136
7158
|
with st.form("settings_form"):
|
|
7137
7159
|
with st.expander("Niche Finder — Kaggle", expanded=False):
|
|
7138
7160
|
st.caption("O dataset permanece no Kaggle. O Thunderbolt usa estas credenciais apenas para publicar/executar a kernel e obter os resultados pequenos da análise.")
|
|
@@ -4,6 +4,8 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
import requests
|
|
6
6
|
|
|
7
|
+
from app.modules.token_optimizer import compress_text
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
DEFAULT_BASE_URLS = {
|
|
9
11
|
"openai": "https://api.openai.com/v1",
|
|
@@ -54,13 +56,16 @@ def summarize_transcript(transcript: str, settings: dict[str, Any], *, timeout:
|
|
|
54
56
|
"esta estrutura: INTRODUÇÃO: um parágrafo sobre como o vídeo começa. ESTRUTURA: uma lista com "
|
|
55
57
|
"os pontos principais e subpontos da progressão do vídeo."
|
|
56
58
|
)
|
|
59
|
+
prepared_transcript = transcript
|
|
60
|
+
if len(transcript) >= 50000:
|
|
61
|
+
prepared_transcript = compress_text(transcript, "log", settings=settings).content
|
|
57
62
|
payload = {
|
|
58
63
|
"model": model,
|
|
59
64
|
"temperature": 0.2,
|
|
60
65
|
"max_tokens": 1200,
|
|
61
66
|
"messages": [
|
|
62
67
|
{"role": "system", "content": system_prompt},
|
|
63
|
-
{"role": "user", "content":
|
|
68
|
+
{"role": "user", "content": prepared_transcript[:120000]},
|
|
64
69
|
],
|
|
65
70
|
}
|
|
66
71
|
try:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Native jusTokenMax integration for Thunderbolt LLM contexts."""
|
|
2
|
+
|
|
3
|
+
from .compressor import TokenOptimizationResult, compress_text, compress_payload, check_installation, retrieve_original
|
|
4
|
+
from .metrics import get_stats
|
|
5
|
+
|
|
6
|
+
__all__ = ["TokenOptimizationResult", "compress_text", "compress_payload", "check_installation", "retrieve_original", "get_stats"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Reversible local cache management for jusTokenMax-derived contexts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from hermes_ui import storage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cache_directory() -> Path:
|
|
13
|
+
return storage.STATE / "token_optimizer_originals"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def clear_derived_cache() -> int:
|
|
17
|
+
directory = cache_directory()
|
|
18
|
+
if not directory.exists():
|
|
19
|
+
return 0
|
|
20
|
+
count = sum(1 for item in directory.iterdir() if item.is_file())
|
|
21
|
+
shutil.rmtree(directory, ignore_errors=True)
|
|
22
|
+
return count
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Safe adapter around the official jusTokenMax Python package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
|
|
12
|
+
from hermes_ui import storage
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class TokenOptimizationResult:
|
|
17
|
+
content: str
|
|
18
|
+
original_hash: str
|
|
19
|
+
before_chars: int
|
|
20
|
+
after_chars: int
|
|
21
|
+
kind: str
|
|
22
|
+
applied: bool
|
|
23
|
+
fallback: bool = False
|
|
24
|
+
note: str = ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _settings() -> dict[str, Any]:
|
|
28
|
+
value = storage.read_json("settings.json", {})
|
|
29
|
+
return value if isinstance(value, dict) else {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _enabled(settings: Mapping[str, Any], kind: str) -> bool:
|
|
33
|
+
if not bool(settings.get("token_optimizer_enabled", True)):
|
|
34
|
+
return False
|
|
35
|
+
return bool(settings.get(f"token_optimizer_{kind}_enabled", True))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _hash(content: str) -> str:
|
|
39
|
+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _record(result: TokenOptimizationResult) -> None:
|
|
43
|
+
path = storage.STATE / "token_optimizer_metrics.json"
|
|
44
|
+
current = storage.read_json("token_optimizer_metrics.json", {"calls": 0, "applied": 0, "fallbacks": 0, "before_chars": 0, "after_chars": 0})
|
|
45
|
+
if not isinstance(current, dict):
|
|
46
|
+
current = {"calls": 0, "applied": 0, "fallbacks": 0, "before_chars": 0, "after_chars": 0}
|
|
47
|
+
current["calls"] = int(current.get("calls", 0)) + 1
|
|
48
|
+
current["applied"] = int(current.get("applied", 0)) + int(result.applied)
|
|
49
|
+
current["fallbacks"] = int(current.get("fallbacks", 0)) + int(result.fallback)
|
|
50
|
+
current["before_chars"] = int(current.get("before_chars", 0)) + result.before_chars
|
|
51
|
+
current["after_chars"] = int(current.get("after_chars", 0)) + result.after_chars
|
|
52
|
+
storage.write_json(path.name, current)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _cache_original(content: str, digest: str) -> None:
|
|
56
|
+
directory = storage.STATE / "token_optimizer_originals"
|
|
57
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
target = directory / f"{digest}.txt"
|
|
59
|
+
if not target.exists():
|
|
60
|
+
target.write_text(content, encoding="utf-8")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _compress_with_jus_token_max(content: str, kind: str) -> str:
|
|
64
|
+
from justokenmax.jsoncompress import compress_json
|
|
65
|
+
from justokenmax.logs import compress_log
|
|
66
|
+
from justokenmax.redact import mask_secrets
|
|
67
|
+
|
|
68
|
+
redacted, _ = mask_secrets(content)
|
|
69
|
+
redacted = re.sub(
|
|
70
|
+
r'(["\'](?:api[_-]?key|token|password|secret)["\']\s*:\s*["\'])([^"\']+)(["\'])',
|
|
71
|
+
r'\1****\3', redacted, flags=re.IGNORECASE,
|
|
72
|
+
)
|
|
73
|
+
if kind == "json":
|
|
74
|
+
compressed, _stats = compress_json(redacted)
|
|
75
|
+
return compressed
|
|
76
|
+
if kind == "log":
|
|
77
|
+
compressed, _stats = compress_log(redacted)
|
|
78
|
+
return compressed
|
|
79
|
+
return redacted
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def compress_text(text: str, content_type: str = "auto", *, settings: Mapping[str, Any] | None = None) -> TokenOptimizationResult:
|
|
83
|
+
raw = str(text or "")
|
|
84
|
+
kind = str(content_type or "auto").strip().casefold()
|
|
85
|
+
if kind == "auto":
|
|
86
|
+
try:
|
|
87
|
+
json.loads(raw)
|
|
88
|
+
kind = "json"
|
|
89
|
+
except (TypeError, ValueError):
|
|
90
|
+
kind = "text"
|
|
91
|
+
if kind not in {"json", "log"} or not _enabled(settings or _settings(), kind):
|
|
92
|
+
result = TokenOptimizationResult(raw, _hash(raw), len(raw), len(raw), kind, False, note="disabled_or_unsupported")
|
|
93
|
+
_record(result)
|
|
94
|
+
return result
|
|
95
|
+
if len(raw) < 8000:
|
|
96
|
+
result = TokenOptimizationResult(raw, _hash(raw), len(raw), len(raw), kind, False, note="below_minimum_size")
|
|
97
|
+
_record(result)
|
|
98
|
+
return result
|
|
99
|
+
digest = _hash(raw)
|
|
100
|
+
try:
|
|
101
|
+
optimized = _compress_with_jus_token_max(raw, kind)
|
|
102
|
+
if not optimized or len(optimized) >= len(raw):
|
|
103
|
+
result = TokenOptimizationResult(raw, digest, len(raw), len(raw), kind, False, note="no_reduction")
|
|
104
|
+
else:
|
|
105
|
+
_cache_original(raw, digest)
|
|
106
|
+
result = TokenOptimizationResult(optimized, digest, len(raw), len(optimized), kind, True)
|
|
107
|
+
except (ImportError, OSError, RuntimeError, TypeError, ValueError) as exc:
|
|
108
|
+
result = TokenOptimizationResult(raw, digest, len(raw), len(raw), kind, False, True, f"jusTokenMax indisponível: {type(exc).__name__}")
|
|
109
|
+
_record(result)
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def compress_payload(payload: Any, *, settings: Mapping[str, Any] | None = None) -> TokenOptimizationResult:
|
|
114
|
+
raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
|
115
|
+
result = compress_text(raw, "json", settings=settings)
|
|
116
|
+
if result.applied:
|
|
117
|
+
return result
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def retrieve_original(original_hash: str) -> str | None:
|
|
122
|
+
digest = str(original_hash or "").strip().lower()
|
|
123
|
+
if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
|
|
124
|
+
return None
|
|
125
|
+
path = storage.STATE / "token_optimizer_originals" / f"{digest}.txt"
|
|
126
|
+
try:
|
|
127
|
+
return path.read_text(encoding="utf-8")
|
|
128
|
+
except OSError:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def check_installation() -> dict[str, Any]:
|
|
133
|
+
try:
|
|
134
|
+
import importlib.metadata
|
|
135
|
+
return {"installed": True, "version": importlib.metadata.version("justokenmax")}
|
|
136
|
+
except (ImportError, importlib.metadata.PackageNotFoundError):
|
|
137
|
+
return {"installed": False, "version": "", "message": "Instale o jusTokenMax para activar a compressão nativa."}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Persistent token optimizer settings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from hermes_ui import storage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULTS = {
|
|
11
|
+
"token_optimizer_enabled": True,
|
|
12
|
+
"token_optimizer_json_enabled": True,
|
|
13
|
+
"token_optimizer_log_enabled": True,
|
|
14
|
+
"token_optimizer_pdf_enabled": True,
|
|
15
|
+
"token_optimizer_csv_enabled": True,
|
|
16
|
+
"token_optimizer_diff_enabled": True,
|
|
17
|
+
"token_optimizer_code_enabled": True,
|
|
18
|
+
"token_optimizer_redact_enabled": True,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load(settings: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
23
|
+
result = dict(DEFAULTS)
|
|
24
|
+
source = settings if isinstance(settings, dict) else storage.read_json("settings.json", {})
|
|
25
|
+
if isinstance(source, dict):
|
|
26
|
+
result.update({key: source.get(key, value) for key, value in DEFAULTS.items()})
|
|
27
|
+
return result
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def save(values: dict[str, Any]) -> dict[str, Any]:
|
|
31
|
+
current = storage.read_json("settings.json", {})
|
|
32
|
+
if not isinstance(current, dict):
|
|
33
|
+
current = {}
|
|
34
|
+
current.update({key: bool(values.get(key, default)) for key, default in DEFAULTS.items()})
|
|
35
|
+
storage.write_json("settings.json", current)
|
|
36
|
+
return current
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Local metrics for token optimization effectiveness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from hermes_ui import storage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_stats() -> dict[str, Any]:
|
|
11
|
+
value = storage.read_json("token_optimizer_metrics.json", {})
|
|
12
|
+
stats = value if isinstance(value, dict) else {}
|
|
13
|
+
before = int(stats.get("before_chars", 0) or 0)
|
|
14
|
+
after = int(stats.get("after_chars", 0) or 0)
|
|
15
|
+
return {
|
|
16
|
+
"calls": int(stats.get("calls", 0) or 0),
|
|
17
|
+
"applied": int(stats.get("applied", 0) or 0),
|
|
18
|
+
"fallbacks": int(stats.get("fallbacks", 0) or 0),
|
|
19
|
+
"before_chars": before,
|
|
20
|
+
"after_chars": after,
|
|
21
|
+
"reduction_percent": round(max(0, before - after) * 100 / before, 2) if before else 0.0,
|
|
22
|
+
}
|
|
@@ -26,6 +26,7 @@ from .llm_providers import (
|
|
|
26
26
|
provider_definition,
|
|
27
27
|
)
|
|
28
28
|
from .storage import STORAGE, atomic_write, ensure_storage
|
|
29
|
+
from app.modules.token_optimizer import compress_text
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
POOL_LLM = "llm_text"
|
|
@@ -450,6 +451,8 @@ def route_json_request(
|
|
|
450
451
|
|
|
451
452
|
def route_llm_json(settings: Mapping[str, Any], system_prompt: str, user_prompt: str) -> RoutedResponse:
|
|
452
453
|
"""Send an OpenAI-compatible JSON chat call through the LLM pool."""
|
|
454
|
+
optimized_user_prompt = compress_text(user_prompt, "json", settings=settings) if len(str(user_prompt or "")) >= 50000 else None
|
|
455
|
+
prepared_user_prompt = optimized_user_prompt.content if optimized_user_prompt is not None else user_prompt
|
|
453
456
|
def request(card: dict[str, Any]) -> Any:
|
|
454
457
|
definition = provider_definition(card.get("provider"))
|
|
455
458
|
base_url = str(card.get("base_url") or definition.default_base_url).strip().rstrip("/")
|
|
@@ -462,7 +465,7 @@ def route_llm_json(settings: Mapping[str, Any], system_prompt: str, user_prompt:
|
|
|
462
465
|
"model": str(card.get("model") or "").strip(),
|
|
463
466
|
"messages": [
|
|
464
467
|
{"role": "system", "content": system_prompt},
|
|
465
|
-
{"role": "user", "content":
|
|
468
|
+
{"role": "user", "content": prepared_user_prompt},
|
|
466
469
|
],
|
|
467
470
|
"response_format": {"type": "json_object"},
|
|
468
471
|
}
|
package/package.json
CHANGED
package/requirements.txt
CHANGED