@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 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": transcript[:120000]},
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": user_prompt},
468
+ {"role": "user", "content": prepared_user_prompt},
466
469
  ],
467
470
  "response_format": {"type": "json_object"},
468
471
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.79",
3
+ "version": "0.4.81",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",
package/requirements.txt CHANGED
@@ -23,3 +23,4 @@ supabase>=2.4,<3
23
23
  bilibili-api-python>=17.4,<18
24
24
  playwright>=1.45,<2
25
25
  composio>=0.21,<1
26
+ justokenmax @ https://github.com/Kalmantic/jusTokenMax/archive/refs/heads/main.zip#subdirectory=python