From 6ff2fcc2a7c61fd3768c4f0392dd5f71a4c87ffc Mon Sep 17 00:00:00 2001 From: Nunzi99 <115243475+Nunzi99@users.noreply.github.com> Date: Sun, 26 Oct 2025 14:07:44 +0100 Subject: [PATCH 1/6] Aggiungere LLM Providers (#48) * Refactor configs dei modelli * Aggiunti Providers --- .env.example | 9 ++- configs.yaml | 12 ++++ pyproject.toml | 4 ++ src/app/configs.py | 121 +++++++++++++++++++++----------------- src/app/interface/chat.py | 6 +- 5 files changed, 96 insertions(+), 56 deletions(-) diff --git a/.env.example b/.env.example index 694300e..b999e0e 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,14 @@ # https://makersuite.google.com/app/apikey GOOGLE_API_KEY= - +# https://platform.openai.com/settings/organization/api-keys +OPENAI_API_KEY= +# https://admin.mistral.ai/organization/api-keys +MISTRAL_API_KEY= +# https://platform.deepseek.com/api_keys +DEEPSEEK_API_KEY= +# https://console.x.ai/team/%TEAM_ID%/api-keys +XAI_API_KEY= ############################################################################### # Configurazioni per gli agenti di mercato ############################################################################### diff --git a/configs.yaml b/configs.yaml index f83ae9e..615fcf8 100644 --- a/configs.yaml +++ b/configs.yaml @@ -19,6 +19,18 @@ models: label: Gemini # - name: gemini-2.0-pro # TODO Non funziona, ha un nome diverso # label: Gemini Pro + gpt: + - name: gpt-4o + label: OpenAIChat + deepseek: + - name: deepseek-chat + label: DeepSeek + xai: + - name: grok-3 + label: xAI + mistral: + - name: mistral-large-latest + label: Mistral ollama: - name: gpt-oss:latest label: Ollama GPT diff --git a/pyproject.toml b/pyproject.toml index e7b2209..5e3bdb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,10 @@ dependencies = [ # Modelli supportati e installati (aggiungere qui sotto quelli che si vogliono usare) "google-genai", "ollama", + "openai", + "mistralai", + "deepseek", + "xai", # API di exchange di criptovalute "coinbase-advanced-py", diff --git a/src/app/configs.py b/src/app/configs.py index 45b5b01..edd3e39 100644 --- a/src/app/configs.py +++ b/src/app/configs.py @@ -10,6 +10,10 @@ from agno.tools import Toolkit from agno.models.base import Model from agno.models.google import Gemini from agno.models.ollama import Ollama +from agno.models.openai import OpenAIChat +from agno.models.mistral import MistralChat +from agno.models.deepseek import DeepSeek +from agno.models.xai import xAI log = logging.getLogger(__name__) @@ -19,7 +23,6 @@ class AppModel(BaseModel): name: str = "gemini-2.0-flash" label: str = "Gemini" model: type[Model] | None = None - def get_model(self, instructions: str) -> Model: """ Restituisce un'istanza del modello specificato. @@ -54,22 +57,84 @@ class AppModel(BaseModel): output_schema=output_schema ) + + class APIConfig(BaseModel): retry_attempts: int = 3 retry_delay_seconds: int = 2 + + class Strategy(BaseModel): name: str = "Conservative" label: str = "Conservative" description: str = "Focus on low-risk investments with steady returns." + + class ModelsConfig(BaseModel): gemini: list[AppModel] = [AppModel()] + gpt: list[AppModel] = [AppModel(name="gpt-4o", label="OpenAIChat")] + mistral: list[AppModel] = [AppModel(name="mistral-large-latest", label="Mistral")] + deepseek: list[AppModel] = [AppModel(name="deepseek-chat", label="DeepSeek")] + xai: list[AppModel] = [AppModel(name="grok-3", label="xAI")] ollama: list[AppModel] = [] @property def all_models(self) -> list[AppModel]: - return self.gemini + self.ollama + return self.gemini + self.ollama + self.gpt + self.mistral + self.deepseek + self.xai + + def validate_models(self) -> None: + """ + Validate the configured models for each provider. + """ + self.__validate_online_models(self.gemini, clazz=Gemini, key="GOOGLE_API_KEY") + self.__validate_online_models(self.gpt, clazz=OpenAIChat, key="OPENAI_API_KEY") + self.__validate_online_models(self.mistral, clazz=MistralChat, key="MISTRAL_API_KEY") + self.__validate_online_models(self.deepseek, clazz=DeepSeek, key="DEEPSEEK_API_KEY") + self.__validate_online_models(self.xai, clazz=xAI, key="XAI_API_KEY") + + self.__validate_ollama_models() + + def __validate_online_models(self, models: list[AppModel], clazz: type[Model], key: str | None = None) -> None: + """ + Validate models for online providers like Gemini. + Args: + models: list of AppModel instances to validate + clazz: class of the model (e.g. Gemini) + key: API key required for the provider (optional) + """ + if key and os.getenv(key) is None: + log.warning(f"No {key} set in environment variables for provider.") + models.clear() + return + + for model in models: + model.model = clazz + + def __validate_ollama_models(self) -> None: + """ + Validate models for the Ollama provider. + """ + try: + models_list = ollama.list() + availables = {model['model'] for model in models_list['models']} + not_availables: list[str] = [] + + for model in self.ollama: + if model.name in availables: + model.model = Ollama + else: + not_availables.append(model.name) + if not_availables: + log.warning(f"Ollama models not available: {not_availables}") + + self.ollama = [model for model in self.ollama if model.model] + + except Exception as e: + log.warning(f"Ollama is not running or not reachable: {e}") + + class AgentsConfigs(BaseModel): strategy: str = "Conservative" @@ -118,7 +183,7 @@ class AppConfig(BaseModel): super().__init__(*args, **kwargs) self.set_logging_level() - self.validate_models() + self.models.validate_models() self._initialized = True def get_model_by_name(self, name: str) -> AppModel: @@ -186,53 +251,3 @@ class AppConfig(BaseModel): logger = logging.getLogger(logger_name) logger.handlers.clear() logger.propagate = True - - def validate_models(self) -> None: - """ - Validate the configured models for each provider. - """ - self.__validate_online_models("gemini", clazz=Gemini, key="GOOGLE_API_KEY") - self.__validate_ollama_models() - - def __validate_online_models(self, provider: str, clazz: type[Model], key: str | None = None) -> None: - """ - Validate models for online providers like Gemini. - Args: - provider: name of the provider (e.g. "gemini") - clazz: class of the model (e.g. Gemini) - key: API key required for the provider (optional) - """ - if getattr(self.models, provider) is None: - log.warning(f"No models configured for provider '{provider}'.") - - models: list[AppModel] = getattr(self.models, provider) - if key and os.getenv(key) is None: - log.warning(f"No {key} set in environment variables for {provider}.") - models.clear() - return - - for model in models: - model.model = clazz - - def __validate_ollama_models(self) -> None: - """ - Validate models for the Ollama provider. - """ - try: - models_list = ollama.list() - availables = {model['model'] for model in models_list['models']} - not_availables: list[str] = [] - - for model in self.models.ollama: - if model.name in availables: - model.model = Ollama - else: - not_availables.append(model.name) - if not_availables: - log.warning(f"Ollama models not available: {not_availables}") - - self.models.ollama = [model for model in self.models.ollama if model.model] - - except Exception as e: - log.warning(f"Ollama is not running or not reachable: {e}") - diff --git a/src/app/interface/chat.py b/src/app/interface/chat.py index 150197b..37529f4 100644 --- a/src/app/interface/chat.py +++ b/src/app/interface/chat.py @@ -83,13 +83,15 @@ class ChatManager: label="Modello da usare" ) provider.change(fn=self.inputs.choose_team_leader, inputs=provider, outputs=None) + provider.value = self.inputs.team_leader_model.label - style = gr.Dropdown( + strategy = gr.Dropdown( choices=self.inputs.list_strategies_names(), type="index", label="Stile di investimento" ) - style.change(fn=self.inputs.choose_strategy, inputs=style, outputs=None) + strategy.change(fn=self.inputs.choose_strategy, inputs=strategy, outputs=None) + strategy.value = self.inputs.strategy.label chat = gr.ChatInterface( fn=self.gradio_respond From 830d1933b1f7420fd5e743575cd4683ae6b00a91 Mon Sep 17 00:00:00 2001 From: Giacomo Bertolazzi <31776951+Berack96@users.noreply.github.com> Date: Sun, 26 Oct 2025 16:56:21 +0100 Subject: [PATCH 2/6] Revert "Aggiungere LLM Providers (#48)" (#51) This reverts commit 6ff2fcc2a7c61fd3768c4f0392dd5f71a4c87ffc. --- .env.example | 9 +-- configs.yaml | 12 ---- pyproject.toml | 4 -- src/app/configs.py | 121 +++++++++++++++++--------------------- src/app/interface/chat.py | 6 +- 5 files changed, 56 insertions(+), 96 deletions(-) diff --git a/.env.example b/.env.example index b999e0e..694300e 100644 --- a/.env.example +++ b/.env.example @@ -5,14 +5,7 @@ # https://makersuite.google.com/app/apikey GOOGLE_API_KEY= -# https://platform.openai.com/settings/organization/api-keys -OPENAI_API_KEY= -# https://admin.mistral.ai/organization/api-keys -MISTRAL_API_KEY= -# https://platform.deepseek.com/api_keys -DEEPSEEK_API_KEY= -# https://console.x.ai/team/%TEAM_ID%/api-keys -XAI_API_KEY= + ############################################################################### # Configurazioni per gli agenti di mercato ############################################################################### diff --git a/configs.yaml b/configs.yaml index 615fcf8..f83ae9e 100644 --- a/configs.yaml +++ b/configs.yaml @@ -19,18 +19,6 @@ models: label: Gemini # - name: gemini-2.0-pro # TODO Non funziona, ha un nome diverso # label: Gemini Pro - gpt: - - name: gpt-4o - label: OpenAIChat - deepseek: - - name: deepseek-chat - label: DeepSeek - xai: - - name: grok-3 - label: xAI - mistral: - - name: mistral-large-latest - label: Mistral ollama: - name: gpt-oss:latest label: Ollama GPT diff --git a/pyproject.toml b/pyproject.toml index 5e3bdb7..e7b2209 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,10 +24,6 @@ dependencies = [ # Modelli supportati e installati (aggiungere qui sotto quelli che si vogliono usare) "google-genai", "ollama", - "openai", - "mistralai", - "deepseek", - "xai", # API di exchange di criptovalute "coinbase-advanced-py", diff --git a/src/app/configs.py b/src/app/configs.py index edd3e39..45b5b01 100644 --- a/src/app/configs.py +++ b/src/app/configs.py @@ -10,10 +10,6 @@ from agno.tools import Toolkit from agno.models.base import Model from agno.models.google import Gemini from agno.models.ollama import Ollama -from agno.models.openai import OpenAIChat -from agno.models.mistral import MistralChat -from agno.models.deepseek import DeepSeek -from agno.models.xai import xAI log = logging.getLogger(__name__) @@ -23,6 +19,7 @@ class AppModel(BaseModel): name: str = "gemini-2.0-flash" label: str = "Gemini" model: type[Model] | None = None + def get_model(self, instructions: str) -> Model: """ Restituisce un'istanza del modello specificato. @@ -57,84 +54,22 @@ class AppModel(BaseModel): output_schema=output_schema ) - - class APIConfig(BaseModel): retry_attempts: int = 3 retry_delay_seconds: int = 2 - - class Strategy(BaseModel): name: str = "Conservative" label: str = "Conservative" description: str = "Focus on low-risk investments with steady returns." - - class ModelsConfig(BaseModel): gemini: list[AppModel] = [AppModel()] - gpt: list[AppModel] = [AppModel(name="gpt-4o", label="OpenAIChat")] - mistral: list[AppModel] = [AppModel(name="mistral-large-latest", label="Mistral")] - deepseek: list[AppModel] = [AppModel(name="deepseek-chat", label="DeepSeek")] - xai: list[AppModel] = [AppModel(name="grok-3", label="xAI")] ollama: list[AppModel] = [] @property def all_models(self) -> list[AppModel]: - return self.gemini + self.ollama + self.gpt + self.mistral + self.deepseek + self.xai - - def validate_models(self) -> None: - """ - Validate the configured models for each provider. - """ - self.__validate_online_models(self.gemini, clazz=Gemini, key="GOOGLE_API_KEY") - self.__validate_online_models(self.gpt, clazz=OpenAIChat, key="OPENAI_API_KEY") - self.__validate_online_models(self.mistral, clazz=MistralChat, key="MISTRAL_API_KEY") - self.__validate_online_models(self.deepseek, clazz=DeepSeek, key="DEEPSEEK_API_KEY") - self.__validate_online_models(self.xai, clazz=xAI, key="XAI_API_KEY") - - self.__validate_ollama_models() - - def __validate_online_models(self, models: list[AppModel], clazz: type[Model], key: str | None = None) -> None: - """ - Validate models for online providers like Gemini. - Args: - models: list of AppModel instances to validate - clazz: class of the model (e.g. Gemini) - key: API key required for the provider (optional) - """ - if key and os.getenv(key) is None: - log.warning(f"No {key} set in environment variables for provider.") - models.clear() - return - - for model in models: - model.model = clazz - - def __validate_ollama_models(self) -> None: - """ - Validate models for the Ollama provider. - """ - try: - models_list = ollama.list() - availables = {model['model'] for model in models_list['models']} - not_availables: list[str] = [] - - for model in self.ollama: - if model.name in availables: - model.model = Ollama - else: - not_availables.append(model.name) - if not_availables: - log.warning(f"Ollama models not available: {not_availables}") - - self.ollama = [model for model in self.ollama if model.model] - - except Exception as e: - log.warning(f"Ollama is not running or not reachable: {e}") - - + return self.gemini + self.ollama class AgentsConfigs(BaseModel): strategy: str = "Conservative" @@ -183,7 +118,7 @@ class AppConfig(BaseModel): super().__init__(*args, **kwargs) self.set_logging_level() - self.models.validate_models() + self.validate_models() self._initialized = True def get_model_by_name(self, name: str) -> AppModel: @@ -251,3 +186,53 @@ class AppConfig(BaseModel): logger = logging.getLogger(logger_name) logger.handlers.clear() logger.propagate = True + + def validate_models(self) -> None: + """ + Validate the configured models for each provider. + """ + self.__validate_online_models("gemini", clazz=Gemini, key="GOOGLE_API_KEY") + self.__validate_ollama_models() + + def __validate_online_models(self, provider: str, clazz: type[Model], key: str | None = None) -> None: + """ + Validate models for online providers like Gemini. + Args: + provider: name of the provider (e.g. "gemini") + clazz: class of the model (e.g. Gemini) + key: API key required for the provider (optional) + """ + if getattr(self.models, provider) is None: + log.warning(f"No models configured for provider '{provider}'.") + + models: list[AppModel] = getattr(self.models, provider) + if key and os.getenv(key) is None: + log.warning(f"No {key} set in environment variables for {provider}.") + models.clear() + return + + for model in models: + model.model = clazz + + def __validate_ollama_models(self) -> None: + """ + Validate models for the Ollama provider. + """ + try: + models_list = ollama.list() + availables = {model['model'] for model in models_list['models']} + not_availables: list[str] = [] + + for model in self.models.ollama: + if model.name in availables: + model.model = Ollama + else: + not_availables.append(model.name) + if not_availables: + log.warning(f"Ollama models not available: {not_availables}") + + self.models.ollama = [model for model in self.models.ollama if model.model] + + except Exception as e: + log.warning(f"Ollama is not running or not reachable: {e}") + diff --git a/src/app/interface/chat.py b/src/app/interface/chat.py index 37529f4..150197b 100644 --- a/src/app/interface/chat.py +++ b/src/app/interface/chat.py @@ -83,15 +83,13 @@ class ChatManager: label="Modello da usare" ) provider.change(fn=self.inputs.choose_team_leader, inputs=provider, outputs=None) - provider.value = self.inputs.team_leader_model.label - strategy = gr.Dropdown( + style = gr.Dropdown( choices=self.inputs.list_strategies_names(), type="index", label="Stile di investimento" ) - strategy.change(fn=self.inputs.choose_strategy, inputs=strategy, outputs=None) - strategy.value = self.inputs.strategy.label + style.change(fn=self.inputs.choose_strategy, inputs=style, outputs=None) chat = gr.ChatInterface( fn=self.gradio_respond From 93174afc810e798cfbae12f9d4f4b62143842fc4 Mon Sep 17 00:00:00 2001 From: Giacomo Bertolazzi <31776951+Berack96@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:41:49 +0100 Subject: [PATCH 3/6] Fix chat defaults (#46) * Aggiornato il gestore della chat per impostare i valori predefiniti per il modello e la strategia nel dropdown. --- src/app/interface/chat.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/interface/chat.py b/src/app/interface/chat.py index 150197b..8d626c0 100644 --- a/src/app/interface/chat.py +++ b/src/app/interface/chat.py @@ -79,6 +79,7 @@ class ChatManager: with gr.Row(): provider = gr.Dropdown( choices=self.inputs.list_models_names(), + value=self.inputs.team_leader_model.label, type="index", label="Modello da usare" ) @@ -86,6 +87,7 @@ class ChatManager: style = gr.Dropdown( choices=self.inputs.list_strategies_names(), + value=self.inputs.strategy.label, type="index", label="Stile di investimento" ) @@ -103,4 +105,4 @@ class ChatManager: clear_btn.click(self.gradio_clear, inputs=None, outputs=[chat.chatbot, chat.chatbot_state]) save_btn.click(self.gradio_save, inputs=None, outputs=None) load_btn.click(self.gradio_load, inputs=None, outputs=[chat.chatbot, chat.chatbot_state]) - return interface \ No newline at end of file + return interface From 551b6a049f4c2c86da9594deb190a1822dd9e029 Mon Sep 17 00:00:00 2001 From: Giacomo Bertolazzi <31776951+Berack96@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:42:13 +0100 Subject: [PATCH 4/6] Update telegram interface (#44) * Rename telegram file * Added LLM providers selection * Updated callback handlers * Improved telegram user waiting message --- src/app/agents/core.py | 92 ++++++++ src/app/agents/pipeline.py | 2 +- src/app/api/wrapper_handler.py | 2 +- src/app/interface/__init__.py | 2 +- .../{telegram_app.py => telegram.py} | 217 +++++++++++------- 5 files changed, 226 insertions(+), 89 deletions(-) rename src/app/interface/{telegram_app.py => telegram.py} (51%) diff --git a/src/app/agents/core.py b/src/app/agents/core.py index 4a685cb..1258f54 100644 --- a/src/app/agents/core.py +++ b/src/app/agents/core.py @@ -41,6 +41,13 @@ class PipelineInputs: # ====================== # Dropdown handlers # ====================== + def choose_query_checker(self, index: int): + """ + Sceglie il modello LLM da usare per l'analizzatore di query. + """ + assert index >= 0 and index < len(self.configs.models.all_models), "Index out of range for models list." + self.query_analyzer_model = self.configs.models.all_models[index] + def choose_team_leader(self, index: int): """ Sceglie il modello LLM da usare per il Team Leader. @@ -55,6 +62,13 @@ class PipelineInputs: assert index >= 0 and index < len(self.configs.models.all_models), "Index out of range for models list." self.team_model = self.configs.models.all_models[index] + def choose_report_generator(self, index: int): + """ + Sceglie il modello LLM da usare per il generatore di report. + """ + assert index >= 0 and index < len(self.configs.models.all_models), "Index out of range for models list." + self.report_generation_model = self.configs.models.all_models[index] + def choose_strategy(self, index: int): """ Sceglie la strategia da usare per il Team. @@ -119,3 +133,81 @@ class PipelineInputs: social_tool = SocialAPIsTool() social_tool.handler.set_retries(api.retry_attempts, api.retry_delay_seconds) return market_tool, news_tool, social_tool + + def __str__(self) -> str: + return "\n".join([ + f"Query Check: {self.query_analyzer_model.label}", + f"Team Leader: {self.team_leader_model.label}", + f"Team: {self.team_model.label}", + f"Report: {self.report_generation_model.label}", + f"Strategy: {self.strategy.label}", + f"User Query: \"{self.user_query}\"", + ]) + + +class RunMessage: + """ + Classe per gestire i messaggi di stato durante l'esecuzione della pipeline. + Inizializza il messaggio con gli step e aggiorna lo stato, permettendo di ottenere + il messaggio piรน recente da inviare all'utente. + """ + + def __init__(self, inputs: PipelineInputs, prefix: str = "", suffix: str = ""): + """ + Inizializza il messaggio di esecuzione con gli step iniziali. + Tre stati possibili per ogni step: + - In corso (๐Ÿ”ณ) + - In esecuzione (โžก๏ธ) + - Completato (โœ…) + + Lo stato di esecuzione puรฒ essere assegnato solo ad uno step alla volta. + Args: + inputs (PipelineInputs): Input della pipeline per mostrare la configurazione. + prefix (str, optional): Prefisso del messaggio. Defaults to "". + suffix (str, optional): Suffisso del messaggio. Defaults to "". + """ + self.base_message = f"Running configurations: \n{prefix}{inputs}{suffix}\n\n" + self.emojis = ['๐Ÿ”ณ', 'โžก๏ธ', 'โœ…'] + self.placeholder = '<<<>>>' + self.current = 0 + self.steps_total = [ + (f"{self.placeholder} Query Check", 1), + (f"{self.placeholder} Info Recovery", 0), + (f"{self.placeholder} Report Generation", 0), + ] + + def update(self) -> 'RunMessage': + """ + Sposta lo stato di esecuzione al passo successivo. + Lo step precedente completato viene marcato come completato. + Returns: + RunMessage: L'istanza aggiornata di RunMessage. + """ + text_curr, state_curr = self.steps_total[self.current] + self.steps_total[self.current] = (text_curr, state_curr + 1) + self.current = min(self.current + 1, len(self.steps_total)) + if self.current < len(self.steps_total): + text_curr, state_curr = self.steps_total[self.current] + self.steps_total[self.current] = (text_curr, state_curr + 1) + return self + + def update_step(self, text_extra: str = "") -> 'RunMessage': + """ + Aggiorna il messaggio per lo step corrente. + Args: + text_extra (str, optional): Testo aggiuntivo da includere nello step. Defaults to "". + """ + text_curr, state_curr = self.steps_total[self.current] + if text_extra: + text_curr = f"{text_curr.replace('โ•š', 'โ• ')}\nโ•šโ• {text_extra}" + self.steps_total[self.current] = (text_curr, state_curr) + return self + + def get_latest(self) -> str: + """ + Restituisce il messaggio di esecuzione piรน recente. + Returns: + str: Messaggio di esecuzione aggiornato. + """ + steps = [msg.replace(self.placeholder, self.emojis[state]) for msg, state in self.steps_total] + return self.base_message + "\n".join(steps) diff --git a/src/app/agents/pipeline.py b/src/app/agents/pipeline.py index bcec72d..0498f90 100644 --- a/src/app/agents/pipeline.py +++ b/src/app/agents/pipeline.py @@ -33,7 +33,7 @@ class PipelineEvent(str, Enum): (PipelineEvent.QUERY_ANALYZER, lambda _: logging.info(f"[{run_id}] Query Analyzer completed.")), (PipelineEvent.INFO_RECOVERY, lambda _: logging.info(f"[{run_id}] Info Recovery completed.")), (PipelineEvent.REPORT_GENERATION, lambda _: logging.info(f"[{run_id}] Report Generation completed.")), - (PipelineEvent.TOOL_USED, lambda e: logging.info(f"[{run_id}] Tool used [{e.tool.tool_name}] by {e.agent_name}.")), + (PipelineEvent.TOOL_USED, lambda e: logging.info(f"[{run_id}] Tool used [{e.tool.tool_name} {e.tool.tool_args}] by {e.agent_name}.")), (PipelineEvent.RUN_FINISHED, lambda _: logging.info(f"[{run_id}] Run completed.")), ] diff --git a/src/app/api/wrapper_handler.py b/src/app/api/wrapper_handler.py index 30b3887..9c40567 100644 --- a/src/app/api/wrapper_handler.py +++ b/src/app/api/wrapper_handler.py @@ -87,7 +87,7 @@ class WrapperHandler(Generic[WrapperType]): Exception: If all wrappers fail after retries. """ - logging.info(f"{inspect.getsource(func).strip()} {inspect.getclosurevars(func).nonlocals}") + logging.debug(f"{inspect.getsource(func).strip()} {inspect.getclosurevars(func).nonlocals}") results: dict[str, OutputType] = {} starting_index = self.index diff --git a/src/app/interface/__init__.py b/src/app/interface/__init__.py index 186558a..1788339 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 TelegramApp +from app.interface.telegram import TelegramApp __all__ = ["ChatManager", "TelegramApp"] diff --git a/src/app/interface/telegram_app.py b/src/app/interface/telegram.py similarity index 51% rename from src/app/interface/telegram_app.py rename to src/app/interface/telegram.py index 71ff4c8..b720c43 100644 --- a/src/app/interface/telegram_app.py +++ b/src/app/interface/telegram.py @@ -1,6 +1,8 @@ +import asyncio import io import os import json +from typing import Any import httpx import logging import warnings @@ -9,7 +11,7 @@ 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, MessageHandler, filters -from app.agents.pipeline import Pipeline, PipelineInputs +from app.agents.pipeline import Pipeline, PipelineEvent, PipelineInputs, RunMessage # per per_message di ConversationHandler che rompe sempre qualunque input tu metta warnings.filterwarnings("ignore") @@ -21,23 +23,44 @@ logging = logging.getLogger("telegram") # Un semplice schema delle interazioni: # /start # โ•‘ -# V +# v # โ•”โ•โ• CONFIGS <โ•โ•โ•โ•โ•โ•— # โ•‘ โ•‘ โ•šโ•โ•> SELECT_CONFIG -# โ•‘ V -# โ•‘ start_team (polling for updates) +# โ•‘ v ^ +# โ•‘ MODELS โ•โ•โ•โ•โ•โ•โ• +# โ•‘ +# โ• โ•โ•> start (polling for updates) # โ•‘ โ•‘ -# โ•‘ V +# โ•‘ v # โ•šโ•โ•โ•> END -CONFIGS, SELECT_CONFIG = range(2) +CONFIGS, SELECT_MODEL, SELECT_CONFIG = range(3) # Usato per separare la query arrivata da Telegram QUERY_SEP = "|==|" class ConfigsChat(Enum): + MODEL_CHECK = "Check Model" + MODEL_TEAM_LEADER = "Team Leader Model" MODEL_TEAM = "Team Model" - MODEL_OUTPUT = "Output Model" + MODEL_REPORT = "Report Model" + CHANGE_MODELS = "Change Models" STRATEGY = "Strategy" + CANCEL = "Cancel" + + def get_inline_button(self, value_to_display:str="") -> InlineKeyboardButton: + display = self.value if not value_to_display else f"{self.value}: {value_to_display}" + return InlineKeyboardButton(display, callback_data=self.name) + + def change_value(self, inputs: PipelineInputs, new_value:int) -> None: + functions_map = { + self.MODEL_CHECK.name: inputs.choose_query_checker, + self.MODEL_TEAM_LEADER.name: inputs.choose_team_leader, + self.MODEL_TEAM.name: inputs.choose_team, + self.MODEL_REPORT.name: inputs.choose_report_generator, + self.STRATEGY.name: inputs.choose_strategy, + } + functions_map[self.name](new_value) + class TelegramApp: def __init__(self): @@ -72,14 +95,21 @@ class TelegramApp: entry_points=[CommandHandler('start', self.__start)], states={ CONFIGS: [ - CallbackQueryHandler(self.__model_team, pattern=ConfigsChat.MODEL_TEAM.name), - CallbackQueryHandler(self.__model_output, pattern=ConfigsChat.MODEL_OUTPUT.name), + CallbackQueryHandler(self.__models, pattern=ConfigsChat.CHANGE_MODELS.name), CallbackQueryHandler(self.__strategy, pattern=ConfigsChat.STRATEGY.name), - CallbackQueryHandler(self.__cancel, pattern='^cancel$'), - MessageHandler(filters.TEXT, self.__start_team) # Any text message + CallbackQueryHandler(self.__cancel, pattern='^CANCEL$'), + MessageHandler(filters.TEXT, self.__start_llms) # Any text message + ], + SELECT_MODEL: [ + CallbackQueryHandler(self.__model_select, pattern=ConfigsChat.MODEL_CHECK.name), + CallbackQueryHandler(self.__model_select, pattern=ConfigsChat.MODEL_TEAM_LEADER.name), + CallbackQueryHandler(self.__model_select, pattern=ConfigsChat.MODEL_TEAM.name), + CallbackQueryHandler(self.__model_select, pattern=ConfigsChat.MODEL_REPORT.name), + CallbackQueryHandler(self.__go_to_start, pattern='^CANCEL$'), ], SELECT_CONFIG: [ CallbackQueryHandler(self.__select_config, pattern=f"^__select_config{QUERY_SEP}.*$"), + CallbackQueryHandler(self.__go_to_start, pattern='^CANCEL$'), ] }, fallbacks=[CommandHandler('start', self.__start)], @@ -87,45 +117,28 @@ class TelegramApp: self.app = app def run(self) -> None: + """ + Start the Telegram bot polling. This will keep the bot running and listening for updates.\n + This function blocks until the bot is stopped. + """ self.app.run_polling() ######################################## # Funzioni di utilitร  ######################################## - async def start_message(self, user: User, query: CallbackQuery | Message) -> None: - confs = self.user_requests.setdefault(user, PipelineInputs()) - - str_model_team = f"{ConfigsChat.MODEL_TEAM.value}: {confs.team_model.label}" - str_model_output = f"{ConfigsChat.MODEL_OUTPUT.value}: {confs.team_leader_model.label}" - str_strategy = f"{ConfigsChat.STRATEGY.value}: {confs.strategy.label}" - - msg, keyboard = ( - "Please choose an option or write your query", - InlineKeyboardMarkup([ - [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')] - ]) - ) - - 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') - 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" + assert update.callback_query, "Update callback_query is None" + assert update.effective_user, "Update effective_user is None" query = update.callback_query await query.answer() # Acknowledge the callback query - return query, query.from_user + return query, update.effective_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 + def handle_message(self, update: Update) -> tuple[Message, User]: + assert update.message and update.effective_user, "Update message or user is None" + return update.message, update.effective_user def build_callback_data(self, callback: str, config: ConfigsChat, labels: list[str]) -> list[tuple[str, str]]: - return [(label, QUERY_SEP.join((callback, config.value, str(i)))) for i, label in enumerate(labels)] + return [(label, QUERY_SEP.join((callback, config.name, str(i)))) for i, label in enumerate(labels)] async def __error_handler(self, update: object, context: ContextTypes.DEFAULT_TYPE) -> None: try: @@ -142,28 +155,69 @@ class TelegramApp: logging.exception("Exception in the error handler") ######################################### - # Funzioni async per i comandi e messaggi + # Funzioni base di gestione stati ######################################### 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 self.start_message(user, message) + user = update.effective_user.username if update.effective_user else "Unknown" + logging.info(f"@{user} started the conversation.") + return await self.__go_to_start(update, context) + + async def __go_to_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + user = update.effective_user + assert user, "Update effective_user is None" + msg = update.callback_query if update.callback_query else update.message + assert msg, "Update message and callback_query are both None" + + confs = self.user_requests.setdefault(user, PipelineInputs()) # despite the name, it creates a default only if not present + args: dict[str, Any] = { + "text": "Please choose an option or write your query", + "parse_mode": 'MarkdownV2', + "reply_markup": InlineKeyboardMarkup([ + [ConfigsChat.CHANGE_MODELS.get_inline_button()], + [ConfigsChat.STRATEGY.get_inline_button(confs.strategy.label)], + [ConfigsChat.CANCEL.get_inline_button()], + ]) + } + + await (msg.edit_message_text(**args) if isinstance(msg, CallbackQuery) else msg.reply_text(**args)) return CONFIGS - async def __model_team(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - return await self._model_select(update, ConfigsChat.MODEL_TEAM) + 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 self.user_requests: + del self.user_requests[user] + await query.edit_message_text("Conversation canceled. Use /start to begin again.") + return ConversationHandler.END - async def __model_output(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - return await self._model_select(update, ConfigsChat.MODEL_OUTPUT) + ########################################## + # Configurazioni + ########################################## + async def __models(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + query, user = await self.handle_callbackquery(update) + req = self.user_requests[user] - async def _model_select(self, update: Update, state: ConfigsChat, msg: str | None = None) -> int: + await query.edit_message_text("Select a model", reply_markup=InlineKeyboardMarkup([ + [ConfigsChat.MODEL_CHECK.get_inline_button(req.query_analyzer_model.label)], + [ConfigsChat.MODEL_TEAM_LEADER.get_inline_button(req.team_leader_model.label)], + [ConfigsChat.MODEL_TEAM.get_inline_button(req.team_model.label)], + [ConfigsChat.MODEL_REPORT.get_inline_button(req.report_generation_model.label)], + [ConfigsChat.CANCEL.get_inline_button()] + ])) + return SELECT_MODEL + + async def __model_select(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: query, user = await self.handle_callbackquery(update) + if not query.data: + logging.error("Callback query data is None") + return CONFIGS + req = self.user_requests[user] - models = self.build_callback_data("__select_config", state, req.list_models_names()) + models = self.build_callback_data("__select_config", ConfigsChat[query.data], req.list_models_names()) 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("Select a model", reply_markup=InlineKeyboardMarkup(inline_btns)) return SELECT_CONFIG async def __strategy(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: @@ -182,71 +236,62 @@ class TelegramApp: req = self.user_requests[user] _, state, index = str(query.data).split(QUERY_SEP) - if state == str(ConfigsChat.MODEL_TEAM): - req.choose_team(int(index)) - if state == str(ConfigsChat.MODEL_OUTPUT): - req.choose_team_leader(int(index)) - if state == str(ConfigsChat.STRATEGY): - req.choose_strategy(int(index)) + ConfigsChat[state].change_value(req, int(index)) - await self.start_message(user, query) - return CONFIGS + return await self.__go_to_start(update, context) - async def __start_team(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - message, user = await self.handle_message(update) + async def __start_llms(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + message, user = self.handle_message(update) confs = self.user_requests[user] confs.user_query = message.text or "" - logging.info(f"@{user.username} started the team with [{confs.team_model.label}, {confs.team_leader_model.label}, {confs.strategy.label}]") - await self.__run_team(update, confs) + logging.info(f"@{user.username} started the team with [{confs.query_analyzer_model.label}, {confs.team_model.label}, {confs.team_leader_model.label}, {confs.report_generation_model.label}, {confs.strategy.label}]") + await self.__run(update, confs) logging.info(f"@{user.username} team finished.") return ConversationHandler.END - 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 self.user_requests: - del self.user_requests[user] - await query.edit_message_text("Conversation canceled. Use /start to begin again.") - return ConversationHandler.END - async def __run_team(self, update: Update, inputs: PipelineInputs) -> None: + ########################################## + # RUN APP + ########################################## + async def __run(self, update: Update, inputs: PipelineInputs) -> None: if not update.message: return bot = update.get_bot() msg_id = update.message.message_id - 1 chat_id = update.message.chat_id - configs_str = [ - 'Running with configurations: ', - f'Team: {inputs.team_model.label}', - f'Output: {inputs.team_leader_model.label}', - f'Strategy: {inputs.strategy.label}', - f'Query: "{inputs.user_query}"' - ] - full_message = f"""```\n{'\n'.join(configs_str)}\n```\n\n""" - 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') + run_message = RunMessage(inputs, prefix="```\n", suffix="\n```") + msg = await bot.edit_message_text(chat_id=chat_id, message_id=msg_id, text=run_message.get_latest(), 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 migliorare messaggi di attesa + def update_user(update_step: str = "") -> None: + if update_step: run_message.update_step(update_step) + else: run_message.update() + + message = run_message.get_latest() + if msg.text != message: + asyncio.create_task(msg.edit_text(message, parse_mode='MarkdownV2')) + await bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING) pipeline = Pipeline(inputs) - report_content = await pipeline.interact_async() - await msg.delete() + report_content = await pipeline.interact_async(listeners=[ + (PipelineEvent.QUERY_CHECK, lambda _: update_user()), + (PipelineEvent.TOOL_USED, lambda e: update_user(e.tool.tool_name.replace('get_', '').replace("_", "\\_"))), + (PipelineEvent.INFO_RECOVERY, lambda _: update_user()), + (PipelineEvent.REPORT_GENERATION, lambda _: update_user()), + ]) # attach report file to the message 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) - await bot.send_document(chat_id=chat_id, document=document, filename="report.pdf", parse_mode='MarkdownV2', caption=full_message) - + await msg.reply_document(document=document, filename="report.pdf") From 08774bee1b6b93d3934ddbefcf19afe5154beaba Mon Sep 17 00:00:00 2001 From: Giacomo Bertolazzi <31776951+Berack96@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:42:58 +0100 Subject: [PATCH 5/6] Demos (#42) * Rimossi old docs & demos * Aggiornata la documentazione dell'architettura dell'app * Aggiunti nuovi script demo per i provider di mercato * Fix problems with socials * Fix Dockerfile per dipendenze di X --- demos/agno_agent.py | 4 +- demos/api_market_providers.py | 12 + demos/api_news_providers.py | 16 + demos/api_socials_providers.py | 17 ++ demos/market_providers_api_demo.py | 353 ---------------------- demos/news_api.py | 18 -- demos/ollama_demo.py | 32 +- docs/App_Architecture_Diagrams.md | 375 +++++++++--------------- docs/Async_Implementation_Detail.md | 203 ------------- docs/Market_Data_Implementation_Plan.md | 96 ------ docs/Piano di Sviluppo.md | 73 ----- 11 files changed, 194 insertions(+), 1005 deletions(-) create mode 100644 demos/api_market_providers.py create mode 100644 demos/api_news_providers.py create mode 100644 demos/api_socials_providers.py delete mode 100644 demos/market_providers_api_demo.py delete mode 100644 demos/news_api.py delete mode 100644 docs/Async_Implementation_Detail.md delete mode 100644 docs/Market_Data_Implementation_Plan.md delete mode 100644 docs/Piano di Sviluppo.md diff --git a/demos/agno_agent.py b/demos/agno_agent.py index 35acf59..02ebc75 100644 --- a/demos/agno_agent.py +++ b/demos/agno_agent.py @@ -8,9 +8,7 @@ try: reasoning_agent = Agent( model=Gemini(), - tools=[ - ReasoningTools(), - ], + tools=[ReasoningTools()], instructions="Use tables to display data.", markdown=True, ) diff --git a/demos/api_market_providers.py b/demos/api_market_providers.py new file mode 100644 index 0000000..a9ba754 --- /dev/null +++ b/demos/api_market_providers.py @@ -0,0 +1,12 @@ +from dotenv import load_dotenv +from app.api.tools import MarketAPIsTool + +def main(): + api = MarketAPIsTool() + prices_aggregated = api.get_historical_prices_aggregated("BTC", limit=5) + for price in prices_aggregated: + print(f"== [{price.timestamp}] {price.low:.2f} - {price.high:.2f} ==") + +if __name__ == "__main__": + load_dotenv() + main() diff --git a/demos/api_news_providers.py b/demos/api_news_providers.py new file mode 100644 index 0000000..4ac2f01 --- /dev/null +++ b/demos/api_news_providers.py @@ -0,0 +1,16 @@ +from dotenv import load_dotenv +from app.api.tools import NewsAPIsTool + +def main(): + api = NewsAPIsTool() + articles_aggregated = api.get_latest_news_aggregated(query="bitcoin", limit=2) + for provider, articles in articles_aggregated.items(): + print("===================================") + print(f"Provider: {provider}") + for article in articles: + print(f"== [{article.time}] {article.title} ==") + print(f" {article.description}") + +if __name__ == "__main__": + load_dotenv() + main() diff --git a/demos/api_socials_providers.py b/demos/api_socials_providers.py new file mode 100644 index 0000000..fb9da6e --- /dev/null +++ b/demos/api_socials_providers.py @@ -0,0 +1,17 @@ +from dotenv import load_dotenv +from app.api.tools import SocialAPIsTool + +def main(): + api = SocialAPIsTool() + articles_aggregated = api.get_top_crypto_posts_aggregated(limit_per_wrapper=2) + for provider, posts in articles_aggregated.items(): + print("===================================") + print(f"Provider: {provider}") + for post in posts: + print(f"== [{post.time}] - {post.title} ==") + print(f" {post.description}") + print(f" {len(post.comments)}") + +if __name__ == "__main__": + load_dotenv() + main() diff --git a/demos/market_providers_api_demo.py b/demos/market_providers_api_demo.py deleted file mode 100644 index a958532..0000000 --- a/demos/market_providers_api_demo.py +++ /dev/null @@ -1,353 +0,0 @@ -#!/usr/bin/env python3 -""" -Demo Completo per Market Data Providers -======================================== - -Questo script dimostra l'utilizzo di tutti i wrapper che implementano BaseWrapper: -- CoinBaseWrapper (richiede credenziali) -- CryptoCompareWrapper (richiede API key) -- BinanceWrapper (richiede credenziali) -- PublicBinanceAgent (accesso pubblico) -- YFinanceWrapper (accesso gratuito a dati azionari e crypto) - -Lo script effettua chiamate GET a diversi provider e visualizza i dati -in modo strutturato con informazioni dettagliate su timestamp, stato -delle richieste e formattazione tabellare. -""" - -import sys -import os -from pathlib import Path -from datetime import datetime -from typing import Dict, List, Optional, Any -import traceback - -# Aggiungi il path src al PYTHONPATH -project_root = Path(__file__).parent.parent -sys.path.insert(0, str(project_root / "src")) - -from dotenv import load_dotenv -from app.api.markets import ( - CoinBaseWrapper, - CryptoCompareWrapper, - BinanceWrapper, - YFinanceWrapper, - MarketWrapper -) - -# Carica variabili d'ambiente -load_dotenv() - -class DemoFormatter: - """Classe per formattare l'output del demo in modo strutturato.""" - - @staticmethod - def print_header(title: str, char: str = "=", width: int = 80): - """Stampa un'intestazione formattata.""" - print(f"\n{char * width}") - print(f"{title:^{width}}") - print(f"{char * width}") - - @staticmethod - def print_subheader(title: str, char: str = "-", width: int = 60): - """Stampa una sotto-intestazione formattata.""" - print(f"\n{char * width}") - print(f" {title}") - print(f"{char * width}") - - @staticmethod - def print_request_info(provider_name: str, method: str, timestamp: datetime, - status: str, error: Optional[str] = None): - """Stampa informazioni sulla richiesta.""" - print(f"๐Ÿ•’ Timestamp: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}") - print(f"๐Ÿท๏ธ Provider: {provider_name}") - print(f"๐Ÿ”ง Method: {method}") - print(f"๐Ÿ“Š Status: {status}") - if error: - print(f"โŒ Error: {error}") - print() - - @staticmethod - def print_product_table(products: List[Any], title: str = "Products"): - """Stampa una tabella di prodotti.""" - if not products: - print(f"๐Ÿ“‹ {title}: Nessun prodotto trovato") - return - - print(f"๐Ÿ“‹ {title} ({len(products)} items):") - print(f"{'Symbol':<15} {'ID':<20} {'Price':<12} {'Quote':<10} {'Status':<10}") - print("-" * 67) - - for product in products[:10]: # Mostra solo i primi 10 - symbol = getattr(product, 'symbol', 'N/A') - product_id = getattr(product, 'id', 'N/A') - price = getattr(product, 'price', 0.0) - quote = getattr(product, 'quote_currency', 'N/A') - status = getattr(product, 'status', 'N/A') - - # Tronca l'ID se troppo lungo - if len(product_id) > 18: - product_id = product_id[:15] + "..." - - price_str = f"${price:.2f}" if price > 0 else "N/A" - - print(f"{symbol:<15} {product_id:<20} {price_str:<12} {quote:<10} {status:<10}") - - if len(products) > 10: - print(f"... e altri {len(products) - 10} prodotti") - print() - - @staticmethod - def print_prices_table(prices: List[Any], title: str = "Historical Prices"): - """Stampa una tabella di prezzi storici.""" - if not prices: - print(f"๐Ÿ’ฐ {title}: Nessun prezzo trovato") - return - - print(f"๐Ÿ’ฐ {title} ({len(prices)} entries):") - print(f"{'Time':<12} {'Open':<12} {'High':<12} {'Low':<12} {'Close':<12} {'Volume':<15}") - print("-" * 75) - - for price in prices[:5]: # Mostra solo i primi 5 - time_str = getattr(price, 'time', 'N/A') - # Il time รจ giร  una stringa, non serve strftime - if len(time_str) > 10: - time_str = time_str[:10] # Tronca se troppo lungo - - open_price = f"${getattr(price, 'open', 0):.2f}" - high_price = f"${getattr(price, 'high', 0):.2f}" - low_price = f"${getattr(price, 'low', 0):.2f}" - close_price = f"${getattr(price, 'close', 0):.2f}" - volume = f"{getattr(price, 'volume', 0):,.0f}" - - print(f"{time_str:<12} {open_price:<12} {high_price:<12} {low_price:<12} {close_price:<12} {volume:<15}") - - if len(prices) > 5: - print(f"... e altri {len(prices) - 5} prezzi") - print() - -class ProviderTester: - """Classe per testare i provider di market data.""" - - def __init__(self): - self.formatter = DemoFormatter() - self.test_symbols = ["BTC", "ETH", "ADA"] - - def test_provider(self, wrapper: MarketWrapper, provider_name: str) -> Dict[str, Any]: - """Testa un provider specifico con tutti i metodi disponibili.""" - results: Dict[str, Any] = { - "provider_name": provider_name, - "tests": {}, - "overall_status": "SUCCESS" - } - - self.formatter.print_subheader(f"๐Ÿ” Testing {provider_name}") - - # Test get_product - for symbol in self.test_symbols: - timestamp = datetime.now() - try: - product = wrapper.get_product(symbol) - self.formatter.print_request_info( - provider_name, f"get_product({symbol})", timestamp, "โœ… SUCCESS" - ) - if product: - print(f"๐Ÿ“ฆ Product: {product.symbol} (ID: {product.id})") - print(f" Price: ${product.price:.2f}, Quote: {product.currency}") - print(f" Volume 24h: {product.volume_24h:,.2f}") - else: - print(f"๐Ÿ“ฆ Product: Nessun prodotto trovato per {symbol}") - - results["tests"][f"get_product_{symbol}"] = "SUCCESS" - - except Exception as e: - error_msg = str(e) - self.formatter.print_request_info( - provider_name, f"get_product({symbol})", timestamp, "โŒ ERROR", error_msg - ) - results["tests"][f"get_product_{symbol}"] = f"ERROR: {error_msg}" - results["overall_status"] = "PARTIAL" - - # Test get_products - timestamp = datetime.now() - try: - products = wrapper.get_products(self.test_symbols) - self.formatter.print_request_info( - provider_name, f"get_products({self.test_symbols})", timestamp, "โœ… SUCCESS" - ) - self.formatter.print_product_table(products, f"{provider_name} Products") - results["tests"]["get_products"] = "SUCCESS" - - except Exception as e: - error_msg = str(e) - self.formatter.print_request_info( - provider_name, f"get_products({self.test_symbols})", timestamp, "โŒ ERROR", error_msg - ) - results["tests"]["get_products"] = f"ERROR: {error_msg}" - results["overall_status"] = "PARTIAL" - - # Test get_historical_prices - timestamp = datetime.now() - try: - prices = wrapper.get_historical_prices("BTC") - self.formatter.print_request_info( - provider_name, "get_historical_prices(BTC)", timestamp, "โœ… SUCCESS" - ) - self.formatter.print_prices_table(prices, f"{provider_name} BTC Historical Prices") - results["tests"]["get_historical_prices"] = "SUCCESS" - - except Exception as e: - error_msg = str(e) - self.formatter.print_request_info( - provider_name, "get_historical_prices(BTC)", timestamp, "โŒ ERROR", error_msg - ) - results["tests"]["get_historical_prices"] = f"ERROR: {error_msg}" - results["overall_status"] = "PARTIAL" - - return results - -def check_environment_variables() -> Dict[str, bool]: - """Verifica la presenza delle variabili d'ambiente necessarie.""" - env_vars = { - "COINBASE_API_KEY": bool(os.getenv("COINBASE_API_KEY")), - "COINBASE_API_SECRET": bool(os.getenv("COINBASE_API_SECRET")), - "CRYPTOCOMPARE_API_KEY": bool(os.getenv("CRYPTOCOMPARE_API_KEY")), - "BINANCE_API_KEY": bool(os.getenv("BINANCE_API_KEY")), - "BINANCE_API_SECRET": bool(os.getenv("BINANCE_API_SECRET")), - } - return env_vars - -def initialize_providers() -> Dict[str, MarketWrapper]: - """Inizializza tutti i provider disponibili.""" - providers: Dict[str, MarketWrapper] = {} - env_vars = check_environment_variables() - - # CryptoCompareWrapper - if env_vars["CRYPTOCOMPARE_API_KEY"]: - try: - providers["CryptoCompare"] = CryptoCompareWrapper() - print("โœ… CryptoCompareWrapper inizializzato con successo") - except Exception as e: - print(f"โŒ Errore nell'inizializzazione di CryptoCompareWrapper: {e}") - else: - print("โš ๏ธ CryptoCompareWrapper saltato: CRYPTOCOMPARE_API_KEY non trovata") - - # CoinBaseWrapper - if env_vars["COINBASE_API_KEY"] and env_vars["COINBASE_API_SECRET"]: - try: - providers["CoinBase"] = CoinBaseWrapper() - print("โœ… CoinBaseWrapper inizializzato con successo") - except Exception as e: - print(f"โŒ Errore nell'inizializzazione di CoinBaseWrapper: {e}") - else: - print("โš ๏ธ CoinBaseWrapper saltato: credenziali Coinbase non complete") - - # BinanceWrapper - try: - providers["Binance"] = BinanceWrapper() - print("โœ… BinanceWrapper inizializzato con successo") - except Exception as e: - print(f"โŒ Errore nell'inizializzazione di BinanceWrapper: {e}") - - # YFinanceWrapper (sempre disponibile - dati azionari e crypto gratuiti) - try: - providers["YFinance"] = YFinanceWrapper() - print("โœ… YFinanceWrapper inizializzato con successo") - except Exception as e: - print(f"โŒ Errore nell'inizializzazione di YFinanceWrapper: {e}") - return providers - -def print_summary(results: List[Dict[str, Any]]): - """Stampa un riassunto finale dei risultati.""" - formatter = DemoFormatter() - formatter.print_header("๐Ÿ“Š RIASSUNTO FINALE", "=", 80) - - total_providers = len(results) - successful_providers = sum(1 for r in results if r["overall_status"] == "SUCCESS") - partial_providers = sum(1 for r in results if r["overall_status"] == "PARTIAL") - - print(f"๐Ÿ”ข Provider testati: {total_providers}") - print(f"โœ… Provider completamente funzionanti: {successful_providers}") - print(f"โš ๏ธ Provider parzialmente funzionanti: {partial_providers}") - print(f"โŒ Provider non funzionanti: {total_providers - successful_providers - partial_providers}") - - print("\n๐Ÿ“‹ Dettaglio per provider:") - for result in results: - provider_name = result["provider_name"] - status = result["overall_status"] - status_icon = "โœ…" if status == "SUCCESS" else "โš ๏ธ" if status == "PARTIAL" else "โŒ" - - print(f"\n{status_icon} {provider_name}:") - for test_name, test_result in result["tests"].items(): - test_icon = "โœ…" if test_result == "SUCCESS" else "โŒ" - print(f" {test_icon} {test_name}: {test_result}") - -def main(): - """Funzione principale del demo.""" - formatter = DemoFormatter() - - # Intestazione principale - formatter.print_header("๐Ÿš€ DEMO COMPLETO MARKET DATA PROVIDERS", "=", 80) - - print(f"๐Ÿ•’ Avvio demo: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - print("๐Ÿ“ Questo demo testa tutti i wrapper BaseWrapper disponibili") - print("๐Ÿ” Ogni test include timestamp, stato della richiesta e dati formattati") - - # Verifica variabili d'ambiente - formatter.print_subheader("๐Ÿ” Verifica Configurazione") - env_vars = check_environment_variables() - - print("Variabili d'ambiente:") - for var_name, is_present in env_vars.items(): - status = "โœ… Presente" if is_present else "โŒ Mancante" - print(f" {var_name}: {status}") - - # Inizializza provider - formatter.print_subheader("๐Ÿ—๏ธ Inizializzazione Provider") - providers = initialize_providers() - - if not providers: - print("โŒ Nessun provider disponibile. Verifica la configurazione.") - return - - print(f"\n๐ŸŽฏ Provider disponibili per il test: {list(providers.keys())}") - - # Testa ogni provider - formatter.print_header("๐Ÿงช ESECUZIONE TEST PROVIDER", "=", 80) - - tester = ProviderTester() - all_results: List[Dict[str, Any]] = [] - - for provider_name, wrapper in providers.items(): - try: - result = tester.test_provider(wrapper, provider_name) - all_results.append(result) - except Exception as e: - print(f"โŒ Errore critico nel test di {provider_name}: {e}") - traceback.print_exc() - all_results.append({ - "provider_name": provider_name, - "tests": {}, - "overall_status": "CRITICAL_ERROR", - "error": str(e) - }) - - # Stampa riassunto finale - print_summary(all_results) - - # Informazioni aggiuntive - formatter.print_header("โ„น๏ธ INFORMAZIONI AGGIUNTIVE", "=", 80) - print("๐Ÿ“š Documentazione:") - print(" - BaseWrapper: src/app/markets/base.py") - print(" - Test completi: tests/agents/test_market.py") - print(" - Configurazione: .env") - - print("\n๐Ÿ”ง Per abilitare tutti i provider:") - print(" 1. Configura le credenziali nel file .env") - print(" 2. Segui la documentazione di ogni provider") - print(" 3. Riavvia il demo") - - print(f"\n๐Ÿ Demo completato: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/demos/news_api.py b/demos/news_api.py deleted file mode 100644 index ef71974..0000000 --- a/demos/news_api.py +++ /dev/null @@ -1,18 +0,0 @@ -#### FOR ALL FILES OUTSIDE src/ FOLDER #### -import sys -import os -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) -########################################### - -from dotenv import load_dotenv -from app.api.news import NewsApiWrapper - -def main(): - api = NewsApiWrapper() - articles = api.get_latest_news(query="bitcoin", limit=5) - assert len(articles) > 0 - print("ok") - -if __name__ == "__main__": - load_dotenv() - main() \ No newline at end of file diff --git a/demos/ollama_demo.py b/demos/ollama_demo.py index 1e52f5f..8a327a6 100644 --- a/demos/ollama_demo.py +++ b/demos/ollama_demo.py @@ -1,20 +1,4 @@ -#!/usr/bin/env python3 -""" -Demo di Ollama (Python) โ€“ mostra: - 1. Elenco dei modelli disponibili - 2. Generazione di testo semplice - 3. Chat con streaming - 4. Calcolo di embeddings - 5. Esempio (opzionale) di function calling / tools - -Uso: - python ollama_demo.py - -Requisiti: - pip install ollama - Avviare il server Ollama (es. 'ollama serve' o l'app desktop) e avere i modelli giร  pullati. -""" - +from typing import Any import ollama # Configurazione modelli @@ -33,8 +17,8 @@ def list_models(): print(" (Nessun modello trovato)") return for m in models: - name = getattr(m, 'model', None) or (m.get('model') if isinstance(m, dict) else 'sconosciuto') - details = getattr(m, 'details', None) + name = getattr(m, 'model', None) or (m.get('model') if isinstance(m, dict) else 'sconosciuto') # type: ignore + details = getattr(m, 'details', None) # type: ignore fmt = getattr(details, 'format', None) if details else 'unknown' print(f" โ€ข {name} โ€“ {fmt}") except Exception as e: @@ -46,7 +30,7 @@ def list_models(): def generate_text(model: str, prompt: str, max_tokens: int = 200) -> str: """Genera testo dal modello indicato.""" print(f"\n[2] Generazione testo con '{model}'") - response = ollama.chat( + response = ollama.chat( # type: ignore model=model, messages=[{"role": "user", "content": prompt}] ) @@ -57,10 +41,10 @@ def generate_text(model: str, prompt: str, max_tokens: int = 200) -> str: # 3. Chat con streaming -------------------------------------------------------- -def chat_streaming(model: str, messages: list) -> str: +def chat_streaming(model: str, messages: list[dict[str, str]]) -> str: """Esegue una chat mostrando progressivamente la risposta.""" print(f"\n[3] Chat (streaming) con '{model}'") - stream = ollama.chat(model=model, messages=messages, stream=True) + stream = ollama.chat(model=model, messages=messages, stream=True) # type: ignore full = "" for chunk in stream: if 'message' in chunk and 'content' in chunk['message']: @@ -91,7 +75,7 @@ def get_embedding(model: str, text: str): def try_tools(model: str): """Esempio di function calling; se non supportato mostra messaggio informativo.""" print(f"\n[5] Function calling / tools con '{model}'") - tools = [ + tools: list[dict[str, Any]] = [ { "type": "function", "function": { @@ -109,7 +93,7 @@ def try_tools(model: str): } ] try: - response = ollama.chat( + response = ollama.chat( # type: ignore model=model, messages=[{"role": "user", "content": "Che tempo fa a Milano?"}], tools=tools diff --git a/docs/App_Architecture_Diagrams.md b/docs/App_Architecture_Diagrams.md index a498770..6eb4308 100644 --- a/docs/App_Architecture_Diagrams.md +++ b/docs/App_Architecture_Diagrams.md @@ -1,255 +1,160 @@ -# ๐Ÿ“Š Architettura e Flussi dell'App upo-appAI +# ๐Ÿ“Š Architettura upo-appAI -## ๐Ÿ—๏ธ Diagramma Architettura Generale +## ๐Ÿ—๏ธ Architettura Generale ``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๐ŸŒ GRADIO UI โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ User Input โ”‚ โ”‚ Provider โ”‚ โ”‚ Style โ”‚ โ”‚ -โ”‚ โ”‚ (Query) โ”‚ โ”‚ (Model) โ”‚ โ”‚ (Conservative/ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Aggressive) โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๐Ÿ”ง TOOL AGENT โ”‚ -โ”‚ (Central Orchestrator) โ”‚ -โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ 1. Collect Data โ”‚ โ”‚ 2. Analyze โ”‚ โ”‚ 3. Predict & โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ Sentiment โ”‚ โ”‚ Recommend โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๐Ÿ“Š AGENT ECOSYSTEM โ”‚ -โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ -โ”‚ โ”‚ MARKET โ”‚ โ”‚ NEWS โ”‚ โ”‚ SOCIAL โ”‚ โ”‚ PREDICTOR โ”‚โ”‚ -โ”‚ โ”‚ AGENT โ”‚ โ”‚ AGENT โ”‚ โ”‚ AGENT โ”‚ โ”‚ AGENT โ”‚โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚โ”‚ -โ”‚ โ”‚ ๐Ÿ“ˆ Coinbase โ”‚ โ”‚ ๐Ÿ“ฐ News API โ”‚ โ”‚ ๐Ÿฆ Social โ”‚ โ”‚ ๐Ÿค– LLM โ”‚โ”‚ -โ”‚ โ”‚ ๐Ÿ“Š CryptoCmpโ”‚ โ”‚ โ”‚ โ”‚ Media โ”‚ โ”‚ Analysis โ”‚โ”‚ -โ”‚ โ”‚ ๐ŸŸก Binance โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +INTERFACCE UTENTE +โ”œโ”€โ”€ ๐Ÿ’ฌ Gradio Web (Chat + Dropdown modelli/strategie) +โ””โ”€โ”€ ๐Ÿ“ฑ Telegram Bot (Mini App) + โ”‚ + โ–ผ +CHAT MANAGER +โ”œโ”€โ”€ Storico messaggi +โ”œโ”€โ”€ Gestione PipelineInputs +โ””โ”€โ”€ Salva/Carica chat + โ”‚ + โ–ผ +AGNO WORKFLOW PIPELINE (4 Steps) +โ”œโ”€โ”€ 1. Query Check โ†’ Verifica crypto +โ”œโ”€โ”€ 2. Condition โ†’ Valida procedere +โ”œโ”€โ”€ 3. Info Recovery โ†’ Team raccolta dati +โ””โ”€โ”€ 4. Report Generation โ†’ Report finale + โ”‚ + โ–ผ +AGNO AGENT ECOSYSTEM +โ”œโ”€โ”€ ๐Ÿ‘” TEAM LEADER (coordina Market, News, Social) +โ”‚ Tools: ReasoningTools, PlanMemoryTool, CryptoSymbolsTools +โ”œโ”€โ”€ ๐Ÿ“ˆ MARKET AGENT โ†’ MarketAPIsTool +โ”œโ”€โ”€ ๐Ÿ“ฐ NEWS AGENT โ†’ NewsAPIsTool +โ”œโ”€โ”€ ๐Ÿฆ SOCIAL AGENT โ†’ SocialAPIsTool +โ”œโ”€โ”€ ๐Ÿ” QUERY CHECK AGENT โ†’ QueryOutputs (is_crypto: bool) +โ””โ”€โ”€ ๐Ÿ“‹ REPORT GENERATOR AGENT โ†’ Strategia applicata ``` -## ๐Ÿ”„ Flusso di Esecuzione Dettagliato +## ๐Ÿ”„ Flusso Esecuzione -``` -๐Ÿ‘ค USER REQUEST - โ”‚ - โ”‚ "Analizza Bitcoin con strategia aggressiva" - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๐Ÿ”ง TOOL AGENT โ”‚ -โ”‚ โ”‚ -โ”‚ def interact(query, provider, style): โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿ“Š market_data = market_agent.analyze(query) โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿ“ฐ news_sentiment = news_agent.analyze(query) โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿฆ social_sentiment = social_agent.analyze(query) โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€ ๐Ÿค– prediction = predictor_agent.predict(...) โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -๐Ÿ“Š MARKET AGENT - Parallel Data Collection -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ โ”‚ -โ”‚ ๐Ÿ” Auto-detect Available Providers: โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Coinbase โ”‚ โ”‚ CryptoComp โ”‚ โ”‚ Binance โ”‚ โ”‚ -โ”‚ โ”‚ REST โ”‚ โ”‚ API โ”‚ โ”‚ Mock โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โœ… Active โ”‚ โ”‚ โœ… Active โ”‚ โ”‚ โœ… Active โ”‚ โ”‚ -โ”‚ โ”‚ $63,500 BTC โ”‚ โ”‚ $63,450 BTC โ”‚ โ”‚ $63,600 BTC โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ -โ”‚ ๐Ÿ“ˆ Aggregated Result: โ”‚ -โ”‚ { โ”‚ -โ”‚ "aggregated_data": { โ”‚ -โ”‚ "BTC_USD": { โ”‚ -โ”‚ "price": 63516.67, โ”‚ -โ”‚ "confidence": 0.94, โ”‚ -โ”‚ "sources_count": 3 โ”‚ -โ”‚ } โ”‚ -โ”‚ }, โ”‚ -โ”‚ "individual_sources": {...}, โ”‚ -โ”‚ "market_signals": {...} โ”‚ -โ”‚ } โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -๐Ÿ“ฐ NEWS AGENT + ๐Ÿฆ SOCIAL AGENT -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ โ”‚ -โ”‚ ๐Ÿ“ฐ News Sentiment: "Positive momentum, institutional โ”‚ -โ”‚ adoption increasing..." โ”‚ -โ”‚ โ”‚ -โ”‚ ๐Ÿฆ Social Sentiment: "Bullish sentiment on Reddit, โ”‚ -โ”‚ Twitter mentions up 15%..." โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -๐Ÿค– PREDICTOR AGENT -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ โ”‚ -โ”‚ Input: โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿ“Š Market Data (aggregated + confidence) โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿ“ฐ๐Ÿฆ Combined Sentiment โ”‚ -โ”‚ โ”œโ”€โ”€ ๐ŸŽฏ Style: "aggressive" โ”‚ -โ”‚ โ””โ”€โ”€ ๐Ÿค– Provider: "openai/anthropic/google..." โ”‚ -โ”‚ โ”‚ -โ”‚ ๐Ÿง  LLM Processing: โ”‚ -โ”‚ "Based on high confidence market data (0.94) showing โ”‚ -โ”‚ $63,516 BTC with positive sentiment across news and โ”‚ -โ”‚ social channels, aggressive strategy recommendation..." โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -๐Ÿ“‹ FINAL OUTPUT -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๐Ÿ“Š Market Data Summary โ”‚ -โ”‚ ๐Ÿ“ฐ๐Ÿฆ Sentiment Analysis โ”‚ -โ”‚ ๐Ÿ“ˆ Final Recommendation: โ”‚ -โ”‚ "Strong BUY signal with 85% confidence..." โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +**Input:** "Analizza Bitcoin con strategia aggressiva" + +1. CHAT MANAGER riceve e prepara PipelineInputs +2. WORKFLOW PIPELINE esegue 4 step: + - Query Check: valida `is_crypto: true` + - Condition: se false, termina + - Info Recovery: Team raccoglie dati + - Report Generation: genera report +3. OUTPUT: Report con analisi + raccomandazioni + +## ๐Ÿ›๏ธ Architettura API + +**Tools (Agno Toolkit):** +- MarketAPIsTool: Binance, YFinance, CoinBase, CryptoCompare +- NewsAPIsTool: NewsAPI, GoogleNews, DuckDuckGo, CryptoPanic +- SocialAPIsTool: Reddit, X, 4chan +- CryptoSymbolsTools: `resources/cryptos.csv` + +**WrapperHandler:** Failover automatico (3 tentativi/wrapper, 2s delay) + +## ๐Ÿ“Š Data Aggregation + +**ProductInfo:** +- Volume: media tra sources +- Price: weighted average (price ร— volume) +- Confidence: spread + numero sources + +**Historical Price:** +- Align per timestamp +- Media: high, low, open, close, volume + +## ๐ŸŽฏ Configuration + +**configs.yaml:** +```yaml +port: 8000 +models: [Ollama, OpenAI, Anthropic, Google] +strategies: [Conservative, Aggressive] +agents: {team_model, team_leader_model, ...} +api: {retry_attempts: 3, retry_delay_seconds: 2} ``` -## ๐Ÿ›๏ธ Architettura dei Provider (Market Agent) +**.env (API Keys):** +- Market: CDP_API_KEY, CRYPTOCOMPARE_API_KEY, ... +- News: NEWS_API_KEY, CRYPTOPANIC_API_KEY, ... +- Social: REDDIT_CLIENT_ID, X_API_KEY, ... +- LLM: OPENAI_API_KEY, ANTHROPIC_API_KEY, ... +- Bot: TELEGRAM_BOT_TOKEN + +## ๐Ÿ—‚๏ธ Struttura Progetto ``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๐Ÿ“Š MARKET AGENT โ”‚ -โ”‚ โ”‚ -โ”‚ ๐Ÿ” Provider Detection Logic: โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ -โ”‚ โ”‚ def _setup_providers(): โ”‚โ”‚ -โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ”‘ Check CDP_API_KEY_NAME + CDP_API_PRIVATE_KEY โ”‚โ”‚ -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ โœ… Setup Coinbase Advanced Trade โ”‚โ”‚ -โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ”‘ Check CRYPTOCOMPARE_API_KEY โ”‚โ”‚ -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ โœ… Setup CryptoCompare โ”‚โ”‚ -โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ”‘ Check BINANCE_API_KEY (future) โ”‚โ”‚ -โ”‚ โ”‚ โ””โ”€โ”€ โœ… Setup Binance API โ”‚โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ -โ”‚ โ”‚ -โ”‚ ๐Ÿ“ก Data Flow: โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Provider 1 โ”‚โ”€โ”€โ”€โ–ถโ”‚ โ”‚โ—€โ”€โ”€โ”€โ”‚ Provider 2 โ”‚ โ”‚ -โ”‚ โ”‚ Coinbase โ”‚ โ”‚ AGGREGATOR โ”‚ โ”‚ CryptoComp โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ Real-time โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ Real-time โ”‚ โ”‚ -โ”‚ โ”‚ Market Data โ”‚ โ”‚ โ”‚Confidenceโ”‚ โ”‚ โ”‚ Market Data โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚Scoring โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ Spread โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Provider 3 โ”‚โ”€โ”€โ”€โ–ถโ”‚ โ”‚Analysis โ”‚ โ”‚โ—€โ”€โ”€โ”€โ”‚ Provider N โ”‚ โ”‚ -โ”‚ โ”‚ Binance โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Future โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ Mock Data โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +src/app/ +โ”œโ”€โ”€ __main__.py +โ”œโ”€โ”€ configs.py +โ”œโ”€โ”€ agents/ +โ”‚ โ”œโ”€โ”€ core.py +โ”‚ โ”œโ”€โ”€ pipeline.py +โ”‚ โ”œโ”€โ”€ plan_memory_tool.py +โ”‚ โ””โ”€โ”€ prompts/ +โ”œโ”€โ”€ api/ +โ”‚ โ”œโ”€โ”€ wrapper_handler.py +โ”‚ โ”œโ”€โ”€ core/ (markets, news, social) +โ”‚ โ”œโ”€โ”€ markets/ (Binance, CoinBase, CryptoCompare, YFinance) +โ”‚ โ”œโ”€โ”€ news/ (NewsAPI, GoogleNews, DuckDuckGo, CryptoPanic) +โ”‚ โ”œโ”€โ”€ social/ (Reddit, X, 4chan) +โ”‚ โ””โ”€โ”€ tools/ (Agno Toolkits) +โ””โ”€โ”€ interface/ (chat.py, telegram_app.py) + +tests/ +demos/ +resources/cryptos.csv +docs/ +configs.yaml +.env ``` -## ๐Ÿ”ง Signers Architecture +## ๐Ÿ”‘ Componenti Chiave -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๐Ÿ” SIGNERS ECOSYSTEM โ”‚ -โ”‚ โ”‚ -โ”‚ ๐Ÿ“ src/app/signers/market_signers/ โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿฆ coinbase_rest_signer.py โ”‚ -โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ”‘ Uses: CDP_API_KEY_NAME + CDP_API_PRIVATE_KEY โ”‚ -โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ก RESTClient from coinbase.rest โ”‚ -โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“Š get_asset_info() โ†’ Real Coinbase data โ”‚ -โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“ˆ get_multiple_assets() โ†’ Bulk data โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿ“Š cryptocompare_signer.py โ”‚ -โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ”‘ Uses: CRYPTOCOMPARE_API_KEY โ”‚ -โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ก Direct HTTP requests โ”‚ -โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ’ฐ get_crypto_prices() โ†’ Multi-currency โ”‚ -โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ† get_top_cryptocurrencies() โ†’ Market cap โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€ ๐ŸŸก binance_signer.py โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿ”‘ Uses: BINANCE_API_KEY (future) โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿ“ก Mock implementation โ”‚ -โ”‚ โ”œโ”€โ”€ ๐ŸŽญ Simulated market data โ”‚ -โ”‚ โ””โ”€โ”€ ๐Ÿ“ˆ Compatible interface โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +1. **Agno Framework**: Agent, Team, Workflow, Toolkit, RunEvent +2. **WrapperHandler**: Failover, Retry logic, Type safety +3. **Data Aggregation**: Multiple sources, Confidence score +4. **Multi-Interface**: Gradio + Telegram +5. **Configuration**: configs.yaml + .env + +## ๐Ÿš€ Deployment + +**Docker:** +```bash +docker-compose up --build -d ``` -## ๐Ÿš€ Future Enhancement: Async Flow - -``` - ๐Ÿ“ฑ USER REQUEST - โ”‚ - โ–ผ - ๐Ÿ”ง TOOL AGENT (async) - โ”‚ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ โ”‚ โ”‚ - โ–ผ โ–ผ โ–ผ - ๐Ÿ“Š Market ๐Ÿ“ฐ News ๐Ÿฆ Social - Agent (async) Agent (async) Agent (async) - โ”‚ โ”‚ โ”‚ - โ”Œโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ - โ–ผ โ–ผ โ–ผ โ”‚ โ”‚ - Coinbase โ”‚ Binance โ”‚ โ”‚ - CC โ”‚ โ”‚ โ”‚ - โ–ผโ–ผโ–ผ โ–ผ โ–ผ - ๐Ÿ”„ Parallel ๐Ÿ“ฐ Sentiment ๐Ÿฆ Sentiment - Aggregation Analysis Analysis - โ”‚ โ”‚ โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ–ผ - ๐Ÿค– PREDICTOR AGENT - (LLM Analysis) - โ”‚ - โ–ผ - ๐Ÿ“‹ FINAL RESULT - (JSON + Confidence) +**Local (UV):** +```bash +uv venv +uv pip install -e . +uv run src/app ``` -## ๐Ÿ“Š Data Flow Example +## ๐ŸŽฏ Workflow Asincrono -``` -Input: "Analyze Bitcoin aggressive strategy" -โ”‚ -โ”œโ”€โ”€ ๐Ÿ“Š Market Agent Output: -โ”‚ { -โ”‚ "aggregated_data": { -โ”‚ "BTC_USD": {"price": 63516.67, "confidence": 0.94} -โ”‚ }, -โ”‚ "individual_sources": { -โ”‚ "coinbase": {"price": 63500, "volume": "1.2M"}, -โ”‚ "cryptocompare": {"price": 63450, "volume": "N/A"}, -โ”‚ "binance": {"price": 63600, "volume": "2.1M"} -โ”‚ }, -โ”‚ "market_signals": { -โ”‚ "spread_analysis": "Low spread (0.24%) - healthy liquidity", -โ”‚ "price_divergence": "Max deviation: 0.24% - Normal range" -โ”‚ } -โ”‚ } -โ”‚ -โ”œโ”€โ”€ ๐Ÿ“ฐ News Sentiment: "Positive institutional adoption news..." -โ”œโ”€โ”€ ๐Ÿฆ Social Sentiment: "Bullish Reddit sentiment, +15% mentions" -โ”‚ -โ””โ”€โ”€ ๐Ÿค– Predictor Output: - "๐Ÿ“ˆ Strong BUY recommendation based on: - - High confidence market data (94%) - - Positive news sentiment - - Bullish social indicators - - Low spread indicates healthy liquidity - - Aggressive Strategy: Consider 15-20% portfolio allocation" +```python +workflow = Workflow(steps=[ + query_check, condition, + info_recovery, report_generation +]) + +iterator = await workflow.arun(query, stream=True) + +async for event in iterator: + if event.event == PipelineEvent.TOOL_USED: + log(f"Tool: {event.tool.tool_name}") ``` ---- -*Diagrammi creati: 2025-09-23* -*Sistema: upo-appAI Market Analysis Platform* \ No newline at end of file +**Vantaggi:** Asincrono, Streaming, Condizionale, Retry + +## ๐Ÿ“ˆ Future Enhancements + +- Parallel Tool Execution +- Caching (Redis) +- Database (PostgreSQL) +- Real-time WebSocket +- ML Models +- User Profiles +- Backtesting diff --git a/docs/Async_Implementation_Detail.md b/docs/Async_Implementation_Detail.md deleted file mode 100644 index 9642dff..0000000 --- a/docs/Async_Implementation_Detail.md +++ /dev/null @@ -1,203 +0,0 @@ -# ๐Ÿš€ Diagramma Dettaglio: Implementazione Asincrona - -## โšก Async Market Data Collection (Fase 3) - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๐Ÿ”ง TOOL AGENT โ”‚ -โ”‚ โ”‚ -โ”‚ async def interact(query, provider, style): โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿ“Š market_data = await market_agent.analyze_async() โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿ“ฐ news_data = await news_agent.analyze_async() โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿฆ social_data = await social_agent.analyze_async() โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€ ๐Ÿค– prediction = await predictor.predict_async(...) โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๐Ÿ“Š MARKET AGENT - ASYNC IMPLEMENTATION โ”‚ -โ”‚ โ”‚ -โ”‚ async def analyze_async(self, query): โ”‚ -โ”‚ symbols = extract_symbols(query) # ["BTC", "ETH"] โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€ ๐Ÿ”„ tasks = [ โ”‚ -โ”‚ โ”‚ self._query_coinbase_async(symbols), โ”‚ -โ”‚ โ”‚ self._query_cryptocompare_async(symbols), โ”‚ -โ”‚ โ”‚ self._query_binance_async(symbols) โ”‚ -โ”‚ โ”‚ ] โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€ ๐Ÿ“Š results = await asyncio.gather(*tasks) โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ–ผ โ”‚ -โ”‚ ๐Ÿงฎ aggregate_results(results) โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ โฑ๏ธ TIMING DIAGRAM โ”‚ -โ”‚ โ”‚ -โ”‚ Time: 0ms 500ms 1000ms 1500ms 2000ms โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ ๐Ÿ“ก Start all requests โ”‚ -โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ ๐Ÿฆ Coinbase Request โ”‚ โ”‚ -โ”‚ โ”‚ โœ… Response โ”‚ (1.2s) โ”‚ -โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ -โ”‚ โ”‚ ๐Ÿ“Š CryptoCompare Request โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โœ… Response (0.8s) โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ ๐ŸŸก Binance โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โœ… Response (0.3s - mock) โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ Wait for all... โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ ๐Ÿงฎ Aggregate (1.2s total) โ”‚ -โ”‚ โ”‚ -โ”‚ ๐Ÿ“ˆ Performance Gain: โ”‚ -โ”‚ Sequential: 1.2s + 0.8s + 0.3s = 2.3s โ”‚ -โ”‚ Parallel: max(1.2s, 0.8s, 0.3s) = 1.2s โ”‚ -โ”‚ Improvement: ~48% faster! ๐Ÿš€ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -## ๐Ÿงฎ Aggregation Algorithm Detail - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๐Ÿ”ฌ DATA AGGREGATION LOGIC โ”‚ -โ”‚ โ”‚ -โ”‚ def aggregate_market_data(results): โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿ“Š Input Data: โ”‚ -โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ โ”‚ coinbase: {"BTC": 63500, "ETH": 4150} โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ cryptocomp: {"BTC": 63450, "ETH": 4160} โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ binance: {"BTC": 63600, "ETH": 4140} โ”‚ โ”‚ -โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ ๐Ÿงฎ Price Calculation: โ”‚ -โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ โ”‚ BTC_prices = [63500, 63450, 63600] โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ BTC_avg = 63516.67 โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ BTC_std = 75.83 โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ BTC_spread = (max-min)/avg = 0.24% โ”‚ โ”‚ -โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ ๐ŸŽฏ Confidence Scoring: โ”‚ -โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ โ”‚ confidence = 1 - (std_dev / mean) โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ if spread < 0.5%: confidence += 0.1 โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ if sources >= 3: confidence += 0.05 โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ BTC_confidence = 0.94 (excellent!) โ”‚ โ”‚ -โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€ ๐Ÿ“ˆ Market Signals: โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ spread_analysis: โ”‚ โ”‚ -โ”‚ โ”‚ "Low spread (0.24%) indicates healthy liq." โ”‚ โ”‚ -โ”‚ โ”‚ volume_trend: โ”‚ โ”‚ -โ”‚ โ”‚ "Combined volume: 4.1M USD" โ”‚ โ”‚ -โ”‚ โ”‚ price_divergence: โ”‚ โ”‚ -โ”‚ โ”‚ "Max deviation: 0.24% - Normal range" โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -## ๐Ÿ”„ Error Handling & Resilience - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๐Ÿ›ก๏ธ RESILIENCE STRATEGY โ”‚ -โ”‚ โ”‚ -โ”‚ Scenario 1: One Provider Fails โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ ๐Ÿฆ Coinbase: โœ… Success (BTC: $63500) โ”‚ โ”‚ -โ”‚ โ”‚ ๐Ÿ“Š CryptoComp: โŒ Timeout/Error โ”‚ โ”‚ -โ”‚ โ”‚ ๐ŸŸก Binance: โœ… Success (BTC: $63600) โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ Result: Continue with 2 sources โ”‚ โ”‚ -โ”‚ โ”‚ Confidence: 0.89 (slightly reduced) โ”‚ โ”‚ -โ”‚ โ”‚ Note: "CryptoCompare unavailable" โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ -โ”‚ Scenario 2: Multiple Providers Fail โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ ๐Ÿฆ Coinbase: โŒ API Limit โ”‚ โ”‚ -โ”‚ โ”‚ ๐Ÿ“Š CryptoComp: โœ… Success (BTC: $63450) โ”‚ โ”‚ -โ”‚ โ”‚ ๐ŸŸก Binance: โŒ Network Error โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ Result: Single source data โ”‚ โ”‚ -โ”‚ โ”‚ Confidence: 0.60 (low - warn user) โ”‚ โ”‚ -โ”‚ โ”‚ Note: "Limited data - consider waiting" โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ -โ”‚ Scenario 3: All Providers Fail โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ ๐Ÿฆ Coinbase: โŒ Maintenance โ”‚ โ”‚ -โ”‚ โ”‚ ๐Ÿ“Š CryptoComp: โŒ API Down โ”‚ โ”‚ -โ”‚ โ”‚ ๐ŸŸก Binance: โŒ Rate Limit โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ Result: Graceful degradation โ”‚ โ”‚ -โ”‚ โ”‚ Message: "Market data temporarily unavailable" โ”‚ โ”‚ -โ”‚ โ”‚ Fallback: Cached data (if available) โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -## ๐Ÿ“Š JSON Output Schema - -```json -{ - "aggregated_data": { - "BTC_USD": { - "price": 63516.67, - "confidence": 0.94, - "sources_count": 3, - "last_updated": "2025-09-23T17:30:00Z" - }, - "ETH_USD": { - "price": 4150.33, - "confidence": 0.91, - "sources_count": 3, - "last_updated": "2025-09-23T17:30:00Z" - } - }, - "individual_sources": { - "coinbase": { - "BTC": {"price": 63500, "volume": "1.2M", "status": "online"}, - "ETH": {"price": 4150, "volume": "25.6M", "status": "online"} - }, - "cryptocompare": { - "BTC": {"price": 63450, "volume": "N/A", "status": "active"}, - "ETH": {"price": 4160, "volume": "N/A", "status": "active"} - }, - "binance": { - "BTC": {"price": 63600, "volume": "2.1M", "status": "mock"}, - "ETH": {"price": 4140, "volume": "18.3M", "status": "mock"} - } - }, - "market_signals": { - "spread_analysis": "Low spread (0.24%) indicates healthy liquidity", - "volume_trend": "Combined BTC volume: 3.3M USD (+12% from avg)", - "price_divergence": "Max deviation: 0.24% - Normal range", - "data_quality": "High - 3 sources, low variance", - "recommendation": "Data suitable for trading decisions" - }, - "metadata": { - "query_time_ms": 1247, - "sources_queried": ["coinbase", "cryptocompare", "binance"], - "sources_successful": ["coinbase", "cryptocompare", "binance"], - "sources_failed": [], - "aggregation_method": "weighted_average", - "confidence_threshold": 0.75 - } -} -``` - ---- -*Diagramma dettaglio asincrono: 2025-09-23* -*Focus: Performance, Resilienza, Qualitร  Dati* \ No newline at end of file diff --git a/docs/Market_Data_Implementation_Plan.md b/docs/Market_Data_Implementation_Plan.md deleted file mode 100644 index e494ca7..0000000 --- a/docs/Market_Data_Implementation_Plan.md +++ /dev/null @@ -1,96 +0,0 @@ -# ๐Ÿš€ Piano di Implementazione - Market Data Enhancement - -## ๐Ÿ“‹ Roadmap Implementazioni - -### **Fase 1: Binance Mock Provider** -**Obiettivo**: Aggiungere terzo provider per test aggregazione -- โœ… Creare `binance_signer.py` con mock data -- โœ… Integrare nel MarketAgent -- โœ… Testare detection automatica provider -- **Deliverable**: 3 provider funzionanti (Coinbase, CryptoCompare, Binance) - -### **Fase 2: Interrogazione Condizionale** -**Obiettivo**: Auto-detection credenziali e interrogazione intelligente -- โœ… Migliorare detection chiavi API nel MarketAgent -- โœ… Skip provider se credenziali mancanti (no errori) -- โœ… Logging informativo per provider disponibili/non disponibili -- โœ… Gestione graceful degradation -- **Deliverable**: Sistema resiliente che funziona con qualsiasi combinazione di provider - -### **Fase 3: Interrogazione Asincrona + Aggregazione JSON** -**Obiettivo**: Performance boost e formato dati professionale - -#### **3A. Implementazione Asincrona** -- โœ… Refactor MarketAgent per supporto `async/await` -- โœ… Chiamate parallele a tutti i provider disponibili -- โœ… Timeout management per provider lenti -- โœ… Error handling per provider che falliscono - -#### **3B. Aggregazione Dati Intelligente** -- โœ… Calcolo `confidence` basato su concordanza prezzi -- โœ… Analisi `spread` tra provider -- โœ… Detection `price_divergence` per anomalie -- โœ… Volume trend analysis -- โœ… Formato JSON strutturato: - -```json -{ - "aggregated_data": { - "BTC_USD": { - "price": 43250.12, - "confidence": 0.95, - "sources_count": 4 - } - }, - "individual_sources": { - "coinbase": {"price": 43245.67, "volume": "1.2M"}, - "binance": {"price": 43255.89, "volume": "2.1M"}, - "cryptocompare": {"price": 43248.34, "volume": "0.8M"} - }, - "market_signals": { - "spread_analysis": "Low spread (0.02%) indicates healthy liquidity", - "volume_trend": "Volume up 15% from 24h average", - "price_divergence": "Max deviation: 0.05% - Normal range" - } -} -``` - -**Deliverable**: Sistema asincrono con analisi avanzata dei dati di mercato - -## ๐ŸŽฏ Benefici Attesi - -### **Performance** -- โšก Tempo risposta: da ~4s sequenziali a ~1s paralleli -- ๐Ÿ”„ Resilienza: sistema funziona anche se 1-2 provider falliscono -- ๐Ÿ“Š Qualitร  dati: validazione incrociata tra provider - -### **Professionalitร ** -- ๐Ÿ“ˆ Confidence scoring per decisioni informate -- ๐Ÿ” Market signals per trading insights -- ๐Ÿ“‹ Formato standardizzato per integrazioni future - -### **Scalabilitร ** -- โž• Facile aggiunta nuovi provider -- ๐Ÿ”ง Configurazione flessibile via environment -- ๐Ÿ“ Logging completo per debugging - -## ๐Ÿงช Test Strategy - -1. **Unit Tests**: Ogni provider singolarmente -2. **Integration Tests**: Aggregazione multi-provider -3. **Performance Tests**: Confronto sync vs async -4. **Resilience Tests**: Fallimento provider singoli -5. **E2E Tests**: Full workflow con UI Gradio - -## ๐Ÿ“… Timeline Stimata - -- **Fase 1**: ~1h (setup Binance mock) -- **Fase 2**: ~1h (detection condizionale) -- **Fase 3**: ~2-3h (async + aggregazione) -- **Testing**: ~1h (validation completa) - -**Total**: ~5-6h di lavoro strutturato - ---- -*Documento creato: 2025-09-23* -*Versione: 1.0* \ No newline at end of file diff --git a/docs/Piano di Sviluppo.md b/docs/Piano di Sviluppo.md deleted file mode 100644 index c4b1a51..0000000 --- a/docs/Piano di Sviluppo.md +++ /dev/null @@ -1,73 +0,0 @@ -# Guida alla Realizzazione del Progetto - -Questa guida รจ una lista di controllo per l'implementazione del tuo progetto. รˆ divisa in fasi logiche, ognuna con i compiti specifici da svolgere. - -## Fase 1: Preparazione e Architettura di Base - -### Impostazione dell'ambiente - -* Scegliere il linguaggio di programmazione (es. **Python**). -* Utilizzare la libreria `agno` per la creazione di agenti e **LangChain/LlamaIndex** per la gestione dell'LLM e dell'orchestrazione. - -### Definizione dell'Architettura degli agenti - -* Definire la classe base per gli agenti, con metodi comuni come `execute()` e `reason()`. -* Delineare i ruoli e le interfacce di tutti gli agenti (`RicercatoreDati`, `AnalistaSentiment`, `MotorePredittivo`, `Orchestratore`), stabilendo come comunicheranno tra loro. - ---- - -## Fase 2: Implementazione degli Agenti Core - -### Agente `RicercatoreDati` - -* Implementare la logica per connettersi a un'API di exchange (es. **Binance, Coindesk, CoinMarketCap**). -* Testare la capacitร  di recuperare dati in tempo reale per diverse criptovalute (prezzo, volume, capitalizzazione) e **assicurarsi che la gestione degli errori sia robusta**. - -### Agente `AnalistaSentiment` - -* **Agente `Social`:** - * Scegliere un metodo per lo scraping di forum e social media (es. **Reddit API, librerie per Twitter/X, BeautifulSoup per web scraping**). - * Implementare un modulo di analisi del testo per classificare il sentiment (positivo, negativo, neutro) utilizzando **modelli pre-addestrati (es. VADER) o fine-tuning di modelli piรน avanzati**. -* **Agente `News`:** - * Ottenere una chiave API per un servizio di notizie (es. **NewsAPI**). - * Implementare la logica per cercare articoli pertinenti a una criptovaluta specifica o al mercato in generale, e **filtrare le notizie in base a parole chiave rilevanti**. - -### Agente `MotorePredittivo` - -* Definire la logica per integrare i dati numerici del `RicercatoreDati` con il sentiment dell'`AnalistaSentiment`. -* Creare un **prompt avanzato** per l'LLM che lo guidi a generare previsioni e strategie. Dovrai usare tecniche come la **chain-of-thought** per rendere il ragionamento trasparente. Assicurarsi che il prompt includa vincoli specifici per lo stile di investimento (aggressivo/conservativo). - ---- - -## Fase 3: Costruzione dell'Orchestratore e Test di Integrazione - -### Implementazione dell'Agente Orchestratore - -* **Gestione dell'Input Utente:** Creare un metodo che riceve la richiesta dell'utente (es. `analizza_cripto('Bitcoin', 'aggressivo')`). Analizzare il tipo di richiesta e le preferenze utente. -* **Recupero della Memoria Utente:** Integrare la logica per recuperare la cronologia delle richieste passate dal database e preparare i dati come contesto aggiuntivo per l'LLM. -* **Orchestrazione e Flusso di Lavoro:** Chiamare gli agenti (`RicercatoreDati`, `AnalistaSentiment`) e passare i risultati combinati all'**Agente `MotorePredittivo`** per generare previsioni e strategie. -* **Valutazione e Selezione Strategica:** Ricevere le previsioni dal `MotorePredittivo` e applicare le regole di valutazione basate sulle preferenze dell'utente per selezionare le strategie piรน appropriate. -* **Presentazione e Persistenza:** Costruire il report finale e salvare la sessione completa nel database. - ---- - -## Fase 4: Gestione della Persistenza e dell'Interfaccia Utente - -* **Database per la persistenza:** Scegli un database (es. **Firestore, MongoDB, PostgreSQL**) per salvare la cronologia delle richieste degli utenti. Implementa la logica per salvare e recuperare le sessioni di consulenza passate, associandole a un ID utente, e **struttura i dati per una ricerca efficiente**. - -* **Interfaccia utente (UI):** Costruisci un'interfaccia utente semplice e intuitiva che permetta di inserire i parametri di richiesta. Aggiungi una sezione per visualizzare i risultati, inclusi i grafici e le note che spiegano il ragionamento dell'agente. - ---- - -## Fase 5: Test del Sistema - -* **Test unitari:** Esegui test su ogni agente singolarmente per assicurarti che funzioni correttamente (es. l'agente `RicercatoreDati` recupera i dati, l'agente `AnalistaSentiment` classifica correttamente un testo). **Crea dei mock per le API esterne per testare la logica interna senza dipendenze esterne**. -* **Test di integrazione:** Esegui scenari di test completi per l'intero sistema. Verifica che l'orchestrazione tra gli agenti avvenga senza intoppi e che i dati vengano passati correttamente tra di essi. - ---- - -## Fase 6: Valutazione dei Risultati - -* **Valutazione della qualitร :** Verifica la qualitร  delle raccomandazioni generate. L'output รจ logico e ben argomentato? -* **Trasparenza del ragionamento:** Controlla che le note (`Ragionamenti`) siano chiare e forniscano un'effettiva trasparenza del processo decisionale dell'agente. -* **Confronto e validazione:** Confronta le raccomandazioni con dati storici e scenari ipotetici per valutarne la plausibilitร . \ No newline at end of file From 6a9d8b354b3f5a384d1bef0952131cb1623d0ade Mon Sep 17 00:00:00 2001 From: Giacomo Bertolazzi <31776951+Berack96@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:45:40 +0100 Subject: [PATCH 6/6] Fix socials timestamp (#50) * Fix Dockerfile per dipendenze di X * time --> timestamp * fix X command --- .env.example | 8 +------- Dockerfile | 10 +++++++--- src/app/api/core/social.py | 8 ++++---- src/app/api/social/chan.py | 13 +++++++++---- src/app/api/social/reddit.py | 4 ++-- src/app/api/social/x.py | 18 ++++++++++-------- tests/api/test_social_4chan.py | 4 ++-- tests/api/test_social_reddit.py | 2 +- tests/api/test_social_x_api.py | 4 +++- tests/tools/test_socials_tool.py | 4 ++-- 10 files changed, 41 insertions(+), 34 deletions(-) diff --git a/.env.example b/.env.example index 694300e..3127b74 100644 --- a/.env.example +++ b/.env.example @@ -42,13 +42,7 @@ CRYPTOPANIC_API_KEY= REDDIT_API_CLIENT_ID= REDDIT_API_CLIENT_SECRET= -# Per ottenere questa API รจ necessario seguire i seguenti passaggi: -# - Installare l'estensione su chrome X Auth Helper -# - Dargli il permesso di girare in incognito -# - Andare in incognito ed entrare sul proprio account X -# - Aprire l'estensione e fare "get key" -# - Chiudere chrome -# Dovrebbe funzionare per 5 anni o finchรจ non si si fa il log out, in ogni caso si puรฒ ricreare +# https://www.npmjs.com/package/rettiwt-api X_API_KEY= diff --git a/Dockerfile b/Dockerfile index 3a354bb..17e3234 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,10 +2,9 @@ FROM debian:bookworm-slim # Installiamo le dipendenze di sistema -RUN apt-get update && \ - apt-get install -y curl npm && \ +RUN apt update && \ + apt install -y curl && \ rm -rf /var/lib/apt/lists/* -RUN npm install -g rettiwt-api # Installiamo uv RUN curl -LsSf https://astral.sh/uv/install.sh | sh @@ -20,6 +19,11 @@ COPY uv.lock ./ RUN uv sync --frozen --no-dev ENV PYTHONPATH="./src" +# Installiamo le dipendenze per X (rettiwt, nodejs e npm) +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - +RUN apt install -y nodejs && rm -rf /var/lib/apt/lists/* +RUN npm install -g rettiwt-api + # Copiamo i file del progetto COPY LICENSE ./ COPY src/ ./src/ diff --git a/src/app/api/core/social.py b/src/app/api/core/social.py index fe4d5bf..05953a3 100644 --- a/src/app/api/core/social.py +++ b/src/app/api/core/social.py @@ -9,25 +9,25 @@ class SocialPost(BaseModel): """ Represents a social media post with time, title, description, and comments. """ - time: str = "" + timestamp: str = "" title: str = "" description: str = "" comments: list["SocialComment"] = [] def set_timestamp(self, timestamp_ms: int | None = None, timestamp_s: int | None = None) -> None: """ Use the unified_timestamp function to set the time.""" - self.time = unified_timestamp(timestamp_ms, timestamp_s) + self.timestamp = unified_timestamp(timestamp_ms, timestamp_s) class SocialComment(BaseModel): """ Represents a comment on a social media post. """ - time: str = "" + timestamp: str = "" description: str = "" def set_timestamp(self, timestamp_ms: int | None = None, timestamp_s: int | None = None) -> None: """ Use the unified_timestamp function to set the time.""" - self.time = unified_timestamp(timestamp_ms, timestamp_s) + self.timestamp = unified_timestamp(timestamp_ms, timestamp_s) class SocialWrapper: diff --git a/src/app/api/social/chan.py b/src/app/api/social/chan.py index a39e517..66efdb0 100644 --- a/src/app/api/social/chan.py +++ b/src/app/api/social/chan.py @@ -1,15 +1,20 @@ -''' -Usiamo le API di 4chan per ottenere un catalogo di threads dalla board /biz/ -''' import re import html import requests -from bs4 import BeautifulSoup +import warnings +from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning from datetime import datetime from app.api.core.social import * +# Ignora i warning di BeautifulSoup quando incontra HTML malformato o un link, mentre si aspetta un HTML completo +warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning) + class ChanWrapper(SocialWrapper): + """ + Wrapper per l'API di 4chan, in particolare per la board /biz/ (Business & Finance) + Fonte API: https://a.4cdn.org/biz/catalog.json + """ def __init__(self): super().__init__() diff --git a/src/app/api/social/reddit.py b/src/app/api/social/reddit.py index 201166c..306e49e 100644 --- a/src/app/api/social/reddit.py +++ b/src/app/api/social/reddit.py @@ -23,13 +23,13 @@ SUBREDDITS = [ def extract_post(post: Submission) -> SocialPost: social = SocialPost() - social.set_timestamp(timestamp_ms=post.created) + social.set_timestamp(timestamp_s=post.created) social.title = post.title social.description = post.selftext for top_comment in post.comments: comment = SocialComment() - comment.set_timestamp(timestamp_ms=top_comment.created) + comment.set_timestamp(timestamp_s=top_comment.created) comment.description = top_comment.body social.comments.append(comment) diff --git a/src/app/api/social/x.py b/src/app/api/social/x.py index a1b1bd4..30d93c0 100644 --- a/src/app/api/social/x.py +++ b/src/app/api/social/x.py @@ -2,6 +2,7 @@ import os import json import subprocess from shutil import which +from datetime import datetime from app.api.core.social import SocialWrapper, SocialPost @@ -28,19 +29,20 @@ class XWrapper(SocialWrapper): def get_top_crypto_posts(self, limit:int = 5) -> list[SocialPost]: - social_posts: list[SocialPost] = [] + posts: list[SocialPost] = [] for user in X_USERS: - process = subprocess.run(f"rettiwt -k {self.api_key} tweet search -f {str(user)}", capture_output=True) + cmd = ['rettiwt', '-k', self.api_key, 'tweet', 'search', str(limit), '-f', str(user)] + process = subprocess.run(cmd, capture_output=True) results = process.stdout.decode() json_result = json.loads(results) - tweets = json_result['list'] - for tweet in tweets[:limit]: + for tweet in json_result.get('list', []): + time = datetime.fromisoformat(tweet['createdAt']) social_post = SocialPost() - social_post.time = tweet['createdAt'] - social_post.title = str(user) + " tweeted: " + social_post.set_timestamp(timestamp_s=int(time.timestamp())) + social_post.title = f"{user} tweeted: " social_post.description = tweet['fullText'] - social_posts.append(social_post) + posts.append(social_post) - return social_posts + return posts diff --git a/tests/api/test_social_4chan.py b/tests/api/test_social_4chan.py index b39a36d..dcf42d2 100644 --- a/tests/api/test_social_4chan.py +++ b/tests/api/test_social_4chan.py @@ -16,7 +16,7 @@ class TestChanWrapper: assert len(posts) == 2 for post in posts: assert post.title != "" - assert post.time != "" - assert re.match(r'\d{4}-\d{2}-\d{2}', post.time) + assert post.timestamp != "" + assert re.match(r'\d{4}-\d{2}-\d{2}', post.timestamp) assert isinstance(post.comments, list) diff --git a/tests/api/test_social_reddit.py b/tests/api/test_social_reddit.py index a83fe8a..adb4e13 100644 --- a/tests/api/test_social_reddit.py +++ b/tests/api/test_social_reddit.py @@ -19,7 +19,7 @@ class TestRedditWrapper: assert len(posts) == 2 for post in posts: assert post.title != "" - assert re.match(r'\d{4}-\d{2}-\d{2}', post.time) + assert re.match(r'\d{4}-\d{2}-\d{2}', post.timestamp) assert isinstance(post.comments, list) assert len(post.comments) <= MAX_COMMENTS diff --git a/tests/api/test_social_x_api.py b/tests/api/test_social_x_api.py index 15f39c3..39f75f9 100644 --- a/tests/api/test_social_x_api.py +++ b/tests/api/test_social_x_api.py @@ -1,11 +1,13 @@ import os import re import pytest +from shutil import which from app.api.social.x import XWrapper @pytest.mark.social @pytest.mark.api @pytest.mark.skipif(not os.getenv("X_API_KEY"), reason="X_API_KEY not set in environment variables") +@pytest.mark.skipif(which('rettiwt') is None, reason="rettiwt not installed") class TestXWrapper: def test_initialization(self): wrapper = XWrapper() @@ -18,5 +20,5 @@ class TestXWrapper: assert len(posts) == 2 for post in posts: assert post.title != "" - assert re.match(r'\d{4}-\d{2}-\d{2}', post.time) + assert re.match(r'\d{4}-\d{2}-\d{2}', post.timestamp) assert isinstance(post.comments, list) diff --git a/tests/tools/test_socials_tool.py b/tests/tools/test_socials_tool.py index c021a90..3a481f7 100644 --- a/tests/tools/test_socials_tool.py +++ b/tests/tools/test_socials_tool.py @@ -17,7 +17,7 @@ class TestSocialAPIsTool: assert len(result) > 0 for post in result: assert post.title is not None - assert post.time is not None + assert post.timestamp is not None def test_social_api_tool_get_top__all_results(self): tool = SocialAPIsTool() @@ -27,4 +27,4 @@ class TestSocialAPIsTool: for _provider, posts in result.items(): for post in posts: assert post.title is not None - assert post.time is not None + assert post.timestamp is not None