chatflow-agent 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/CHANGELOG.md +40 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/PKG-INFO +41 -1
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/README.md +40 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/pyproject.toml +1 -1
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/__init__.py +1 -1
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/channels/__init__.py +2 -1
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/channels/base.py +36 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/channels/telegram.py +27 -2
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/channels/whatsapp.py +54 -40
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/anthropic.py +57 -12
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/openai_compatible.py +28 -3
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/runner.py +11 -4
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/tools.py +26 -5
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_multi_engine.py +191 -2
- chatflow_agent-0.3.2/tests/test_runner_security.py +168 -0
- chatflow_agent-0.3.2/tests/test_telegram.py +239 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_tools.py +28 -0
- chatflow_agent-0.3.2/tests/test_whatsapp_security.py +406 -0
- chatflow_agent-0.3.0/tests/test_telegram.py +0 -82
- chatflow_agent-0.3.0/tests/test_whatsapp_security.py +0 -190
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/.github/workflows/publish.yml +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/.github/workflows/test.yml +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/.gitignore +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/LICENSE +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/cli_multiagent.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/local_gemma_cli.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/multi_provider_swarm.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/telegram_persistent_bot.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/telegram_triage.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/webhook_api_service.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/whatsapp_persistent_bot.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/whatsapp_service.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/channels/cli.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/__init__.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/agent.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engine.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/__init__.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/base.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/factory.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/gemini.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/memory.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/exceptions.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/memory.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/py.typed +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/types.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/__init__.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_agent.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_cli_channel.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_memory.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_memory_integrity.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_memory_sqlite.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_resilience.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_runner.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_runner_rollback.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_types.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_whatsapp.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_whatsapp_background.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_whatsapp_resilience_outbound.py +0 -0
- {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_whatsapp_split.py +0 -0
|
@@ -12,6 +12,26 @@ Versioning / Versionado: [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
|
|
|
12
12
|
|
|
13
13
|
## English
|
|
14
14
|
|
|
15
|
+
## [Unreleased]
|
|
16
|
+
|
|
17
|
+
## [0.3.2] - 2026-09-29
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
- **Tool Execution Error Sanitization (SEC-02):** Sanitized tool execution errors in `Runner` before exposing them to the LLM, returning a generic error with a unique `trace_id` UUID and logging the original traceback internally to prevent leakage of internal stack traces or environment variables (Issue #3).
|
|
21
|
+
- **Mask Phone Numbers in Exception Logs (SEC-03):** Automatically mask phone numbers (`_mask_phone`) in exception and fallback delivery logs in `WhatsAppChannel` for privacy compliance (Issue #5).
|
|
22
|
+
- **Admin-Only /reset in Telegram Groups (SEC-04):** Restricted the `/reset` command in Telegram group and supergroup chats strictly to chat creators and administrators (Issue #7).
|
|
23
|
+
- **Disabled FastAPI OpenAPI / Docs Endpoints by Default (SEC-05):** `WhatsAppChannel` now sets `docs_url=None`, `redoc_url=None`, and `openapi_url=None` by default to prevent internal schema and route exposure on public webhook endpoints. Interactive documentation can be optionally enabled for development via `enable_docs=True` (Issue #9).
|
|
24
|
+
- **Reverse Proxy Rate Limiting Recommendation (SEC-06):** Added production deployment guidance for placing Uvicorn behind a reverse proxy (e.g., Nginx with `limit_req_zone` or Cloudflare Rate Limiting) to protect `/webhook` and `/health` endpoints against flood attacks and CPU exhaustion from unauthenticated requests, avoiding in-app rate limiting that could drop legitimate bursts of Meta webhooks (Issue #10).
|
|
25
|
+
- **API Key Leakage Prevention on Custom Base URLs (SEC-07):** `OpenAIEngine` now prevents forwarding global environment credentials (`OPENAI_API_KEY`) to untrusted or custom remote endpoints. When a custom `base_url` (not matching official presets or localhost/127.0.0.1) is provided without an explicit `api_key`, a `ValueError` is raised, requiring an explicit `api_key` (or `api_key=""` if unauthenticated) (Issue #11).
|
|
26
|
+
- **Outbound Message Splitting in Telegram (SEC-08):** `TelegramChannel` now splits outbound responses exceeding Telegram's 4,096-character limit into progressive chunks using `split_message()`, preventing systematic delivery failures and error fallbacks on long LLM responses. `split_message()` now resides in `chatflow_agent.channels.base` and is re-exported from `whatsapp` for backwards compatibility (Issue #12).
|
|
27
|
+
- **Async vs Sync Tool Execution Clarification and Guard (SEC-09):** Documented that asynchronous tools must always be invoked via `await tool.execute_async(**kwargs)` in asynchronous environments (FastAPI, Runners, Telegram handlers). Calling `tool.execute()` on an async tool from within an active running event loop now immediately raises a clear `ToolExecutionError` explaining why synchronous re-entry of the running event loop is unsupported and directing the caller to `execute_async()` (Issue #13).
|
|
28
|
+
- **Retry and Backoff in AnthropicEngine (SEC-10):** `AnthropicEngine` now includes exponential backoff with jitter and retry handling for transient API errors (HTTP 429, 500, 502, 503, 504, 529 Overloaded, and connection errors `httpx.ConnectError`, `httpx.ConnectTimeout`). Honors the `Retry-After` header when provided (Issue #14).
|
|
29
|
+
|
|
30
|
+
## [0.3.1] - 2026-09-25
|
|
31
|
+
|
|
32
|
+
### Security
|
|
33
|
+
- **WhatsApp Webhook Request Body Size Limit (SEC-01):** Fixed vulnerability where inbound `POST /webhook` requests did not validate the payload size prior to buffering, enabling potential Denial of Service (DoS via Out-of-Memory / OOM). The endpoint now validates the `Content-Length` header before reading the body, enforcing a configurable limit (default: 1 MB via `DEFAULT_MAX_BODY_SIZE`) and rejecting oversized payloads with HTTP 413, missing headers with HTTP 411, and malformed headers with HTTP 400.
|
|
34
|
+
|
|
15
35
|
## [0.3.0] - 2026-09-21
|
|
16
36
|
|
|
17
37
|
### Security
|
|
@@ -47,6 +67,26 @@ If you are upgrading from v0.2.0:
|
|
|
47
67
|
|
|
48
68
|
## Español
|
|
49
69
|
|
|
70
|
+
## [Unreleased]
|
|
71
|
+
|
|
72
|
+
## [0.3.2] - 2026-09-29
|
|
73
|
+
|
|
74
|
+
### Seguridad
|
|
75
|
+
- **Sanitización de Errores de Tools (SEC-02):** Se sanitizaron los errores de ejecución de herramientas en `Runner` antes de exponerlos al LLM, retornando un mensaje genérico con un UUID `trace_id` único y registrando el traceback internamente para prevenir la fuga de stack traces o variables de entorno (Issue #3).
|
|
76
|
+
- **Enmascaramiento de Teléfonos en Logs (SEC-03):** Se enmascaran automáticamente los números telefónicos (`_mask_phone`) en logs de excepciones y fallbacks en `WhatsAppChannel` para cumplimiento de privacidad (Issue #5).
|
|
77
|
+
- **Comando /reset Exclusivo para Administradores en Telegram (SEC-04):** Se restringió la ejecución del comando `/reset` en grupos y supergrupos de Telegram exclusivamente a creadores y administradores (Issue #7).
|
|
78
|
+
- **Desactivación de Endpoints OpenAPI / Docs de FastAPI por Defecto (SEC-05):** `WhatsAppChannel` ahora configura `docs_url=None`, `redoc_url=None` y `openapi_url=None` por defecto para prevenir la exposición de rutas y esquemas internos en endpoints públicos de webhook. La documentación interactiva puede habilitarse opcionalmente para desarrollo mediante `enable_docs=True` (Issue #9).
|
|
79
|
+
- **Recomendación de Rate Limiting con Reverse Proxy (SEC-06):** Se añadió documentación de despliegue en producción recomendando ubicar Uvicorn detrás de un reverse proxy (como Nginx con `limit_req_zone` o Cloudflare Rate Limiting) para proteger los endpoints `/webhook` y `/health` contra ataques de inundación y agotamiento de CPU por peticiones no autenticadas, evitando rate limiting a nivel aplicación que pudiera descartar ráfagas legítimas de webhooks de Meta (Issue #10).
|
|
80
|
+
- **Prevención de Fuga de Claves API en Base URLs Personalizadas (SEC-07):** `OpenAIEngine` ahora previene el reenvío de credenciales globales de entorno (`OPENAI_API_KEY`) a endpoints remotos personalizados o no oficiales. Cuando se proporciona un `base_url` personalizado (que no coincide con los presets oficiales ni con localhost/127.0.0.1) sin una `api_key` explícita, se levanta un `ValueError`, exigiendo una `api_key` explícita (o `api_key=""` si no requiere autenticación) (Issue #11).
|
|
81
|
+
- **División de Mensajes Salientes en Telegram (SEC-08):** `TelegramChannel` ahora divide las respuestas salientes que superan el límite de 4.096 caracteres de Telegram en fragmentos progresivos usando `split_message()`, previniendo fallos sistemáticos de entrega y respuestas de error en respuestas extensas de LLM. `split_message()` ahora reside en `chatflow_agent.channels.base` y se reexporta desde `whatsapp` para retrocompatibilidad (Issue #12).
|
|
82
|
+
- **Clarificación y Validación de Ejecución de Tools Asíncronas vs Síncronas (SEC-09):** Se documentó que las herramientas asíncronas deben invocarse siempre mediante `await tool.execute_async(**kwargs)` en entornos asíncronos (FastAPI, Runners, handlers de Telegram). Ejecutar `tool.execute()` sobre una herramienta asíncrona dentro de un bucle de eventos activo ahora levanta inmediatamente un `ToolExecutionError` claro explicando que la reentrada síncrona al bucle de eventos no está soportada e indicando el uso de `execute_async()` (Issue #13).
|
|
83
|
+
- **Reintentos y Backoff Exponencial en AnthropicEngine (SEC-10):** `AnthropicEngine` ahora incluye reintentos automáticos con backoff exponencial y jitter ante errores transitorios de la API (HTTP 429, 500, 502, 503, 504, 529 Sobrecargado, y errores de conexión `httpx.ConnectError`, `httpx.ConnectTimeout`). Respeta la cabecera `Retry-After` cuando está presente (Issue #14).
|
|
84
|
+
|
|
85
|
+
## [0.3.1] - 2026-09-25
|
|
86
|
+
|
|
87
|
+
### Seguridad
|
|
88
|
+
- **Límite de Tamaño en Request Body del Webhook de WhatsApp (SEC-01):** Se corrigió una vulnerabilidad donde las peticiones entrantes `POST /webhook` no validaban el tamaño del payload antes de cargarlo en memoria, permitiendo ataques de Denegación de Servicio por agotamiento de memoria (DoS / OOM). Ahora se valida el encabezado `Content-Length` antes de leer el cuerpo, aplicando un límite configurable (por defecto 1 MB mediante `DEFAULT_MAX_BODY_SIZE`) y rechazando payloads excesivos con HTTP 413, encabezados ausentes con HTTP 411 y encabezados malformados con HTTP 400.
|
|
89
|
+
|
|
50
90
|
## [0.3.0] - 2026-09-21
|
|
51
91
|
|
|
52
92
|
### Seguridad
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: chatflow-agent
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Lightweight, async multi-agent framework with native handoffs for Gemini, Gemma, Grok, OpenAI & Claude.
|
|
5
5
|
Project-URL: Homepage, https://github.com/GustavoBaranda/chatflow-agent
|
|
6
6
|
Project-URL: Source Code, https://github.com/GustavoBaranda/chatflow-agent
|
|
@@ -583,6 +583,26 @@ if __name__ == "__main__":
|
|
|
583
583
|
|
|
584
584
|
> **Testing locally?** Use a tunnel like [ngrok](https://ngrok.com) (`ngrok http 8000`) or Cloudflare Tunnels to provide Meta with a public HTTPS URL (`https://your-domain.ngrok-free.app/webhook`).
|
|
585
585
|
|
|
586
|
+
> [!NOTE]
|
|
587
|
+
> **API Documentation in Development:** For security, FastAPI interactive documentation (`/docs`, `/redoc`, and `/openapi.json`) is disabled by default in production. To enable Swagger UI and OpenAPI schemas during local development or debugging, pass `enable_docs=True` when instantiating `WhatsAppChannel`:
|
|
588
|
+
> ```python
|
|
589
|
+
> channel = WhatsAppChannel(
|
|
590
|
+
> ...,
|
|
591
|
+
> enable_docs=True, # Enables /docs, /redoc, and /openapi.json for local development
|
|
592
|
+
> )
|
|
593
|
+
> ```
|
|
594
|
+
|
|
595
|
+
> [!TIP]
|
|
596
|
+
> **Production Rate Limiting & Reverse Proxy:** To protect `/webhook` and `/health` endpoints against flood attacks, denial of service (DoS), and CPU exhaustion from unauthenticated requests without risking dropping legitimate Meta webhook bursts at the application level, always place Uvicorn behind a reverse proxy in production (such as Nginx with `limit_req_zone` or Cloudflare Rate Limiting):
|
|
597
|
+
> ```nginx
|
|
598
|
+
> limit_req_zone $binary_remote_addr zone=whatsapp_limit:10m rate=10r/s;
|
|
599
|
+
> location /webhook {
|
|
600
|
+
> limit_req zone=whatsapp_limit burst=20 nodelay;
|
|
601
|
+
> proxy_pass http://127.0.0.1:8000;
|
|
602
|
+
> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
603
|
+
> }
|
|
604
|
+
> ```
|
|
605
|
+
|
|
586
606
|
---
|
|
587
607
|
|
|
588
608
|
#### Telegram Channel
|
|
@@ -1197,6 +1217,26 @@ if __name__ == "__main__":
|
|
|
1197
1217
|
|
|
1198
1218
|
> **¿Probando localmente?** Usa un tunel como [ngrok](https://ngrok.com) (`ngrok http 8000`) o Cloudflare Tunnels para brindarle a Meta la URL publica HTTPS (`https://tu-dominio.ngrok-free.app/webhook`).
|
|
1199
1219
|
|
|
1220
|
+
> [!NOTE]
|
|
1221
|
+
> **Documentación interactiva en desarrollo:** Por motivos de seguridad, la documentación interactiva de FastAPI (`/docs`, `/redoc` y `/openapi.json`) está deshabilitada por defecto en producción. Para habilitar Swagger UI y los esquemas OpenAPI durante el desarrollo local o debugging, pasa `enable_docs=True` al instanciar `WhatsAppChannel`:
|
|
1222
|
+
> ```python
|
|
1223
|
+
> canal_whatsapp = WhatsAppChannel(
|
|
1224
|
+
> ...,
|
|
1225
|
+
> enable_docs=True, # Habilita /docs, /redoc y /openapi.json para desarrollo local
|
|
1226
|
+
> )
|
|
1227
|
+
> ```
|
|
1228
|
+
|
|
1229
|
+
> [!TIP]
|
|
1230
|
+
> **Rate Limiting y Reverse Proxy en Producción:** Para proteger los endpoints `/webhook` y `/health` contra ataques de inundación, denegación de servicio (DoS) y agotamiento de CPU por peticiones no autenticadas sin riesgo de descartar ráfagas legítimas de webhooks de Meta a nivel aplicación, ubica siempre Uvicorn detrás de un reverse proxy en producción (como Nginx con `limit_req_zone` o Cloudflare Rate Limiting):
|
|
1231
|
+
> ```nginx
|
|
1232
|
+
> limit_req_zone $binary_remote_addr zone=whatsapp_limit:10m rate=10r/s;
|
|
1233
|
+
> location /webhook {
|
|
1234
|
+
> limit_req zone=whatsapp_limit burst=20 nodelay;
|
|
1235
|
+
> proxy_pass http://127.0.0.1:8000;
|
|
1236
|
+
> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
1237
|
+
> }
|
|
1238
|
+
> ```
|
|
1239
|
+
|
|
1200
1240
|
---
|
|
1201
1241
|
|
|
1202
1242
|
#### Canal de Telegram
|
|
@@ -537,6 +537,26 @@ if __name__ == "__main__":
|
|
|
537
537
|
|
|
538
538
|
> **Testing locally?** Use a tunnel like [ngrok](https://ngrok.com) (`ngrok http 8000`) or Cloudflare Tunnels to provide Meta with a public HTTPS URL (`https://your-domain.ngrok-free.app/webhook`).
|
|
539
539
|
|
|
540
|
+
> [!NOTE]
|
|
541
|
+
> **API Documentation in Development:** For security, FastAPI interactive documentation (`/docs`, `/redoc`, and `/openapi.json`) is disabled by default in production. To enable Swagger UI and OpenAPI schemas during local development or debugging, pass `enable_docs=True` when instantiating `WhatsAppChannel`:
|
|
542
|
+
> ```python
|
|
543
|
+
> channel = WhatsAppChannel(
|
|
544
|
+
> ...,
|
|
545
|
+
> enable_docs=True, # Enables /docs, /redoc, and /openapi.json for local development
|
|
546
|
+
> )
|
|
547
|
+
> ```
|
|
548
|
+
|
|
549
|
+
> [!TIP]
|
|
550
|
+
> **Production Rate Limiting & Reverse Proxy:** To protect `/webhook` and `/health` endpoints against flood attacks, denial of service (DoS), and CPU exhaustion from unauthenticated requests without risking dropping legitimate Meta webhook bursts at the application level, always place Uvicorn behind a reverse proxy in production (such as Nginx with `limit_req_zone` or Cloudflare Rate Limiting):
|
|
551
|
+
> ```nginx
|
|
552
|
+
> limit_req_zone $binary_remote_addr zone=whatsapp_limit:10m rate=10r/s;
|
|
553
|
+
> location /webhook {
|
|
554
|
+
> limit_req zone=whatsapp_limit burst=20 nodelay;
|
|
555
|
+
> proxy_pass http://127.0.0.1:8000;
|
|
556
|
+
> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
557
|
+
> }
|
|
558
|
+
> ```
|
|
559
|
+
|
|
540
560
|
---
|
|
541
561
|
|
|
542
562
|
#### Telegram Channel
|
|
@@ -1151,6 +1171,26 @@ if __name__ == "__main__":
|
|
|
1151
1171
|
|
|
1152
1172
|
> **¿Probando localmente?** Usa un tunel como [ngrok](https://ngrok.com) (`ngrok http 8000`) o Cloudflare Tunnels para brindarle a Meta la URL publica HTTPS (`https://tu-dominio.ngrok-free.app/webhook`).
|
|
1153
1173
|
|
|
1174
|
+
> [!NOTE]
|
|
1175
|
+
> **Documentación interactiva en desarrollo:** Por motivos de seguridad, la documentación interactiva de FastAPI (`/docs`, `/redoc` y `/openapi.json`) está deshabilitada por defecto en producción. Para habilitar Swagger UI y los esquemas OpenAPI durante el desarrollo local o debugging, pasa `enable_docs=True` al instanciar `WhatsAppChannel`:
|
|
1176
|
+
> ```python
|
|
1177
|
+
> canal_whatsapp = WhatsAppChannel(
|
|
1178
|
+
> ...,
|
|
1179
|
+
> enable_docs=True, # Habilita /docs, /redoc y /openapi.json para desarrollo local
|
|
1180
|
+
> )
|
|
1181
|
+
> ```
|
|
1182
|
+
|
|
1183
|
+
> [!TIP]
|
|
1184
|
+
> **Rate Limiting y Reverse Proxy en Producción:** Para proteger los endpoints `/webhook` y `/health` contra ataques de inundación, denegación de servicio (DoS) y agotamiento de CPU por peticiones no autenticadas sin riesgo de descartar ráfagas legítimas de webhooks de Meta a nivel aplicación, ubica siempre Uvicorn detrás de un reverse proxy en producción (como Nginx con `limit_req_zone` o Cloudflare Rate Limiting):
|
|
1185
|
+
> ```nginx
|
|
1186
|
+
> limit_req_zone $binary_remote_addr zone=whatsapp_limit:10m rate=10r/s;
|
|
1187
|
+
> location /webhook {
|
|
1188
|
+
> limit_req zone=whatsapp_limit burst=20 nodelay;
|
|
1189
|
+
> proxy_pass http://127.0.0.1:8000;
|
|
1190
|
+
> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
1191
|
+
> }
|
|
1192
|
+
> ```
|
|
1193
|
+
|
|
1154
1194
|
---
|
|
1155
1195
|
|
|
1156
1196
|
#### Canal de Telegram
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "chatflow-agent"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Lightweight, async multi-agent framework with native handoffs for Gemini, Gemma, Grok, OpenAI & Claude."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Communication channels for chatflow-agent: CLI, WhatsApp, Telegram, and Webhook."""
|
|
2
2
|
|
|
3
|
-
from chatflow_agent.channels.base import BaseChannel, ChannelError
|
|
3
|
+
from chatflow_agent.channels.base import BaseChannel, ChannelError, split_message
|
|
4
4
|
from chatflow_agent.channels.cli import CLIChannel
|
|
5
5
|
from chatflow_agent.channels.telegram import TelegramChannel
|
|
6
6
|
from chatflow_agent.channels.whatsapp import WhatsAppChannel
|
|
@@ -11,4 +11,5 @@ __all__ = [
|
|
|
11
11
|
"ChannelError",
|
|
12
12
|
"TelegramChannel",
|
|
13
13
|
"WhatsAppChannel",
|
|
14
|
+
"split_message",
|
|
14
15
|
]
|
|
@@ -12,6 +12,42 @@ class ChannelError(ChatFlowError):
|
|
|
12
12
|
"""Raised when an operation on a channel fails or runner is detached."""
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def split_message(text: str, max_len: int = 4096) -> list[str]:
|
|
16
|
+
"""Split long outbound text into sequential chunks within max_len.
|
|
17
|
+
|
|
18
|
+
Splits progressively by paragraph (\n\n), line (\n), sentence (. ), or word ( )
|
|
19
|
+
to preserve formatting and readability across messaging channels.
|
|
20
|
+
"""
|
|
21
|
+
if len(text) <= max_len:
|
|
22
|
+
return [text]
|
|
23
|
+
|
|
24
|
+
chunks: list[str] = []
|
|
25
|
+
remaining = text
|
|
26
|
+
|
|
27
|
+
while len(remaining) > max_len:
|
|
28
|
+
candidate = remaining[:max_len]
|
|
29
|
+
split_idx = -1
|
|
30
|
+
|
|
31
|
+
for sep in ("\n\n", "\n", ". ", " "):
|
|
32
|
+
idx = candidate.rfind(sep)
|
|
33
|
+
if idx != -1:
|
|
34
|
+
split_idx = idx + len(sep)
|
|
35
|
+
break
|
|
36
|
+
|
|
37
|
+
if split_idx <= 0:
|
|
38
|
+
split_idx = max_len
|
|
39
|
+
|
|
40
|
+
chunk = remaining[:split_idx].rstrip()
|
|
41
|
+
if chunk:
|
|
42
|
+
chunks.append(chunk)
|
|
43
|
+
remaining = remaining[split_idx:].lstrip()
|
|
44
|
+
|
|
45
|
+
if remaining:
|
|
46
|
+
chunks.append(remaining)
|
|
47
|
+
|
|
48
|
+
return chunks
|
|
49
|
+
|
|
50
|
+
|
|
15
51
|
class BaseChannel(ABC):
|
|
16
52
|
"""Abstract base class for all communication channels (CLI, WhatsApp, Telegram, Webhook)."""
|
|
17
53
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from chatflow_agent.channels.base import BaseChannel
|
|
6
|
+
from chatflow_agent.channels.base import BaseChannel, split_message
|
|
7
7
|
from chatflow_agent.exceptions import DependencyError
|
|
8
8
|
from chatflow_agent.types import AgentResponse
|
|
9
9
|
|
|
@@ -68,6 +68,29 @@ class TelegramChannel(BaseChannel):
|
|
|
68
68
|
if not update.effective_chat or not update.message:
|
|
69
69
|
return
|
|
70
70
|
|
|
71
|
+
# Restrict /reset in group and supergroup chats to creators and administrators
|
|
72
|
+
if getattr(update.effective_chat, "type", None) in ("group", "supergroup"):
|
|
73
|
+
is_admin = False
|
|
74
|
+
if update.effective_user and hasattr(context, "bot") and context.bot:
|
|
75
|
+
try:
|
|
76
|
+
member = await context.bot.get_chat_member(
|
|
77
|
+
chat_id=update.effective_chat.id,
|
|
78
|
+
user_id=update.effective_user.id,
|
|
79
|
+
)
|
|
80
|
+
if getattr(member, "status", None) in ("creator", "administrator"):
|
|
81
|
+
is_admin = True
|
|
82
|
+
except Exception as err:
|
|
83
|
+
logger.error(
|
|
84
|
+
f"Failed to verify chat member permissions for /reset: {err}"
|
|
85
|
+
)
|
|
86
|
+
is_admin = False
|
|
87
|
+
|
|
88
|
+
if not is_admin:
|
|
89
|
+
await update.message.reply_text(
|
|
90
|
+
"Only group admins can reset the conversation."
|
|
91
|
+
)
|
|
92
|
+
return
|
|
93
|
+
|
|
71
94
|
chat_id = str(update.effective_chat.id)
|
|
72
95
|
if self.runner:
|
|
73
96
|
session = self.runner.get_session(chat_id)
|
|
@@ -103,7 +126,9 @@ class TelegramChannel(BaseChannel):
|
|
|
103
126
|
"username": user_name,
|
|
104
127
|
},
|
|
105
128
|
)
|
|
106
|
-
|
|
129
|
+
chunks = split_message(response.content, max_len=4096)
|
|
130
|
+
for chunk in chunks:
|
|
131
|
+
await update.message.reply_text(chunk)
|
|
107
132
|
except Exception as err:
|
|
108
133
|
logger.error(f"Error processing Telegram message: {err}")
|
|
109
134
|
await update.message.reply_text(self.fallback_message)
|
|
@@ -8,13 +8,15 @@ import json
|
|
|
8
8
|
import logging
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
from chatflow_agent.channels.base import BaseChannel
|
|
11
|
+
from chatflow_agent.channels.base import BaseChannel, split_message
|
|
12
12
|
from chatflow_agent.core.memory import DEFAULT_DEDUP_TTL_HOURS
|
|
13
13
|
from chatflow_agent.exceptions import DependencyError
|
|
14
14
|
from chatflow_agent.types import AgentResponse
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger("chatflow_agent.whatsapp")
|
|
17
17
|
|
|
18
|
+
DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024 # 1 MB maximum allowed webhook payload size
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
def _mask_phone(phone: str) -> str:
|
|
20
22
|
"""Mask phone number for GDPR/privacy compliance in logs (e.g. 5491***678)."""
|
|
@@ -23,40 +25,7 @@ def _mask_phone(phone: str) -> str:
|
|
|
23
25
|
return f"{phone[:4]}***{phone[-3:]}"
|
|
24
26
|
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
"""Split long outbound text into sequential chunks within max_len.
|
|
28
|
-
|
|
29
|
-
Splits progressively by paragraph (\n\n), line (\n), sentence (. ), or word ( )
|
|
30
|
-
to preserve formatting and readability across messaging channels.
|
|
31
|
-
"""
|
|
32
|
-
if len(text) <= max_len:
|
|
33
|
-
return [text]
|
|
34
|
-
|
|
35
|
-
chunks: list[str] = []
|
|
36
|
-
remaining = text
|
|
37
|
-
|
|
38
|
-
while len(remaining) > max_len:
|
|
39
|
-
candidate = remaining[:max_len]
|
|
40
|
-
split_idx = -1
|
|
41
|
-
|
|
42
|
-
for sep in ("\n\n", "\n", ". ", " "):
|
|
43
|
-
idx = candidate.rfind(sep)
|
|
44
|
-
if idx != -1:
|
|
45
|
-
split_idx = idx + len(sep)
|
|
46
|
-
break
|
|
47
|
-
|
|
48
|
-
if split_idx <= 0:
|
|
49
|
-
split_idx = max_len
|
|
50
|
-
|
|
51
|
-
chunk = remaining[:split_idx].rstrip()
|
|
52
|
-
if chunk:
|
|
53
|
-
chunks.append(chunk)
|
|
54
|
-
remaining = remaining[split_idx:].lstrip()
|
|
55
|
-
|
|
56
|
-
if remaining:
|
|
57
|
-
chunks.append(remaining)
|
|
58
|
-
|
|
59
|
-
return chunks
|
|
28
|
+
__all__ = ["WhatsAppChannel", "split_message"]
|
|
60
29
|
|
|
61
30
|
|
|
62
31
|
class WhatsAppChannel(BaseChannel):
|
|
@@ -83,6 +52,8 @@ class WhatsAppChannel(BaseChannel):
|
|
|
83
52
|
outbound_max_delay: float = 30.0,
|
|
84
53
|
on_24h_window_expired: Any | None = None,
|
|
85
54
|
dedup_ttl_hours: float = DEFAULT_DEDUP_TTL_HOURS,
|
|
55
|
+
max_body_size: int = DEFAULT_MAX_BODY_SIZE,
|
|
56
|
+
enable_docs: bool = False,
|
|
86
57
|
) -> None:
|
|
87
58
|
super().__init__()
|
|
88
59
|
# SEC-01: Fail-closed — require app_secret when signature verification is active.
|
|
@@ -112,6 +83,8 @@ class WhatsAppChannel(BaseChannel):
|
|
|
112
83
|
self.outbound_max_delay = outbound_max_delay
|
|
113
84
|
self.on_24h_window_expired = on_24h_window_expired
|
|
114
85
|
self.dedup_ttl_hours = dedup_ttl_hours
|
|
86
|
+
self.max_body_size = max_body_size
|
|
87
|
+
self.enable_docs = enable_docs
|
|
115
88
|
|
|
116
89
|
# Verify optional dependencies
|
|
117
90
|
try:
|
|
@@ -126,7 +99,16 @@ class WhatsAppChannel(BaseChannel):
|
|
|
126
99
|
|
|
127
100
|
self._httpx = httpx
|
|
128
101
|
self._uvicorn = uvicorn
|
|
129
|
-
|
|
102
|
+
# SEC-05: Disable OpenAPI schema, Swagger UI (/docs) and ReDoc (/redoc) by default in production
|
|
103
|
+
docs_url = "/docs" if enable_docs else None
|
|
104
|
+
redoc_url = "/redoc" if enable_docs else None
|
|
105
|
+
openapi_url = "/openapi.json" if enable_docs else None
|
|
106
|
+
self.app = FastAPI(
|
|
107
|
+
title="ChatFlow WhatsApp Webhook Server",
|
|
108
|
+
docs_url=docs_url,
|
|
109
|
+
redoc_url=redoc_url,
|
|
110
|
+
openapi_url=openapi_url,
|
|
111
|
+
)
|
|
130
112
|
self._setup_routes()
|
|
131
113
|
|
|
132
114
|
def _setup_routes(self) -> None:
|
|
@@ -164,8 +146,39 @@ class WhatsAppChannel(BaseChannel):
|
|
|
164
146
|
async def handle_incoming_message(
|
|
165
147
|
request: Request, background_tasks: BackgroundTasks
|
|
166
148
|
) -> Response:
|
|
167
|
-
"""Validate HMAC, deduplicate by wamid, and dispatch processing to background tasks."""
|
|
149
|
+
"""Validate Content-Length, HMAC, deduplicate by wamid, and dispatch processing to background tasks."""
|
|
150
|
+
# SEC-01: Validate Content-Length before buffering body to prevent OOM DoS
|
|
151
|
+
content_length_header = request.headers.get("Content-Length")
|
|
152
|
+
if not content_length_header:
|
|
153
|
+
raise HTTPException(
|
|
154
|
+
status_code=status.HTTP_411_LENGTH_REQUIRED,
|
|
155
|
+
detail="Missing Content-Length header",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
content_length = int(content_length_header)
|
|
160
|
+
except (ValueError, TypeError) as err:
|
|
161
|
+
raise HTTPException(
|
|
162
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
163
|
+
detail="Invalid Content-Length header",
|
|
164
|
+
) from err
|
|
165
|
+
|
|
166
|
+
status_413 = getattr(
|
|
167
|
+
status, "HTTP_413_CONTENT_TOO_LARGE", status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if content_length < 0 or content_length > self.max_body_size:
|
|
171
|
+
raise HTTPException(
|
|
172
|
+
status_code=status_413,
|
|
173
|
+
detail=f"Payload size exceeds maximum limit of {self.max_body_size} bytes",
|
|
174
|
+
)
|
|
175
|
+
|
|
168
176
|
raw_body = await request.body()
|
|
177
|
+
if len(raw_body) > self.max_body_size:
|
|
178
|
+
raise HTTPException(
|
|
179
|
+
status_code=status_413,
|
|
180
|
+
detail=f"Payload body exceeds maximum limit of {self.max_body_size} bytes",
|
|
181
|
+
)
|
|
169
182
|
|
|
170
183
|
# SEC-01: Validate X-Hub-Signature-256 when verify_signature is active
|
|
171
184
|
if self.verify_signature and self.app_secret:
|
|
@@ -273,6 +286,7 @@ class WhatsAppChannel(BaseChannel):
|
|
|
273
286
|
self, wamid: str, sender_phone: str, user_text: str | None
|
|
274
287
|
) -> None:
|
|
275
288
|
"""Process one WhatsApp message asynchronously with full anti-500 error shield."""
|
|
289
|
+
masked_phone = _mask_phone(sender_phone)
|
|
276
290
|
try:
|
|
277
291
|
if user_text is None:
|
|
278
292
|
if self.access_token and self.phone_number_id:
|
|
@@ -293,12 +307,12 @@ class WhatsAppChannel(BaseChannel):
|
|
|
293
307
|
reply_text = response.content
|
|
294
308
|
except asyncio.TimeoutError:
|
|
295
309
|
logger.error(
|
|
296
|
-
f"LLM dispatch timed out after {self.llm_timeout_seconds}s for wamid={wamid} from {
|
|
310
|
+
f"LLM dispatch timed out after {self.llm_timeout_seconds}s for wamid={wamid} from {masked_phone}"
|
|
297
311
|
)
|
|
298
312
|
reply_text = self.fallback_message
|
|
299
313
|
except Exception as err:
|
|
300
314
|
logger.exception(
|
|
301
|
-
f"Error dispatching wamid={wamid} from {
|
|
315
|
+
f"Error dispatching wamid={wamid} from {masked_phone}: {err}"
|
|
302
316
|
)
|
|
303
317
|
reply_text = self.fallback_message
|
|
304
318
|
|
|
@@ -314,7 +328,7 @@ class WhatsAppChannel(BaseChannel):
|
|
|
314
328
|
)
|
|
315
329
|
except Exception as fb_err:
|
|
316
330
|
logger.exception(
|
|
317
|
-
f"Failed to deliver fallback to {
|
|
331
|
+
f"Failed to deliver fallback to {masked_phone}: {fb_err}"
|
|
318
332
|
)
|
|
319
333
|
|
|
320
334
|
def _extract_message_and_sender(
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"""Anthropic Claude engine for chatflow-agent."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import json
|
|
5
|
+
import logging
|
|
4
6
|
import os
|
|
7
|
+
import random
|
|
5
8
|
from typing import TYPE_CHECKING, Any
|
|
6
9
|
|
|
7
10
|
import httpx
|
|
@@ -11,6 +14,8 @@ from chatflow_agent.core.engines.openai_compatible import _lowercase_schema_type
|
|
|
11
14
|
from chatflow_agent.exceptions import ProviderError
|
|
12
15
|
from chatflow_agent.types import Message, Role, ToolCall
|
|
13
16
|
|
|
17
|
+
logger = logging.getLogger("chatflow_agent.engine.anthropic")
|
|
18
|
+
|
|
14
19
|
if TYPE_CHECKING:
|
|
15
20
|
from chatflow_agent.core.agent import Agent
|
|
16
21
|
|
|
@@ -30,6 +35,9 @@ class AnthropicEngine(BaseEngine):
|
|
|
30
35
|
self.base_url = base_url.rstrip("/")
|
|
31
36
|
self.anthropic_version = anthropic_version
|
|
32
37
|
self.timeout = timeout
|
|
38
|
+
self.max_retries = 3
|
|
39
|
+
self.base_delay = 1.0
|
|
40
|
+
self.max_delay = 10.0
|
|
33
41
|
self._external_client = http_client
|
|
34
42
|
|
|
35
43
|
def _format_history(self, history: list[Message]) -> list[dict[str, Any]]:
|
|
@@ -115,18 +123,55 @@ class AnthropicEngine(BaseEngine):
|
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
url = f"{self.base_url}/messages"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
|
|
127
|
+
retries = 0
|
|
128
|
+
while True:
|
|
129
|
+
client = self._external_client or httpx.AsyncClient(timeout=self.timeout)
|
|
130
|
+
try:
|
|
131
|
+
resp = await client.post(url, json=payload, headers=headers)
|
|
132
|
+
if resp.status_code in (429, 500, 502, 503, 504, 529):
|
|
133
|
+
if retries < self.max_retries:
|
|
134
|
+
retry_after_str = resp.headers.get("Retry-After")
|
|
135
|
+
parsed_delay = None
|
|
136
|
+
if retry_after_str:
|
|
137
|
+
try:
|
|
138
|
+
parsed_delay = float(retry_after_str)
|
|
139
|
+
except (ValueError, TypeError):
|
|
140
|
+
parsed_delay = None
|
|
141
|
+
|
|
142
|
+
base = parsed_delay if parsed_delay is not None else self.base_delay * (2 ** retries)
|
|
143
|
+
delay = min(self.max_delay, base + random.uniform(0.0, 0.25 * base))
|
|
144
|
+
logger.warning(
|
|
145
|
+
f"Anthropic returned HTTP {resp.status_code}. Retrying in {delay:.2f}s (attempt {retries + 1}/{self.max_retries})..."
|
|
146
|
+
)
|
|
147
|
+
await asyncio.sleep(delay)
|
|
148
|
+
retries += 1
|
|
149
|
+
continue
|
|
150
|
+
raise ProviderError(
|
|
151
|
+
f"Anthropic API error ({resp.status_code}): {resp.text}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if resp.status_code >= 400:
|
|
155
|
+
raise ProviderError(f"Anthropic API error ({resp.status_code}): {resp.text}")
|
|
156
|
+
|
|
157
|
+
data = resp.json()
|
|
158
|
+
break
|
|
159
|
+
except (httpx.ConnectError, httpx.ConnectTimeout) as net_err:
|
|
160
|
+
if retries < self.max_retries:
|
|
161
|
+
base = self.base_delay * (2 ** retries)
|
|
162
|
+
delay = min(self.max_delay, base + random.uniform(0.0, 0.25 * base))
|
|
163
|
+
logger.warning(
|
|
164
|
+
f"Anthropic connect error: {net_err}. Retrying in {delay:.2f}s..."
|
|
165
|
+
)
|
|
166
|
+
await asyncio.sleep(delay)
|
|
167
|
+
retries += 1
|
|
168
|
+
continue
|
|
169
|
+
raise ProviderError(f"Anthropic connection error: {net_err}") from net_err
|
|
170
|
+
except httpx.RequestError as err:
|
|
171
|
+
raise ProviderError(f"Anthropic connection error: {err}") from err
|
|
172
|
+
finally:
|
|
173
|
+
if not self._external_client:
|
|
174
|
+
await client.aclose()
|
|
130
175
|
|
|
131
176
|
text_chunks: list[str] = []
|
|
132
177
|
extracted_calls: list[ToolCall] = []
|
{chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/openai_compatible.py
RENAMED
|
@@ -96,10 +96,35 @@ class OpenAIEngine(BaseEngine):
|
|
|
96
96
|
or preset.get("base_url", "https://api.openai.com/v1")
|
|
97
97
|
).rstrip("/")
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
official_base_urls = {
|
|
100
|
+
p["base_url"].rstrip("/") for p in PROVIDER_PRESETS.values()
|
|
101
|
+
}
|
|
102
|
+
is_official = self.base_url in official_base_urls
|
|
103
|
+
is_local = any(
|
|
104
|
+
host in self.base_url for host in ("localhost", "127.0.0.1", "0.0.0.0")
|
|
102
105
|
)
|
|
106
|
+
|
|
107
|
+
env_key_name = preset.get("env_key", "OPENAI_API_KEY")
|
|
108
|
+
|
|
109
|
+
self.api_key: str | None
|
|
110
|
+
if api_key is not None:
|
|
111
|
+
self.api_key = api_key
|
|
112
|
+
elif is_local:
|
|
113
|
+
# For local services (Ollama, LM Studio, vLLM on localhost), do not leak global cloud API keys
|
|
114
|
+
self.api_key = (
|
|
115
|
+
os.environ.get(env_key_name)
|
|
116
|
+
if self.provider in ("ollama", "gemma")
|
|
117
|
+
else None
|
|
118
|
+
) or "ollama"
|
|
119
|
+
elif is_official:
|
|
120
|
+
self.api_key = os.environ.get(env_key_name)
|
|
121
|
+
else:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"A custom or unofficial base_url was provided ({self.base_url!r}) without an explicit api_key. "
|
|
124
|
+
f"To prevent accidental leakage of your global {env_key_name} to untrusted endpoints, "
|
|
125
|
+
"fallback to environment API keys is disabled. "
|
|
126
|
+
"Please provide api_key explicitly (or pass api_key='' if no authentication is required)."
|
|
127
|
+
)
|
|
103
128
|
self.timeout = timeout
|
|
104
129
|
self.max_retries = 3
|
|
105
130
|
self.base_delay = 1.0
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Multi-agent Swarm-style execution Runner for chatflow-agent."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
4
6
|
from typing import Any
|
|
5
7
|
|
|
6
8
|
from chatflow_agent.core.agent import Agent
|
|
@@ -15,6 +17,8 @@ from chatflow_agent.types import (
|
|
|
15
17
|
ToolResult,
|
|
16
18
|
)
|
|
17
19
|
|
|
20
|
+
logger = logging.getLogger("chatflow_agent.runner")
|
|
21
|
+
|
|
18
22
|
|
|
19
23
|
class Runner:
|
|
20
24
|
"""Orchestrates multi-agent conversation loops, tool calling, and autonomous handoffs."""
|
|
@@ -122,11 +126,16 @@ class Runner:
|
|
|
122
126
|
try:
|
|
123
127
|
raw_output = await tool_instance.execute_async(**call.args)
|
|
124
128
|
except Exception as err:
|
|
129
|
+
trace_id = str(uuid.uuid4())
|
|
130
|
+
logger.error(
|
|
131
|
+
f"Tool execution failed [trace_id: {trace_id}] - Tool: '{call.name}' on agent '{current_agent.name}': {err}",
|
|
132
|
+
exc_info=True,
|
|
133
|
+
)
|
|
125
134
|
tool_results.append(
|
|
126
135
|
ToolResult(
|
|
127
136
|
tool_call_id=call.id,
|
|
128
137
|
name=call.name,
|
|
129
|
-
content=f"
|
|
138
|
+
content=f"Tool execution failed [trace_id: {trace_id}]. Please try a different approach or inform the user.",
|
|
130
139
|
is_error=True,
|
|
131
140
|
)
|
|
132
141
|
)
|
|
@@ -226,9 +235,7 @@ class Runner:
|
|
|
226
235
|
self.run_async(session_id, user_message, metadata=metadata)
|
|
227
236
|
)
|
|
228
237
|
except RuntimeError:
|
|
229
|
-
return asyncio.run(
|
|
230
|
-
self.run_async(session_id, user_message, metadata=metadata)
|
|
231
|
-
)
|
|
238
|
+
return asyncio.run(self.run_async(session_id, user_message, metadata=metadata))
|
|
232
239
|
|
|
233
240
|
def __repr__(self) -> str:
|
|
234
241
|
return (
|