From b5e203ddfe185a8281dd9c2e27de97d7963a5fdc Mon Sep 17 00:00:00 2001 From: Berack96 Date: Thu, 2 Oct 2025 10:58:26 +0200 Subject: [PATCH 01/19] Aggiungi supporto per il bot Telegram: aggiorna .env.example, pyproject.toml e uv.lock --- .env.example | 11 ++++++++ docs/Telegram_Integration_plan.md | 46 +++++++++++++++++++++++++++++++ pyproject.toml | 1 + uv.lock | 14 ++++++++++ 4 files changed, 72 insertions(+) create mode 100644 docs/Telegram_Integration_plan.md diff --git a/.env.example b/.env.example index fd9a427..ce6f756 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ # https://makersuite.google.com/app/apikey GOOGLE_API_KEY= + ############################################################################### # Configurazioni per gli agenti di mercato ############################################################################### @@ -21,6 +22,7 @@ CRYPTOCOMPARE_API_KEY= BINANCE_API_KEY= BINANCE_API_SECRET= + ############################################################################### # Configurazioni per gli agenti di notizie ############################################################################### @@ -31,6 +33,7 @@ NEWS_API_KEY= # https://cryptopanic.com/developers/api/ CRYPTOPANIC_API_KEY= + ############################################################################### # Configurazioni per API di social media ############################################################################### @@ -38,3 +41,11 @@ CRYPTOPANIC_API_KEY= # https://www.reddit.com/prefs/apps REDDIT_API_CLIENT_ID= REDDIT_API_CLIENT_SECRET= + + +############################################################################### +# Configurazioni per API di messaggistica +############################################################################### + +# https://core.telegram.org/bots/features#creating-a-new-bot +TELEGRAM_BOT_TOKEN= diff --git a/docs/Telegram_Integration_plan.md b/docs/Telegram_Integration_plan.md new file mode 100644 index 0000000..8f477f6 --- /dev/null +++ b/docs/Telegram_Integration_plan.md @@ -0,0 +1,46 @@ +# Implementazione Bot Telegram (Python + InlineKeyboard) + +Il progetto si basa sulla libreria **`python-telegram-bot`** e sull'uso di **`InlineKeyboard`** per gestire le scelte dell'utente, assicurando un'interfaccia rapida e pulita. + +## 1. Setup e Flusso Iniziale + +### Inizializzazione + +Dovrai innanzitutto inizializzare l'oggetto bot con il tuo **token API** di Telegram e configurare l'**Application** (o il Dispatcher). + +### Handler Principali + +* **Comando `/start`** : Implementa l'handler per questo comando. La sua funzione è inviare un messaggio di benvenuto e presentare immediatamente la prima **`InlineKeyboard`** per la scelta della strategia (A/B). + +## 2. Gestione dei Menu (InlineKeyboard) + +Per le scelte di Strategia (A/B) e LLM (Dropdown), la soluzione è basata interamente sulla gestione delle **`InlineKeyboard`** e dei `CallbackQuery`. + +### Tasti e Azioni + +* **Strategia (A/B)** : Crea una `InlineKeyboard` con i pulsanti 'A' e 'B', ciascuno con un `callback_data` univoco (es. `strategy_A`). +* **Selezione LLM** : Dopo la scelta della strategia, invia una nuova `InlineKeyboard` per la selezione dell'LLM (es. GPT-3.5, Gemini), assegnando un `callback_data` (es. `llm_gpt35`) ad ogni opzione. +* **`CallbackQuery` Handler** : Un unico handler catturerà la pressione di tutti questi pulsanti. Questo gestore deve analizzare il `callback_data` per determinare quale scelta è stata fatta. + +### Gestione dello Stato + +È fondamentale utilizzare un meccanismo (come il **`ConversationHandler`** della libreria o un sistema di stato personalizzato) per **memorizzare le scelte** dell'utente (`Strategia` e `LLM`) man mano che vengono fatte, guidando il flusso verso la fase successiva. + +## 3. Interazione con la LLM e Output + +### Acquisizione del Prompt + +* **Prompt Handler** : Dopo che l'utente ha selezionato sia la Strategia che l'LLM, il bot deve attendere un **messaggio di testo** dall'utente. Questo handler si attiverà solo quando lo stato dell'utente indica che le scelte iniziali sono state fatte. + +### Feedback e Output (Gestione degli aggiornamenti) + +Questo è il punto cruciale per evitare lo spam in chat: + +1. **Indicatore di Lavoro** : Appena ricevuto il prompt, invia l'azione **`ChatAction.TYPING`** (`sta scrivendo...`) per dare feedback immediato all'utente. +2. **Messaggio Placeholder** : Invia un messaggio iniziale (es. "Elaborazione in corso, attendere...") e **memorizza il suo `message_id`** . +3. **Aggiornamento in Tempo Reale** : Per ogni *output parziale* (`poutput`) ricevuto dalla tua LLM, utilizza la funzione **`edit_message_text`** passando l'ID del messaggio memorizzato. Questo aggiornerà continuamente l'unico messaggio esistente in chat. +4. **Output Finale** : Una volta che la LLM ha terminato, esegui l'ultima modifica del messaggio (o inviane uno nuovo, a tua discrezione) e **resetta lo stato** dell'utente per un nuovo ciclo di interazione. + +### Gestione degli Errori + +Integra un gestore di eccezioni (`try...except`) per catturare eventuali errori durante la chiamata all'API della LLM, inviando un messaggio informativo e di scuse all'utente. diff --git a/pyproject.toml b/pyproject.toml index d039c6b..e901f1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "pytest", # Test "dotenv", # Gestire variabili d'ambiente (generalmente API keys od opzioni) "gradio", # UI web semplice con user_input e output + "python-telegram-bot", # Interfaccia Telegram Bot # Per costruire agenti (ovvero modelli che possono fare più cose tramite tool) https://github.com/agno-agi/agno # altamente consigliata dato che ha anche tools integrati per fare scraping, calcoli e molto altro diff --git a/uv.lock b/uv.lock index d8114d6..b8b4dad 100644 --- a/uv.lock +++ b/uv.lock @@ -1289,6 +1289,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "python-telegram-bot" +version = "22.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/6b/400f88e5c29a270c1c519a3ca8ad0babc650ec63dbfbd1b73babf625ed54/python_telegram_bot-22.5.tar.gz", hash = "sha256:82d4efd891d04132f308f0369f5b5929e0b96957901f58bcef43911c5f6f92f8", size = 1488269, upload-time = "2025-09-27T13:50:27.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/c3/340c7520095a8c79455fcf699cbb207225e5b36490d2b9ee557c16a7b21b/python_telegram_bot-22.5-py3-none-any.whl", hash = "sha256:4b7cd365344a7dce54312cc4520d7fa898b44d1a0e5f8c74b5bd9b540d035d16", size = 730976, upload-time = "2025-09-27T13:50:25.93Z" }, +] + [[package]] name = "pytz" version = "2025.2" @@ -1614,6 +1626,7 @@ dependencies = [ { name = "praw" }, { name = "pytest" }, { name = "python-binance" }, + { name = "python-telegram-bot" }, { name = "yfinance" }, ] @@ -1631,6 +1644,7 @@ requires-dist = [ { name = "praw" }, { name = "pytest" }, { name = "python-binance" }, + { name = "python-telegram-bot" }, { name = "yfinance" }, ] -- 2.49.1 From 991686aa45e17dfb9e97c4c66f449534759854c5 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Mon, 6 Oct 2025 16:21:25 +0200 Subject: [PATCH 02/19] demo per bot Telegram con gestione comandi e inline keyboard --- demos/telegram_bot_demo.py | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 demos/telegram_bot_demo.py diff --git a/demos/telegram_bot_demo.py b/demos/telegram_bot_demo.py new file mode 100644 index 0000000..2a2b7d9 --- /dev/null +++ b/demos/telegram_bot_demo.py @@ -0,0 +1,59 @@ +import os +from dotenv import load_dotenv +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters, ContextTypes + +# Esempio di funzione per gestire il comando /start +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if not update.message: return + await update.message.reply_text('Ciao! Inviami un messaggio e ti risponderò!') + + +# Esempio di funzione per fare echo del messaggio ricevuto +async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE): + message = update.message + if not message: return + + print(f"Ricevuto messaggio: {message.text} da chat id: {message.chat.id}") + await message.reply_text(text=f"Hai detto: {message.text}") + + +# Esempio di funzione per far partire una inline keyboard (comando /keyboard) +async def inline_keyboard(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if not update.message: return + keyboard = [ + [ + InlineKeyboardButton("Option 1", callback_data='1'), + InlineKeyboardButton("Option 2", callback_data='2'), + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text('Please choose:', reply_markup=reply_markup) + + +async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + query = update.callback_query + if not query: return + await query.answer() + await query.edit_message_text(text=f"Selected option: {query.data}") + + + + + +def main(): + print("Bot in ascolto...") + + load_dotenv() + token = os.getenv("TELEGRAM_BOT_TOKEN", '') + app = Application.builder().token(token).build() + + app.add_handler(CommandHandler("start", start)) + app.add_handler(CommandHandler("keyboard", inline_keyboard)) + app.add_handler(MessageHandler(filters=filters.TEXT, callback=echo)) + app.add_handler(CallbackQueryHandler(button_handler)) + + app.run_polling(allowed_updates=Update.ALL_TYPES) + +if __name__ == "__main__": + main() \ No newline at end of file -- 2.49.1 From f5be7c82aa84c96cb0fae744b9c05e0a61e9165b Mon Sep 17 00:00:00 2001 From: Berack96 Date: Tue, 7 Oct 2025 12:07:23 +0200 Subject: [PATCH 03/19] Implementazione del bot Telegram con gestione dei comandi e stati di conversazione iniziali --- src/app/utils/telegram_app.py | 184 ++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 src/app/utils/telegram_app.py diff --git a/src/app/utils/telegram_app.py b/src/app/utils/telegram_app.py new file mode 100644 index 0000000..f26b013 --- /dev/null +++ b/src/app/utils/telegram_app.py @@ -0,0 +1,184 @@ +import os +from enum import Enum +from typing import Any +from agno.utils.log import log_info # type: ignore +from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User +from telegram.ext import Application, CommandHandler, ContextTypes, ConversationHandler, ExtBot, JobQueue, CallbackQueryHandler +from app.models import AppModels +from app.predictor import PredictorStyle + + +# conversation states +class ConfigStates(Enum): + MODEL_TEAM = "Team Model" + MODEL_OUTPUT = "Output Model" + STRATEGY = "Strategy" + +# conversation stages (checkpoints) +class Checkpoints(Enum): + CONFIGS = 1 + TEAM_RUNNING = 2 + END = 3 + +class RunConfigs: + model_team: AppModels + model_output: AppModels + strategy: PredictorStyle + + def __init__(self): + self.model_team = AppModels.OLLAMA_QWEN_1B + self.model_output = AppModels.OLLAMA_QWEN_1B + self.strategy = PredictorStyle.CONSERVATIVE + +class BotFunctions: + + # In theory this is already thread-safe if run with CPython + users_req: dict[User, RunConfigs] = {} + app_models: list[AppModels] = AppModels.availables() + strategies: list[PredictorStyle] = list(PredictorStyle) + + # che incubo di typing + @staticmethod + def create_bot() -> Application[ExtBot[None], ContextTypes.DEFAULT_TYPE, dict[str, Any], dict[str, Any], dict[str, Any], JobQueue[ContextTypes.DEFAULT_TYPE]]: + """ + Create a Telegram bot application instance. + Assumes the TELEGRAM_BOT_TOKEN environment variable is set. + Returns: + Application: The Telegram bot application instance. + Raises: + AssertionError: If the TELEGRAM_BOT_TOKEN environment variable is not set. + """ + + token = os.getenv("TELEGRAM_BOT_TOKEN", '') + assert token, "TELEGRAM_BOT_TOKEN environment variable not set" + + app = Application.builder().token(token).build() + + conv_handler = ConversationHandler( + entry_points=[CommandHandler('start', BotFunctions.__start)], + states={ + Checkpoints.CONFIGS: [ + CallbackQueryHandler(BotFunctions.__model_team, pattern=ConfigStates.MODEL_TEAM.name), + CallbackQueryHandler(BotFunctions.__model_output, pattern=ConfigStates.MODEL_OUTPUT.name), + CallbackQueryHandler(BotFunctions.__strategy, pattern=ConfigStates.STRATEGY.name), + CallbackQueryHandler(BotFunctions.__next, pattern='^__next'), + CallbackQueryHandler(BotFunctions.__cancel, pattern='^cancel$') + ], + Checkpoints.TEAM_RUNNING: [], + Checkpoints.END: [ + ] + }, + fallbacks=[CommandHandler('start', BotFunctions.__start)], + ) + + app.add_handler(conv_handler) + + log_info("Telegram bot application created successfully.") + return app + + ######################################## + # Funzioni di utilità + ######################################## + @staticmethod + async def start_message(user: User, query: CallbackQuery | Message) -> None: + confs = BotFunctions.users_req.setdefault(user, RunConfigs()) + + str_model_team = f"{ConfigStates.MODEL_TEAM.value}:\t\t {confs.model_team.name}" + str_model_output = f"{ConfigStates.MODEL_OUTPUT.value}:\t\t {confs.model_output.name}" + str_strategy = f"{ConfigStates.STRATEGY.value}:\t\t {confs.strategy.name}" + + msg, keyboard = ( + "Please choose an option or write your query", + InlineKeyboardMarkup([ + [InlineKeyboardButton(str_model_team, callback_data=ConfigStates.MODEL_TEAM.name)], + [InlineKeyboardButton(str_model_output, callback_data=ConfigStates.MODEL_OUTPUT.name)], + [InlineKeyboardButton(str_strategy, callback_data=ConfigStates.STRATEGY.name)], + [InlineKeyboardButton("Cancel", callback_data='cancel')] + ]) + ) + + if isinstance(query, CallbackQuery): + await query.edit_message_text(msg, reply_markup=keyboard, parse_mode='MarkdownV2') + else: + await query.reply_text(msg, reply_markup=keyboard, parse_mode='MarkdownV2') + + @staticmethod + async def handle_configs(update: Update, state: ConfigStates, msg: str | None = None) -> Checkpoints: + query, _ = await BotFunctions.handle_callbackquery(update) + + models = [(m.name, f"__next:{state}:{m.name}") for m in BotFunctions.app_models] + inline_btns = [[InlineKeyboardButton(name, callback_data=callback_data)] for name, callback_data in models] + + await query.edit_message_text(msg or state.value, reply_markup=InlineKeyboardMarkup(inline_btns)) + return Checkpoints.CONFIGS + + @staticmethod + async def handle_callbackquery(update: Update) -> tuple[CallbackQuery, User]: + assert update.callback_query and update.callback_query.from_user, "Update callback_query or user is None" + query = update.callback_query + await query.answer() # Acknowledge the callback query + return query, query.from_user + + + ######################################### + # Funzioni async per i comandi e messaggi + ######################################### + @staticmethod + async def __start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints: + assert update.message and update.message.from_user, "Update message or user is None" + user = update.message.from_user + log_info(f"@{user.username} started the conversation.") + await BotFunctions.start_message(user, update.message) + return Checkpoints.CONFIGS + + @staticmethod + async def __cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints: + query, user = await BotFunctions.handle_callbackquery(update) + log_info(f"@{user.username} canceled the conversation.") + if user in BotFunctions.users_req: + del BotFunctions.users_req[user] + await query.edit_message_text("Conversation canceled. Use /start to begin again.") + return Checkpoints.END + + @staticmethod + async def __model_team(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints: + return await BotFunctions.handle_configs(update, ConfigStates.MODEL_TEAM) + + @staticmethod + async def __model_output(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints: + return await BotFunctions.handle_configs(update, ConfigStates.MODEL_OUTPUT) + + @staticmethod + async def __strategy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints: + query, _ = await BotFunctions.handle_callbackquery(update) + + strategies = [(s.name, f"__next:{ConfigStates.STRATEGY}:{s.name}") for s in BotFunctions.strategies] + inline_btns = [[InlineKeyboardButton(name, callback_data=callback_data)] for name, callback_data in strategies] + + await query.edit_message_text("Select a strategy", reply_markup=InlineKeyboardMarkup(inline_btns)) + return Checkpoints.CONFIGS + + @staticmethod + async def __next(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints: + query, user = await BotFunctions.handle_callbackquery(update) + log_info(f"@{user.username} --> {query.data}") + + req = BotFunctions.users_req[user] + + _, state, model_name = str(query.data).split(':') + if state == str(ConfigStates.MODEL_TEAM): + req.model_team = AppModels[model_name] + if state == str(ConfigStates.MODEL_OUTPUT): + req.model_output = AppModels[model_name] + if state == str(ConfigStates.STRATEGY): + req.strategy = PredictorStyle[model_name] + + await BotFunctions.start_message(user, query) + return Checkpoints.CONFIGS + +if __name__ == "__main__": + from dotenv import load_dotenv + load_dotenv() + bot_app = BotFunctions.create_bot() + bot_app.run_polling() + -- 2.49.1 From d7e3dfef68ecf6c4b966ba6fd8e2f503ff9db7d9 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Tue, 7 Oct 2025 15:09:51 +0200 Subject: [PATCH 04/19] Aggiorna la gestione delle configurazioni nel bot Telegram: modifica gli stati della conversazione e aggiungi il supporto per la gestione dei messaggi. --- src/app/utils/telegram_app.py | 151 +++++++++++++++++++++------------- 1 file changed, 96 insertions(+), 55 deletions(-) diff --git a/src/app/utils/telegram_app.py b/src/app/utils/telegram_app.py index f26b013..fbe4af7 100644 --- a/src/app/utils/telegram_app.py +++ b/src/app/utils/telegram_app.py @@ -3,24 +3,32 @@ from enum import Enum from typing import Any from agno.utils.log import log_info # type: ignore from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User -from telegram.ext import Application, CommandHandler, ContextTypes, ConversationHandler, ExtBot, JobQueue, CallbackQueryHandler +from telegram.ext import Application, CommandHandler, ContextTypes, ConversationHandler, ExtBot, JobQueue, CallbackQueryHandler, MessageHandler, filters from app.models import AppModels from app.predictor import PredictorStyle -# conversation states -class ConfigStates(Enum): +# Lo stato cambia in base al valore di ritorno delle funzioni async +# END state è già definito in telegram.ext.ConversationHandler +# Un semplice schema delle interazioni: +# /start +# ║ +# V +# ╔══ CONFIGS <═════╗ +# ║ ║ ╚══> SELECT_CONFIG +# ║ V +# ║ start_team (polling for updates) +# ║ ║ +# ║ V +# ╚═══> END +CONFIGS, SELECT_CONFIG = range(2) + +class ConfigsChat(Enum): MODEL_TEAM = "Team Model" MODEL_OUTPUT = "Output Model" STRATEGY = "Strategy" -# conversation stages (checkpoints) -class Checkpoints(Enum): - CONFIGS = 1 - TEAM_RUNNING = 2 - END = 3 - -class RunConfigs: +class ConfigsRun: model_team: AppModels model_output: AppModels strategy: PredictorStyle @@ -30,10 +38,12 @@ class RunConfigs: self.model_output = AppModels.OLLAMA_QWEN_1B self.strategy = PredictorStyle.CONSERVATIVE + + class BotFunctions: # In theory this is already thread-safe if run with CPython - users_req: dict[User, RunConfigs] = {} + users_req: dict[User, ConfigsRun] = {} app_models: list[AppModels] = AppModels.availables() strategies: list[PredictorStyle] = list(PredictorStyle) @@ -55,17 +65,18 @@ class BotFunctions: app = Application.builder().token(token).build() conv_handler = ConversationHandler( + per_message=False, # capire a cosa serve perchè da un warning quando parte il server entry_points=[CommandHandler('start', BotFunctions.__start)], states={ - Checkpoints.CONFIGS: [ - CallbackQueryHandler(BotFunctions.__model_team, pattern=ConfigStates.MODEL_TEAM.name), - CallbackQueryHandler(BotFunctions.__model_output, pattern=ConfigStates.MODEL_OUTPUT.name), - CallbackQueryHandler(BotFunctions.__strategy, pattern=ConfigStates.STRATEGY.name), - CallbackQueryHandler(BotFunctions.__next, pattern='^__next'), - CallbackQueryHandler(BotFunctions.__cancel, pattern='^cancel$') + CONFIGS: [ + CallbackQueryHandler(BotFunctions.__model_team, pattern=ConfigsChat.MODEL_TEAM.name), + CallbackQueryHandler(BotFunctions.__model_output, pattern=ConfigsChat.MODEL_OUTPUT.name), + CallbackQueryHandler(BotFunctions.__strategy, pattern=ConfigsChat.STRATEGY.name), + CallbackQueryHandler(BotFunctions.__cancel, pattern='^cancel$'), + MessageHandler(filters.TEXT, BotFunctions.__start_team) # Any text message ], - Checkpoints.TEAM_RUNNING: [], - Checkpoints.END: [ + SELECT_CONFIG: [ + CallbackQueryHandler(BotFunctions.__select_config, pattern='^__select_config:.*$'), ] }, fallbacks=[CommandHandler('start', BotFunctions.__start)], @@ -81,18 +92,18 @@ class BotFunctions: ######################################## @staticmethod async def start_message(user: User, query: CallbackQuery | Message) -> None: - confs = BotFunctions.users_req.setdefault(user, RunConfigs()) + confs = BotFunctions.users_req.setdefault(user, ConfigsRun()) - str_model_team = f"{ConfigStates.MODEL_TEAM.value}:\t\t {confs.model_team.name}" - str_model_output = f"{ConfigStates.MODEL_OUTPUT.value}:\t\t {confs.model_output.name}" - str_strategy = f"{ConfigStates.STRATEGY.value}:\t\t {confs.strategy.name}" + str_model_team = f"{ConfigsChat.MODEL_TEAM.value}: {confs.model_team.name}" + str_model_output = f"{ConfigsChat.MODEL_OUTPUT.value}:\t\t {confs.model_output.name}" + str_strategy = f"{ConfigsChat.STRATEGY.value}:\t\t {confs.strategy.name}" msg, keyboard = ( "Please choose an option or write your query", InlineKeyboardMarkup([ - [InlineKeyboardButton(str_model_team, callback_data=ConfigStates.MODEL_TEAM.name)], - [InlineKeyboardButton(str_model_output, callback_data=ConfigStates.MODEL_OUTPUT.name)], - [InlineKeyboardButton(str_strategy, callback_data=ConfigStates.STRATEGY.name)], + [InlineKeyboardButton(str_model_team, callback_data=ConfigsChat.MODEL_TEAM.name)], + [InlineKeyboardButton(str_model_output, callback_data=ConfigsChat.MODEL_OUTPUT.name)], + [InlineKeyboardButton(str_strategy, callback_data=ConfigsChat.STRATEGY.name)], [InlineKeyboardButton("Cancel", callback_data='cancel')] ]) ) @@ -103,14 +114,14 @@ class BotFunctions: await query.reply_text(msg, reply_markup=keyboard, parse_mode='MarkdownV2') @staticmethod - async def handle_configs(update: Update, state: ConfigStates, msg: str | None = None) -> Checkpoints: + async def handle_configs(update: Update, state: ConfigsChat, msg: str | None = None) -> int: query, _ = await BotFunctions.handle_callbackquery(update) - models = [(m.name, f"__next:{state}:{m.name}") for m in BotFunctions.app_models] + models = [(m.name, f"__select_config:{state}:{m.name}") for m in BotFunctions.app_models] inline_btns = [[InlineKeyboardButton(name, callback_data=callback_data)] for name, callback_data in models] await query.edit_message_text(msg or state.value, reply_markup=InlineKeyboardMarkup(inline_btns)) - return Checkpoints.CONFIGS + return SELECT_CONFIG @staticmethod async def handle_callbackquery(update: Update) -> tuple[CallbackQuery, User]: @@ -119,62 +130,92 @@ class BotFunctions: await query.answer() # Acknowledge the callback query return query, query.from_user + @staticmethod + async def handle_message(update: Update) -> tuple[Message, User]: + assert update.message and update.message.from_user, "Update message or user is None" + return update.message, update.message.from_user + ######################################### # Funzioni async per i comandi e messaggi ######################################### @staticmethod - async def __start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints: - assert update.message and update.message.from_user, "Update message or user is None" - user = update.message.from_user + async def __start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + message, user = await BotFunctions.handle_message(update) log_info(f"@{user.username} started the conversation.") - await BotFunctions.start_message(user, update.message) - return Checkpoints.CONFIGS + await BotFunctions.start_message(user, message) + return CONFIGS @staticmethod - async def __cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints: - query, user = await BotFunctions.handle_callbackquery(update) - log_info(f"@{user.username} canceled the conversation.") - if user in BotFunctions.users_req: - del BotFunctions.users_req[user] - await query.edit_message_text("Conversation canceled. Use /start to begin again.") - return Checkpoints.END + async def __model_team(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + return await BotFunctions.handle_configs(update, ConfigsChat.MODEL_TEAM) @staticmethod - async def __model_team(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints: - return await BotFunctions.handle_configs(update, ConfigStates.MODEL_TEAM) + async def __model_output(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + return await BotFunctions.handle_configs(update, ConfigsChat.MODEL_OUTPUT) @staticmethod - async def __model_output(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints: - return await BotFunctions.handle_configs(update, ConfigStates.MODEL_OUTPUT) - - @staticmethod - async def __strategy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints: + async def __strategy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: query, _ = await BotFunctions.handle_callbackquery(update) - strategies = [(s.name, f"__next:{ConfigStates.STRATEGY}:{s.name}") for s in BotFunctions.strategies] + strategies = [(s.name, f"__select_config:{ConfigsChat.STRATEGY}:{s.name}") for s in BotFunctions.strategies] inline_btns = [[InlineKeyboardButton(name, callback_data=callback_data)] for name, callback_data in strategies] await query.edit_message_text("Select a strategy", reply_markup=InlineKeyboardMarkup(inline_btns)) - return Checkpoints.CONFIGS + return SELECT_CONFIG @staticmethod - async def __next(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints: + async def __select_config(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: query, user = await BotFunctions.handle_callbackquery(update) log_info(f"@{user.username} --> {query.data}") req = BotFunctions.users_req[user] _, state, model_name = str(query.data).split(':') - if state == str(ConfigStates.MODEL_TEAM): + if state == str(ConfigsChat.MODEL_TEAM): req.model_team = AppModels[model_name] - if state == str(ConfigStates.MODEL_OUTPUT): + if state == str(ConfigsChat.MODEL_OUTPUT): req.model_output = AppModels[model_name] - if state == str(ConfigStates.STRATEGY): + if state == str(ConfigsChat.STRATEGY): req.strategy = PredictorStyle[model_name] await BotFunctions.start_message(user, query) - return Checkpoints.CONFIGS + return CONFIGS + + @staticmethod + async def __start_team(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + message, user = await BotFunctions.handle_message(update) + msg2 = await message.reply_text("Elaborating your request...") + + confs = BotFunctions.users_req[user] + log_info(f"@{user.username} started the team with [{confs.model_team}, {confs.model_output}, {confs.strategy}]") + + await BotFunctions.__run_team(confs, msg2) + return ConversationHandler.END + + @staticmethod + async def __cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + query, user = await BotFunctions.handle_callbackquery(update) + log_info(f"@{user.username} canceled the conversation.") + if user in BotFunctions.users_req: + del BotFunctions.users_req[user] + await query.edit_message_text("Conversation canceled. Use /start to begin again.") + return ConversationHandler.END + + @staticmethod + async def __run_team(confs: ConfigsRun, msg: Message) -> None: + # TODO fare il run effettivo del team + import asyncio + + # Simulate a long-running task + n_simulations = 3 + for i in range(n_simulations): + await msg.edit_text(f"Working... {i+1}/{n_simulations}") + await asyncio.sleep(2) + await msg.edit_text("Team work completed.") + + + if __name__ == "__main__": from dotenv import load_dotenv -- 2.49.1 From 109863a0390e02c8656bee5aad4841de25d92bae Mon Sep 17 00:00:00 2001 From: Berack96 Date: Tue, 7 Oct 2025 15:29:10 +0200 Subject: [PATCH 05/19] fix static models & readme --- README.md | 2 ++ src/app/utils/telegram_app.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a545c92..4d31a09 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ Usando la libreria ``gradio`` è stata creata un'interfaccia web semplice per in - **Social Agent**: Analizza i sentimenti sui social media riguardo alle criptovalute. - **Predictor Agent**: Utilizza i dati raccolti dagli altri agenti per fare previsioni. +Si può accedere all'interfaccia anche tramite un Bot di Telegram se si inserisce la chiave ottenuta da [BotFather](https://core.telegram.org/bots/features#creating-a-new-bot). + ## Tests Per eseguire i test, assicurati di aver configurato correttamente le variabili d'ambiente nel file `.env` come descritto sopra. Poi esegui il comando: diff --git a/src/app/utils/telegram_app.py b/src/app/utils/telegram_app.py index fbe4af7..d8d3ddc 100644 --- a/src/app/utils/telegram_app.py +++ b/src/app/utils/telegram_app.py @@ -34,8 +34,8 @@ class ConfigsRun: strategy: PredictorStyle def __init__(self): - self.model_team = AppModels.OLLAMA_QWEN_1B - self.model_output = AppModels.OLLAMA_QWEN_1B + self.model_team = BotFunctions.app_models[0] + self.model_output = BotFunctions.app_models[0] self.strategy = PredictorStyle.CONSERVATIVE -- 2.49.1 From e6e40f96f0aa1f18e6fe2fd50ca6d2c0269872b8 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 8 Oct 2025 21:19:25 +0200 Subject: [PATCH 06/19] aggiunto il supporto per la query dell'utente e modificata la visualizzazione dei messaggi di stato. --- src/app/utils/telegram_app.py | 63 ++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/src/app/utils/telegram_app.py b/src/app/utils/telegram_app.py index d8d3ddc..45f945e 100644 --- a/src/app/utils/telegram_app.py +++ b/src/app/utils/telegram_app.py @@ -3,10 +3,9 @@ from enum import Enum from typing import Any from agno.utils.log import log_info # type: ignore from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User -from telegram.ext import Application, CommandHandler, ContextTypes, ConversationHandler, ExtBot, JobQueue, CallbackQueryHandler, MessageHandler, filters -from app.models import AppModels -from app.predictor import PredictorStyle - +from telegram.constants import ChatAction +from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, ConversationHandler, ExtBot, JobQueue, MessageHandler, filters +from app.agents import AppModels, PredictorStyle # Lo stato cambia in base al valore di ritorno delle funzioni async # END state è già definito in telegram.ext.ConversationHandler @@ -29,14 +28,11 @@ class ConfigsChat(Enum): STRATEGY = "Strategy" class ConfigsRun: - model_team: AppModels - model_output: AppModels - strategy: PredictorStyle - def __init__(self): self.model_team = BotFunctions.app_models[0] self.model_output = BotFunctions.app_models[0] self.strategy = PredictorStyle.CONSERVATIVE + self.user_query = "" @@ -94,9 +90,9 @@ class BotFunctions: async def start_message(user: User, query: CallbackQuery | Message) -> None: confs = BotFunctions.users_req.setdefault(user, ConfigsRun()) - str_model_team = f"{ConfigsChat.MODEL_TEAM.value}: {confs.model_team.name}" - str_model_output = f"{ConfigsChat.MODEL_OUTPUT.value}:\t\t {confs.model_output.name}" - str_strategy = f"{ConfigsChat.STRATEGY.value}:\t\t {confs.strategy.name}" + str_model_team = f"{ConfigsChat.MODEL_TEAM.value}: {confs.model_team.name}" + str_model_output = f"{ConfigsChat.MODEL_OUTPUT.value}: {confs.model_output.name}" + str_strategy = f"{ConfigsChat.STRATEGY.value}: {confs.strategy.name}" msg, keyboard = ( "Please choose an option or write your query", @@ -185,12 +181,14 @@ class BotFunctions: @staticmethod async def __start_team(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: message, user = await BotFunctions.handle_message(update) - msg2 = await message.reply_text("Elaborating your request...") confs = BotFunctions.users_req[user] - log_info(f"@{user.username} started the team with [{confs.model_team}, {confs.model_output}, {confs.strategy}]") + confs.user_query = message.text or "" - await BotFunctions.__run_team(confs, msg2) + log_info(f"@{user.username} started the team with [{confs.model_team}, {confs.model_output}, {confs.strategy}]") + await BotFunctions.__run_team(update, confs) + + log_info(f"@{user.username} team finished.") return ConversationHandler.END @staticmethod @@ -203,16 +201,43 @@ class BotFunctions: return ConversationHandler.END @staticmethod - async def __run_team(confs: ConfigsRun, msg: Message) -> None: - # TODO fare il run effettivo del team - import asyncio + async def __run_team(update: Update, confs: ConfigsRun) -> None: + if not update.message: return + bot = update.get_bot() + msg_id = update.message.message_id - 1 + chat_id = update.message.chat_id + + configs = [ + 'Running with configurations: ', + f'Team: {confs.model_team.name}', + f'Output: {confs.model_output.name}', + f'Strategy: {confs.strategy.name}', + f'Query: "{confs.user_query}"' + ] + full_message = f"""```\n{'\n'.join(configs)}\n```\n\n""" + msg = await bot.edit_message_text(chat_id=chat_id, message_id=msg_id, text=full_message, parse_mode='MarkdownV2') + if isinstance(msg, bool): return + + # Remove user query and bot message + await bot.delete_message(chat_id=chat_id, message_id=update.message.id) + + # TODO fare il run effettivo del team # Simulate a long-running task n_simulations = 3 + import asyncio + await bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING) for i in range(n_simulations): - await msg.edit_text(f"Working... {i+1}/{n_simulations}") + await msg.edit_text(f"{full_message}Working {i+1}/{n_simulations}", parse_mode='MarkdownV2') await asyncio.sleep(2) - await msg.edit_text("Team work completed.") + await msg.delete() + + # attach report file to the message + import io + report_content = f"# Report\n\nThis is a sample report generated by the team." + document = io.BytesIO(report_content.encode('utf-8')) + await bot.send_document(chat_id=chat_id, document=document, filename="report.md", parse_mode='MarkdownV2', caption=full_message) + -- 2.49.1 From 52e9cb2996365cbda12171a14702559946921b14 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Thu, 9 Oct 2025 01:28:51 +0200 Subject: [PATCH 07/19] Aggiunto il supporto per la gestione del bot Telegram e aggiornata la configurazione del pipeline --- src/app/__main__.py | 32 ++++++++++++++++++------ src/app/utils/__init__.py | 3 ++- src/app/utils/telegram_app.py | 47 +++++++++++++++++++++-------------- 3 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/app/__main__.py b/src/app/__main__.py index 578ef35..93b6174 100644 --- a/src/app/__main__.py +++ b/src/app/__main__.py @@ -3,12 +3,17 @@ from dotenv import load_dotenv from agno.utils.log import log_info #type: ignore from app.utils import ChatManager from app.agents import Pipeline +from app.utils.telegram_app import BotFunctions -if __name__ == "__main__": - # Inizializzazioni - load_dotenv() - pipeline = Pipeline() +# Disabilita TUTTI i log di livello inferiore a WARNING +# La maggior parte arrivano da httpx +import logging +logging.getLogger().setLevel(logging.WARNING) + + + +def gradio_app(pipeline: Pipeline, server: str = "0.0.0.0", port: int = 8000) -> str: chat = ChatManager() ######################################## @@ -73,7 +78,18 @@ if __name__ == "__main__": save_btn.click(save_current_chat, inputs=None, outputs=None) load_btn.click(load_previous_chat, inputs=None, outputs=[chatbot, chatbot]) - server, port = ("0.0.0.0", 8000) # 0.0.0.0 per accesso esterno (Docker) - server_log = "localhost" if server == "0.0.0.0" else server - log_info(f"Starting UPO AppAI Chat on http://{server_log}:{port}") # noqa - demo.launch(server_name=server, server_port=port, quiet=True) + _app, local, share = demo.launch(server_name=server, server_port=port, quiet=True, prevent_thread_lock=True) + log_info(f"UPO AppAI Chat is running on {local} and {share}") + return share + + + +if __name__ == "__main__": + load_dotenv() # Carica le variabili d'ambiente dal file .env + + pipeline = Pipeline() + url = gradio_app(pipeline) + + telegram = BotFunctions.create_bot(pipeline, url) + telegram.run_polling() + diff --git a/src/app/utils/__init__.py b/src/app/utils/__init__.py index 1a511c1..96cfda9 100644 --- a/src/app/utils/__init__.py +++ b/src/app/utils/__init__.py @@ -1,5 +1,6 @@ from app.utils.market_aggregation import aggregate_history_prices, aggregate_product_info from app.utils.wrapper_handler import WrapperHandler from app.utils.chat_manager import ChatManager +from app.utils.telegram_app import BotFunctions -__all__ = ["aggregate_history_prices", "aggregate_product_info", "WrapperHandler", "ChatManager"] +__all__ = ["aggregate_history_prices", "aggregate_product_info", "WrapperHandler", "ChatManager", "BotFunctions"] diff --git a/src/app/utils/telegram_app.py b/src/app/utils/telegram_app.py index 45f945e..945a7d6 100644 --- a/src/app/utils/telegram_app.py +++ b/src/app/utils/telegram_app.py @@ -1,4 +1,6 @@ import os +import json +import httpx from enum import Enum from typing import Any from agno.utils.log import log_info # type: ignore @@ -6,6 +8,7 @@ from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, from telegram.constants import ChatAction from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, ConversationHandler, ExtBot, JobQueue, MessageHandler, filters from app.agents import AppModels, PredictorStyle +from app.agents.pipeline import Pipeline # Lo stato cambia in base al valore di ritorno delle funzioni async # END state è già definito in telegram.ext.ConversationHandler @@ -29,9 +32,9 @@ class ConfigsChat(Enum): class ConfigsRun: def __init__(self): - self.model_team = BotFunctions.app_models[0] - self.model_output = BotFunctions.app_models[0] - self.strategy = PredictorStyle.CONSERVATIVE + self.model_team = BotFunctions.pipeline.available_models[0] + self.model_output = BotFunctions.pipeline.available_models[0] + self.strategy = BotFunctions.pipeline.all_styles[0] self.user_query = "" @@ -39,13 +42,12 @@ class ConfigsRun: class BotFunctions: # In theory this is already thread-safe if run with CPython - users_req: dict[User, ConfigsRun] = {} - app_models: list[AppModels] = AppModels.availables() - strategies: list[PredictorStyle] = list(PredictorStyle) + users_req: dict[User, ConfigsRun] + pipeline: Pipeline # che incubo di typing @staticmethod - def create_bot() -> Application[ExtBot[None], ContextTypes.DEFAULT_TYPE, dict[str, Any], dict[str, Any], dict[str, Any], JobQueue[ContextTypes.DEFAULT_TYPE]]: + def create_bot(pipeline: Pipeline, miniapp_url: str | None = None) -> Application[ExtBot[None], ContextTypes.DEFAULT_TYPE, dict[str, Any], dict[str, Any], dict[str, Any], JobQueue[ContextTypes.DEFAULT_TYPE]]: """ Create a Telegram bot application instance. Assumes the TELEGRAM_BOT_TOKEN environment variable is set. @@ -54,10 +56,13 @@ class BotFunctions: Raises: AssertionError: If the TELEGRAM_BOT_TOKEN environment variable is not set. """ + BotFunctions.users_req = {} + BotFunctions.pipeline = pipeline token = os.getenv("TELEGRAM_BOT_TOKEN", '') assert token, "TELEGRAM_BOT_TOKEN environment variable not set" + if miniapp_url: BotFunctions.update_miniapp_url(miniapp_url, token) app = Application.builder().token(token).build() conv_handler = ConversationHandler( @@ -113,7 +118,7 @@ class BotFunctions: async def handle_configs(update: Update, state: ConfigsChat, msg: str | None = None) -> int: query, _ = await BotFunctions.handle_callbackquery(update) - models = [(m.name, f"__select_config:{state}:{m.name}") for m in BotFunctions.app_models] + models = [(m.name, f"__select_config:{state}:{m.name}") for m in BotFunctions.pipeline.available_models] inline_btns = [[InlineKeyboardButton(name, callback_data=callback_data)] for name, callback_data in models] await query.edit_message_text(msg or state.value, reply_markup=InlineKeyboardMarkup(inline_btns)) @@ -131,6 +136,20 @@ class BotFunctions: assert update.message and update.message.from_user, "Update message or user is None" return update.message, update.message.from_user + @staticmethod + def update_miniapp_url(url: str, token: str) -> None: + try: + endpoint = f"https://api.telegram.org/bot{token}/setChatMenuButton" + payload = {"menu_button": json.dumps({ + "type": "web_app", + "text": "Apri Mini App", # Il testo che appare sul pulsante + "web_app": { + "url": url + } + })} + httpx.post(endpoint, data=payload) + except httpx.HTTPError as e: + log_info(f"Failed to update mini app URL: {e}") ######################################### # Funzioni async per i comandi e messaggi @@ -154,7 +173,7 @@ class BotFunctions: async def __strategy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: query, _ = await BotFunctions.handle_callbackquery(update) - strategies = [(s.name, f"__select_config:{ConfigsChat.STRATEGY}:{s.name}") for s in BotFunctions.strategies] + strategies = [(s.name, f"__select_config:{ConfigsChat.STRATEGY}:{s.name}") for s in BotFunctions.pipeline.all_styles] inline_btns = [[InlineKeyboardButton(name, callback_data=callback_data)] for name, callback_data in strategies] await query.edit_message_text("Select a strategy", reply_markup=InlineKeyboardMarkup(inline_btns)) @@ -238,13 +257,3 @@ class BotFunctions: document = io.BytesIO(report_content.encode('utf-8')) await bot.send_document(chat_id=chat_id, document=document, filename="report.md", parse_mode='MarkdownV2', caption=full_message) - - - - -if __name__ == "__main__": - from dotenv import load_dotenv - load_dotenv() - bot_app = BotFunctions.create_bot() - bot_app.run_polling() - -- 2.49.1 From 40c424765af200365940bed0ba7a6bac7c238b9a Mon Sep 17 00:00:00 2001 From: Berack96 Date: Thu, 9 Oct 2025 01:48:36 +0200 Subject: [PATCH 08/19] Aggiornato .gitignore per includere la cartella .gradio e rimosso chroma_db. Aggiunto il supporto per la generazione di report in PDF utilizzando markdown-pdf nel bot Telegram. --- .gitignore | 8 ++++---- pyproject.toml | 5 ++++- src/app/utils/telegram_app.py | 10 ++++++++-- uv.lock | 36 ++++++++++++++++++++++++++++++++--- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index b532676..609ad99 100644 --- a/.gitignore +++ b/.gitignore @@ -173,8 +173,8 @@ cython_debug/ # PyPI configuration file .pypirc -# chroma db -./chroma_db/ - # VS Code -.vscode/ \ No newline at end of file +.vscode/ + +# Gradio +.gradio/ diff --git a/pyproject.toml b/pyproject.toml index e901f1f..8d3e9b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "pytest", # Test "dotenv", # Gestire variabili d'ambiente (generalmente API keys od opzioni) "gradio", # UI web semplice con user_input e output - "python-telegram-bot", # Interfaccia Telegram Bot # Per costruire agenti (ovvero modelli che possono fare più cose tramite tool) https://github.com/agno-agi/agno # altamente consigliata dato che ha anche tools integrati per fare scraping, calcoli e molto altro @@ -36,6 +35,10 @@ dependencies = [ # API di social media "praw", # Reddit + + # Per telegram bot + "python-telegram-bot", # Interfaccia Telegram Bot + "markdown-pdf", # Per convertire markdown in pdf ] [tool.pytest.ini_options] diff --git a/src/app/utils/telegram_app.py b/src/app/utils/telegram_app.py index 945a7d6..ff3ddc7 100644 --- a/src/app/utils/telegram_app.py +++ b/src/app/utils/telegram_app.py @@ -253,7 +253,13 @@ class BotFunctions: # attach report file to the message import io + from markdown_pdf import MarkdownPdf, Section report_content = f"# Report\n\nThis is a sample report generated by the team." - document = io.BytesIO(report_content.encode('utf-8')) - await bot.send_document(chat_id=chat_id, document=document, filename="report.md", parse_mode='MarkdownV2', caption=full_message) + pdf = MarkdownPdf(toc_level=2, optimize=True) + pdf.add_section(Section(report_content, toc=False)) + + document = io.BytesIO() + pdf.save_bytes(document) + document.seek(0) + await bot.send_document(chat_id=chat_id, document=document, filename="report.pdf", parse_mode='MarkdownV2', caption=full_message) diff --git a/uv.lock b/uv.lock index b8b4dad..e46b3a8 100644 --- a/uv.lock +++ b/uv.lock @@ -804,14 +804,27 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-pdf" +version = "1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pymupdf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/e6/969311a194074afa9672324244adbf64a7e8663f2ba0003395b7140f5c4a/markdown_pdf-1.10.tar.gz", hash = "sha256:bcf23d816baa56aec3a60f940681652c4e46ee048c6335835cddf86d1ff20a8e", size = 17783, upload-time = "2025-09-24T19:01:38.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/78/c593979cf1525be786d63b285a7a67afae397fc132382158432490ebd1ed/markdown_pdf-1.10-py3-none-any.whl", hash = "sha256:1863e78454e5aa9bcb34c125f385d4ff045c727660c5172877e82e69d06fae6d", size = 17994, upload-time = "2025-09-24T19:01:37.155Z" }, ] [[package]] @@ -1226,6 +1239,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] +[[package]] +name = "pymupdf" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/35/031556dfc0d332d8e9ed9b61ca105138606d3f8971b9eb02e20118629334/pymupdf-1.26.4.tar.gz", hash = "sha256:be13a066d42bfaed343a488168656637c4d9843ddc63b768dc827c9dfc6b9989", size = 83077563, upload-time = "2025-08-25T14:20:29.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/ae/3be722886cc7be2093585cd94f466db1199133ab005645a7a567b249560f/pymupdf-1.26.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cb95562a0a63ce906fd788bdad5239063b63068cf4a991684f43acb09052cb99", size = 23061974, upload-time = "2025-08-25T14:16:58.811Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b0/9a451d837e1fe18ecdbfbc34a6499f153c8a008763229cc634725383a93f/pymupdf-1.26.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:67e9e6b45832c33726651c2a031e9a20108fd9e759140b9e843f934de813a7ff", size = 22410112, upload-time = "2025-08-25T14:17:24.511Z" }, + { url = "https://files.pythonhosted.org/packages/d8/13/0916e8e02cb5453161fb9d9167c747d0a20d58633e30728645374153f815/pymupdf-1.26.4-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2604f687dd02b6a1b98c81bd8becfc0024899a2d2085adfe3f9e91607721fd22", size = 23454948, upload-time = "2025-08-25T21:20:07.71Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c6/d3cfafc75d383603884edeabe4821a549345df954a88d79e6764e2c87601/pymupdf-1.26.4-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:973a6dda61ebd34040e4df3753bf004b669017663fbbfdaa294d44eceba98de0", size = 24060686, upload-time = "2025-08-25T14:17:56.536Z" }, + { url = "https://files.pythonhosted.org/packages/72/08/035e9d22c801e801bba50c6745bc90ba8696a042fe2c68793e28bf0c3b07/pymupdf-1.26.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:299a49797df5b558e695647fa791329ba3911cbbb31ed65f24a6266c118ef1a7", size = 24265046, upload-time = "2025-08-25T14:18:21.238Z" }, + { url = "https://files.pythonhosted.org/packages/28/8c/c201e4846ec0fb6ae5d52aa3a5d66f9355f0c69fb94230265714df0de65e/pymupdf-1.26.4-cp39-abi3-win32.whl", hash = "sha256:51b38379aad8c71bd7a8dd24d93fbe7580c2a5d9d7e1f9cd29ebbba315aa1bd1", size = 17127332, upload-time = "2025-08-25T14:18:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c4/87d27b108c2f6d773aa5183c5ae367b2a99296ea4bc16eb79f453c679e30/pymupdf-1.26.4-cp39-abi3-win_amd64.whl", hash = "sha256:0b6345a93a9afd28de2567e433055e873205c52e6b920b129ca50e836a3aeec6", size = 18743491, upload-time = "2025-08-25T14:19:01.104Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -1621,6 +1649,7 @@ dependencies = [ { name = "gnews" }, { name = "google-genai" }, { name = "gradio" }, + { name = "markdown-pdf" }, { name = "newsapi-python" }, { name = "ollama" }, { name = "praw" }, @@ -1639,6 +1668,7 @@ requires-dist = [ { name = "gnews" }, { name = "google-genai" }, { name = "gradio" }, + { name = "markdown-pdf" }, { name = "newsapi-python" }, { name = "ollama" }, { name = "praw" }, -- 2.49.1 From e7c32cc2270f1bb433a5ffcbb885bb372827399d Mon Sep 17 00:00:00 2001 From: Berack96 Date: Thu, 9 Oct 2025 12:01:47 +0200 Subject: [PATCH 09/19] Refactor pipeline and chat manager for improved structure and functionality --- src/app/__main__.py | 97 +++++------------------------------ src/app/agents/pipeline.py | 17 +++--- src/app/utils/chat_manager.py | 69 ++++++++++++++++++++++++- src/app/utils/telegram_app.py | 45 ++++++++-------- 4 files changed, 114 insertions(+), 114 deletions(-) diff --git a/src/app/__main__.py b/src/app/__main__.py index 93b6174..c5dc50c 100644 --- a/src/app/__main__.py +++ b/src/app/__main__.py @@ -1,95 +1,24 @@ -import gradio as gr +# IMPORTANTE: Carichiamo le variabili d'ambiente PRIMA di qualsiasi altra cosa from dotenv import load_dotenv +load_dotenv() + + + +# IMPORTARE LIBRERIE DA QUI IN POI +from app.utils import ChatManager, BotFunctions from agno.utils.log import log_info #type: ignore -from app.utils import ChatManager -from app.agents import Pipeline -from app.utils.telegram_app import BotFunctions -# Disabilita TUTTI i log di livello inferiore a WARNING -# La maggior parte arrivano da httpx -import logging -logging.getLogger().setLevel(logging.WARNING) - - - -def gradio_app(pipeline: Pipeline, server: str = "0.0.0.0", port: int = 8000) -> str: - chat = ChatManager() - - ######################################## - # Funzioni Gradio - ######################################## - def respond(message: str, history: list[dict[str, str]]) -> tuple[list[dict[str, str]], list[dict[str, str]], str]: - chat.send_message(message) - response = pipeline.interact(message) - chat.receive_message(response) - history.append({"role": "user", "content": message}) - history.append({"role": "assistant", "content": response}) - return history, history, "" - - def save_current_chat() -> str: - chat.save_chat("chat.json") - return "💾 Chat salvata in chat.json" - - def load_previous_chat() -> tuple[list[dict[str, str]], list[dict[str, str]]]: - chat.load_chat("chat.json") - history: list[dict[str, str]] = [] - for m in chat.get_history(): - history.append({"role": m["role"], "content": m["content"]}) - return history, history - - def reset_chat() -> tuple[list[dict[str, str]], list[dict[str, str]]]: - chat.reset_chat() - return [], [] - - ######################################## - # Interfaccia Gradio - ######################################## - with gr.Blocks() as demo: - gr.Markdown("# 🤖 Agente di Analisi e Consulenza Crypto (Chat)") - - # Dropdown provider e stile - with gr.Row(): - provider = gr.Dropdown( - choices=pipeline.list_providers(), - type="index", - label="Modello da usare" - ) - provider.change(fn=pipeline.choose_predictor, inputs=provider, outputs=None) - - style = gr.Dropdown( - choices=pipeline.list_styles(), - type="index", - label="Stile di investimento" - ) - style.change(fn=pipeline.choose_style, inputs=style, outputs=None) - - chatbot = gr.Chatbot(label="Conversazione", height=500, type="messages") - msg = gr.Textbox(label="Scrivi la tua richiesta", placeholder="Es: Quali sono le crypto interessanti oggi?") - - with gr.Row(): - clear_btn = gr.Button("🗑️ Reset Chat") - save_btn = gr.Button("💾 Salva Chat") - load_btn = gr.Button("📂 Carica Chat") - - # Eventi e interazioni - msg.submit(respond, inputs=[msg, chatbot], outputs=[chatbot, chatbot, msg]) - clear_btn.click(reset_chat, inputs=None, outputs=[chatbot, chatbot]) - save_btn.click(save_current_chat, inputs=None, outputs=None) - load_btn.click(load_previous_chat, inputs=None, outputs=[chatbot, chatbot]) - - _app, local, share = demo.launch(server_name=server, server_port=port, quiet=True, prevent_thread_lock=True) - log_info(f"UPO AppAI Chat is running on {local} and {share}") - return share - if __name__ == "__main__": - load_dotenv() # Carica le variabili d'ambiente dal file .env + server, port, share = ("0.0.0.0", 8000, False) # TODO Temp configs, maybe read from env/yaml/ini file later - pipeline = Pipeline() - url = gradio_app(pipeline) + chat = ChatManager() + gradio = chat.gradio_build_interface() + _app, local_url, share_url = gradio.launch(server_name=server, server_port=port, quiet=True, prevent_thread_lock=True, share=share) + log_info(f"UPO AppAI Chat is running on {local_url} and {share_url}") - telegram = BotFunctions.create_bot(pipeline, url) + telegram = BotFunctions.create_bot(share_url) telegram.run_polling() diff --git a/src/app/agents/pipeline.py b/src/app/agents/pipeline.py index a7d1001..a01479f 100644 --- a/src/app/agents/pipeline.py +++ b/src/app/agents/pipeline.py @@ -12,11 +12,12 @@ class Pipeline: e scelto dall'utente tramite i dropdown dell'interfaccia grafica. """ - def __init__(self): - self.available_models = AppModels.availables() - self.all_styles = list(PredictorStyle) + # Variabili statiche + available_models = AppModels.availables() + all_styles = list(PredictorStyle) - self.style = self.all_styles[0] + def __init__(self): + self.style = Pipeline.all_styles[0] self.team = create_team_with(AppModels.OLLAMA_QWEN_1B) self.choose_predictor(0) # Modello di default @@ -27,7 +28,7 @@ class Pipeline: """ Sceglie il modello LLM da usare per il Predictor. """ - model = self.available_models[index] + model = Pipeline.available_models[index] self.predictor = model.get_agent( PREDICTOR_INSTRUCTIONS, output_schema=PredictorOutput, @@ -37,7 +38,7 @@ class Pipeline: """ Sceglie lo stile (conservativo/aggressivo) da usare per il Predictor. """ - self.style = self.all_styles[index] + self.style = Pipeline.all_styles[index] # ====================== # Helpers @@ -46,13 +47,13 @@ class Pipeline: """ Restituisce la lista dei nomi dei modelli disponibili. """ - return [model.name for model in self.available_models] + return [model.name for model in Pipeline.available_models] def list_styles(self) -> list[str]: """ Restituisce la lista degli stili di previsione disponibili. """ - return [style.value for style in self.all_styles] + return [style.value for style in Pipeline.all_styles] # ====================== # Core interaction diff --git a/src/app/utils/chat_manager.py b/src/app/utils/chat_manager.py index d51819d..1230c41 100644 --- a/src/app/utils/chat_manager.py +++ b/src/app/utils/chat_manager.py @@ -1,5 +1,8 @@ -import json import os +import json +import gradio as gr +from app.agents.pipeline import Pipeline + class ChatManager: """ @@ -11,6 +14,7 @@ class ChatManager: def __init__(self): self.history: list[dict[str, str]] = [] # [{"role": "user"/"assistant", "content": "..."}] + self.pipeline = Pipeline() def send_message(self, message: str) -> None: """ @@ -56,3 +60,66 @@ class ChatManager: Restituisce lo storico completo della chat. """ return self.history + + + ######################################## + # Funzioni Gradio + ######################################## + def gradio_respond(self, message: str, history: list[dict[str, str]]) -> tuple[list[dict[str, str]], list[dict[str, str]], str]: + self.send_message(message) + response = self.pipeline.interact(message) + self.receive_message(response) + history.append({"role": "user", "content": message}) + history.append({"role": "assistant", "content": response}) + return history, history, "" + + def gradio_save(self) -> str: + self.save_chat("chat.json") + return "💾 Chat salvata in chat.json" + + def gradio_load(self) -> tuple[list[dict[str, str]], list[dict[str, str]]]: + self.load_chat("chat.json") + history: list[dict[str, str]] = [] + for m in self.get_history(): + history.append({"role": m["role"], "content": m["content"]}) + return history, history + + def gradio_clear(self) -> tuple[list[dict[str, str]], list[dict[str, str]]]: + self.reset_chat() + return [], [] + + def gradio_build_interface(self) -> gr.Blocks: + with gr.Blocks() as interface: + gr.Markdown("# 🤖 Agente di Analisi e Consulenza Crypto (Chat)") + + # Dropdown provider e stile + with gr.Row(): + provider = gr.Dropdown( + choices=self.pipeline.list_providers(), + type="index", + label="Modello da usare" + ) + provider.change(fn=self.pipeline.choose_predictor, inputs=provider, outputs=None) + + style = gr.Dropdown( + choices=self.pipeline.list_styles(), + type="index", + label="Stile di investimento" + ) + style.change(fn=self.pipeline.choose_style, inputs=style, outputs=None) + + chatbot = gr.Chatbot(label="Conversazione", height=500, type="messages") + msg = gr.Textbox(label="Scrivi la tua richiesta", placeholder="Es: Quali sono le crypto interessanti oggi?") + + with gr.Row(): + clear_btn = gr.Button("🗑️ Reset Chat") + save_btn = gr.Button("💾 Salva Chat") + load_btn = gr.Button("📂 Carica Chat") + + # Eventi e interazioni + msg.submit(self.gradio_respond, inputs=[msg, chatbot], outputs=[chatbot, chatbot, msg]) + clear_btn.click(self.gradio_clear, inputs=None, outputs=[chatbot, chatbot]) + save_btn.click(self.gradio_save, inputs=None, outputs=None) + load_btn.click(self.gradio_load, inputs=None, outputs=[chatbot, chatbot]) + + return interface \ No newline at end of file diff --git a/src/app/utils/telegram_app.py b/src/app/utils/telegram_app.py index ff3ddc7..b03b840 100644 --- a/src/app/utils/telegram_app.py +++ b/src/app/utils/telegram_app.py @@ -1,15 +1,22 @@ +import io import os import json import httpx +import warnings from enum import Enum from typing import Any from agno.utils.log import log_info # type: ignore +from markdown_pdf import MarkdownPdf, Section from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User from telegram.constants import ChatAction from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, ConversationHandler, ExtBot, JobQueue, MessageHandler, filters from app.agents import AppModels, PredictorStyle from app.agents.pipeline import Pipeline +# per per_message di ConversationHandler che rompe sempre qualunque input tu metta +warnings.filterwarnings("ignore") + + # Lo stato cambia in base al valore di ritorno delle funzioni async # END state è già definito in telegram.ext.ConversationHandler # Un semplice schema delle interazioni: @@ -32,9 +39,9 @@ class ConfigsChat(Enum): class ConfigsRun: def __init__(self): - self.model_team = BotFunctions.pipeline.available_models[0] - self.model_output = BotFunctions.pipeline.available_models[0] - self.strategy = BotFunctions.pipeline.all_styles[0] + self.model_team = Pipeline.available_models[0] + self.model_output = Pipeline.available_models[0] + self.strategy = Pipeline.all_styles[0] self.user_query = "" @@ -43,11 +50,10 @@ class BotFunctions: # In theory this is already thread-safe if run with CPython users_req: dict[User, ConfigsRun] - pipeline: Pipeline # che incubo di typing @staticmethod - def create_bot(pipeline: Pipeline, miniapp_url: str | None = None) -> Application[ExtBot[None], ContextTypes.DEFAULT_TYPE, dict[str, Any], dict[str, Any], dict[str, Any], JobQueue[ContextTypes.DEFAULT_TYPE]]: + def create_bot(miniapp_url: str | None = None) -> Application[ExtBot[None], ContextTypes.DEFAULT_TYPE, dict[str, Any], dict[str, Any], dict[str, Any], JobQueue[ContextTypes.DEFAULT_TYPE]]: """ Create a Telegram bot application instance. Assumes the TELEGRAM_BOT_TOKEN environment variable is set. @@ -57,7 +63,6 @@ class BotFunctions: AssertionError: If the TELEGRAM_BOT_TOKEN environment variable is not set. """ BotFunctions.users_req = {} - BotFunctions.pipeline = pipeline token = os.getenv("TELEGRAM_BOT_TOKEN", '') assert token, "TELEGRAM_BOT_TOKEN environment variable not set" @@ -118,7 +123,7 @@ class BotFunctions: async def handle_configs(update: Update, state: ConfigsChat, msg: str | None = None) -> int: query, _ = await BotFunctions.handle_callbackquery(update) - models = [(m.name, f"__select_config:{state}:{m.name}") for m in BotFunctions.pipeline.available_models] + models = [(m.name, f"__select_config:{state}:{m.name}") for m in Pipeline.available_models] inline_btns = [[InlineKeyboardButton(name, callback_data=callback_data)] for name, callback_data in models] await query.edit_message_text(msg or state.value, reply_markup=InlineKeyboardMarkup(inline_btns)) @@ -142,7 +147,7 @@ class BotFunctions: endpoint = f"https://api.telegram.org/bot{token}/setChatMenuButton" payload = {"menu_button": json.dumps({ "type": "web_app", - "text": "Apri Mini App", # Il testo che appare sul pulsante + "text": "MiniApp", "web_app": { "url": url } @@ -173,7 +178,7 @@ class BotFunctions: async def __strategy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: query, _ = await BotFunctions.handle_callbackquery(update) - strategies = [(s.name, f"__select_config:{ConfigsChat.STRATEGY}:{s.name}") for s in BotFunctions.pipeline.all_styles] + strategies = [(s.name, f"__select_config:{ConfigsChat.STRATEGY}:{s.name}") for s in Pipeline.all_styles] inline_btns = [[InlineKeyboardButton(name, callback_data=callback_data)] for name, callback_data in strategies] await query.edit_message_text("Select a strategy", reply_markup=InlineKeyboardMarkup(inline_btns)) @@ -227,37 +232,35 @@ class BotFunctions: msg_id = update.message.message_id - 1 chat_id = update.message.chat_id - configs = [ + configs_str = [ 'Running with configurations: ', f'Team: {confs.model_team.name}', f'Output: {confs.model_output.name}', f'Strategy: {confs.strategy.name}', f'Query: "{confs.user_query}"' ] - full_message = f"""```\n{'\n'.join(configs)}\n```\n\n""" + full_message = f"""```\n{'\n'.join(configs_str)}\n```\n\n""" msg = await bot.edit_message_text(chat_id=chat_id, message_id=msg_id, text=full_message, parse_mode='MarkdownV2') if isinstance(msg, bool): return # Remove user query and bot message await bot.delete_message(chat_id=chat_id, message_id=update.message.id) - # TODO fare il run effettivo del team - # Simulate a long-running task - n_simulations = 3 - import asyncio + # Start TEAM + # TODO migliorare messaggi di attesa + pipeline = Pipeline() + pipeline.choose_predictor(Pipeline.available_models.index(confs.model_team)) + pipeline.choose_style(Pipeline.all_styles.index(confs.strategy)) + await bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING) - for i in range(n_simulations): - await msg.edit_text(f"{full_message}Working {i+1}/{n_simulations}", parse_mode='MarkdownV2') - await asyncio.sleep(2) + report_content = pipeline.interact(confs.user_query) await msg.delete() # attach report file to the message - import io - from markdown_pdf import MarkdownPdf, Section - report_content = f"# Report\n\nThis is a sample report generated by the team." pdf = MarkdownPdf(toc_level=2, optimize=True) pdf.add_section(Section(report_content, toc=False)) + # TODO vedere se ha senso dare il pdf o solo il messaggio document = io.BytesIO() pdf.save_bytes(document) document.seek(0) -- 2.49.1 From 2642b0a221bcc539d5cc126d418245c0ceba9a05 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Thu, 9 Oct 2025 12:43:27 +0200 Subject: [PATCH 10/19] Better logging --- pyproject.toml | 1 + src/app/__main__.py | 31 +++++++++++++- src/app/agents/models.py | 8 ++-- src/app/agents/pipeline.py | 72 +++++++++++++++++++------------- src/app/utils/telegram_app.py | 29 ++++++------- src/app/utils/wrapper_handler.py | 13 +++--- uv.lock | 14 +++++++ 7 files changed, 111 insertions(+), 57 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8d3e9b4..127d77a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "pytest", # Test "dotenv", # Gestire variabili d'ambiente (generalmente API keys od opzioni) "gradio", # UI web semplice con user_input e output + "colorlog", # Log colorati in console # Per costruire agenti (ovvero modelli che possono fare più cose tramite tool) https://github.com/agno-agi/agno # altamente consigliata dato che ha anche tools integrati per fare scraping, calcoli e molto altro diff --git a/src/app/__main__.py b/src/app/__main__.py index c5dc50c..4132755 100644 --- a/src/app/__main__.py +++ b/src/app/__main__.py @@ -3,10 +3,37 @@ from dotenv import load_dotenv load_dotenv() +# Modifico il comportamento del logging (dato che ci sono molte librerie che lo usano) +import logging.config +logging.config.dictConfig({ + 'version': 1, + 'disable_existing_loggers': False, # Mantiene i logger esistenti (es. di terze parti) + 'formatters': { + 'colored': { + '()': 'colorlog.ColoredFormatter', + 'format': '%(log_color)s%(levelname)s%(reset)s [%(asctime)s] (%(name)s) - %(message)s' + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'colored', + 'level': 'INFO' + }, + }, + 'root': { # Configura il logger root + 'handlers': ['console'], + 'level': 'INFO', + }, + 'loggers': { + 'httpx': {'level': 'WARNING'}, # Troppo spam per INFO + } +}) + + # IMPORTARE LIBRERIE DA QUI IN POI from app.utils import ChatManager, BotFunctions -from agno.utils.log import log_info #type: ignore @@ -17,7 +44,7 @@ if __name__ == "__main__": chat = ChatManager() gradio = chat.gradio_build_interface() _app, local_url, share_url = gradio.launch(server_name=server, server_port=port, quiet=True, prevent_thread_lock=True, share=share) - log_info(f"UPO AppAI Chat is running on {local_url} and {share_url}") + logging.info(f"UPO AppAI Chat is running on {local_url} and {share_url}") telegram = BotFunctions.create_bot(share_url) telegram.run_polling() diff --git a/src/app/agents/models.py b/src/app/agents/models.py index 79d4a26..ecec121 100644 --- a/src/app/agents/models.py +++ b/src/app/agents/models.py @@ -1,14 +1,16 @@ import os import ollama +import logging from enum import Enum from agno.agent import Agent from agno.models.base import Model from agno.models.google import Gemini from agno.models.ollama import Ollama from agno.tools import Toolkit -from agno.utils.log import log_warning #type: ignore from pydantic import BaseModel +logging = logging.getLogger(__name__) + class AppModels(Enum): """ @@ -36,7 +38,7 @@ class AppModels(Enum): app_models = [model for model in AppModels if model.name.startswith("OLLAMA")] return [model for model in app_models if model.value in availables] except Exception as e: - log_warning(f"Ollama is not running or not reachable: {e}") + logging.warning(f"Ollama is not running or not reachable: {e}") return [] @staticmethod @@ -46,7 +48,7 @@ class AppModels(Enum): come variabili d'ambiente e ritorna una lista di provider disponibili. """ if not os.getenv("GOOGLE_API_KEY"): - log_warning("No GOOGLE_API_KEY set in environment variables.") + logging.warning("No GOOGLE_API_KEY set in environment variables.") return [] availables = [AppModels.GEMINI, AppModels.GEMINI_PRO] return availables diff --git a/src/app/agents/pipeline.py b/src/app/agents/pipeline.py index a01479f..b6d50b4 100644 --- a/src/app/agents/pipeline.py +++ b/src/app/agents/pipeline.py @@ -1,9 +1,12 @@ +import logging from agno.run.agent import RunOutput from app.agents.models import AppModels from app.agents.team import create_team_with from app.agents.predictor import PREDICTOR_INSTRUCTIONS, PredictorInput, PredictorOutput, PredictorStyle from app.base.markets import ProductInfo +logging = logging.getLogger(__name__) + class Pipeline: """ @@ -65,42 +68,51 @@ class Pipeline: 3. Invoca Predictor 4. Restituisce la strategia finale """ + # Step 1: raccolta output dai membri del Team + logging.info(f"Pipeline received query: {query}") team_outputs = self.team.run(query) # type: ignore - # Step 2: aggregazione output strutturati - all_products: list[ProductInfo] = [] - sentiments: list[str] = [] + # Step 2: recupero ouput + if not isinstance(team_outputs.content, str): + logging.error(f"Team output is not a string: {team_outputs.content}") + raise ValueError("Team output is not a string") + logging.info(f"Team finished") + return team_outputs.content - for agent_output in team_outputs.member_responses: - if isinstance(agent_output, RunOutput) and agent_output.metadata is not None: - keys = agent_output.metadata.keys() - if "products" in keys: - all_products.extend(agent_output.metadata["products"]) - if "sentiment_news" in keys: - sentiments.append(agent_output.metadata["sentiment_news"]) - if "sentiment_social" in keys: - sentiments.append(agent_output.metadata["sentiment_social"]) + # # Step 2: aggregazione output strutturati + # all_products: list[ProductInfo] = [] + # sentiments: list[str] = [] - aggregated_sentiment = "\n".join(sentiments) + # for agent_output in team_outputs.member_responses: + # if isinstance(agent_output, RunOutput) and agent_output.metadata is not None: + # keys = agent_output.metadata.keys() + # if "products" in keys: + # all_products.extend(agent_output.metadata["products"]) + # if "sentiment_news" in keys: + # sentiments.append(agent_output.metadata["sentiment_news"]) + # if "sentiment_social" in keys: + # sentiments.append(agent_output.metadata["sentiment_social"]) - # Step 3: invocazione Predictor - predictor_input = PredictorInput( - data=all_products, - style=self.style, - sentiment=aggregated_sentiment - ) + # aggregated_sentiment = "\n".join(sentiments) - result = self.predictor.run(predictor_input) # type: ignore - if not isinstance(result.content, PredictorOutput): - return "❌ Errore: il modello non ha restituito un output valido." - prediction: PredictorOutput = result.content + # # Step 3: invocazione Predictor + # predictor_input = PredictorInput( + # data=all_products, + # style=self.style, + # sentiment=aggregated_sentiment + # ) + + # result = self.predictor.run(predictor_input) # type: ignore + # if not isinstance(result.content, PredictorOutput): + # return "❌ Errore: il modello non ha restituito un output valido." + # prediction: PredictorOutput = result.content # Step 4: restituzione strategia finale - portfolio_lines = "\n".join( - [f"{item.asset} ({item.percentage}%): {item.motivation}" for item in prediction.portfolio] - ) - return ( - f"📊 Strategia ({self.style.value}): {prediction.strategy}\n\n" - f"💼 Portafoglio consigliato:\n{portfolio_lines}" - ) + # portfolio_lines = "\n".join( + # [f"{item.asset} ({item.percentage}%): {item.motivation}" for item in prediction.portfolio] + # ) + # return ( + # f"📊 Strategia ({self.style.value}): {prediction.strategy}\n\n" + # f"💼 Portafoglio consigliato:\n{portfolio_lines}" + # ) diff --git a/src/app/utils/telegram_app.py b/src/app/utils/telegram_app.py index b03b840..c35ea22 100644 --- a/src/app/utils/telegram_app.py +++ b/src/app/utils/telegram_app.py @@ -2,10 +2,10 @@ import io import os import json import httpx +import logging import warnings from enum import Enum from typing import Any -from agno.utils.log import log_info # type: ignore from markdown_pdf import MarkdownPdf, Section from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User from telegram.constants import ChatAction @@ -15,6 +15,7 @@ from app.agents.pipeline import Pipeline # per per_message di ConversationHandler che rompe sempre qualunque input tu metta warnings.filterwarnings("ignore") +logging = logging.getLogger(__name__) # Lo stato cambia in base al valore di ritorno delle funzioni async @@ -70,7 +71,7 @@ class BotFunctions: if miniapp_url: BotFunctions.update_miniapp_url(miniapp_url, token) app = Application.builder().token(token).build() - conv_handler = ConversationHandler( + app.add_handler(ConversationHandler( per_message=False, # capire a cosa serve perchè da un warning quando parte il server entry_points=[CommandHandler('start', BotFunctions.__start)], states={ @@ -86,11 +87,7 @@ class BotFunctions: ] }, fallbacks=[CommandHandler('start', BotFunctions.__start)], - ) - - app.add_handler(conv_handler) - - log_info("Telegram bot application created successfully.") + )) return app ######################################## @@ -154,7 +151,7 @@ class BotFunctions: })} httpx.post(endpoint, data=payload) except httpx.HTTPError as e: - log_info(f"Failed to update mini app URL: {e}") + logging.info(f"Failed to update mini app URL: {e}") ######################################### # Funzioni async per i comandi e messaggi @@ -162,7 +159,7 @@ class BotFunctions: @staticmethod async def __start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: message, user = await BotFunctions.handle_message(update) - log_info(f"@{user.username} started the conversation.") + logging.info(f"@{user.username} started the conversation.") await BotFunctions.start_message(user, message) return CONFIGS @@ -187,7 +184,7 @@ class BotFunctions: @staticmethod async def __select_config(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: query, user = await BotFunctions.handle_callbackquery(update) - log_info(f"@{user.username} --> {query.data}") + logging.info(f"@{user.username} --> {query.data}") req = BotFunctions.users_req[user] @@ -209,16 +206,16 @@ class BotFunctions: confs = BotFunctions.users_req[user] confs.user_query = message.text or "" - log_info(f"@{user.username} started the team with [{confs.model_team}, {confs.model_output}, {confs.strategy}]") + logging.info(f"@{user.username} started the team with [{confs.model_team}, {confs.model_output}, {confs.strategy}]") await BotFunctions.__run_team(update, confs) - log_info(f"@{user.username} team finished.") + logging.info(f"@{user.username} team finished.") return ConversationHandler.END @staticmethod async def __cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: query, user = await BotFunctions.handle_callbackquery(update) - log_info(f"@{user.username} canceled the conversation.") + logging.info(f"@{user.username} canceled the conversation.") if user in BotFunctions.users_req: del BotFunctions.users_req[user] await query.edit_message_text("Conversation canceled. Use /start to begin again.") @@ -246,12 +243,12 @@ class BotFunctions: # Remove user query and bot message await bot.delete_message(chat_id=chat_id, message_id=update.message.id) - # Start TEAM - # TODO migliorare messaggi di attesa + # TODO settare correttamente i modelli pipeline = Pipeline() - pipeline.choose_predictor(Pipeline.available_models.index(confs.model_team)) + #pipeline.choose_predictor(Pipeline.available_models.index(confs.model_team)) pipeline.choose_style(Pipeline.all_styles.index(confs.strategy)) + # TODO migliorare messaggi di attesa await bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING) report_content = pipeline.interact(confs.user_query) await msg.delete() diff --git a/src/app/utils/wrapper_handler.py b/src/app/utils/wrapper_handler.py index 504cf41..67f2f1c 100644 --- a/src/app/utils/wrapper_handler.py +++ b/src/app/utils/wrapper_handler.py @@ -1,9 +1,10 @@ import inspect +import logging import time import traceback from typing import Any, Callable, Generic, TypeVar -from agno.utils.log import log_info, log_warning #type: ignore +logging = logging.getLogger(__name__) WrapperType = TypeVar("WrapperType") WrapperClassType = TypeVar("WrapperClassType") OutputType = TypeVar("OutputType") @@ -76,7 +77,7 @@ class WrapperHandler(Generic[WrapperType]): Exception: If all wrappers fail after retries. """ - log_info(f"{inspect.getsource(func).strip()} {inspect.getclosurevars(func).nonlocals}") + logging.info(f"{inspect.getsource(func).strip()} {inspect.getclosurevars(func).nonlocals}") results: dict[str, OutputType] = {} starting_index = self.index @@ -86,18 +87,18 @@ class WrapperHandler(Generic[WrapperType]): wrapper_name = wrapper.__class__.__name__ if not try_all: - log_info(f"try_call {wrapper_name}") + logging.info(f"try_call {wrapper_name}") for try_count in range(1, self.retry_per_wrapper + 1): try: result = func(wrapper) - log_info(f"{wrapper_name} succeeded") + logging.info(f"{wrapper_name} succeeded") results[wrapper_name] = result break except Exception as e: error = WrapperHandler.__concise_error(e) - log_warning(f"{wrapper_name} failed {try_count}/{self.retry_per_wrapper}: {error}") + logging.warning(f"{wrapper_name} failed {try_count}/{self.retry_per_wrapper}: {error}") time.sleep(self.retry_delay) if not try_all and results: @@ -143,6 +144,6 @@ class WrapperHandler(Generic[WrapperType]): wrapper = wrapper_class(**(kwargs or {})) result.append(wrapper) except Exception as e: - log_warning(f"{wrapper_class} cannot be initialized: {e}") + logging.warning(f"'{wrapper_class.__name__}' cannot be initialized: {e}") return WrapperHandler(result, try_per_wrapper, retry_delay) \ No newline at end of file diff --git a/uv.lock b/uv.lock index e46b3a8..000517c 100644 --- a/uv.lock +++ b/uv.lock @@ -285,6 +285,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "colorlog" +version = "6.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624, upload-time = "2024-10-29T18:34:51.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" }, +] + [[package]] name = "cryptography" version = "46.0.2" @@ -1644,6 +1656,7 @@ source = { virtual = "." } dependencies = [ { name = "agno" }, { name = "coinbase-advanced-py" }, + { name = "colorlog" }, { name = "ddgs" }, { name = "dotenv" }, { name = "gnews" }, @@ -1663,6 +1676,7 @@ dependencies = [ requires-dist = [ { name = "agno" }, { name = "coinbase-advanced-py" }, + { name = "colorlog" }, { name = "ddgs" }, { name = "dotenv" }, { name = "gnews" }, -- 2.49.1 From 9cd3184bd24519d523ed71b4f6f02f1634866b05 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Thu, 9 Oct 2025 13:49:38 +0200 Subject: [PATCH 11/19] Aggiornato il comportamento del logging per i logger di agno. Aggiunto il supporto per l'opzione check_for_async nella configurazione di RedditWrapper. --- docs/Telegram_Integration_plan.md | 46 ------------------------------- src/app/__main__.py | 11 ++++++-- src/app/social/reddit.py | 1 + 3 files changed, 10 insertions(+), 48 deletions(-) delete mode 100644 docs/Telegram_Integration_plan.md diff --git a/docs/Telegram_Integration_plan.md b/docs/Telegram_Integration_plan.md deleted file mode 100644 index 8f477f6..0000000 --- a/docs/Telegram_Integration_plan.md +++ /dev/null @@ -1,46 +0,0 @@ -# Implementazione Bot Telegram (Python + InlineKeyboard) - -Il progetto si basa sulla libreria **`python-telegram-bot`** e sull'uso di **`InlineKeyboard`** per gestire le scelte dell'utente, assicurando un'interfaccia rapida e pulita. - -## 1. Setup e Flusso Iniziale - -### Inizializzazione - -Dovrai innanzitutto inizializzare l'oggetto bot con il tuo **token API** di Telegram e configurare l'**Application** (o il Dispatcher). - -### Handler Principali - -* **Comando `/start`** : Implementa l'handler per questo comando. La sua funzione è inviare un messaggio di benvenuto e presentare immediatamente la prima **`InlineKeyboard`** per la scelta della strategia (A/B). - -## 2. Gestione dei Menu (InlineKeyboard) - -Per le scelte di Strategia (A/B) e LLM (Dropdown), la soluzione è basata interamente sulla gestione delle **`InlineKeyboard`** e dei `CallbackQuery`. - -### Tasti e Azioni - -* **Strategia (A/B)** : Crea una `InlineKeyboard` con i pulsanti 'A' e 'B', ciascuno con un `callback_data` univoco (es. `strategy_A`). -* **Selezione LLM** : Dopo la scelta della strategia, invia una nuova `InlineKeyboard` per la selezione dell'LLM (es. GPT-3.5, Gemini), assegnando un `callback_data` (es. `llm_gpt35`) ad ogni opzione. -* **`CallbackQuery` Handler** : Un unico handler catturerà la pressione di tutti questi pulsanti. Questo gestore deve analizzare il `callback_data` per determinare quale scelta è stata fatta. - -### Gestione dello Stato - -È fondamentale utilizzare un meccanismo (come il **`ConversationHandler`** della libreria o un sistema di stato personalizzato) per **memorizzare le scelte** dell'utente (`Strategia` e `LLM`) man mano che vengono fatte, guidando il flusso verso la fase successiva. - -## 3. Interazione con la LLM e Output - -### Acquisizione del Prompt - -* **Prompt Handler** : Dopo che l'utente ha selezionato sia la Strategia che l'LLM, il bot deve attendere un **messaggio di testo** dall'utente. Questo handler si attiverà solo quando lo stato dell'utente indica che le scelte iniziali sono state fatte. - -### Feedback e Output (Gestione degli aggiornamenti) - -Questo è il punto cruciale per evitare lo spam in chat: - -1. **Indicatore di Lavoro** : Appena ricevuto il prompt, invia l'azione **`ChatAction.TYPING`** (`sta scrivendo...`) per dare feedback immediato all'utente. -2. **Messaggio Placeholder** : Invia un messaggio iniziale (es. "Elaborazione in corso, attendere...") e **memorizza il suo `message_id`** . -3. **Aggiornamento in Tempo Reale** : Per ogni *output parziale* (`poutput`) ricevuto dalla tua LLM, utilizza la funzione **`edit_message_text`** passando l'ID del messaggio memorizzato. Questo aggiornerà continuamente l'unico messaggio esistente in chat. -4. **Output Finale** : Una volta che la LLM ha terminato, esegui l'ultima modifica del messaggio (o inviane uno nuovo, a tua discrezione) e **resetta lo stato** dell'utente per un nuovo ciclo di interazione. - -### Gestione degli Errori - -Integra un gestore di eccezioni (`try...except`) per catturare eventuali errori durante la chiamata all'API della LLM, inviando un messaggio informativo e di scuse all'utente. diff --git a/src/app/__main__.py b/src/app/__main__.py index 4132755..cc9a848 100644 --- a/src/app/__main__.py +++ b/src/app/__main__.py @@ -30,7 +30,15 @@ logging.config.dictConfig({ } }) - +# Modifichiamo i logger di agno +import agno.utils.log # type: ignore +agno_logger_names = ["agno", "agno-team", "agno-workflow"] +for logger_name in agno_logger_names: + logger = logging.getLogger(logger_name) + logger.handlers.clear() + # Imposta la propagazione su True affinché i log passino al logger root + # e usino i tuoi handler configurati nel logger root. + logger.propagate = True # IMPORTARE LIBRERIE DA QUI IN POI from app.utils import ChatManager, BotFunctions @@ -48,4 +56,3 @@ if __name__ == "__main__": telegram = BotFunctions.create_bot(share_url) telegram.run_polling() - diff --git a/src/app/social/reddit.py b/src/app/social/reddit.py index eeca968..f036b1b 100644 --- a/src/app/social/reddit.py +++ b/src/app/social/reddit.py @@ -59,6 +59,7 @@ class RedditWrapper(SocialWrapper): client_id=client_id, client_secret=client_secret, user_agent="upo-appAI", + check_for_async=False, ) self.subreddits = self.tool.subreddit("+".join(SUBREDDITS)) -- 2.49.1 From 78208f19b56971b715054a36aa8aa2e41e4fd310 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Thu, 9 Oct 2025 16:55:52 +0200 Subject: [PATCH 12/19] Rimosso codice commentato e import non utilizzati nella classe Pipeline per semplificare la struttura --- src/app/agents/pipeline.py | 41 +------------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/src/app/agents/pipeline.py b/src/app/agents/pipeline.py index b6d50b4..0d4aa4b 100644 --- a/src/app/agents/pipeline.py +++ b/src/app/agents/pipeline.py @@ -1,9 +1,7 @@ import logging -from agno.run.agent import RunOutput from app.agents.models import AppModels from app.agents.team import create_team_with -from app.agents.predictor import PREDICTOR_INSTRUCTIONS, PredictorInput, PredictorOutput, PredictorStyle -from app.base.markets import ProductInfo +from app.agents.predictor import PREDICTOR_INSTRUCTIONS, PredictorOutput, PredictorStyle logging = logging.getLogger(__name__) @@ -79,40 +77,3 @@ class Pipeline: raise ValueError("Team output is not a string") logging.info(f"Team finished") return team_outputs.content - - # # Step 2: aggregazione output strutturati - # all_products: list[ProductInfo] = [] - # sentiments: list[str] = [] - - # for agent_output in team_outputs.member_responses: - # if isinstance(agent_output, RunOutput) and agent_output.metadata is not None: - # keys = agent_output.metadata.keys() - # if "products" in keys: - # all_products.extend(agent_output.metadata["products"]) - # if "sentiment_news" in keys: - # sentiments.append(agent_output.metadata["sentiment_news"]) - # if "sentiment_social" in keys: - # sentiments.append(agent_output.metadata["sentiment_social"]) - - # aggregated_sentiment = "\n".join(sentiments) - - # # Step 3: invocazione Predictor - # predictor_input = PredictorInput( - # data=all_products, - # style=self.style, - # sentiment=aggregated_sentiment - # ) - - # result = self.predictor.run(predictor_input) # type: ignore - # if not isinstance(result.content, PredictorOutput): - # return "❌ Errore: il modello non ha restituito un output valido." - # prediction: PredictorOutput = result.content - - # Step 4: restituzione strategia finale - # portfolio_lines = "\n".join( - # [f"{item.asset} ({item.percentage}%): {item.motivation}" for item in prediction.portfolio] - # ) - # return ( - # f"📊 Strategia ({self.style.value}): {prediction.strategy}\n\n" - # f"💼 Portafoglio consigliato:\n{portfolio_lines}" - # ) -- 2.49.1 From d004d344be01c4faa990222d2863a53ef7565d38 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Fri, 10 Oct 2025 18:03:25 +0200 Subject: [PATCH 13/19] Aggiornata la sezione "Applicazione" nel README & fix main --- README.md | 21 ++++++++++++++------- src/app/__main__.py | 8 ++++++-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b6563af..1aba0c5 100644 --- a/README.md +++ b/README.md @@ -91,15 +91,22 @@ uv run python src/app # **Applicazione** -***L'applicazione è attualmente in fase di sviluppo.*** +> [!CAUTION]\ +> ***L'applicazione è attualmente in fase di sviluppo.*** -Usando la libreria ``gradio`` è stata creata un'interfaccia web semplice per interagire con l'agente principale. Gli agenti secondari si trovano nella cartella `src/app/agents` e sono: -- **Market Agent**: Agente unificato che supporta multiple fonti di dati con auto-retry e gestione degli errori. -- **News Agent**: Recupera le notizie finanziarie più recenti sul mercato delle criptovalute. -- **Social Agent**: Analizza i sentimenti sui social media riguardo alle criptovalute. -- **Predictor Agent**: Utilizza i dati raccolti dagli altri agenti per fare previsioni. +L'applicazione viene fatta partire tramite il file [src/app/\_\_main\_\_.py](src/app/__main__.py) che inizializza l'agente principale e gli agenti secondari. -Si può accedere all'interfaccia anche tramite un Bot di Telegram se si inserisce la chiave ottenuta da [BotFather](https://core.telegram.org/bots/features#creating-a-new-bot). +In esso viene creato il server `gradio` per l'interfaccia web e viene anche inizializzato il bot di Telegram (se è stata inserita la chiave nel file `.env` ottenuta da [BotFather](https://core.telegram.org/bots/features#creating-a-new-bot)). + +L'interazione è guidata, sia tramite l'interfaccia web che tramite il bot di Telegram; l'utente può scegliere prima di tutto delle opzioni generali (come il modello e la strategia di investimento), dopodiché può inviare un messaggio di testo libero per chiedere consigli o informazioni specifiche. Per esempio: "Qual è l'andamento attuale di Bitcoin?" o "Consigliami quali sono le migliori criptovalute in cui investire questo mese". + +L'applicazione, una volta ricevuta la richiesta, la passa al [Team](src/app/agents/team.py) di agenti che si occupano di raccogliere i dati necessari per rispondere in modo completo e ragionato. + +Gli agenti coinvolti nel Team sono: +- **Leader**: Coordina gli altri agenti e fornisce la risposta finale all'utente. +- **Market Agent**: Recupera i dati di mercato attuali delle criptovalute da Binance e Yahoo Finance. +- **News Agent**: Recupera le ultime notizie sul mercato delle criptovalute da NewsAPI e GNews. +- **Social Agent**: Recupera i dati dai social media (Reddit) per analizzare il sentiment del mercato. ## Struttura del codice del Progetto diff --git a/src/app/__main__.py b/src/app/__main__.py index cc9a848..21a6881 100644 --- a/src/app/__main__.py +++ b/src/app/__main__.py @@ -54,5 +54,9 @@ if __name__ == "__main__": _app, local_url, share_url = gradio.launch(server_name=server, server_port=port, quiet=True, prevent_thread_lock=True, share=share) logging.info(f"UPO AppAI Chat is running on {local_url} and {share_url}") - telegram = BotFunctions.create_bot(share_url) - telegram.run_polling() + try: + telegram = BotFunctions.create_bot(share_url) + telegram.run_polling() + except Exception as _: + logging.warning("Telegram bot could not be started. Continuing without it.") + gradio.queue().block_thread() # Keep the Gradio interface running -- 2.49.1 From b42500b067529e349a41bd99abf0e9dc7ff3e430 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Sun, 12 Oct 2025 20:18:06 +0200 Subject: [PATCH 14/19] Telegram instance instead of static --- src/app/__main__.py | 9 +- src/app/agents/pipeline.py | 14 +- src/app/configs.py | 17 ++- src/app/interface/__init__.py | 4 +- src/app/interface/chat.py | 6 +- src/app/interface/telegram_app.py | 231 +++++++++++++++--------------- 6 files changed, 147 insertions(+), 134 deletions(-) diff --git a/src/app/__main__.py b/src/app/__main__.py index a1dd453..7c6557a 100644 --- a/src/app/__main__.py +++ b/src/app/__main__.py @@ -3,7 +3,7 @@ import asyncio import logging from dotenv import load_dotenv from app.configs import AppConfig -from app.interface import ChatManager, BotFunctions +from app.interface import * from app.agents import Pipeline @@ -14,14 +14,15 @@ if __name__ == "__main__": configs = AppConfig.load() pipeline = Pipeline(configs) - chat = ChatManager() + chat = ChatManager(pipeline) gradio = chat.gradio_build_interface() _app, local_url, share_url = gradio.launch(server_name="0.0.0.0", server_port=configs.port, quiet=True, prevent_thread_lock=True, share=configs.gradio_share) logging.info(f"UPO AppAI Chat is running on {share_url or local_url}") try: - telegram = BotFunctions.create_bot(share_url) - telegram.run_polling() + telegram = TelegramApp(pipeline) + telegram.add_miniapp_url(share_url) + telegram.run() except Exception as _: logging.warning("Telegram bot could not be started. Continuing without it.") asyncio.get_event_loop().run_forever() diff --git a/src/app/agents/pipeline.py b/src/app/agents/pipeline.py index 82a80da..f52cf28 100644 --- a/src/app/agents/pipeline.py +++ b/src/app/agents/pipeline.py @@ -18,7 +18,8 @@ class Pipeline: # Stato iniziale self.leader_model = self.configs.get_model_by_name(self.configs.agents.team_leader_model) - self.choose_strategy(0) + self.team_model = self.configs.get_model_by_name(self.configs.agents.team_model) + self.strategy = self.configs.get_strategy_by_name(self.configs.agents.strategy) # ====================== # Dropdown handlers @@ -33,7 +34,7 @@ class Pipeline: """ Sceglie la strategia da usare per il Predictor. """ - self.strat = self.configs.strategies[index].description + self.strategy = self.configs.strategies[index] # ====================== # Helpers @@ -61,14 +62,15 @@ class Pipeline: 4. Restituisce la strategia finale """ # Step 1: Creazione Team - team_model = self.configs.get_model_by_name(self.configs.agents.team_model) - team = create_team_with(self.configs, team_model, self.leader_model) + team = create_team_with(self.configs, self.team_model, self.leader_model) - # Step 1: raccolta output dai membri del Team + # Step 2: raccolta output dai membri del Team logging.info(f"Pipeline received query: {query}") + # TODO migliorare prompt (?) + query = f"The user query is: {query}\n\n They requested a {self.strategy.label} investment strategy." team_outputs = team.run(query) # type: ignore - # Step 2: recupero ouput + # Step 3: recupero ouput if not isinstance(team_outputs.content, str): logging.error(f"Team output is not a string: {team_outputs.content}") raise ValueError("Team output is not a string") diff --git a/src/app/configs.py b/src/app/configs.py index 6da942f..f0cd797 100644 --- a/src/app/configs.py +++ b/src/app/configs.py @@ -4,7 +4,7 @@ import ollama import yaml import logging.config import agno.utils.log # type: ignore -from typing import Any +from typing import Any, ClassVar from pydantic import BaseModel from agno.agent import Agent from agno.tools import Toolkit @@ -88,7 +88,7 @@ class AppConfig(BaseModel): models: ModelsConfig = ModelsConfig() agents: AgentsConfigs = AgentsConfigs() - __lock = threading.Lock() + _lock: ClassVar[threading.Lock] = threading.Lock() @classmethod def load(cls, file_path: str = "configs.yaml") -> 'AppConfig': @@ -110,7 +110,7 @@ class AppConfig(BaseModel): return configs def __new__(cls, *args: Any, **kwargs: Any) -> 'AppConfig': - with cls.__lock: + with cls._lock: if not hasattr(cls, 'instance'): cls.instance = super(AppConfig, cls).__new__(cls) return cls.instance @@ -145,6 +145,17 @@ class AppConfig(BaseModel): return strat raise ValueError(f"Strategy with name '{name}' not found.") + def get_defaults(self) -> tuple[AppModel, AppModel, Strategy]: + """ + Retrieve the default team model, leader model, and strategy. + Returns: + A tuple containing the default team model (AppModel), leader model (AppModel), and strategy (Strategy). + """ + team_model = self.get_model_by_name(self.agents.team_model) + leader_model = self.get_model_by_name(self.agents.team_leader_model) + strategy = self.get_strategy_by_name(self.agents.strategy) + return team_model, leader_model, strategy + def set_logging_level(self) -> None: """ Set the logging level based on the configuration. diff --git a/src/app/interface/__init__.py b/src/app/interface/__init__.py index a58cd50..186558a 100644 --- a/src/app/interface/__init__.py +++ b/src/app/interface/__init__.py @@ -1,4 +1,4 @@ from app.interface.chat import ChatManager -from app.interface.telegram_app import BotFunctions +from app.interface.telegram_app import TelegramApp -__all__ = ["ChatManager", "BotFunctions"] +__all__ = ["ChatManager", "TelegramApp"] diff --git a/src/app/interface/chat.py b/src/app/interface/chat.py index 6ce4807..aaba2af 100644 --- a/src/app/interface/chat.py +++ b/src/app/interface/chat.py @@ -12,9 +12,9 @@ class ChatManager: - salva e ricarica le chat """ - def __init__(self): + def __init__(self, pipeline: Pipeline): self.history: list[dict[str, str]] = [] # [{"role": "user"/"assistant", "content": "..."}] - self.pipeline = Pipeline() + self.pipeline = pipeline def send_message(self, message: str) -> None: """ @@ -106,7 +106,7 @@ class ChatManager: type="index", label="Stile di investimento" ) - style.change(fn=self.pipeline.choose_style, inputs=style, outputs=None) + style.change(fn=self.pipeline.choose_strategy, inputs=style, outputs=None) chatbot = gr.Chatbot(label="Conversazione", height=500, type="messages") msg = gr.Textbox(label="Scrivi la tua richiesta", placeholder="Es: Quali sono le crypto interessanti oggi?") diff --git a/src/app/interface/telegram_app.py b/src/app/interface/telegram_app.py index e9559ee..a091797 100644 --- a/src/app/interface/telegram_app.py +++ b/src/app/interface/telegram_app.py @@ -5,12 +5,12 @@ import httpx import logging import warnings from enum import Enum -from typing import Any from markdown_pdf import MarkdownPdf, Section from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User from telegram.constants import ChatAction -from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, ConversationHandler, ExtBot, JobQueue, MessageHandler, filters +from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, ConversationHandler, MessageHandler, filters from app.agents.pipeline import Pipeline +from app.configs import AppConfig # per per_message di ConversationHandler che rompe sempre qualunque input tu metta warnings.filterwarnings("ignore") @@ -32,73 +32,83 @@ logging = logging.getLogger(__name__) # ╚═══> END CONFIGS, SELECT_CONFIG = range(2) +# Usato per separare la query arrivata da Telegram +QUERY_SEP = "|==|" + class ConfigsChat(Enum): MODEL_TEAM = "Team Model" MODEL_OUTPUT = "Output Model" STRATEGY = "Strategy" class ConfigsRun: - def __init__(self): - self.model_team = Pipeline.available_models[0] - self.model_output = Pipeline.available_models[0] - self.strategy = Pipeline.all_styles[0] + def __init__(self, configs: AppConfig): + team, leader, strategy = configs.get_defaults() + self.team_model = team + self.leader_model = leader + self.strategy = strategy self.user_query = "" - -class BotFunctions: - - # In theory this is already thread-safe if run with CPython - users_req: dict[User, ConfigsRun] - - # che incubo di typing - @staticmethod - def create_bot(miniapp_url: str | None = None) -> Application[ExtBot[None], ContextTypes.DEFAULT_TYPE, dict[str, Any], dict[str, Any], dict[str, Any], JobQueue[ContextTypes.DEFAULT_TYPE]]: - """ - Create a Telegram bot application instance. - Assumes the TELEGRAM_BOT_TOKEN environment variable is set. - Returns: - Application: The Telegram bot application instance. - Raises: - AssertionError: If the TELEGRAM_BOT_TOKEN environment variable is not set. - """ - BotFunctions.users_req = {} - - token = os.getenv("TELEGRAM_BOT_TOKEN", '') +class TelegramApp: + def __init__(self, pipeline: Pipeline): + token = os.getenv("TELEGRAM_BOT_TOKEN") assert token, "TELEGRAM_BOT_TOKEN environment variable not set" - if miniapp_url: BotFunctions.update_miniapp_url(miniapp_url, token) - app = Application.builder().token(token).build() + self.user_requests: dict[User, ConfigsRun] = {} + self.pipeline = pipeline + self.token = token + self.create_bot() + def add_miniapp_url(self, url: str) -> None: + try: + endpoint = f"https://api.telegram.org/bot{self.token}/setChatMenuButton" + payload = {"menu_button": json.dumps({ + "type": "web_app", + "text": "MiniApp", + "web_app": { "url": url } + })} + httpx.post(endpoint, data=payload) + except httpx.HTTPError as e: + logging.info(f"Failed to update mini app URL: {e}") + + def create_bot(self) -> None: + """ + Initialize the Telegram bot and set up the conversation handler. + """ + app = Application.builder().token(self.token).build() + + app.add_error_handler(self.__error_handler) app.add_handler(ConversationHandler( per_message=False, # capire a cosa serve perchè da un warning quando parte il server - entry_points=[CommandHandler('start', BotFunctions.__start)], + entry_points=[CommandHandler('start', self.__start)], states={ CONFIGS: [ - CallbackQueryHandler(BotFunctions.__model_team, pattern=ConfigsChat.MODEL_TEAM.name), - CallbackQueryHandler(BotFunctions.__model_output, pattern=ConfigsChat.MODEL_OUTPUT.name), - CallbackQueryHandler(BotFunctions.__strategy, pattern=ConfigsChat.STRATEGY.name), - CallbackQueryHandler(BotFunctions.__cancel, pattern='^cancel$'), - MessageHandler(filters.TEXT, BotFunctions.__start_team) # Any text message + CallbackQueryHandler(self.__model_team, pattern=ConfigsChat.MODEL_TEAM.name), + CallbackQueryHandler(self.__model_output, pattern=ConfigsChat.MODEL_OUTPUT.name), + CallbackQueryHandler(self.__strategy, pattern=ConfigsChat.STRATEGY.name), + CallbackQueryHandler(self.__cancel, pattern='^cancel$'), + MessageHandler(filters.TEXT, self.__start_team) # Any text message ], SELECT_CONFIG: [ - CallbackQueryHandler(BotFunctions.__select_config, pattern='^__select_config:.*$'), + CallbackQueryHandler(self.__select_config, pattern=f"^__select_config{QUERY_SEP}.*$"), ] }, - fallbacks=[CommandHandler('start', BotFunctions.__start)], + fallbacks=[CommandHandler('start', self.__start)], )) - return app + self.app = app + + def run(self) -> None: + self.app.run_polling() ######################################## # Funzioni di utilità ######################################## - @staticmethod - async def start_message(user: User, query: CallbackQuery | Message) -> None: - confs = BotFunctions.users_req.setdefault(user, ConfigsRun()) + async def start_message(self, user: User, query: CallbackQuery | Message) -> None: + confs = self.user_requests.setdefault(user, ConfigsRun(self.pipeline.configs)) - str_model_team = f"{ConfigsChat.MODEL_TEAM.value}: {confs.model_team.name}" - str_model_output = f"{ConfigsChat.MODEL_OUTPUT.value}: {confs.model_output.name}" - str_strategy = f"{ConfigsChat.STRATEGY.value}: {confs.strategy.name}" + str_model_team = f"{ConfigsChat.MODEL_TEAM.value}: {confs.team_model.label}" + str_model_output = f"{ConfigsChat.MODEL_OUTPUT.value}: {confs.leader_model.label}" + str_strategy = f"{ConfigsChat.STRATEGY.value}: {confs.strategy.label}" msg, keyboard = ( "Please choose an option or write your query", @@ -115,113 +125,103 @@ class BotFunctions: else: await query.reply_text(msg, reply_markup=keyboard, parse_mode='MarkdownV2') - @staticmethod - async def handle_configs(update: Update, state: ConfigsChat, msg: str | None = None) -> int: - query, _ = await BotFunctions.handle_callbackquery(update) - - models = [(m.name, f"__select_config:{state}:{m.name}") for m in Pipeline.available_models] - inline_btns = [[InlineKeyboardButton(name, callback_data=callback_data)] for name, callback_data in models] - - await query.edit_message_text(msg or state.value, reply_markup=InlineKeyboardMarkup(inline_btns)) - return SELECT_CONFIG - - @staticmethod - async def handle_callbackquery(update: Update) -> tuple[CallbackQuery, User]: + async def handle_callbackquery(self, update: Update) -> tuple[CallbackQuery, User]: assert update.callback_query and update.callback_query.from_user, "Update callback_query or user is None" query = update.callback_query await query.answer() # Acknowledge the callback query return query, query.from_user - @staticmethod - async def handle_message(update: Update) -> tuple[Message, User]: + async def handle_message(self, update: Update) -> tuple[Message, User]: assert update.message and update.message.from_user, "Update message or user is None" return update.message, update.message.from_user - @staticmethod - def update_miniapp_url(url: str, token: str) -> None: + def callback_data(self, strings: list[str]) -> str: + return QUERY_SEP.join(strings) + + async def __error_handler(self, update: object, context: ContextTypes.DEFAULT_TYPE) -> None: try: - endpoint = f"https://api.telegram.org/bot{token}/setChatMenuButton" - payload = {"menu_button": json.dumps({ - "type": "web_app", - "text": "MiniApp", - "web_app": { - "url": url - } - })} - httpx.post(endpoint, data=payload) - except httpx.HTTPError as e: - logging.info(f"Failed to update mini app URL: {e}") + logging.exception(f"Unhandled exception in Telegram handler {context.error}") + + # Try to notify the user in chat if possible + if isinstance(update, Update) and update.effective_chat: + chat_id = update.effective_chat.id + msg = "Si è verificato un errore inatteso. Gli sviluppatori sono stati avvisati." + await context.bot.send_message(chat_id=chat_id, text=msg) + + except Exception: + # Ensure we never raise from the error handler itself + logging.exception("Exception in the error handler") ######################################### # Funzioni async per i comandi e messaggi ######################################### - @staticmethod - async def __start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - message, user = await BotFunctions.handle_message(update) + async def __start(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + message, user = await self.handle_message(update) logging.info(f"@{user.username} started the conversation.") - await BotFunctions.start_message(user, message) + await self.start_message(user, message) return CONFIGS - @staticmethod - async def __model_team(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - return await BotFunctions.handle_configs(update, ConfigsChat.MODEL_TEAM) + async def __model_team(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + return await self._model_select(update, ConfigsChat.MODEL_TEAM) - @staticmethod - async def __model_output(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - return await BotFunctions.handle_configs(update, ConfigsChat.MODEL_OUTPUT) + async def __model_output(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + return await self._model_select(update, ConfigsChat.MODEL_OUTPUT) - @staticmethod - async def __strategy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - query, _ = await BotFunctions.handle_callbackquery(update) + async def _model_select(self, update: Update, state: ConfigsChat, msg: str | None = None) -> int: + query, _ = await self.handle_callbackquery(update) - strategies = [(s.name, f"__select_config:{ConfigsChat.STRATEGY}:{s.name}") for s in Pipeline.all_styles] + models = [(m.label, self.callback_data([f"__select_config", str(state), m.name])) for m in self.pipeline.configs.models.all_models] + inline_btns = [[InlineKeyboardButton(name, callback_data=callback_data)] for name, callback_data in models] + + await query.edit_message_text(msg or state.value, reply_markup=InlineKeyboardMarkup(inline_btns)) + return SELECT_CONFIG + + async def __strategy(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + query, _ = await self.handle_callbackquery(update) + + strategies = [(s.label, self.callback_data([f"__select_config", str(ConfigsChat.STRATEGY), s.name])) for s in self.pipeline.configs.strategies] inline_btns = [[InlineKeyboardButton(name, callback_data=callback_data)] for name, callback_data in strategies] await query.edit_message_text("Select a strategy", reply_markup=InlineKeyboardMarkup(inline_btns)) return SELECT_CONFIG - @staticmethod - async def __select_config(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - query, user = await BotFunctions.handle_callbackquery(update) - logging.info(f"@{user.username} --> {query.data}") + async def __select_config(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + query, user = await self.handle_callbackquery(update) + logging.debug(f"@{user.username} --> {query.data}") - req = BotFunctions.users_req[user] - - _, state, model_name = str(query.data).split(':') + req = self.user_requests[user] + _, state, model_name = str(query.data).split(QUERY_SEP) if state == str(ConfigsChat.MODEL_TEAM): - req.model_team = AppModels[model_name] + req.team_model = self.pipeline.configs.get_model_by_name(model_name) if state == str(ConfigsChat.MODEL_OUTPUT): - req.model_output = AppModels[model_name] + req.leader_model = self.pipeline.configs.get_model_by_name(model_name) if state == str(ConfigsChat.STRATEGY): - req.strategy = PredictorStyle[model_name] + req.strategy = self.pipeline.configs.get_strategy_by_name(model_name) - await BotFunctions.start_message(user, query) + await self.start_message(user, query) return CONFIGS - @staticmethod - async def __start_team(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - message, user = await BotFunctions.handle_message(update) + async def __start_team(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + message, user = await self.handle_message(update) - confs = BotFunctions.users_req[user] + confs = self.user_requests[user] confs.user_query = message.text or "" - logging.info(f"@{user.username} started the team with [{confs.model_team}, {confs.model_output}, {confs.strategy}]") - await BotFunctions.__run_team(update, confs) + logging.info(f"@{user.username} started the team with [{confs.team_model}, {confs.leader_model}, {confs.strategy}]") + await self.__run_team(update, confs) logging.info(f"@{user.username} team finished.") return ConversationHandler.END - @staticmethod - async def __cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - query, user = await BotFunctions.handle_callbackquery(update) + async def __cancel(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + query, user = await self.handle_callbackquery(update) logging.info(f"@{user.username} canceled the conversation.") - if user in BotFunctions.users_req: - del BotFunctions.users_req[user] + if user in self.user_requests: + del self.user_requests[user] await query.edit_message_text("Conversation canceled. Use /start to begin again.") return ConversationHandler.END - @staticmethod - async def __run_team(update: Update, confs: ConfigsRun) -> None: + async def __run_team(self, update: Update, confs: ConfigsRun) -> None: if not update.message: return bot = update.get_bot() @@ -230,9 +230,9 @@ class BotFunctions: configs_str = [ 'Running with configurations: ', - f'Team: {confs.model_team.name}', - f'Output: {confs.model_output.name}', - f'Strategy: {confs.strategy.name}', + f'Team: {confs.team_model.label}', + f'Output: {confs.leader_model.label}', + f'Strategy: {confs.strategy.label}', f'Query: "{confs.user_query}"' ] full_message = f"""```\n{'\n'.join(configs_str)}\n```\n\n""" @@ -242,14 +242,13 @@ class BotFunctions: # Remove user query and bot message await bot.delete_message(chat_id=chat_id, message_id=update.message.id) - # TODO settare correttamente i modelli - pipeline = Pipeline() - #pipeline.choose_predictor(Pipeline.available_models.index(confs.model_team)) - pipeline.choose_style(Pipeline.all_styles.index(confs.strategy)) + self.pipeline.leader_model = confs.leader_model + self.pipeline.team_model = confs.team_model + self.pipeline.strategy = confs.strategy # TODO migliorare messaggi di attesa await bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING) - report_content = pipeline.interact(confs.user_query) + report_content = self.pipeline.interact(confs.user_query) await msg.delete() # attach report file to the message -- 2.49.1 From 30077801600bd51ecc7f0cd49bfbc0f6ce218430 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Sun, 12 Oct 2025 20:26:02 +0200 Subject: [PATCH 15/19] Fix logging to use labels for team model, leader model, and strategy --- src/app/interface/telegram_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/interface/telegram_app.py b/src/app/interface/telegram_app.py index a091797..039022c 100644 --- a/src/app/interface/telegram_app.py +++ b/src/app/interface/telegram_app.py @@ -207,7 +207,7 @@ class TelegramApp: confs = self.user_requests[user] confs.user_query = message.text or "" - logging.info(f"@{user.username} started the team with [{confs.team_model}, {confs.leader_model}, {confs.strategy}]") + logging.info(f"@{user.username} started the team with [{confs.team_model.label}, {confs.leader_model.label}, {confs.strategy.label}]") await self.__run_team(update, confs) logging.info(f"@{user.username} team finished.") -- 2.49.1 From 5f7a483884501dcfdace20fb72e6a810f1c1de0d Mon Sep 17 00:00:00 2001 From: Berack96 Date: Sun, 12 Oct 2025 23:48:59 +0200 Subject: [PATCH 16/19] Rinomina il lock da _lock a __lock per garantire l'incapsulamento nella classe AppConfig --- src/app/configs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/configs.py b/src/app/configs.py index f0cd797..29c2178 100644 --- a/src/app/configs.py +++ b/src/app/configs.py @@ -88,7 +88,7 @@ class AppConfig(BaseModel): models: ModelsConfig = ModelsConfig() agents: AgentsConfigs = AgentsConfigs() - _lock: ClassVar[threading.Lock] = threading.Lock() + __lock: ClassVar[threading.Lock] = threading.Lock() @classmethod def load(cls, file_path: str = "configs.yaml") -> 'AppConfig': @@ -110,7 +110,7 @@ class AppConfig(BaseModel): return configs def __new__(cls, *args: Any, **kwargs: Any) -> 'AppConfig': - with cls._lock: + with cls.__lock: if not hasattr(cls, 'instance'): cls.instance = super(AppConfig, cls).__new__(cls) return cls.instance -- 2.49.1 From 01fbd5607cc24d916c817a48873a5aaeeafac6d8 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Mon, 13 Oct 2025 00:31:56 +0200 Subject: [PATCH 17/19] Rinomina i logger per una migliore identificazione e gestisce le eccezioni nel bot di Telegram --- src/app/__main__.py | 11 ++++++----- src/app/agents/pipeline.py | 8 +++++++- src/app/api/wrapper_handler.py | 2 +- src/app/interface/telegram_app.py | 4 ++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/app/__main__.py b/src/app/__main__.py index 7c6557a..dca46fb 100644 --- a/src/app/__main__.py +++ b/src/app/__main__.py @@ -1,4 +1,3 @@ -# IMPORTANTE: Carichiamo le variabili d'ambiente PRIMA di qualsiasi altra cosa import asyncio import logging from dotenv import load_dotenv @@ -8,7 +7,6 @@ from app.agents import Pipeline if __name__ == "__main__": - # Inizializzazioni load_dotenv() configs = AppConfig.load() @@ -23,8 +21,11 @@ if __name__ == "__main__": telegram = TelegramApp(pipeline) telegram.add_miniapp_url(share_url) telegram.run() - except Exception as _: - logging.warning("Telegram bot could not be started. Continuing without it.") - asyncio.get_event_loop().run_forever() + except AssertionError as e: + try: + logging.warning(f"Telegram bot could not be started: {e}") + asyncio.get_event_loop().run_forever() + except KeyboardInterrupt: + logging.info("Shutting down due to KeyboardInterrupt") finally: gradio.close() diff --git a/src/app/agents/pipeline.py b/src/app/agents/pipeline.py index f52cf28..3338cb8 100644 --- a/src/app/agents/pipeline.py +++ b/src/app/agents/pipeline.py @@ -3,7 +3,7 @@ from app.agents.team import create_team_with from app.agents.prompts import * from app.configs import AppConfig -logging = logging.getLogger(__name__) +logging = logging.getLogger("pipeline") class Pipeline: @@ -30,6 +30,12 @@ class Pipeline: """ self.leader_model = self.configs.models.all_models[index] + def choose_team(self, index: int): + """ + Sceglie il modello LLM da usare per il Team. + """ + self.team_model = self.configs.models.all_models[index] + def choose_strategy(self, index: int): """ Sceglie la strategia da usare per il Predictor. diff --git a/src/app/api/wrapper_handler.py b/src/app/api/wrapper_handler.py index 4760eaf..cf6ce74 100644 --- a/src/app/api/wrapper_handler.py +++ b/src/app/api/wrapper_handler.py @@ -4,7 +4,7 @@ import time import traceback from typing import Any, Callable, Generic, TypeVar -logging = logging.getLogger(__name__) +logging = logging.getLogger("wrapper_handler") WrapperType = TypeVar("WrapperType") WrapperClassType = TypeVar("WrapperClassType") OutputType = TypeVar("OutputType") diff --git a/src/app/interface/telegram_app.py b/src/app/interface/telegram_app.py index 039022c..a9fbfbe 100644 --- a/src/app/interface/telegram_app.py +++ b/src/app/interface/telegram_app.py @@ -14,7 +14,7 @@ from app.configs import AppConfig # per per_message di ConversationHandler che rompe sempre qualunque input tu metta warnings.filterwarnings("ignore") -logging = logging.getLogger(__name__) +logging = logging.getLogger("telegram") # Lo stato cambia in base al valore di ritorno delle funzioni async @@ -69,7 +69,7 @@ class TelegramApp: })} httpx.post(endpoint, data=payload) except httpx.HTTPError as e: - logging.info(f"Failed to update mini app URL: {e}") + logging.warning(f"Failed to update mini app URL: {e}") def create_bot(self) -> None: """ -- 2.49.1 From 8512985ca65068e3eb22a21c2258bc4a181bfbdb Mon Sep 17 00:00:00 2001 From: Berack96 Date: Mon, 13 Oct 2025 00:54:25 +0200 Subject: [PATCH 18/19] Aggiorna i messaggi di errore nel gestore Telegram per una migliore chiarezza e modifica il commento nel file di configurazione per riflettere lo stato del modello. --- configs.yaml | 4 ++-- src/app/interface/telegram_app.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/configs.yaml b/configs.yaml index e2f444d..5d70b13 100644 --- a/configs.yaml +++ b/configs.yaml @@ -17,8 +17,8 @@ models: gemini: - name: gemini-2.0-flash label: Gemini - - name: gemini-2.0-pro - label: Gemini Pro + # - name: gemini-2.0-pro # TODO Non funziona, ha un nome diverso + # label: Gemini Pro ollama: - name: gpt-oss:latest label: Ollama GPT diff --git a/src/app/interface/telegram_app.py b/src/app/interface/telegram_app.py index a9fbfbe..5717ff2 100644 --- a/src/app/interface/telegram_app.py +++ b/src/app/interface/telegram_app.py @@ -140,12 +140,12 @@ class TelegramApp: async def __error_handler(self, update: object, context: ContextTypes.DEFAULT_TYPE) -> None: try: - logging.exception(f"Unhandled exception in Telegram handler {context.error}") + logging.error(f"Unhandled exception in Telegram handler: {context.error}") # Try to notify the user in chat if possible if isinstance(update, Update) and update.effective_chat: chat_id = update.effective_chat.id - msg = "Si è verificato un errore inatteso. Gli sviluppatori sono stati avvisati." + msg = "An error occurred while processing your request." await context.bot.send_message(chat_id=chat_id, text=msg) except Exception: -- 2.49.1 From 1cbf9b1acbf56254c14954b232e03fb2becf0d0c Mon Sep 17 00:00:00 2001 From: Berack96 Date: Mon, 13 Oct 2025 10:45:10 +0200 Subject: [PATCH 19/19] Aggiungi un messaggio di attesa durante la generazione del report nel bot di Telegram --- src/app/interface/telegram_app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/interface/telegram_app.py b/src/app/interface/telegram_app.py index 5717ff2..3bef9d9 100644 --- a/src/app/interface/telegram_app.py +++ b/src/app/interface/telegram_app.py @@ -236,7 +236,8 @@ class TelegramApp: f'Query: "{confs.user_query}"' ] full_message = f"""```\n{'\n'.join(configs_str)}\n```\n\n""" - msg = await bot.edit_message_text(chat_id=chat_id, message_id=msg_id, text=full_message, parse_mode='MarkdownV2') + first_message = full_message + "Generating report, please wait" + msg = await bot.edit_message_text(chat_id=chat_id, message_id=msg_id, text=first_message, parse_mode='MarkdownV2') if isinstance(msg, bool): return # Remove user query and bot message -- 2.49.1