diff --git a/python/valuecell/agents/research_agent/core.py b/python/valuecell/agents/research_agent/core.py index a036b4421..205917e39 100644 --- a/python/valuecell/agents/research_agent/core.py +++ b/python/valuecell/agents/research_agent/core.py @@ -3,8 +3,6 @@ from agno.agent import Agent from agno.db.in_memory import InMemoryDb -from agno.models.google import Gemini -from agno.models.openrouter import OpenRouter from edgar import set_identity from loguru import logger @@ -16,28 +14,28 @@ from valuecell.agents.research_agent.sources import ( fetch_event_sec_filings, fetch_periodic_sec_filings, + web_search, ) from valuecell.agents.utils.context import build_ctx_from_dep from valuecell.core.agent.responses import streaming from valuecell.core.types import BaseAgent, StreamResponse from valuecell.utils.env import agent_debug_mode_enabled - - -def _get_model_based_on_env(): - model_id = os.getenv("RESEARCH_AGENT_MODEL_ID") - if os.getenv("GOOGLE_API_KEY"): - return Gemini(id=model_id or "gemini-2.5-flash") - return OpenRouter(id=model_id or "google/gemini-2.5-flash", max_tokens=None) +from valuecell.utils.model import get_model class ResearchAgent(BaseAgent): def __init__(self, **kwargs): super().__init__(**kwargs) + tools = [ + fetch_periodic_sec_filings, + fetch_event_sec_filings, + web_search, + ] self.knowledge_research_agent = Agent( - model=_get_model_based_on_env(), + model=get_model("RESEARCH_AGENT_MODEL_ID"), instructions=[KNOWLEDGE_AGENT_INSTRUCTION], expected_output=KNOWLEDGE_AGENT_EXPECTED_OUTPUT, - tools=[fetch_periodic_sec_filings, fetch_event_sec_filings], + tools=tools, knowledge=knowledge, db=InMemoryDb(), # context @@ -81,15 +79,3 @@ async def stream( logger.info("Financial data analysis completed") yield streaming.done() - - -if __name__ == "__main__": - import asyncio - - async def main(): - agent = ResearchAgent() - query = "Provide a summary of Apple's 2024 all quarterly and annual reports." - async for response in agent.stream(query, "test_session", "test_task"): - print(response) - - asyncio.run(main()) diff --git a/python/valuecell/agents/research_agent/sources.py b/python/valuecell/agents/research_agent/sources.py index a0b0dbf5b..50f4869f6 100644 --- a/python/valuecell/agents/research_agent/sources.py +++ b/python/valuecell/agents/research_agent/sources.py @@ -1,8 +1,12 @@ +import os from datetime import date, datetime from pathlib import Path from typing import Iterable, List, Optional, Sequence import aiofiles +from agno.agent import Agent +from agno.models.google import Gemini +from agno.models.openrouter import OpenRouter from edgar import Company from edgar.entity.filings import EntityFilings @@ -187,3 +191,37 @@ async def fetch_event_sec_filings( filtered = filtered[:limit] return await _write_and_ingest(filtered, Path(get_knowledge_path())) + + +async def web_search(query: str) -> str: + """Search web for the given query and return a summary of the top results. + + Args: + query: The search query string. + + Returns: + A summary of the top search results. + """ + + if os.getenv("WEB_SEARCH_PROVIDER", "google").lower() == "google" and os.getenv( + "GOOGLE_API_KEY" + ): + return await _web_search_google(query) + + model = OpenRouter(id="perplexity/sonar", max_tokens=None) + response = await Agent(model=model).arun(query) + return response.content + + +async def _web_search_google(query: str) -> str: + """Search Google for the given query and return a summary of the top results. + + Args: + query: The search query string. + + Returns: + A summary of the top search results. + """ + model = Gemini(id="gemini-2.5-flash", search=True) + response = await Agent(model=model).arun(query) + return response.content diff --git a/python/valuecell/core/coordinate/planner.py b/python/valuecell/core/coordinate/planner.py index eca84a19b..119af5261 100644 --- a/python/valuecell/core/coordinate/planner.py +++ b/python/valuecell/core/coordinate/planner.py @@ -12,14 +12,12 @@ import asyncio import logging -import os from datetime import datetime from typing import Callable, List, Optional from a2a.types import AgentCard from agno.agent import Agent from agno.db.in_memory import InMemoryDb -from agno.models.openrouter import OpenRouter from valuecell.core.agent.connect import RemoteConnections from valuecell.core.coordinate.planner_prompts import ( @@ -30,6 +28,7 @@ from valuecell.core.types import UserInput from valuecell.utils import generate_uuid from valuecell.utils.env import agent_debug_mode_enabled +from valuecell.utils.model import get_model from .models import ExecutionPlan, PlannerInput, PlannerResponse @@ -152,10 +151,7 @@ async def _analyze_input_and_create_tasks( """ # Create planning agent with appropriate tools and instructions agent = Agent( - model=OpenRouter( - id=os.getenv("PLANNER_MODEL_ID", "google/gemini-2.5-flash"), - max_tokens=None, - ), + model=get_model("PLANNER_MODEL_ID"), tools=[ # TODO: enable UserControlFlowTools when stable # UserControlFlowTools(), diff --git a/python/valuecell/core/coordinate/planner_prompts.py b/python/valuecell/core/coordinate/planner_prompts.py index ae5bd8094..4d7fbb8c2 100644 --- a/python/valuecell/core/coordinate/planner_prompts.py +++ b/python/valuecell/core/coordinate/planner_prompts.py @@ -9,88 +9,44 @@ # noqa: E501 PLANNER_INSTRUCTION = """ -You are an AI Agent execution planner that analyzes user requests and creates executable task plans using available agents. +You are an AI Agent execution planner that forwards user requests to the specified target agent as simple, executable tasks. - + +1) Default pass-through +- Assume `target_agent_name` is always provided. +- Create exactly one task with the user's query unchanged. +- Set `pattern` to `once` by default. -**Step 1: Identify Query Type** +2) Avoid optimization +- Do NOT rewrite, optimize, summarize, or split the query. +- Only block when the request is clearly unusable (e.g., illegal content or impossible instruction). In that case, return `adequate: false` with a short reason and no tasks. - -If the query is a short or contextual reply (e.g., "Go on", "yes", "tell me more", "this one", "that's good"): -- Forward it directly without rewriting or splitting -- These are continuations of an ongoing conversation and should be preserved as-is -- Create a single task with the query unchanged - +3) Contextual and preference statements +- Treat short/contextual replies (e.g., "Go on", "yes", "tell me more") and user preferences/rules (e.g., "do not provide investment advice") as valid inputs; forward them unchanged as a single task. - -If the query is vague or ambiguous without conversation context: -- Return `adequate: false` -- Provide specific clarification questions in the `reason` field - +4) Recurring intent confirmation +- If the query suggests recurring monitoring or periodic updates, DO NOT create tasks yet. Return `adequate: false` and ask for confirmation in `reason` (e.g., "Do you want regular updates on this, or a one-time analysis?"). +- After explicit confirmation, create a single task with `pattern: recurring` and keep the original query unchanged. - -If the query suggests recurring monitoring or periodic updates: -- Return `adequate: false` -- Ask for confirmation in the `reason` field: "Do you want regular updates on this, or a one-time analysis?" -- Only create recurring tasks after explicit user confirmation - - -**Step 2: Create Task Plan** - -For clear, actionable queries: -- Create specific tasks with optimized queries -- Use `**bold**` to highlight key details (stock symbols, dates, names) -- Set appropriate pattern (once/recurring) -- Provide brief reasoning - - - - -Trust the target agent's capabilities: -- Do not over-validate or rewrite queries unless fundamentally broken (illegal, nonsensical, or completely out of scope) -- Do not split queries into multiple tasks unless complexity genuinely requires it -- For contextual/short replies, forward directly without rewriting -- For reasonable domain-specific requests, pass through unchanged or lightly optimized - +5) Agent targeting policy +- Trust the specified agent's capabilities; do not over-validate or split into multiple tasks. + """ PLANNER_EXPECTED_OUTPUT = """ - -**For contextual/short replies:** -- Forward as-is: "Go on", "yes", "no", "this", "that", "tell me more" -- Preserve conversation continuity without rewriting - -**For actionable queries:** -- Transform vague requests into clear, specific tasks -- Use formatting (`**bold**`) to highlight critical details (stock symbols, dates, names) -- Be precise and avoid ambiguous language -- For complex queries, break down into specific tasks with clear objectives (but avoid over-splitting) -- Ensure each task is self-contained and actionable by the target agent + +- Default to pass-through: create a single task addressed to the provided `target_agent_name` with the user's query unchanged. +- Set `pattern` to `once` unless the user explicitly confirms recurring intent. +- Avoid query optimization and task splitting. + -**When to avoid optimization:** -- Query is already clear and specific -- Query contains contextual references that need conversation history -- Over-optimization would lose user intent or context - - - -- **ONCE**: Single execution with immediate results (default) -- **RECURRING**: Periodic execution for ongoing monitoring/updates - - Use only when user explicitly requests regular updates - - Always confirm intent before creating recurring tasks: "Do you want regular updates on this?" - - - -- If user specifies a target agent name, do not split user query into multiple tasks; create a single task for the specified agent. -- Avoid splitting tasks into excessively fine-grained steps. Tasks should be actionable by the target agent without requiring manual orchestration of many micro-steps. -- Aim for a small set of clear tasks (typical target: 1–5 tasks) for straightforward requests. For complex research, group related micro-steps under a single task with an internal subtask description. -- Do NOT create separate tasks for trivial UI interactions or internal implementation details (e.g., "open page", "click button"). Instead, express the goal the agent should achieve (e.g., "Retrieve Q4 2024 revenue from the 10-Q and cite the filing"). -- When a user requests very deep or multi-stage research, it's acceptable to create a short sequence (e.g., 3–8 tasks) but prefer grouping and clear handoffs. -- If unsure about granularity, prefer slightly larger tasks and include explicit guidance in the task's query about intermediate checks or tolerances. - + +- If the request is clearly unusable (illegal content or impossible instruction), return `adequate: false` with a short reason and no tasks. +- If the request suggests recurring monitoring, return `adequate: false` with a confirmation question; after explicit confirmation, create a single `recurring` task with the original query unchanged. + @@ -101,7 +57,7 @@ { "tasks": [ { - "query": "Clear, specific task description with **key details** highlighted", + "query": "User's original query, unchanged", "agent_name": "target_agent_name", "pattern": "once" | "recurring" } @@ -115,7 +71,7 @@ - + Input: { "target_agent_name": "research_agent", @@ -132,11 +88,11 @@ } ], "adequate": true, - "reason": "Clear, specific query; forwarding as-is." + "reason": "Pass-through to the specified agent." } - + - + Input: { "target_agent_name": "research_agent", @@ -153,54 +109,12 @@ } ], "adequate": true, - "reason": "Contextual continuation; forwarding directly to current agent." -} - - - -Input: -{ - "target_agent_name": "research_agent", - "query": "Tell me more about that risk" -} - -Output: -{ - "tasks": [ - { - "query": "Tell me more about that risk", - "agent_name": "research_agent", - "pattern": "once" - } - ], - "adequate": true, - "reason": "Contextual query with reference pronoun; preserving as-is for conversation continuity." -} - - - -Input: -{ - "target_agent_name": "research_agent", - "query": "yes" -} - -Output: -{ - "tasks": [ - { - "query": "yes", - "agent_name": "research_agent", - "pattern": "once" - } - ], - "adequate": true, - "reason": "User confirmation; forwarding to current agent." + "reason": "Contextual continuation; forwarded unchanged." } - + - -// Step 1: User requests recurring monitoring (needs confirmation) + +// Step 1: needs confirmation Input: { "target_agent_name": "research_agent", @@ -211,10 +125,10 @@ { "tasks": [], "adequate": false, - "reason": "User request suggests recurring monitoring. Need to confirm: 'Do you want me to set up regular updates for Apple's quarterly earnings, or would you prefer a one-time analysis of the latest report?'" + "reason": "This suggests recurring monitoring. Do you want regular updates on this, or a one-time analysis?" } -// Step 2: User confirms recurring intent +// Step 2: user confirms Input: { "target_agent_name": "research_agent", @@ -225,51 +139,15 @@ { "tasks": [ { - "query": "Retrieve and analyze **Apple's** latest quarterly earnings report, highlighting revenue, net income, and key business segment performance", + "query": "Yes, set up regular updates", "agent_name": "research_agent", "pattern": "recurring" } ], "adequate": true, - "reason": "User confirmed recurring monitoring intent. Created recurring task for quarterly earnings tracking." -} - - - -Input: -{ - "target_agent_name": "research_agent", - "query": "Tell me about Apple's recent performance" -} - -Output: -{ - "tasks": [ - { - "query": "Analyze **Apple's** most recent quarterly financial performance, including revenue, profit margins, and key business segment results from the latest 10-Q filing", - "agent_name": "research_agent", - "pattern": "once" - } - ], - "adequate": true, - "reason": "Vague query optimized to specific, actionable task with clear objectives." -} - - - -Input: -{ - "target_agent_name": "research_agent", - "query": "What about the numbers?" -} - -Output: -{ - "tasks": [], - "adequate": false, - "reason": "Query is too vague without conversation context. Need clarification: Which company's numbers? Which metrics (revenue, earnings, margins)? Which time period?" + "reason": "User confirmed recurring intent; created a single recurring task with the original query." } - + """ diff --git a/python/valuecell/utils/model.py b/python/valuecell/utils/model.py new file mode 100644 index 000000000..ff82bc6fa --- /dev/null +++ b/python/valuecell/utils/model.py @@ -0,0 +1,11 @@ +import os + +from agno.models.google import Gemini +from agno.models.openrouter import OpenRouter + + +def get_model(env_key: str): + model_id = os.getenv(env_key) + if os.getenv("GOOGLE_API_KEY"): + return Gemini(id=model_id or "gemini-2.5-flash") + return OpenRouter(id=model_id or "google/gemini-2.5-flash", max_tokens=None)