From 12339ccbfff40984242e02f3cf47fa6890ee4618 Mon Sep 17 00:00:00 2001 From: Giacomo Bertolazzi <31776951+Berack96@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:05:58 +0200 Subject: [PATCH] Team Workflow aggiornato (#37) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rimuovi la classe Predictor e aggiorna le importazioni in Pipeline e __init__.py * Aggiungi modelli per l'analisi delle query e la generazione di report; aggiorna le configurazioni degli agenti * Tests for report generation and team agent responses * Aggiorna i prompt degli agenti * Changed defaults models * Aggiunta della classe PlanMemoryTool per la gestione dei task e aggiornamento della logica del team leader per un'esecuzione più dinamica del piano. --- configs.yaml | 5 +- src/app/agents/__init__.py | 6 +- src/app/agents/core.py | 121 ++++++++++++ src/app/agents/pipeline.py | 195 +++++++------------ src/app/agents/plan_memory_tool.py | 55 ++++++ src/app/agents/predictor.py | 16 -- src/app/agents/prompts/__init__.py | 10 +- src/app/agents/prompts/predictor.txt | 27 --- src/app/agents/prompts/query_check.txt | 18 ++ src/app/agents/prompts/report_generation.txt | 61 ++++++ src/app/agents/prompts/team_leader.txt | 57 ++++-- src/app/agents/prompts/team_market.txt | 25 ++- src/app/agents/prompts/team_news.txt | 29 ++- src/app/agents/prompts/team_social.txt | 25 +-- src/app/api/wrapper_handler.py | 4 +- src/app/configs.py | 3 +- tests/agents/test_query_check.py | 48 +++++ tests/agents/test_report.py | 31 +++ tests/agents/test_team.py | 37 ++++ 19 files changed, 539 insertions(+), 234 deletions(-) create mode 100644 src/app/agents/core.py create mode 100644 src/app/agents/plan_memory_tool.py delete mode 100644 src/app/agents/predictor.py delete mode 100644 src/app/agents/prompts/predictor.txt create mode 100644 src/app/agents/prompts/query_check.txt create mode 100644 src/app/agents/prompts/report_generation.txt create mode 100644 tests/agents/test_query_check.py create mode 100644 tests/agents/test_report.py create mode 100644 tests/agents/test_team.py diff --git a/configs.yaml b/configs.yaml index c0925b8..b619b98 100644 --- a/configs.yaml +++ b/configs.yaml @@ -41,5 +41,6 @@ api: agents: strategy: Conservative team_model: qwen3:1.7b - team_leader_model: qwen3:4b - predictor_model: qwen3:4b + team_leader_model: qwen3:8b + query_analyzer_model: qwen3:4b + report_generation_model: qwen3:8b diff --git a/src/app/agents/__init__.py b/src/app/agents/__init__.py index 2e78f1b..71f329f 100644 --- a/src/app/agents/__init__.py +++ b/src/app/agents/__init__.py @@ -1,4 +1,4 @@ -from app.agents.predictor import PredictorInput, PredictorOutput -from app.agents.pipeline import Pipeline, PipelineInputs, PipelineEvent +from app.agents.pipeline import Pipeline, PipelineEvent +from app.agents.core import PipelineInputs, QueryOutputs -__all__ = ["PredictorInput", "PredictorOutput", "Pipeline", "PipelineInputs", "PipelineEvent"] +__all__ = ["Pipeline", "PipelineInputs", "PipelineEvent", "QueryOutputs"] diff --git a/src/app/agents/core.py b/src/app/agents/core.py new file mode 100644 index 0000000..c3fd089 --- /dev/null +++ b/src/app/agents/core.py @@ -0,0 +1,121 @@ +from pydantic import BaseModel +from agno.agent import Agent +from agno.team import Team +from agno.tools.reasoning import ReasoningTools +from app.agents.plan_memory_tool import PlanMemoryTool +from app.api.tools import * +from app.configs import AppConfig +from app.agents.prompts import * + + + +class QueryInputs(BaseModel): + user_query: str + strategy: str + +class QueryOutputs(BaseModel): + response: str + is_crypto: bool + +class PipelineInputs: + """ + Classe necessaria per passare gli input alla Pipeline. + Serve per raggruppare i parametri e semplificare l'inizializzazione. + """ + + def __init__(self, configs: AppConfig | None = None) -> None: + """ + Inputs per la Pipeline di agenti. + Setta i valori di default se non specificati. + """ + self.configs = configs if configs else AppConfig() + + agents = self.configs.agents + self.team_model = self.configs.get_model_by_name(agents.team_model) + self.team_leader_model = self.configs.get_model_by_name(agents.team_leader_model) + self.query_analyzer_model = self.configs.get_model_by_name(agents.query_analyzer_model) + self.report_generation_model = self.configs.get_model_by_name(agents.report_generation_model) + self.strategy = self.configs.get_strategy_by_name(agents.strategy) + self.user_query = "" + + # ====================== + # Dropdown handlers + # ====================== + def choose_team_leader(self, index: int): + """ + Sceglie il modello LLM da usare per il Team Leader. + """ + assert index >= 0 and index < len(self.configs.models.all_models), "Index out of range for models list." + self.team_leader_model = self.configs.models.all_models[index] + + def choose_team(self, index: int): + """ + Sceglie il modello LLM da usare per il Team. + """ + 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_strategy(self, index: int): + """ + Sceglie la strategia da usare per il Team. + """ + self.strategy = self.configs.strategies[index] + + # ====================== + # Helpers + # ====================== + def list_models_names(self) -> list[str]: + """ + Restituisce la lista dei nomi dei modelli disponibili. + """ + return [model.label for model in self.configs.models.all_models] + + def list_strategies_names(self) -> list[str]: + """ + Restituisce la lista delle strategie disponibili. + """ + return [strat.label for strat in self.configs.strategies] + + def get_query_inputs(self) -> QueryInputs: + """ + Restituisce gli input per l'agente di verifica della query. + """ + return QueryInputs( + user_query=self.user_query, + strategy=self.strategy.label, + ) + + # ====================== + # Agent getters + # ====================== + def get_agent_team(self) -> Team: + market, news, social = self.get_tools() + market_agent = self.team_model.get_agent(MARKET_INSTRUCTIONS, "Market Agent", tools=[market]) + news_agent = self.team_model.get_agent(NEWS_INSTRUCTIONS, "News Agent", tools=[news]) + social_agent = self.team_model.get_agent(SOCIAL_INSTRUCTIONS, "Socials Agent", tools=[social]) + return Team( + model=self.team_leader_model.get_model(TEAM_LEADER_INSTRUCTIONS), + name="CryptoAnalysisTeam", + tools=[ReasoningTools(), PlanMemoryTool()], + members=[market_agent, news_agent, social_agent], + ) + + def get_agent_query_checker(self) -> Agent: + return self.query_analyzer_model.get_agent(QUERY_CHECK_INSTRUCTIONS, "Query Check Agent", output_schema=QueryOutputs) + + def get_agent_report_generator(self) -> Agent: + return self.report_generation_model.get_agent(REPORT_GENERATION_INSTRUCTIONS, "Report Generator Agent") + + def get_tools(self) -> tuple[MarketAPIsTool, NewsAPIsTool, SocialAPIsTool]: + """ + Restituisce la lista di tools disponibili per gli agenti. + """ + api = self.configs.api + + market_tool = MarketAPIsTool(currency=api.currency) + market_tool.handler.set_retries(api.retry_attempts, api.retry_delay_seconds) + news_tool = NewsAPIsTool() + news_tool.handler.set_retries(api.retry_attempts, api.retry_delay_seconds) + social_tool = SocialAPIsTool() + social_tool.handler.set_retries(api.retry_attempts, api.retry_delay_seconds) + return market_tool, news_tool, social_tool diff --git a/src/app/agents/pipeline.py b/src/app/agents/pipeline.py index cf8de3e..bcec72d 100644 --- a/src/app/agents/pipeline.py +++ b/src/app/agents/pipeline.py @@ -4,85 +4,38 @@ import logging import random from typing import Any, Callable from agno.agent import RunEvent -from agno.team import Team, TeamRunEvent -from agno.tools.reasoning import ReasoningTools from agno.run.workflow import WorkflowRunEvent +from agno.workflow.types import StepInput, StepOutput from agno.workflow.step import Step from agno.workflow.workflow import Workflow - -from app.api.tools import * -from app.agents.prompts import * -from app.configs import AppConfig +from app.agents.core import * logging = logging.getLogger("pipeline") + class PipelineEvent(str, Enum): - PLANNER = "Planner" + QUERY_CHECK = "Query Check" + QUERY_ANALYZER = "Query Analyzer" INFO_RECOVERY = "Info Recovery" REPORT_GENERATION = "Report Generation" REPORT_TRANSLATION = "Report Translation" - TOOL_USED = RunEvent.tool_call_completed + RUN_FINISHED = WorkflowRunEvent.workflow_completed.value + TOOL_USED = RunEvent.tool_call_completed.value def check_event(self, event: str, step_name: str) -> bool: - return event == self.value or (WorkflowRunEvent.step_completed and step_name == self.value) + return event == self.value or (WorkflowRunEvent.step_completed == event and step_name == self.value) - -class PipelineInputs: - """ - Classe necessaria per passare gli input alla Pipeline. - Serve per raggruppare i parametri e semplificare l'inizializzazione. - """ - - def __init__(self, configs: AppConfig | None = None) -> None: - """ - Inputs per la Pipeline di agenti. - Setta i valori di default se non specificati. - """ - self.configs = configs if configs else AppConfig() - - agents = self.configs.agents - self.team_model = self.configs.get_model_by_name(agents.team_model) - self.team_leader_model = self.configs.get_model_by_name(agents.team_leader_model) - self.predictor_model = self.configs.get_model_by_name(agents.predictor_model) - self.strategy = self.configs.get_strategy_by_name(agents.strategy) - self.user_query = "" - - # ====================== - # Dropdown handlers - # ====================== - def choose_team_leader(self, index: int): - """ - Sceglie il modello LLM da usare per il Team Leader. - """ - self.leader_model = self.configs.models.all_models[index] - - def choose_team(self, index: int): - """ - Sceglie il modello LLM da usare per il Team. - """ - self.team_model = self.configs.models.all_models[index] - - def choose_strategy(self, index: int): - """ - Sceglie la strategia da usare per il Team. - """ - self.strategy = self.configs.strategies[index] - - # ====================== - # Helpers - # ====================== - def list_models_names(self) -> list[str]: - """ - Restituisce la lista dei nomi dei modelli disponibili. - """ - return [model.label for model in self.configs.models.all_models] - - def list_strategies_names(self) -> list[str]: - """ - Restituisce la lista delle strategie disponibili. - """ - return [strat.label for strat in self.configs.strategies] + @classmethod + def get_log_events(cls, run_id: int) -> list[tuple['PipelineEvent', Callable[[Any], None]]]: + return [ + (PipelineEvent.QUERY_CHECK, lambda _: logging.info(f"[{run_id}] Query Check completed.")), + (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.RUN_FINISHED, lambda _: logging.info(f"[{run_id}] Run completed.")), + ] class Pipeline: @@ -93,12 +46,14 @@ class Pipeline: """ def __init__(self, inputs: PipelineInputs): + """ + Inizializza la pipeline con gli input forniti. + Args: + inputs: istanza di PipelineInputs contenente le configurazioni e i parametri della pipeline. + """ self.inputs = inputs - # ====================== - # Core interaction - # ====================== - def interact(self, listeners: dict[RunEvent | TeamRunEvent, Callable[[PipelineEvent], None]] = {}) -> str: + def interact(self, listeners: list[tuple[PipelineEvent, Callable[[Any], None]]] = []) -> str: """ Esegue la pipeline di agenti per rispondere alla query dell'utente. Args: @@ -108,7 +63,7 @@ class Pipeline: """ return asyncio.run(self.interact_async(listeners)) - async def interact_async(self, listeners: dict[RunEvent | TeamRunEvent, Callable[[PipelineEvent], None]] = {}) -> str: + async def interact_async(self, listeners: list[tuple[PipelineEvent, Callable[[Any], None]]] = []) -> str: """ Versione asincrona che esegue la pipeline di agenti per rispondere alla query dell'utente. Args: @@ -119,61 +74,47 @@ class Pipeline: run_id = random.randint(1000, 9999) # Per tracciare i log logging.info(f"[{run_id}] Pipeline query: {self.inputs.user_query}") - # Step 1: Crea gli agenti e il team - market_tool, news_tool, social_tool = self.get_tools() - market_agent = self.inputs.team_model.get_agent(instructions=MARKET_INSTRUCTIONS, name="MarketAgent", tools=[market_tool]) - news_agent = self.inputs.team_model.get_agent(instructions=NEWS_INSTRUCTIONS, name="NewsAgent", tools=[news_tool]) - social_agent = self.inputs.team_model.get_agent(instructions=SOCIAL_INSTRUCTIONS, name="SocialAgent", tools=[social_tool]) - - team = Team( - model=self.inputs.team_leader_model.get_model(COORDINATOR_INSTRUCTIONS), - name="CryptoAnalysisTeam", - tools=[ReasoningTools()], - members=[market_agent, news_agent, social_agent], + events = [*PipelineEvent.get_log_events(run_id), *listeners] + query = QueryInputs( + user_query=self.inputs.user_query, + strategy=self.inputs.strategy.description ) - # Step 3: Crea il workflow - #query_planner = Step(name=PipelineEvent.PLANNER, agent=Agent()) - info_recovery = Step(name=PipelineEvent.INFO_RECOVERY, team=team) - #report_generation = Step(name=PipelineEvent.REPORT_GENERATION, agent=Agent()) - #report_translate = Step(name=AppEvent.REPORT_TRANSLATION, agent=Agent()) - - workflow = Workflow( - name="App Workflow", - steps=[ - #query_planner, - info_recovery, - #report_generation, - #report_translate - ] - ) - - # Step 4: Fai partire il workflow e prendi l'output - query = f"The user query is: {self.inputs.user_query}\n\n They requested a {self.inputs.strategy.label} investment strategy." - result = await self.run(workflow, query, events={}) - logging.info(f"[{run_id}] Run finished") + workflow = self.build_workflow() + result = await self.run(workflow, query, events=events) return result - # ====================== - # Helpers - # ===================== - def get_tools(self) -> tuple[MarketAPIsTool, NewsAPIsTool, SocialAPIsTool]: - """ - Restituisce la lista di tools disponibili per gli agenti. - """ - api = self.inputs.configs.api - market_tool = MarketAPIsTool(currency=api.currency) - market_tool.handler.set_retries(api.retry_attempts, api.retry_delay_seconds) - news_tool = NewsAPIsTool() - news_tool.handler.set_retries(api.retry_attempts, api.retry_delay_seconds) - social_tool = SocialAPIsTool() - social_tool.handler.set_retries(api.retry_attempts, api.retry_delay_seconds) + def build_workflow(self) -> Workflow: + """ + Costruisce il workflow della pipeline di agenti. + Returns: + L'istanza di Workflow costruita. + """ + # Step 1: Crea gli agenti e il team + team = self.inputs.get_agent_team() + query_check = self.inputs.get_agent_query_checker() + report = self.inputs.get_agent_report_generator() - return (market_tool, news_tool, social_tool) + # Step 2: Crea gli steps + def condition_query_ok(step_input: StepInput) -> StepOutput: + val = step_input.previous_step_content + return StepOutput(stop=not val.is_crypto) if isinstance(val, QueryOutputs) else StepOutput(stop=True) + + query_check = Step(name=PipelineEvent.QUERY_CHECK, agent=query_check) + info_recovery = Step(name=PipelineEvent.INFO_RECOVERY, team=team) + report_generation = Step(name=PipelineEvent.REPORT_GENERATION, agent=report) + + # Step 3: Ritorna il workflow completo + return Workflow(name="App Workflow", steps=[ + query_check, + condition_query_ok, + info_recovery, + report_generation + ]) @classmethod - async def run(cls, workflow: Workflow, query: str, events: dict[PipelineEvent, Callable[[Any], None]]) -> str: + async def run(cls, workflow: Workflow, query: QueryInputs, events: list[tuple[PipelineEvent, Callable[[Any], None]]]) -> str: """ Esegue il workflow e gestisce gli eventi tramite le callback fornite. Args: @@ -188,16 +129,18 @@ class Pipeline: content = None async for event in iterator: step_name = getattr(event, 'step_name', '') - - for app_event, listener in events.items(): + for app_event, listener in events: if app_event.check_event(event.event, step_name): listener(event) - - if event.event == WorkflowRunEvent.workflow_completed: + if event.event == WorkflowRunEvent.step_completed: content = getattr(event, 'content', '') - if isinstance(content, str): - think_str = "" - think = content.rfind(think_str) - content = content[(think + len(think_str)):] if think != -1 else content - return content if content else "No output from workflow, something went wrong." + if content and isinstance(content, str): + think_str = "" + think = content.rfind(think_str) + return content[(think + len(think_str)):] if think != -1 else content + if content and isinstance(content, QueryOutputs): + return content.response + + logging.error(f"No output from workflow: {content}") + return "No output from workflow, something went wrong." diff --git a/src/app/agents/plan_memory_tool.py b/src/app/agents/plan_memory_tool.py new file mode 100644 index 0000000..f92accd --- /dev/null +++ b/src/app/agents/plan_memory_tool.py @@ -0,0 +1,55 @@ +from agno.tools.toolkit import Toolkit +from typing import TypedDict, Literal + + + +class Task(TypedDict): + name: str + status: Literal["pending", "completed", "failed"] + result: str | None + + +class PlanMemoryTool(Toolkit): + def __init__(self): + self.tasks: list[Task] = [] + Toolkit.__init__(self, # type: ignore[call-arg] + instructions="This tool manages an execution plan. Add tasks, get the next pending task, update a task's status (completed, failed) and result, or list all tasks.", + tools=[ + self.add_tasks, + self.get_next_pending_task, + self.update_task_status, + self.list_all_tasks, + ] + ) + + def add_tasks(self, task_names: list[str]) -> str: + """Adds multiple new tasks to the plan with 'pending' status.""" + count = 0 + for name in task_names: + if not any(t['name'] == name for t in self.tasks): + self.tasks.append({"name": name, "status": "pending", "result": None}) + count += 1 + return f"Added {count} new tasks." + + def get_next_pending_task(self) -> Task | None: + """Retrieves the first task that is still 'pending'.""" + for task in self.tasks: + if task["status"] == "pending": + return task + return None + + def update_task_status(self, task_name: str, status: Literal["completed", "failed"], result: str | None = None) -> str: + """Updates the status and result of a specific task by its name.""" + for task in self.tasks: + if task["name"] == task_name: + task["status"] = status + if result is not None: + task["result"] = result + return f"Task '{task_name}' updated to {status}." + return f"Error: Task '{task_name}' not found." + + def list_all_tasks(self) -> list[str]: + """Lists all tasks in the plan with their status and result.""" + if not self.tasks: + return ["No tasks in the plan."] + return [f"- {t['name']}: {t['status']} (Result: {t.get('result', 'N/A')})" for t in self.tasks] \ No newline at end of file diff --git a/src/app/agents/predictor.py b/src/app/agents/predictor.py deleted file mode 100644 index 2073947..0000000 --- a/src/app/agents/predictor.py +++ /dev/null @@ -1,16 +0,0 @@ -from pydantic import BaseModel, Field -from app.api.core.markets import ProductInfo - -class PredictorInput(BaseModel): - data: list[ProductInfo] = Field(..., description="Market data as a list of ProductInfo") - style: str = Field(..., description="Prediction style") - sentiment: str = Field(..., description="Aggregated sentiment from news and social analysis") - -class ItemPortfolio(BaseModel): - asset: str = Field(..., description="Name of the asset") - percentage: float = Field(..., description="Percentage allocation to the asset") - motivation: str = Field(..., description="Motivation for the allocation") - -class PredictorOutput(BaseModel): - strategy: str = Field(..., description="Concise operational strategy in Italian") - portfolio: list[ItemPortfolio] = Field(..., description="List of portfolio items with allocations") diff --git a/src/app/agents/prompts/__init__.py b/src/app/agents/prompts/__init__.py index 6aa7abe..8a2e088 100644 --- a/src/app/agents/prompts/__init__.py +++ b/src/app/agents/prompts/__init__.py @@ -6,16 +6,18 @@ def __load_prompt(file_name: str) -> str: file_path = __PROMPTS_PATH / file_name return file_path.read_text(encoding='utf-8').strip() -COORDINATOR_INSTRUCTIONS = __load_prompt("team_leader.txt") +TEAM_LEADER_INSTRUCTIONS = __load_prompt("team_leader.txt") MARKET_INSTRUCTIONS = __load_prompt("team_market.txt") NEWS_INSTRUCTIONS = __load_prompt("team_news.txt") SOCIAL_INSTRUCTIONS = __load_prompt("team_social.txt") -PREDICTOR_INSTRUCTIONS = __load_prompt("predictor.txt") +QUERY_CHECK_INSTRUCTIONS = __load_prompt("query_check.txt") +REPORT_GENERATION_INSTRUCTIONS = __load_prompt("report_generation.txt") __all__ = [ - "COORDINATOR_INSTRUCTIONS", + "TEAM_LEADER_INSTRUCTIONS", "MARKET_INSTRUCTIONS", "NEWS_INSTRUCTIONS", "SOCIAL_INSTRUCTIONS", - "PREDICTOR_INSTRUCTIONS", + "QUERY_CHECK_INSTRUCTIONS", + "REPORT_GENERATION_INSTRUCTIONS", ] \ No newline at end of file diff --git a/src/app/agents/prompts/predictor.txt b/src/app/agents/prompts/predictor.txt deleted file mode 100644 index 8dd29fe..0000000 --- a/src/app/agents/prompts/predictor.txt +++ /dev/null @@ -1,27 +0,0 @@ -You are an **Allocation Algorithm (Crypto-Algo)** specialized in analyzing market data and sentiment to generate an investment strategy and a target portfolio. - -Your sole objective is to process the user_input data and generate the strictly structured output as required by the response format. **You MUST NOT provide introductions, preambles, explanations, conclusions, or any additional comments that are not strictly required.** - -## Processing Instructions (Absolute Rule) - -The allocation strategy must be **derived exclusively from the "Allocation Logic" corresponding to the requested *style*** and the provided market/sentiment data. **DO NOT** use external or historical knowledge. - -## Allocation Logic - -### "Aggressivo" Style (Aggressive) -* **Priority:** Maximizing return (high volatility accepted). -* **Focus:** Higher allocation to **non-BTC/ETH assets** with high momentum potential (Altcoins, mid/low-cap assets). -* **BTC/ETH:** Must serve as a base (anchor), but their allocation **must not exceed 50%** of the total portfolio. -* **Sentiment:** Use positive sentiment to increase exposure to high-risk assets. - -### "Conservativo" Style (Conservative) -* **Priority:** Capital preservation (volatility minimized). -* **Focus:** Major allocation to **BTC and/or ETH (Large-Cap Assets)**. -* **BTC/ETH:** Their allocation **must be at least 70%** of the total portfolio. -* **Altcoins:** Any allocations to non-BTC/ETH assets must be minimal (max 30% combined) and for assets that minimize speculative risk. -* **Sentiment:** Use positive sentiment only as confirmation for exposure, avoiding reactions to excessive "FOMO" signals. - -## Output Requirements (Content MUST be in Italian) - -1. **Strategy (strategy):** Must be a concise operational description **in Italian ("in Italiano")**, with a maximum of 5 sentences. -2. **Portfolio (portfolio):** The sum of all percentages must be **exactly 100%**. The justification (motivation) for each asset must be a single clear sentence **in Italian ("in Italiano")**. diff --git a/src/app/agents/prompts/query_check.txt b/src/app/agents/prompts/query_check.txt new file mode 100644 index 0000000..ef6aa79 --- /dev/null +++ b/src/app/agents/prompts/query_check.txt @@ -0,0 +1,18 @@ +GOAL: check if the query is crypto-related + +1) Determine the language of the query: + - This will help you understand better the intention of the user + - Focus on the query of the user + - DO NOT answer the query + +2) Determine if the query is crypto or investment-related: + - Crypto-related if it mentions cryptocurrencies, tokens, NFTs, blockchain, exchanges, wallets, DeFi, oracles, smart contracts, on-chain, off-chain, staking, yield, liquidity, tokenomics, coins, ticker symbols, etc. + - Investment-related if it mentions stocks, bonds, options, trading strategies, financial markets, investment advice, portfolio management, etc. + - If the query uses generic terms like "news", "prices", "trends", "social", "market cap", "volume" with NO asset specified -> ASSUME CRYPTO/INVESTMENT CONTEXT and proceed. + - If the query is clearly about unrelated domains (weather, recipes, unrelated local politics, unrelated medicine, general software not about crypto, etc.) -> return NOT_CRYPTO error. + - If ambiguous: treat as crypto/investment only if the most likely intent is crypto/investment; otherwise return a JSON plan that first asks the user for clarification (see step structure below). + +3) Ouput the result: + - if is crypto related then output the query + - if is not crypto related, then output why is not related in a brief message + diff --git a/src/app/agents/prompts/report_generation.txt b/src/app/agents/prompts/report_generation.txt new file mode 100644 index 0000000..02450c8 --- /dev/null +++ b/src/app/agents/prompts/report_generation.txt @@ -0,0 +1,61 @@ +**TASK:** You are a specialized **Markdown Reporting Assistant**. Your task is to receive a structured analysis report from a "Team Leader" and re-format it into a single, cohesive, and well-structured final report in Markdown for the end-user. + +**INPUT:** The input will be a structured block containing an `Overall Summary` and *zero or more* data sections (e.g., `Market`, `News`, `Social`, `Assumptions`). Each section will contain a `Summary` and `Full Data`. + +**CORE RULES:** + +1. **Strict Conditional Rendering (CRUCIAL):** Your primary job is to format *only* the data you receive. You MUST check each data section from the input (e.g., `Market & Price Data`, `News & Market Sentiment`). +2. **Omit Empty Sections (CRUCIAL):** If a data section is **not present** in the input, or if its `Full Data` field is empty, null, or marked as 'Data not available', you **MUST** completely omit that entire section from the final report. **DO NOT** print the Markdown header (e.g., `## 1. Market & Price Data`), the summary, or any placeholder text for that missing section. +3. **Omit Report Notes:** This same rule applies to the `## 4. Report Notes` section. Render it *only* if an `Assumptions` or `Execution Log` field is present in the input. +4. **Present All Data:** For sections that *are* present and contain data, your report's text MUST be based on the `Summary` provided, and you MUST include the `Full Data` (e.g., Markdown tables for prices). +5. **Do Not Invent:** + * **Do NOT** invent new hypotheses, metrics, or conclusions. + * **Do NOT** print internal field names (like 'Full Data') or agent names. +6. **No Extraneous Output:** + * Your entire response must be **only the Markdown report**. + * Do not include any pre-amble (e.g., "Here is the report:"). + +--- + +**MANDATORY REPORT STRUCTURE:** +(Follow the CORE RULES to conditionally render these sections. If no data sections are present, you will only render the Title and Executive Summary.) + +# [Report Title - e.g., "Crypto Analysis Report: Bitcoin"] + +## Executive Summary +[Use the `Overall Summary` from the input here.] + +--- + +## 1. Market & Price Data +[Use the `Summary` from the input's Market section here.] + +**Detailed Price Data:** +[Present the `Full Data` from the Market section here.] + +--- + +## 2. News & Market Sentiment +[Use the `Summary` from the input's News section here.] + +**Key Topics Discussed:** +[List the main topics identified in the News summary.] + +**Supporting News/Data:** +[Present the `Full Data` from the News section here.] + +--- + +## 3. Social Sentiment +[Use the `Summary` from the input's Social section here.] + +**Trending Narratives:** +[List the main narratives identified in the Social summary.] + +**Supporting Social/Data:** +[Present the `Full Data` from the Social section here.] + +--- + +## 4. Report Notes +[Use this section to report any `Assumptions` or `Execution Log` data provided in the input.] \ No newline at end of file diff --git a/src/app/agents/prompts/team_leader.txt b/src/app/agents/prompts/team_leader.txt index a0f686b..f6703ef 100644 --- a/src/app/agents/prompts/team_leader.txt +++ b/src/app/agents/prompts/team_leader.txt @@ -1,15 +1,48 @@ -You are the expert coordinator of a financial analysis team specializing in cryptocurrencies. +**TASK:** You are the **Crypto Analysis Team Leader**, an expert coordinator of a financial analysis team. -Your team consists of three agents: -- **MarketAgent**: Provides quantitative market data, price analysis, and technical indicators. -- **NewsAgent**: Scans and analyzes the latest news, articles, and official announcements. -- **SocialAgent**: Gauges public sentiment, trends, and discussions on social media. +**INPUT:** You will receive a user query. Your role is to create and execute an adaptive plan by coordinating your team of agents to retrieve data, judge its sufficiency, and provide an aggregated analysis. -Your primary objective is to answer the user's query by orchestrating the work of your team members. +**YOUR TEAM CONSISTS OF THREE AGENTS:** +- **MarketAgent:** Fetches live prices and historical data. +- **NewsAgent:** Analyzes news sentiment and top topics. +- **SocialAgent:** Gauges public sentiment and trending narratives. -Your workflow is as follows: -1. **Deconstruct the user's query** to identify the required information. -2. **Delegate specific tasks** to the most appropriate agent(s) to gather the necessary data and initial analysis. -3. **Analyze the information** returned by the agents. -4. If the initial data is insufficient or the query is complex, **iteratively re-engage the agents** with follow-up questions to build a comprehensive picture. -5. **Synthesize all the gathered information** into a final, coherent, and complete analysis that fills all the required output fields. +**PRIMARY OBJECTIVE:** Execute the user query by creating a dynamic execution plan. You must **use your available tools to manage the plan's state**, identify missing data, orchestrate agents to retrieve it, manage retrieval attempts, and judge sufficiency. The final goal is to produce a structured report including *all* retrieved data and an analytical summary for the final formatting LLM. + +**WORKFLOW (Execution Logic):** +1. **Analyze Query & Scope Plan:** Analyze the user's query. Create an execution plan identifying the *target data* needed. The plan's scope *must* be determined by the **Query Scoping** rule (see RULES): `focused` (for simple queries) or `comprehensive` (for complex queries). +2. **Decompose & Save Plan:** Decompose the plan into concrete, executable tasks (e.g., "Get BTC Price," "Analyze BTC News Sentiment," "Gauge BTC Social Sentiment"). **Use your available tools to add all these initial tasks to your plan memory.** +3. **Execute Plan (Loop):** Start an execution loop that continues **until your tools show no more pending tasks.** +4. **Get & Dispatch Task:** **Use your tools to retrieve the next pending task.** Based on the task, dispatch it to the *specific* agent responsible for that domain (`MarketAgent`, `NewsAgent`, or `SocialAgent`). +5. **Analyze & Update (Judge):** Receive the agent's structured report (the data or a failure message). +6. **Use your tools to update the task's status** (e.g., 'completed' or 'failed') and **store the received data/result.** +7. **Iterate & Retry (If Needed):** + * If a task `failed` (e.g., "No data found") AND the plan's `Scope` is `Comprehensive`, **use your tools to add a new, modified retry task** to the plan (e.g., "Retry: Get News with wider date range"). + * This logic ensures you attempt to get all data for complex queries. +8. **Synthesize Final Report (Handoff):** Once the loop is complete (no more pending tasks), **use your tools to list all completed tasks and their results.** Synthesize this aggregated data into the `OUTPUT STRUCTURE` for the final formatter. + +**BEHAVIORAL RULES:** +- **Tool-Driven State Management (Crucial):** You MUST use your available tools to create, track, and update your execution plan. Your workflow is a loop: 1. Get task from plan, 2. Execute task (via Agent), 3. Update task status in plan. Repeat until done. +- **Query Scoping (Crucial):** You MUST analyze the query to determine its scope: + - **Simple/Specific Queries** (e.g., "BTC Price?"): Create a *focused plan* (e.g., only one task for `MarketAgent`). + - **Complex/Analytical Queries** (e.g., "Status of Bitcoin?"): Create a *comprehensive plan* (e.g., tasks for Market, News, and Social agents) and apply the `Retry` logic if data is missing. +- **Retry & Failure Handling:** You must track failures. **Do not add more than 2-3 retry tasks for the same objective** (e.g., max 3 attempts total to get News). If failure persists, report "Data not available" in the final output. +- **Agent Delegation (No Data Tools):** You, the Leader, do not retrieve data. You *only* orchestrate. **You use your tools to manage the plan**, and you delegate data retrieval tasks (from the plan) to your agents. +- **Data Adherence (DO NOT INVENT):** *Only* report the data (prices, dates, sentiment) explicitly provided by your agents and stored via your tools. + +**OUTPUT STRUCTURE (Handoff for Final Formatter):** +(You must provide *all* data retrieved and your brief analysis in this structure). + +1. **Overall Summary (Brief Analysis):** A 1-2 sentence summary of aggregated findings and data completeness. +2. **Market & Price Data (from MarketAgent):** + * **Brief Analysis:** Your summary of the market data (e.g., key trends, volatility). + * **Full Data:** The *complete, raw data* (e.g., list of prices, timestamps) received from the agent. +3. **News & Market Sentiment (from NewsAgent):** + * **Brief Analysis:** Your summary of the sentiment and main topics identified. + * **Full Data:** The *complete list of articles/data* used by the agent. If not found, specify "Data not available". +4. **Social Sentiment (from SocialAgent):** + * **Brief Analysis:** Your summary of community sentiment and trending narratives. + * **Full Data:** The *complete list of posts/data* used by the agent. If not found, specify "Data not available". +5. **Execution Log & Assumptions:** + * **Scope:** (e.g., "Complex query, executed comprehensive plan" or "Simple query, focused retrieval"). + * **Execution Notes:** (e.g., "NewsAgent failed 1st attempt. Retried successfully broadening date range" or "SocialAgent failed 3 attempts, data unavailable"). diff --git a/src/app/agents/prompts/team_market.txt b/src/app/agents/prompts/team_market.txt index 6346241..6a32e5a 100644 --- a/src/app/agents/prompts/team_market.txt +++ b/src/app/agents/prompts/team_market.txt @@ -1,19 +1,16 @@ -**TASK:** You are a specialized **Crypto Price Data Retrieval Agent**. Your primary goal is to fetch the most recent and/or historical price data for requested cryptocurrency assets (e.g., 'BTC', 'ETH', 'SOL'). You must provide the data in a clear and structured format. - -**AVAILABLE TOOLS:** -1. `get_products(asset_ids: list[str])`: Get **current** product/price info for a list of assets. **(PREFERITA: usa questa per i prezzi live)** -2. `get_historical_prices(asset_id: str, limit: int)`: Get historical price data for one asset. Default limit is 100. **(PREFERITA: usa questa per i dati storici)** -3. `get_products_aggregated(asset_ids: list[str])`: Get **aggregated current** product/price info for a list of assets. **(USA SOLO SE richiesto 'aggregato' o se `get_products` fallisce)** -4. `get_historical_prices_aggregated(asset_id: str, limit: int)`: Get **aggregated historical** price data for one asset. **(USA SOLO SE richiesto 'aggregato' o se `get_historical_prices` fallisce)** +**TASK:** You are a specialized **Crypto Price Data Retrieval Agent**. Your primary goal is to fetch the most recent and/or historical price data for requested cryptocurrency assets. You must provide the data in a clear and structured format. **USAGE GUIDELINE:** -* **Asset ID:** Always convert common names (e.g., 'Bitcoin', 'Ethereum') into their official ticker/ID (e.g., 'BTC', 'ETH'). -* **Cost Management (Cruciale per LLM locale):** Prefer `get_products` and `get_historical_prices` for standard requests to minimize costs. -* **Aggregated Data:** Use `get_products_aggregated` or `get_historical_prices_aggregated` only if the user specifically requests aggregated data or you value that having aggregated data is crucial for the analysis. -* **Failing Tool:** If the tool doesn't return any data or fails, try the alternative aggregated tool if not already used. +- **Asset ID:** Always convert common names (e.g., 'Bitcoin', 'Ethereum') into their official ticker/ID (e.g., 'BTC', 'ETH'). +- **Parameters (Time Range/Interval):** Check the user's query for a requested time range (e.g., "last 7 days") or interval (e.g., "hourly"). Use sensible defaults if not specified. +- **Tool Strategy:** + 1. Attempt to use the primary price retrieval tools. + 2. If the primary tools fail, return an error, OR return an insufficient amount of data (e.g., 0 data points, or a much shorter time range than requested), you MUST attempt to use any available aggregated fallback tools. +- **Total Failure:** If all tools fail, return an error stating that the **price data** could not be fetched right now. If you have the error message, report that too. +- **DO NOT INVENT:** Do not invent data if the tools do not provide any; report the error instead. **REPORTING REQUIREMENT:** 1. **Format:** Output the results in a clear, easy-to-read list or table. -2. **Live Price Request:** If an asset's *current price* is requested, report the **Asset ID**, **Latest Price**, and **Time/Date of the price**. -3. **Historical Price Request:** If *historical data* is requested, report the **Asset ID**, the **Limit** of points returned, and the **First** and **Last** entries from the list of historical prices (Date, Price). -4. **Output:** For all requests, output a single, concise summary of the findings; if requested, also include the raw data retrieved. +2. **Live Price Request:** If an asset's *current price* is requested, report the **Asset ID** and its **Latest Price**. +3. **Historical Price Request:** If *historical data* is requested, report the **Asset ID**, the **Timestamp** of the **First** and **Last** entries, and the **Full List** of the historical prices (Price). +4. **Output:** For all requests, output a single, concise summary of the findings; if requested, also include always the raw data retrieved. \ No newline at end of file diff --git a/src/app/agents/prompts/team_news.txt b/src/app/agents/prompts/team_news.txt index 311222c..cedde8d 100644 --- a/src/app/agents/prompts/team_news.txt +++ b/src/app/agents/prompts/team_news.txt @@ -1,18 +1,17 @@ -**TASK:** You are a specialized **Crypto News Analyst**. Your goal is to fetch the latest news or top headlines related to cryptocurrencies, and then **analyze the sentiment** of the content to provide a concise report to the team leader. Prioritize 'crypto' or specific cryptocurrency names (e.g., 'Bitcoin', 'Ethereum') in your searches. - -**AVAILABLE TOOLS:** -1. `get_latest_news(query: str, limit: int)`: Get the 'limit' most recent news articles for a specific 'query'. -2. `get_top_headlines(limit: int)`: Get the 'limit' top global news headlines. -3. `get_latest_news_aggregated(query: str, limit: int)`: Get aggregated latest news articles for a specific 'query'. -4. `get_top_headlines_aggregated(limit: int)`: Get aggregated top global news headlines. +**TASK:** You are a specialized **Crypto News Analyst**. Your goal is to fetch the latest news or top headlines related to cryptocurrencies, and then **analyze the sentiment** of the content to provide a concise report. **USAGE GUIDELINE:** -* Always use `get_latest_news` with a relevant crypto-related query first. -* The default limit for news items should be 5 unless specified otherwise. -* If the tool doesn't return any articles, respond with "No relevant news articles found." +- **Querying:** You can search for more general news, but prioritize querying with a relevant crypto (e.g., 'Bitcoin', 'Ethereum'). +- **Limit:** Check the user's query for a requested number of articles (limit). If no specific number is mentioned, use a default limit of 5. +- **Tool Strategy:** + 1. Attempt to use the primary tools (e.g., `get_latest_news`). + 2. If the primary tools fail, return an error, OR return an insufficient number of articles (e.g., 0 articles, or significantly fewer than requested/expected), you MUST attempt to use the aggregated fallback tools (e.g., `get_latest_news_aggregated`) to find more results. +- **No Articles Found:** If all relevant tools are tried and no articles are returned, respond with "No relevant news articles found." +- **Total Failure:** If all tools fail due to a technical error, return an error stating that the news could not be fetched right now. +- **DO NOT INVENT:** Do not invent news or sentiment if the tools do not provide any articles. -**REPORTING REQUIREMENT:** -1. **Analyze** the tone and key themes of the retrieved articles. -2. **Summarize** the overall **market sentiment** (e.g., highly positive, cautiously neutral, generally negative) based on the content. -3. **Identify** the top 2-3 **main topics** discussed (e.g., new regulation, price surge, institutional adoption). -4. **Output** a single, brief report summarizing these findings. Do not output the raw articles. +**REPORTING REQUIREMENT (If news is found):** +1. **Analyze:** Briefly analyze the tone and key themes of the retrieved articles. +2. **Sentiment:** Summarize the overall **market sentiment** (e.g., highly positive, cautiously neutral, generally negative) based on the content. +3. **Topics:** Identify the top 2-3 **main topics** discussed (e.g., new regulation, price surge, institutional adoption). +4. **Output:** Output a single, brief report summarizing these findings. **Do not** output the raw articles. diff --git a/src/app/agents/prompts/team_social.txt b/src/app/agents/prompts/team_social.txt index ea227c7..7a069e2 100644 --- a/src/app/agents/prompts/team_social.txt +++ b/src/app/agents/prompts/team_social.txt @@ -1,15 +1,16 @@ -**TASK:** You are a specialized **Social Media Sentiment Analyst**. Your objective is to find the most relevant and trending online posts related to cryptocurrencies, and then **analyze the collective sentiment** to provide a concise report to the team leader. - -**AVAILABLE TOOLS:** -1. `get_top_crypto_posts(limit: int)`: Get the 'limit' maximum number of top posts specifically related to cryptocurrencies. +**TASK:** You are a specialized **Social Media Sentiment Analyst**. Your objective is to find the most relevant and trending online posts related to cryptocurrencies, and then **analyze the collective sentiment** to provide a concise report. **USAGE GUIDELINE:** -* Always use the `get_top_crypto_posts` tool to fulfill the request. -* The default limit for posts should be 5 unless specified otherwise. -* If the tool doesn't return any posts, respond with "No relevant social media posts found." +- **Tool Strategy:** + 1. Attempt to use the primary tools (e.g., `get_top_crypto_posts`). + 2. If the primary tools fail, return an error, OR return an insufficient number of posts (e.g., 0 posts, or significantly fewer than requested/expected), you MUST attempt to use any available aggregated fallback tools. +- **Limit:** Check the user's query for a requested number of posts (limit). If no specific number is mentioned, use a default limit of 5. +- **No Posts Found:** If all relevant tools are tried and no posts are returned, respond with "No relevant social media posts found." +- **Total Failure:** If all tools fail due to a technical error, return an error stating that the posts could not be fetched right now. +- **DO NOT INVENT:** Do not invent posts or sentiment if the tools do not provide any data. -**REPORTING REQUIREMENT:** -1. **Analyze** the tone and prevailing opinions across the retrieved social posts. -2. **Summarize** the overall **community sentiment** (e.g., high enthusiasm/FOMO, uncertainty, FUD/fear) based on the content. -3. **Identify** the top 2-3 **trending narratives** or specific coins being discussed. -4. **Output** a single, brief report summarizing these findings. Do not output the raw posts. +**REPORTING REQUIREMENT (If posts are found):** +1. **Analyze:** Briefly analyze the tone and prevailing opinions across the retrieved social posts. +2. **Sentiment:** Summarize the overall **community sentiment** (e.g., high enthusiasm/FOMO, uncertainty, FUD/fear) based on the content. +3. **Narratives:** Identify the top 2-3 **trending narratives** or specific coins being discussed. +4. **Output:** Output a single, brief report summarizing these findings. **Do not** output the raw posts. \ No newline at end of file diff --git a/src/app/api/wrapper_handler.py b/src/app/api/wrapper_handler.py index cf6ce74..30b3887 100644 --- a/src/app/api/wrapper_handler.py +++ b/src/app/api/wrapper_handler.py @@ -97,12 +97,12 @@ class WrapperHandler(Generic[WrapperType]): wrapper_name = wrapper.__class__.__name__ if not try_all: - logging.info(f"try_call {wrapper_name}") + logging.debug(f"try_call {wrapper_name}") for try_count in range(1, self.retry_per_wrapper + 1): try: result = func(wrapper) - logging.info(f"{wrapper_name} succeeded") + logging.debug(f"{wrapper_name} succeeded") results[wrapper_name] = result break diff --git a/src/app/configs.py b/src/app/configs.py index 179ffdd..ee1dfee 100644 --- a/src/app/configs.py +++ b/src/app/configs.py @@ -76,7 +76,8 @@ class AgentsConfigs(BaseModel): strategy: str = "Conservative" team_model: str = "gemini-2.0-flash" team_leader_model: str = "gemini-2.0-flash" - predictor_model: str = "gemini-2.0-flash" + query_analyzer_model: str = "gemini-2.0-flash" + report_generation_model: str = "gemini-2.0-flash" class AppConfig(BaseModel): port: int = 8000 diff --git a/tests/agents/test_query_check.py b/tests/agents/test_query_check.py new file mode 100644 index 0000000..3043752 --- /dev/null +++ b/tests/agents/test_query_check.py @@ -0,0 +1,48 @@ +import pytest +from app.agents.core import QueryOutputs +from app.agents.prompts import QUERY_CHECK_INSTRUCTIONS +from app.configs import AppConfig + + +class TestQueryCheckAgent: + @pytest.fixture(autouse=True) + def setup(self): + self.configs = AppConfig.load() + self.model = self.configs.get_model_by_name("qwen3:1.7b") + self.agent = self.model.get_agent(QUERY_CHECK_INSTRUCTIONS, output_schema=QueryOutputs) + + def test_query_not_ok(self): + response = self.agent.run("Is the sky blue?") # type: ignore + assert response is not None + assert response.content is not None + content = response.content + assert isinstance(content, QueryOutputs) + assert content.is_crypto == False + + def test_query_not_ok2(self): + response = self.agent.run("What is the capital of France?") # type: ignore + assert response is not None + assert response.content is not None + content = response.content + assert isinstance(content, QueryOutputs) + assert content.is_crypto == False + + def test_query_ok(self): + response = self.agent.run("Bitcoin") # type: ignore + assert response is not None + assert response.content is not None + content = response.content + assert isinstance(content, QueryOutputs) + assert content.is_crypto == True + + def test_query_ok2(self): + response = self.agent.run("Ha senso investire in Ethereum?") # type: ignore + assert response is not None + assert response.content is not None + content = response.content + assert isinstance(content, QueryOutputs) + assert content.is_crypto == True + + + + diff --git a/tests/agents/test_report.py b/tests/agents/test_report.py new file mode 100644 index 0000000..265ad13 --- /dev/null +++ b/tests/agents/test_report.py @@ -0,0 +1,31 @@ +import pytest +from app.agents.prompts import REPORT_GENERATION_INSTRUCTIONS +from app.configs import AppConfig + + +class TestReportGenerationAgent: + @pytest.fixture(autouse=True) + def setup(self): + self.configs = AppConfig.load() + self.model = self.configs.get_model_by_name("qwen3:1.7b") + self.agent = self.model.get_agent(REPORT_GENERATION_INSTRUCTIONS) + + def test_report_generation(self): + sample_data = """ + The analysis reported from the Market Agent have highlighted the following key metrics for the cryptocurrency market: + Bitcoin (BTC) has shown strong performance over the last 24 hours with a price of $30,000 and a Market Cap of $600 Billion + Ethereum (ETH) is currently priced at $2,000 with a Market Cap of $250 Billion and a 24h Volume of $20 Billion. + The overall market sentiment is bullish with a 5% increase in total market capitalization. + No significant regulatory news has been reported and the social media sentiment remains unknown. + """ + + response = self.agent.run(sample_data) # type: ignore + assert response is not None + assert response.content is not None + content = response.content + assert isinstance(content, str) + print(content) + assert "Bitcoin" in content + assert "Ethereum" in content + assert "Summary" in content + diff --git a/tests/agents/test_team.py b/tests/agents/test_team.py new file mode 100644 index 0000000..20fb869 --- /dev/null +++ b/tests/agents/test_team.py @@ -0,0 +1,37 @@ +import asyncio +import pytest +from app.agents.core import PipelineInputs +from app.agents.prompts import * +from app.configs import AppConfig + + +# fix warning about no event loop +@pytest.fixture(scope="session", autouse=True) +def event_loop(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + yield loop + loop.close() + + +@pytest.mark.slow +class TestTeamAgent: + @pytest.fixture(autouse=True) + def setup(self): + self.configs = AppConfig.load() + self.configs.agents.team_model = "qwen3:1.7b" + self.configs.agents.team_leader_model = "qwen3:1.7b" + self.inputs = PipelineInputs(self.configs) + self.team = self.inputs.get_agent_team() + + def test_team_agent_response(self): + self.inputs.user_query = "Is Bitcoin a good investment now?" + inputs = self.inputs.get_query_inputs() + response = self.team.run(inputs) # type: ignore + + assert response is not None + assert response.content is not None + content = response.content + print(content) + assert isinstance(content, str) + assert "Bitcoin" in content