@danhachuel/thunderbolt 0.4.56 → 0.4.57

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.
@@ -825,3 +825,13 @@ Na interface, uma tarefa em execução mostra a última actividade do helper e o
825
825
  A partir da versão 0.3.78, o card **Backlog Vídeos** mostra a causa terminal e disponibiliza **Ver diagnóstico completo**. As mensagens `Pexels key validation completed`, `installing or verifying project dependencies with uv` e `TASK_DIR` são apenas progresso ou metadados; não são, por si só, uma falha.
826
826
 
827
827
  Se o `uv` falhar, o erro inclui o detalhe seguro do resolver. Se a CLI do MoneyPrinterTurbo falhar, o resumo inclui `MPT_ERROR` e mantém `Log completo` e `Manifesto`. A interface não exibe chaves, cookies ou tokens. O tempo limite da verificação de dependências é de 15 minutos; a etapa é encerrada em vez de ficar indefinidamente sem resposta.
828
+
829
+ ## 18. Upload via Composio
830
+
831
+ Em **Pipeline Vídeos > Upload > Upload via Composio**, o Thunderbolt permite descobrir ferramentas de upload disponíveis no projecto Composio Platform, seleccionar um toolkit e enviar um vídeo pronto da pipeline. Como cada provider expõe ferramentas e campos diferentes, o slug da ferramenta e o campo que recebe o vídeo são escolhidos explicitamente na interface; nenhum destino é assumido automaticamente.
832
+
833
+ Configure primeiro **Configurações > Configuração API > API Keys Upload > Composio**. Active a integração, introduza a API key de projecto Composio Platform e defina um `user ID` estável para associar as contas conectadas. A chave permanece no storage local e não é incluída no histórico, nos logs nem no repositório. O botão de teste faz apenas descoberta read-only.
834
+
835
+ Quando o provider ainda não estiver conectado, use **Autorizar toolkit no Composio**. O Thunderbolt apresenta o Connect Link devolvido pelo Composio; conclua a autorização nesse link e repita o envio. O Thunderbolt não implementa nem armazena um fluxo OAuth próprio.
836
+
837
+ Antes de clicar em **Enviar vídeo via Composio**, confirme o vídeo, o toolkit, o slug da ferramenta, o campo do ficheiro e os argumentos JSON. O resultado guarda apenas um registo local mínimo com o estado e, quando devolvido, o `log_id` do Composio. URLs presigned, tokens, headers e a API key não são guardados no histórico.
package/app/main.py CHANGED
@@ -98,6 +98,7 @@ from integrations.youtube_batch import account_key as youtube_batch_account_key,
98
98
  from integrations.local_runtime import MoneyPrinterRuntime
99
99
  from integrations.moneyprinter_config import sync_moneyprinter_config
100
100
  from integrations.openai_model_discovery import DEFAULT_NVIDIA_NIM_BASE_URL
101
+ from integrations.composio_upload import ComposioUploadError, authorize_toolkit, discover_tools, execute_upload, parse_arguments, test_configuration
101
102
 
102
103
  DEFAULT_UI_LANGUAGE = "en"
103
104
 
@@ -5302,7 +5303,7 @@ def render_music_upload() -> None:
5302
5303
 
5303
5304
  def render_upload():
5304
5305
  st.title("Upload")
5305
- upload_tab, direct_tab, postiz_tab, upload_post_tab = render_localized_tabs(["Upload convencional", "Upload directo", "Postiz", "Upload-Post"])
5306
+ upload_tab, direct_tab, postiz_tab, upload_post_tab, composio_tab = render_localized_tabs(["Upload convencional", "Upload directo", "Postiz", "Upload-Post", "Upload via Composio"])
5306
5307
  with direct_tab:
5307
5308
  render_upload_direct()
5308
5309
  with postiz_tab:
@@ -5311,6 +5312,81 @@ def render_upload():
5311
5312
  render_upload_post()
5312
5313
  with upload_tab:
5313
5314
  render_upload_conventional()
5315
+ with composio_tab:
5316
+ render_upload_composio()
5317
+
5318
+
5319
+ def render_upload_composio():
5320
+ st.subheader("Upload via Composio")
5321
+ st.caption("Descubra uma ferramenta de upload Composio, ligue a conta do provider quando necessário e envie um vídeo seleccionado. O slug e o campo do ficheiro são sempre escolhidos explicitamente.")
5322
+ settings = read_json("settings.json", {})
5323
+ api_key = str(settings.get("composio_api_key") or "").strip()
5324
+ user_id = str(settings.get("composio_user_id") or "").strip()
5325
+ if not settings.get("composio_enabled", False) or not api_key:
5326
+ st.warning("Composio está desactivado ou sem API key. Configure-o em Configuração API > API Keys Upload > Composio.")
5327
+ return
5328
+ query = st.text_input("Pesquisar ferramenta Composio", value="upload a video file", key="composio_upload_query")
5329
+ toolkit_filter = st.text_input("Toolkit preferido (opcional)", value=str(settings.get("composio_toolkit") or ""), key="composio_upload_toolkit")
5330
+ if st.button("Descobrir ferramentas", key="composio_upload_discover", use_container_width=True):
5331
+ try:
5332
+ st.session_state["composio_upload_tools"] = discover_tools(api_key, user_id, query, toolkit_filter)
5333
+ if not st.session_state["composio_upload_tools"]:
5334
+ st.info("Nenhuma ferramenta encontrada. Tente uma pesquisa mais específica ou deixe o toolkit vazio.")
5335
+ else:
5336
+ st.success(f"{len(st.session_state['composio_upload_tools'])} ferramenta(s) encontrada(s).")
5337
+ except ComposioUploadError as exc:
5338
+ st.error(str(exc))
5339
+ tools = [item for item in st.session_state.get("composio_upload_tools", []) if isinstance(item, dict) and item.get("slug")]
5340
+ if not tools:
5341
+ st.info("Clique em Descobrir ferramentas para carregar os destinos disponíveis na sua conta Composio.")
5342
+ return
5343
+ tool_slugs = [str(item["slug"]) for item in tools]
5344
+ selected_slug = st.selectbox("Ferramenta de upload", tool_slugs, format_func=lambda slug: next((f"{item.get('name') or slug} — {item.get('toolkit') or 'toolkit desconhecido'}" for item in tools if item.get("slug") == slug), slug), key="composio_upload_slug")
5345
+ selected_tool = next((item for item in tools if item.get("slug") == selected_slug), tools[0])
5346
+ if selected_tool.get("description"):
5347
+ st.caption(str(selected_tool["description"]))
5348
+ schema = selected_tool.get("schema") or {}
5349
+ with st.expander("Schema da ferramenta", expanded=False):
5350
+ st.json(schema)
5351
+ file_field = st.text_input("Campo que recebe o vídeo", value=str(st.session_state.get("composio_upload_file_field") or "file"), key="composio_upload_file_field")
5352
+ arguments_json = st.text_area("Argumentos JSON adicionais", value=str(st.session_state.get("composio_upload_arguments") or "{}"), height=150, key="composio_upload_arguments")
5353
+ if st.button("Autorizar toolkit no Composio", key="composio_upload_authorize", use_container_width=True):
5354
+ try:
5355
+ auth = authorize_toolkit(api_key, user_id, str(selected_tool.get("toolkit") or ""))
5356
+ if auth.get("redirect_url"):
5357
+ st.success("Connect Link criado. Abra o link para autorizar a conta e depois repita o envio.")
5358
+ st.markdown(f"[Abrir Connect Link do Composio]({auth['redirect_url']})")
5359
+ else:
5360
+ st.info("O Composio não devolveu um Connect Link; confirme o estado da conta no dashboard.")
5361
+ except ComposioUploadError as exc:
5362
+ st.error(str(exc))
5363
+ tasks = [task for task in read_json("tasks.json", []) if task.get("state") == "done" or task.get("artifacts", {}).get("video")]
5364
+ if not tasks:
5365
+ st.info("Não há vídeos prontos para enviar via Composio.")
5366
+ return
5367
+ task_options = {str(task.get("id")): task for task in tasks if task.get("id")}
5368
+ selected_task_id = st.selectbox("Vídeo", list(task_options), format_func=lambda task_id: f"{task_options[task_id].get('topic') or 'Vídeo Thunderbolt'} — {task_options[task_id].get('artifacts', {}).get('video') or 'sem ficheiro'}", key="composio_upload_task")
5369
+ task = task_options[selected_task_id]
5370
+ video_path = str((task.get("artifacts", {}) or {}).get("video") or "")
5371
+ st.caption(video_path or "Sem caminho de vídeo registado")
5372
+ st.info(f"Resumo: `{selected_slug}` · toolkit `{selected_tool.get('toolkit') or 'não indicado'}` · campo `{file_field or 'não indicado'}`")
5373
+ if st.button("Enviar vídeo via Composio", type="primary", key=f"composio_upload_send_{selected_task_id}", use_container_width=True):
5374
+ try:
5375
+ parse_arguments(arguments_json)
5376
+ result = execute_upload(api_key, user_id, selected_slug, video_path, file_field, arguments_json)
5377
+ record = {"id": uuid.uuid4().hex, "task_id": task.get("id"), "destination": "Composio", "target": {"toolkit": selected_tool.get("toolkit"), "slug": selected_slug, "file_field": file_field}, "status": "published" if result.get("successful") else "failed", "message": result.get("error") or "Upload Composio concluído.", "data": result.get("data") or {}, "log_id": result.get("log_id") or "", "created_at": now()}
5378
+ uploads = read_json("uploads.json", [])
5379
+ uploads.append(record)
5380
+ write_json("uploads.json", uploads)
5381
+ reconcile_persisted_notifications()
5382
+ if result.get("successful"):
5383
+ st.success(record["message"])
5384
+ else:
5385
+ st.error(record["message"])
5386
+ if result.get("log_id"):
5387
+ st.caption(f"Composio log ID: {result['log_id']}")
5388
+ except ComposioUploadError as exc:
5389
+ st.error(str(exc))
5314
5390
 
5315
5391
 
5316
5392
  def render_upload_postiz():
@@ -7089,7 +7165,29 @@ def render_settings():
7089
7165
  with st.expander("API Bilibili", expanded=False):
7090
7166
  render_bilibili_api_cards(settings)
7091
7167
  with st.expander("Composio", expanded=False):
7092
- st.info("Secção reservada para uma futura integração Composio.")
7168
+ st.caption("A API key é guardada apenas no storage local. O slug da ferramenta e o provider são descobertos no Upload via Composio; a autenticação da conta é feita pelo Connect Link do Composio.")
7169
+ composio_enabled = st.checkbox("Activar Composio", value=bool(settings.get("composio_enabled", False)), key="upload_composio_enabled")
7170
+ composio_api_key = st.text_input("Composio API key", value=str(settings.get("composio_api_key") or ""), type="password", key="upload_composio_api_key", help="Use a API key de projecto do Composio Platform. Nunca coloque esta chave no GitHub, em URLs ou em mensagens.")
7171
+ composio_user_id = st.text_input("Composio user ID", value=str(settings.get("composio_user_id") or "thunderbolt-local"), key="upload_composio_user_id", help="Identidade estável usada para associar as contas conectadas no Composio.")
7172
+ composio_toolkit = st.text_input("Toolkit preferido (opcional)", value=str(settings.get("composio_toolkit") or ""), key="upload_composio_toolkit", help="Deixe vazio para descobrir ferramentas em todos os toolkits.")
7173
+ _render_credential_status(composio_api_key)
7174
+ composio_action_cols = st.columns(2)
7175
+ with composio_action_cols[0]:
7176
+ save_composio = st.button("Guardar Composio", type="primary", use_container_width=True, key="upload_composio_save")
7177
+ with composio_action_cols[1]:
7178
+ test_composio = st.button("Testar configuração", use_container_width=True, key="upload_composio_test")
7179
+ if save_composio:
7180
+ settings.update({"composio_enabled": bool(composio_enabled), "composio_api_key": composio_api_key.strip(), "composio_user_id": composio_user_id.strip() or "thunderbolt-local", "composio_toolkit": composio_toolkit.strip()})
7181
+ write_json("settings.json", settings)
7182
+ st.success("Configuração Composio guardada.")
7183
+ st.rerun()
7184
+ if test_composio:
7185
+ try:
7186
+ result = test_configuration(composio_api_key, composio_user_id)
7187
+ found = len((result.get("data") or {}).get("tools", []))
7188
+ st.success(f"Configuração Composio válida; {found} ferramenta(s) de upload encontradas.")
7189
+ except ComposioUploadError as exc:
7190
+ st.error(str(exc))
7093
7191
  with st.expander("Upload-Post", expanded=False):
7094
7192
  upload_post_enabled_upload = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)), key="upload_tab_upload_post_enabled")
7095
7193
  upload_post_key_upload = st.text_input("Upload-Post API key", value=str(settings.get("upload_post_api_key") or ""), type="password", key="upload_tab_upload_post_key")
@@ -0,0 +1,174 @@
1
+ """Composio Platform adapter for user-selected video upload tools."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ class ComposioUploadError(ValueError):
10
+ """Safe, user-facing validation or integration error."""
11
+
12
+
13
+ def _safe_value(value: Any) -> Any:
14
+ if value is None or isinstance(value, (str, int, float, bool)):
15
+ return value
16
+ if isinstance(value, dict):
17
+ return {str(key): _safe_value(item) for key, item in value.items()}
18
+ if isinstance(value, (list, tuple)):
19
+ return [_safe_value(item) for item in value]
20
+ for method in ("model_dump", "to_dict", "dict"):
21
+ converter = getattr(value, method, None)
22
+ if callable(converter):
23
+ try:
24
+ return _safe_value(converter())
25
+ except Exception:
26
+ pass
27
+ attributes = getattr(value, "__dict__", None)
28
+ if isinstance(attributes, dict):
29
+ return {str(key): _safe_value(item) for key, item in attributes.items()}
30
+ return str(value)
31
+
32
+
33
+ def _tool_item(tool: Any) -> dict[str, Any]:
34
+ raw = _safe_value(tool)
35
+ if isinstance(raw, dict) and isinstance(raw.get("function"), dict):
36
+ function = raw["function"]
37
+ return {
38
+ "slug": str(function.get("name") or raw.get("name") or ""),
39
+ "name": str(function.get("name") or raw.get("name") or ""),
40
+ "description": str(function.get("description") or raw.get("description") or ""),
41
+ "toolkit": str(raw.get("toolkit") or ""),
42
+ "schema": function.get("parameters") or {},
43
+ }
44
+ if isinstance(raw, dict):
45
+ toolkit = raw.get("toolkit")
46
+ if isinstance(toolkit, dict):
47
+ toolkit = toolkit.get("slug") or toolkit.get("name") or ""
48
+ return {
49
+ "slug": str(raw.get("slug") or raw.get("name") or ""),
50
+ "name": str(raw.get("name") or raw.get("slug") or ""),
51
+ "description": str(raw.get("description") or ""),
52
+ "toolkit": str(toolkit or ""),
53
+ "schema": raw.get("input_parameters") or raw.get("parameters") or {},
54
+ }
55
+ return {"slug": "", "name": "", "description": "", "toolkit": "", "schema": {}}
56
+
57
+
58
+ def _response(result: Any) -> dict[str, Any]:
59
+ raw = _safe_value(result)
60
+ if not isinstance(raw, dict):
61
+ raw = {"data": raw}
62
+ error = raw.get("error")
63
+ return {
64
+ "successful": not bool(error),
65
+ "data": raw.get("data") or {},
66
+ "error": str(error) if error else "",
67
+ "log_id": str(raw.get("log_id") or raw.get("request_id") or ""),
68
+ }
69
+
70
+
71
+ def _require_api_key(api_key: str) -> str:
72
+ value = str(api_key or "").strip()
73
+ if len(value) < 10 or value.lower() in {"placeholder", "your_composio_api_key"}:
74
+ raise ComposioUploadError("Configure uma API key válida do Composio em Configuração API > API Keys Upload > Composio.")
75
+ return value
76
+
77
+
78
+ def _require_user_id(user_id: str) -> str:
79
+ value = str(user_id or "").strip()
80
+ if not value:
81
+ raise ComposioUploadError("Indique um Composio user ID estável antes de continuar.")
82
+ return value
83
+
84
+
85
+ def _client(api_key: str, *, upload_dir: Path | None = None):
86
+ try:
87
+ from composio import Composio
88
+ except ImportError as exc:
89
+ raise ComposioUploadError("A SDK Python composio não está instalada. Execute a instalação do Thunderbolt novamente.") from exc
90
+ kwargs: dict[str, Any] = {"api_key": _require_api_key(api_key), "allow_tracking": False}
91
+ if upload_dir is not None:
92
+ kwargs.update({
93
+ "dangerously_allow_auto_upload_download_files": True,
94
+ "file_upload_dirs": [str(upload_dir.resolve())],
95
+ })
96
+ return Composio(**kwargs)
97
+
98
+
99
+ def discover_tools(api_key: str, user_id: str, query: str, toolkit: str = "") -> list[dict[str, Any]]:
100
+ client = _client(api_key)
101
+ try:
102
+ tools = client.tools.get(
103
+ _require_user_id(user_id),
104
+ search=(query or "upload a video file").strip(),
105
+ toolkits=[toolkit.strip()] if toolkit.strip() else None,
106
+ limit=10,
107
+ )
108
+ return [item for item in (_tool_item(tool) for tool in tools) if item.get("slug")]
109
+ except ComposioUploadError:
110
+ raise
111
+ except Exception as exc:
112
+ raise ComposioUploadError(f"Não foi possível descobrir ferramentas Composio: {type(exc).__name__}: {exc}") from exc
113
+
114
+
115
+ def authorize_toolkit(api_key: str, user_id: str, toolkit: str) -> dict[str, Any]:
116
+ toolkit = str(toolkit or "").strip()
117
+ if not toolkit:
118
+ raise ComposioUploadError("Seleccione uma ferramenta descoberta para saber qual toolkit deve ser autorizado.")
119
+ client = _client(api_key)
120
+ try:
121
+ request = client.create(user_id=_require_user_id(user_id)).authorize(toolkit)
122
+ return {"connected_account_id": str(getattr(request, "id", "") or ""), "redirect_url": str(getattr(request, "redirect_url", "") or "")}
123
+ except ComposioUploadError:
124
+ raise
125
+ except Exception as exc:
126
+ raise ComposioUploadError(f"Não foi possível criar o Connect Link do Composio: {type(exc).__name__}: {exc}") from exc
127
+
128
+
129
+ def parse_arguments(arguments_json: str) -> dict[str, Any]:
130
+ try:
131
+ parsed = json.loads(arguments_json or "{}")
132
+ except json.JSONDecodeError as exc:
133
+ raise ComposioUploadError(f"Os argumentos da ferramenta não são JSON válido: {exc.msg}.") from exc
134
+ if not isinstance(parsed, dict):
135
+ raise ComposioUploadError("Os argumentos da ferramenta devem ser um objecto JSON.")
136
+ return parsed
137
+
138
+
139
+ def execute_upload(api_key: str, user_id: str, slug: str, video_path: str, file_field: str, arguments_json: str = "") -> dict[str, Any]:
140
+ path = Path(str(video_path or "").strip()).expanduser()
141
+ slug = str(slug or "").strip()
142
+ file_field = str(file_field or "").strip()
143
+ if not path.is_file():
144
+ raise ComposioUploadError("O ficheiro de vídeo seleccionado não existe ou não é um ficheiro.")
145
+ if path.stat().st_size <= 0:
146
+ raise ComposioUploadError("O ficheiro de vídeo seleccionado está vazio.")
147
+ if not slug:
148
+ raise ComposioUploadError("Seleccione ou indique o slug de uma ferramenta Composio.")
149
+ if not file_field:
150
+ raise ComposioUploadError("Indique o campo de argumentos que recebe o ficheiro.")
151
+ arguments = parse_arguments(arguments_json)
152
+ if file_field in arguments and arguments[file_field] not in (None, "", str(path)):
153
+ raise ComposioUploadError(f"O campo `{file_field}` já contém um valor. Remova-o antes de injectar o vídeo.")
154
+ arguments[file_field] = str(path.resolve())
155
+ client = _client(api_key, upload_dir=path.parent)
156
+ try:
157
+ result = client.tools.execute(
158
+ slug,
159
+ arguments=arguments,
160
+ user_id=_require_user_id(user_id),
161
+ version="latest",
162
+ dangerously_skip_version_check=True,
163
+ )
164
+ return _response(result)
165
+ except ComposioUploadError:
166
+ raise
167
+ except Exception as exc:
168
+ safe_message = str(exc).replace(str(api_key), "[REDACTED]")
169
+ raise ComposioUploadError(f"A ferramenta Composio falhou: {type(exc).__name__}: {safe_message}") from exc
170
+
171
+
172
+ def test_configuration(api_key: str, user_id: str) -> dict[str, Any]:
173
+ tools = discover_tools(api_key, user_id, "upload a video file")
174
+ return {"successful": True, "data": {"tools": tools}, "error": "", "log_id": ""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.56",
3
+ "version": "0.4.57",
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
@@ -22,3 +22,4 @@ kagglehub>=0.3,<1
22
22
  supabase>=2.4,<3
23
23
  bilibili-api-python>=17.4,<18
24
24
  playwright>=1.45,<2
25
+ composio>=0.21,<1
package/scripts/cli.mjs CHANGED
@@ -116,7 +116,7 @@ function check() {
116
116
  console.error(`Python não encontrado. Execute: npx.cmd --yes @danhachuel/thunderbolt install`);
117
117
  process.exit(1);
118
118
  }
119
- const requiredModules = ["streamlit", "requests", "pandas", "toml", "imageio_ffmpeg", "edge_tts", "sklearn", "mlxtend", "plotly", "seaborn", "kagglehub"];
119
+ const requiredModules = ["streamlit", "requests", "pandas", "toml", "imageio_ffmpeg", "edge_tts", "sklearn", "mlxtend", "plotly", "seaborn", "kagglehub", "composio"];
120
120
  const missing = requiredModules.filter((moduleName) => !moduleAvailable(moduleName));
121
121
  const ffmpeg = moduleAvailable("imageio_ffmpeg");
122
122
  const mptPath = configuredMoneyPrinterPath();