Add Telegram bot support #23
@@ -3,24 +3,32 @@ from enum import Enum
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from agno.utils.log import log_info # type: ignore
|
from agno.utils.log import log_info # type: ignore
|
||||||
from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User
|
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.models import AppModels
|
||||||
from app.predictor import PredictorStyle
|
from app.predictor import PredictorStyle
|
||||||
|
|
||||||
|
|
||||||
# conversation states
|
# Lo stato cambia in base al valore di ritorno delle funzioni async
|
||||||
class ConfigStates(Enum):
|
# 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_TEAM = "Team Model"
|
||||||
MODEL_OUTPUT = "Output Model"
|
MODEL_OUTPUT = "Output Model"
|
||||||
STRATEGY = "Strategy"
|
STRATEGY = "Strategy"
|
||||||
|
|
||||||
# conversation stages (checkpoints)
|
class ConfigsRun:
|
||||||
class Checkpoints(Enum):
|
|
||||||
CONFIGS = 1
|
|
||||||
TEAM_RUNNING = 2
|
|
||||||
END = 3
|
|
||||||
|
|
||||||
class RunConfigs:
|
|
||||||
model_team: AppModels
|
model_team: AppModels
|
||||||
model_output: AppModels
|
model_output: AppModels
|
||||||
strategy: PredictorStyle
|
strategy: PredictorStyle
|
||||||
@@ -30,10 +38,12 @@ class RunConfigs:
|
|||||||
self.model_output = AppModels.OLLAMA_QWEN_1B
|
self.model_output = AppModels.OLLAMA_QWEN_1B
|
||||||
self.strategy = PredictorStyle.CONSERVATIVE
|
self.strategy = PredictorStyle.CONSERVATIVE
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BotFunctions:
|
class BotFunctions:
|
||||||
|
|
||||||
# In theory this is already thread-safe if run with CPython
|
# 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()
|
app_models: list[AppModels] = AppModels.availables()
|
||||||
strategies: list[PredictorStyle] = list(PredictorStyle)
|
strategies: list[PredictorStyle] = list(PredictorStyle)
|
||||||
|
|
||||||
@@ -55,17 +65,18 @@ class BotFunctions:
|
|||||||
app = Application.builder().token(token).build()
|
app = Application.builder().token(token).build()
|
||||||
|
|
||||||
conv_handler = ConversationHandler(
|
conv_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', BotFunctions.__start)],
|
||||||
states={
|
states={
|
||||||
Checkpoints.CONFIGS: [
|
CONFIGS: [
|
||||||
CallbackQueryHandler(BotFunctions.__model_team, pattern=ConfigStates.MODEL_TEAM.name),
|
CallbackQueryHandler(BotFunctions.__model_team, pattern=ConfigsChat.MODEL_TEAM.name),
|
||||||
CallbackQueryHandler(BotFunctions.__model_output, pattern=ConfigStates.MODEL_OUTPUT.name),
|
CallbackQueryHandler(BotFunctions.__model_output, pattern=ConfigsChat.MODEL_OUTPUT.name),
|
||||||
CallbackQueryHandler(BotFunctions.__strategy, pattern=ConfigStates.STRATEGY.name),
|
CallbackQueryHandler(BotFunctions.__strategy, pattern=ConfigsChat.STRATEGY.name),
|
||||||
CallbackQueryHandler(BotFunctions.__next, pattern='^__next'),
|
CallbackQueryHandler(BotFunctions.__cancel, pattern='^cancel$'),
|
||||||
CallbackQueryHandler(BotFunctions.__cancel, pattern='^cancel$')
|
MessageHandler(filters.TEXT, BotFunctions.__start_team) # Any text message
|
||||||
],
|
],
|
||||||
Checkpoints.TEAM_RUNNING: [],
|
SELECT_CONFIG: [
|
||||||
Checkpoints.END: [
|
CallbackQueryHandler(BotFunctions.__select_config, pattern='^__select_config:.*$'),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
fallbacks=[CommandHandler('start', BotFunctions.__start)],
|
fallbacks=[CommandHandler('start', BotFunctions.__start)],
|
||||||
@@ -81,18 +92,18 @@ class BotFunctions:
|
|||||||
########################################
|
########################################
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def start_message(user: User, query: CallbackQuery | Message) -> None:
|
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_team = f"{ConfigsChat.MODEL_TEAM.value}: {confs.model_team.name}"
|
||||||
str_model_output = f"{ConfigStates.MODEL_OUTPUT.value}:\t\t {confs.model_output.name}"
|
str_model_output = f"{ConfigsChat.MODEL_OUTPUT.value}:\t\t {confs.model_output.name}"
|
||||||
str_strategy = f"{ConfigStates.STRATEGY.value}:\t\t {confs.strategy.name}"
|
str_strategy = f"{ConfigsChat.STRATEGY.value}:\t\t {confs.strategy.name}"
|
||||||
|
|
||||||
msg, keyboard = (
|
msg, keyboard = (
|
||||||
"Please choose an option or write your query",
|
"Please choose an option or write your query",
|
||||||
InlineKeyboardMarkup([
|
InlineKeyboardMarkup([
|
||||||
[InlineKeyboardButton(str_model_team, callback_data=ConfigStates.MODEL_TEAM.name)],
|
[InlineKeyboardButton(str_model_team, callback_data=ConfigsChat.MODEL_TEAM.name)],
|
||||||
[InlineKeyboardButton(str_model_output, callback_data=ConfigStates.MODEL_OUTPUT.name)],
|
[InlineKeyboardButton(str_model_output, callback_data=ConfigsChat.MODEL_OUTPUT.name)],
|
||||||
[InlineKeyboardButton(str_strategy, callback_data=ConfigStates.STRATEGY.name)],
|
[InlineKeyboardButton(str_strategy, callback_data=ConfigsChat.STRATEGY.name)],
|
||||||
[InlineKeyboardButton("Cancel", callback_data='cancel')]
|
[InlineKeyboardButton("Cancel", callback_data='cancel')]
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -103,14 +114,14 @@ class BotFunctions:
|
|||||||
await query.reply_text(msg, reply_markup=keyboard, parse_mode='MarkdownV2')
|
await query.reply_text(msg, reply_markup=keyboard, parse_mode='MarkdownV2')
|
||||||
|
|
||||||
@staticmethod
|
@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)
|
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]
|
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))
|
await query.edit_message_text(msg or state.value, reply_markup=InlineKeyboardMarkup(inline_btns))
|
||||||
return Checkpoints.CONFIGS
|
return SELECT_CONFIG
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def handle_callbackquery(update: Update) -> tuple[CallbackQuery, User]:
|
async def handle_callbackquery(update: Update) -> tuple[CallbackQuery, User]:
|
||||||
@@ -119,62 +130,92 @@ class BotFunctions:
|
|||||||
await query.answer() # Acknowledge the callback query
|
await query.answer() # Acknowledge the callback query
|
||||||
return query, query.from_user
|
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
|
# Funzioni async per i comandi e messaggi
|
||||||
#########################################
|
#########################################
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def __start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints:
|
async def __start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
assert update.message and update.message.from_user, "Update message or user is None"
|
message, user = await BotFunctions.handle_message(update)
|
||||||
user = update.message.from_user
|
|
||||||
log_info(f"@{user.username} started the conversation.")
|
log_info(f"@{user.username} started the conversation.")
|
||||||
await BotFunctions.start_message(user, update.message)
|
await BotFunctions.start_message(user, message)
|
||||||
return Checkpoints.CONFIGS
|
return CONFIGS
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def __cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints:
|
async def __model_team(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
query, user = await BotFunctions.handle_callbackquery(update)
|
return await BotFunctions.handle_configs(update, ConfigsChat.MODEL_TEAM)
|
||||||
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
|
@staticmethod
|
||||||
async def __model_team(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints:
|
async def __model_output(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
return await BotFunctions.handle_configs(update, ConfigStates.MODEL_TEAM)
|
return await BotFunctions.handle_configs(update, ConfigsChat.MODEL_OUTPUT)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def __model_output(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Checkpoints:
|
async def __strategy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
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)
|
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]
|
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))
|
await query.edit_message_text("Select a strategy", reply_markup=InlineKeyboardMarkup(inline_btns))
|
||||||
return Checkpoints.CONFIGS
|
return SELECT_CONFIG
|
||||||
|
|
||||||
@staticmethod
|
@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)
|
query, user = await BotFunctions.handle_callbackquery(update)
|
||||||
log_info(f"@{user.username} --> {query.data}")
|
log_info(f"@{user.username} --> {query.data}")
|
||||||
|
|
||||||
req = BotFunctions.users_req[user]
|
req = BotFunctions.users_req[user]
|
||||||
|
|
||||||
_, state, model_name = str(query.data).split(':')
|
_, 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]
|
req.model_team = AppModels[model_name]
|
||||||
if state == str(ConfigStates.MODEL_OUTPUT):
|
if state == str(ConfigsChat.MODEL_OUTPUT):
|
||||||
req.model_output = AppModels[model_name]
|
req.model_output = AppModels[model_name]
|
||||||
if state == str(ConfigStates.STRATEGY):
|
if state == str(ConfigsChat.STRATEGY):
|
||||||
req.strategy = PredictorStyle[model_name]
|
req.strategy = PredictorStyle[model_name]
|
||||||
|
|
||||||
await BotFunctions.start_message(user, query)
|
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__":
|
if __name__ == "__main__":
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|||||||
Reference in New Issue
Block a user