Spaces:
Build error
Build error
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +8 -4
- common/__init__.py +0 -0
- common/aagents/__init__.py +0 -0
- common/aagents/google_agent.py +139 -0
- common/aagents/healthcare_agent.py +100 -0
- common/aagents/news_agent.py +106 -0
- common/aagents/weather_agent.py +69 -0
- common/aagents/web_agent.py +53 -0
- common/aagents/web_research_agent.py +83 -0
- common/aagents/yf_agent.py +78 -0
- common/mcp/README.md +139 -0
- common/mcp/__init__.py +0 -0
- common/mcp/mcp_server.py +171 -0
- common/mcp/tools/__init__.py +0 -0
- common/mcp/tools/google_tools.py +139 -0
- common/mcp/tools/news_tools.py +200 -0
- common/mcp/tools/rag_tool.py +106 -0
- common/mcp/tools/search_tools.py +115 -0
- common/mcp/tools/time_tools.py +32 -0
- common/mcp/tools/weather_tools.py +235 -0
- common/mcp/tools/yf_tools.py +192 -0
- common/rag/rag.py +94 -0
- common/utility/__init__.py +0 -0
- common/utility/embedding_factory.py +49 -0
- common/utility/llm_factory.py +130 -0
- common/utility/llm_factory2.py +75 -0
- common/utility/logger.py +22 -0
- pyproject.toml +2 -0
- run.py +215 -11
- src/chatbot/Dockerfile +35 -0
- src/chatbot/README.md +219 -0
- src/chatbot/app.py +230 -0
- src/chatbot/appagents/FinancialAgent.py +56 -0
- src/chatbot/appagents/InputValidationAgent.py +76 -0
- src/chatbot/appagents/NewsAgent.py +66 -0
- src/chatbot/appagents/OrchestratorAgent.py +167 -0
- src/chatbot/appagents/SearchAgent.py +57 -0
- src/chatbot/appagents/__init__.py +0 -0
- src/chatbot/core/__init__.py +0 -0
- src/chatbot/core/logger.py +22 -0
- src/chatbot/prompts/economic_news.txt +2 -0
- src/chatbot/prompts/market_sentiment.txt +1 -0
- src/chatbot/prompts/news_headlines.txt +5 -0
- src/chatbot/prompts/trade_recommendation.txt +4 -0
- src/chatbot/prompts/upcoming_earnings.txt +1 -0
- src/chatbot/tools/__init__.py +0 -0
- src/chatbot/tools/google_tools.py +145 -0
- src/chatbot/tools/news_tools.py +129 -0
- src/chatbot/tools/time_tools.py +22 -0
- src/chatbot/tools/yahoo_tools.py +206 -0
Dockerfile
CHANGED
|
@@ -2,7 +2,7 @@ FROM python:3.12-slim
|
|
| 2 |
|
| 3 |
ENV PYTHONUNBUFFERED=1 \
|
| 4 |
DEBIAN_FRONTEND=noninteractive \
|
| 5 |
-
PYTHONPATH=/app:$PYTHONPATH
|
| 6 |
|
| 7 |
WORKDIR /app
|
| 8 |
|
|
@@ -19,13 +19,17 @@ ENV PATH="/root/.local/bin:$PATH"
|
|
| 19 |
COPY pyproject.toml .
|
| 20 |
COPY uv.lock .
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# Install dependencies using uv, then export and install with pip to system
|
| 23 |
RUN uv sync --frozen --no-dev && \
|
| 24 |
uv pip install -e . --system
|
| 25 |
|
| 26 |
-
# Copy
|
| 27 |
-
COPY . .
|
| 28 |
|
| 29 |
EXPOSE 7860
|
| 30 |
|
| 31 |
-
CMD ["
|
|
|
|
| 2 |
|
| 3 |
ENV PYTHONUNBUFFERED=1 \
|
| 4 |
DEBIAN_FRONTEND=noninteractive \
|
| 5 |
+
PYTHONPATH=/app:/app/common:$PYTHONPATH
|
| 6 |
|
| 7 |
WORKDIR /app
|
| 8 |
|
|
|
|
| 19 |
COPY pyproject.toml .
|
| 20 |
COPY uv.lock .
|
| 21 |
|
| 22 |
+
# Copy required folders
|
| 23 |
+
COPY common/ ./common/
|
| 24 |
+
COPY src/chatbot/ ./src/chatbot/
|
| 25 |
+
|
| 26 |
# Install dependencies using uv, then export and install with pip to system
|
| 27 |
RUN uv sync --frozen --no-dev && \
|
| 28 |
uv pip install -e . --system
|
| 29 |
|
| 30 |
+
# Copy entry point
|
| 31 |
+
COPY run.py .
|
| 32 |
|
| 33 |
EXPOSE 7860
|
| 34 |
|
| 35 |
+
CMD ["python", "run.py", "chatbot", "--port", "7860"]
|
common/__init__.py
ADDED
|
File without changes
|
common/aagents/__init__.py
ADDED
|
File without changes
|
common/aagents/google_agent.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Google search agent module for web search and information retrieval."""
|
| 2 |
+
import os
|
| 3 |
+
from agents import Agent, OpenAIChatCompletionsModel
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from mcp.tools.google_tools import google_search, google_search_recent
|
| 6 |
+
from mcp.tools.search_tools import duckduckgo_search, fetch_page_content
|
| 7 |
+
from mcp.tools.time_tools import current_datetime
|
| 8 |
+
from openai import AsyncOpenAI
|
| 9 |
+
|
| 10 |
+
# ---------------------------------------------------------
|
| 11 |
+
# Load environment variables
|
| 12 |
+
# ---------------------------------------------------------
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 16 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
| 17 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 18 |
+
gemini_model = OpenAIChatCompletionsModel(model="gemini-2.0-flash-exp", openai_client=gemini_client)
|
| 19 |
+
|
| 20 |
+
GROQ_BASE_URL = "https://api.groq.com/openai/v1"
|
| 21 |
+
groq_api_key = os.getenv('GROQ_API_KEY')
|
| 22 |
+
groq_client = AsyncOpenAI(base_url=GROQ_BASE_URL, api_key=groq_api_key)
|
| 23 |
+
groq_model = OpenAIChatCompletionsModel(model="groq/compound", openai_client=groq_client)
|
| 24 |
+
|
| 25 |
+
google_agent = Agent(
|
| 26 |
+
name="GoogleSearchAgent",
|
| 27 |
+
model=gemini_model,
|
| 28 |
+
tools=[current_datetime, google_search, google_search_recent, duckduckgo_search, fetch_page_content],
|
| 29 |
+
instructions="""
|
| 30 |
+
You are a GoogleSearchAgent specialized in finding and retrieving information from the web.
|
| 31 |
+
Your role is to help users find accurate, relevant, and up-to-date information using web search.
|
| 32 |
+
|
| 33 |
+
## Tool Priority & Usage
|
| 34 |
+
|
| 35 |
+
**PRIMARY TOOLS (Google via Serper.dev API):**
|
| 36 |
+
|
| 37 |
+
1. 'google_search': General Google search with recent results (last 24 hours by default)
|
| 38 |
+
- Use for most search queries
|
| 39 |
+
- Returns: Title, Link, Snippet
|
| 40 |
+
- Input: { "query": "search terms", "num_results": 3 }
|
| 41 |
+
|
| 42 |
+
2. 'google_search_recent': Time-filtered Google search
|
| 43 |
+
- Use when user specifies a time range (today, this week, this month, this year)
|
| 44 |
+
- Timeframes: "d" (day), "w" (week), "m" (month), "y" (year)
|
| 45 |
+
- Input: { "query": "search terms", "num_results": 3, "timeframe": "d" }
|
| 46 |
+
|
| 47 |
+
**FALLBACK TOOL (DuckDuckGo Search):**
|
| 48 |
+
|
| 49 |
+
3. 'duckduckgo_search': Use ONLY when Google tools fail or SERPER_API_KEY is missing
|
| 50 |
+
- Provides similar search functionality
|
| 51 |
+
- Input: { "query": "search terms", "max_results": 5, "search_type": "text", "timelimit": "d" }
|
| 52 |
+
|
| 53 |
+
**CONTENT EXTRACTION:**
|
| 54 |
+
|
| 55 |
+
4. 'fetch_page_content': Extract full text content from a specific URL
|
| 56 |
+
- Use when user wants detailed information from a specific page
|
| 57 |
+
- Use after search to get complete content for analysis
|
| 58 |
+
- Input: { "url": "https://example.com", "timeout": 3 }
|
| 59 |
+
|
| 60 |
+
**TIME CONTEXT:**
|
| 61 |
+
|
| 62 |
+
5. 'current_datetime': Get current date/time for context
|
| 63 |
+
- Input: { "format": "natural" }
|
| 64 |
+
|
| 65 |
+
## Workflow
|
| 66 |
+
|
| 67 |
+
1. **Understand the Query**: Determine what information the user needs
|
| 68 |
+
- General search → use google_search
|
| 69 |
+
- Time-specific search → use google_search_recent with appropriate timeframe
|
| 70 |
+
- Deep dive into a page → use fetch_page_content after getting the URL
|
| 71 |
+
|
| 72 |
+
2. **Try Primary Tools First**: Always attempt Google tools (Serper.dev) before fallback
|
| 73 |
+
|
| 74 |
+
3. **Fallback if Needed**: If Google tools return an error (missing API key, no results),
|
| 75 |
+
automatically use duckduckgo_search
|
| 76 |
+
|
| 77 |
+
4. **Extract Content if Needed**: If user wants detailed information or summary,
|
| 78 |
+
use fetch_page_content on relevant URLs from search results
|
| 79 |
+
|
| 80 |
+
5. **Provide Context**: Use current_datetime when temporal context is important
|
| 81 |
+
|
| 82 |
+
## Search Strategy
|
| 83 |
+
|
| 84 |
+
**For factual queries:**
|
| 85 |
+
- Use google_search or google_search_recent
|
| 86 |
+
- Summarize findings from multiple sources
|
| 87 |
+
- Cite sources with URLs
|
| 88 |
+
|
| 89 |
+
**For recent events/news:**
|
| 90 |
+
- Use google_search_recent with timeframe="d" or "w"
|
| 91 |
+
- Focus on most recent information
|
| 92 |
+
- Include publication dates if available
|
| 93 |
+
|
| 94 |
+
**For in-depth research:**
|
| 95 |
+
- First: Use google_search to find relevant pages
|
| 96 |
+
- Then: Use fetch_page_content to extract full content from top results
|
| 97 |
+
- Synthesize information from multiple sources
|
| 98 |
+
|
| 99 |
+
## Output Format
|
| 100 |
+
|
| 101 |
+
Structure your response based on the query type:
|
| 102 |
+
|
| 103 |
+
**For Search Results:**
|
| 104 |
+
|
| 105 |
+
**Search Results for "[Query]"** - [Current Date]
|
| 106 |
+
|
| 107 |
+
1. **[Title]**
|
| 108 |
+
- Source: [URL]
|
| 109 |
+
- Summary: [Snippet or extracted info]
|
| 110 |
+
|
| 111 |
+
2. **[Next Result]**
|
| 112 |
+
...
|
| 113 |
+
|
| 114 |
+
**Key Findings:**
|
| 115 |
+
- [Synthesized insight 1]
|
| 116 |
+
- [Synthesized insight 2]
|
| 117 |
+
|
| 118 |
+
**For Content Extraction:**
|
| 119 |
+
|
| 120 |
+
**Analysis of [Page Title]**
|
| 121 |
+
|
| 122 |
+
[Summarized content with key points]
|
| 123 |
+
|
| 124 |
+
Source: [URL]
|
| 125 |
+
|
| 126 |
+
## Important Rules
|
| 127 |
+
|
| 128 |
+
- Always cite sources with URLs
|
| 129 |
+
- Prioritize recent information when relevant
|
| 130 |
+
- If API key is missing, inform user and use fallback automatically
|
| 131 |
+
- Never fabricate information or sources
|
| 132 |
+
- Synthesize information from multiple sources when possible
|
| 133 |
+
- Be transparent about limitations (e.g., "Based on search results from...")
|
| 134 |
+
- Use fetch_page_content sparingly (only when deep content is needed)
|
| 135 |
+
- Respect timeouts and handle errors gracefully
|
| 136 |
+
""",
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
__all__ = ["google_agent", "google_search", "google_search_recent", "duckduckgo_search", "fetch_page_content", "current_datetime"]
|
common/aagents/healthcare_agent.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Healthcare RAG Agent - Combines RAG retrieval with web search for comprehensive medical information."""
|
| 2 |
+
import os
|
| 3 |
+
from agents import Agent, OpenAIChatCompletionsModel
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from openai import AsyncOpenAI
|
| 6 |
+
|
| 7 |
+
# Import tools
|
| 8 |
+
from mcp.tools.rag_tool import rag_search, UserContext
|
| 9 |
+
from mcp.tools.search_tools import duckduckgo_search
|
| 10 |
+
from mcp.tools.time_tools import current_datetime
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# ---------------------------------------------------------
|
| 14 |
+
# Load environment variables
|
| 15 |
+
# ---------------------------------------------------------
|
| 16 |
+
load_dotenv()
|
| 17 |
+
|
| 18 |
+
# ---------------------------------------------------------
|
| 19 |
+
# Model Configuration
|
| 20 |
+
# ---------------------------------------------------------
|
| 21 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 22 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
| 23 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 24 |
+
gemini_model = OpenAIChatCompletionsModel(model="gemini-2.0-flash-exp", openai_client=gemini_client)
|
| 25 |
+
|
| 26 |
+
GROQ_BASE_URL = "https://api.groq.com/openai/v1"
|
| 27 |
+
groq_api_key = os.getenv('GROQ_API_KEY')
|
| 28 |
+
groq_client = AsyncOpenAI(base_url=GROQ_BASE_URL, api_key=groq_api_key)
|
| 29 |
+
groq_model = OpenAIChatCompletionsModel(model="groq/compound", openai_client=groq_client)
|
| 30 |
+
|
| 31 |
+
# ---------------------------------------------------------
|
| 32 |
+
# Healthcare RAG Agent
|
| 33 |
+
# ---------------------------------------------------------
|
| 34 |
+
healthcare_agent = Agent[UserContext](
|
| 35 |
+
name="HealthcareRAGAgent",
|
| 36 |
+
model=gemini_model,
|
| 37 |
+
tools=[rag_search, duckduckgo_search],
|
| 38 |
+
instructions="""
|
| 39 |
+
You are a healthcare information retrieval agent. You retrieve information from tools and synthesize it into well-formatted markdown responses.
|
| 40 |
+
|
| 41 |
+
## CRITICAL RULES
|
| 42 |
+
|
| 43 |
+
1. **NEVER use your pre-trained knowledge** - Only use tool results
|
| 44 |
+
2. **ALWAYS call rag_search first** for every question
|
| 45 |
+
3. **Evaluate RAG results carefully** - if content is useless (just references, acknowledgments, page numbers), call duckduckgo_search
|
| 46 |
+
4. **If rag_search returns "No relevant information", MUST call duckduckgo_search**
|
| 47 |
+
5. **Synthesize tool results into clear, well-structured markdown**
|
| 48 |
+
6. **If both tools fail, say "I don't have information on this topic"**
|
| 49 |
+
|
| 50 |
+
## Workflow (MANDATORY)
|
| 51 |
+
|
| 52 |
+
For EVERY question:
|
| 53 |
+
|
| 54 |
+
Step 1: Call `rag_search(query="user question")`
|
| 55 |
+
|
| 56 |
+
Step 2: Evaluate the result:
|
| 57 |
+
- Returns "No relevant information"? → MUST call duckduckgo_search (go to Step 3)
|
| 58 |
+
- Returns content BUT it's NOT useful (just references, acknowledgments, page numbers, file names, credits)? → MUST call duckduckgo_search (go to Step 3)
|
| 59 |
+
- Returns useful information (definitions, explanations, medical details)? → Synthesize and format (go to Step 4)
|
| 60 |
+
|
| 61 |
+
Step 3: Call `duckduckgo_search(params={"query": "user question", "max_results": 3})`
|
| 62 |
+
|
| 63 |
+
Step 4: Synthesize and format response using markdown
|
| 64 |
+
|
| 65 |
+
## Response Format (Markdown)
|
| 66 |
+
|
| 67 |
+
## [Topic Name]
|
| 68 |
+
|
| 69 |
+
[Brief introduction/definition]
|
| 70 |
+
|
| 71 |
+
### Key Points
|
| 72 |
+
- **Point 1**: Description
|
| 73 |
+
- **Point 2**: Description
|
| 74 |
+
|
| 75 |
+
### Detailed Information
|
| 76 |
+
|
| 77 |
+
[Organized paragraphs with medical details]
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
**Source:** Knowledge Base / Web Search
|
| 82 |
+
|
| 83 |
+
**Disclaimer:** This information is for educational purposes only. Always consult a qualified healthcare provider for medical advice.
|
| 84 |
+
|
| 85 |
+
## Critical Reminders
|
| 86 |
+
|
| 87 |
+
🚨 You MUST:
|
| 88 |
+
- Call rag_search first, evaluate if content is useful
|
| 89 |
+
- If RAG content is useless (references/credits), call duckduckgo_search
|
| 90 |
+
- Use proper markdown formatting
|
| 91 |
+
- Cite the source
|
| 92 |
+
|
| 93 |
+
🚨 You MUST NOT:
|
| 94 |
+
- Use your pre-trained knowledge
|
| 95 |
+
- Skip evaluating RAG content quality
|
| 96 |
+
- Accept useless RAG results without calling web search
|
| 97 |
+
""",
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
__all__ = ["healthcare_agent"]
|
common/aagents/news_agent.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""News agent module for fetching and analyzing news articles."""
|
| 2 |
+
import os
|
| 3 |
+
from agents import Agent, OpenAIChatCompletionsModel
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from mcp.tools.news_tools import get_top_headlines, search_news, get_news_by_category
|
| 6 |
+
from mcp.tools.search_tools import duckduckgo_search
|
| 7 |
+
from mcp.tools.time_tools import current_datetime
|
| 8 |
+
from openai import AsyncOpenAI
|
| 9 |
+
|
| 10 |
+
# ---------------------------------------------------------
|
| 11 |
+
# Load environment variables
|
| 12 |
+
# ---------------------------------------------------------
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 16 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
| 17 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 18 |
+
gemini_model = OpenAIChatCompletionsModel(model="gemini-2.0-flash-exp", openai_client=gemini_client)
|
| 19 |
+
|
| 20 |
+
GROQ_BASE_URL = "https://api.groq.com/openai/v1"
|
| 21 |
+
groq_api_key = os.getenv('GROQ_API_KEY')
|
| 22 |
+
groq_client = AsyncOpenAI(base_url=GROQ_BASE_URL, api_key=groq_api_key)
|
| 23 |
+
groq_model = OpenAIChatCompletionsModel(model="groq/compound", openai_client=groq_client)
|
| 24 |
+
|
| 25 |
+
news_agent = Agent(
|
| 26 |
+
name="NewsAgent",
|
| 27 |
+
model=gemini_model,
|
| 28 |
+
tools=[current_datetime, get_top_headlines, search_news, get_news_by_category, duckduckgo_search],
|
| 29 |
+
instructions="""
|
| 30 |
+
You are a NewsAgent specialized in fetching and analyzing recent news articles and headlines.
|
| 31 |
+
Your role is to provide users with up-to-date, relevant news information from reliable sources.
|
| 32 |
+
|
| 33 |
+
## Tool Priority & Usage
|
| 34 |
+
|
| 35 |
+
**PRIMARY TOOLS (NewsAPI.org):**
|
| 36 |
+
1. 'get_top_headlines': Fetch the latest top headlines for a specific country
|
| 37 |
+
- Use when user asks for general news, breaking news, or top stories
|
| 38 |
+
- Input: { "country": "us", "num_results": 5 }
|
| 39 |
+
|
| 40 |
+
2. 'search_news': Search for news articles about a specific topic
|
| 41 |
+
- Use when user asks about a specific subject, company, person, or event
|
| 42 |
+
- Input: { "query": "topic name", "num_results": 5, "days_back": 7 }
|
| 43 |
+
|
| 44 |
+
3. 'get_news_by_category': Fetch headlines by category
|
| 45 |
+
- Use when user asks for category-specific news (business, tech, sports, etc.)
|
| 46 |
+
- Categories: "business", "entertainment", "general", "health", "science", "sports", "technology"
|
| 47 |
+
- Input: { "category": "business", "country": "us", "num_results": 5 }
|
| 48 |
+
|
| 49 |
+
**FALLBACK TOOL (DuckDuckGo Search):**
|
| 50 |
+
4. 'duckduckgo_search': Use ONLY when NewsAPI tools fail or API key is missing
|
| 51 |
+
- Set search_type to "news" for news-specific results
|
| 52 |
+
- Input: { "query": "topic", "max_results": 5, "search_type": "news", "timelimit": "d" }
|
| 53 |
+
|
| 54 |
+
**TIME CONTEXT:**
|
| 55 |
+
5. 'current_datetime': Use to provide current date/time context in your responses
|
| 56 |
+
- Input: { "format": "natural" }
|
| 57 |
+
|
| 58 |
+
## Workflow
|
| 59 |
+
|
| 60 |
+
1. **Determine Intent**: Understand what type of news the user wants
|
| 61 |
+
- General headlines → use get_top_headlines
|
| 62 |
+
- Topic-specific → use search_news
|
| 63 |
+
- Category-specific → use get_news_by_category
|
| 64 |
+
|
| 65 |
+
2. **Try Primary Tools First**: Always attempt NewsAPI tools before fallback
|
| 66 |
+
|
| 67 |
+
3. **Fallback if Needed**: If NewsAPI returns an error (missing API key, no results),
|
| 68 |
+
use duckduckgo_search with search_type="news"
|
| 69 |
+
|
| 70 |
+
4. **Include Time Context**: Use current_datetime to provide temporal context
|
| 71 |
+
|
| 72 |
+
5. **Format Response**: Present news in a clear, organized format with:
|
| 73 |
+
- Headlines/titles
|
| 74 |
+
- Sources
|
| 75 |
+
- Publication dates
|
| 76 |
+
- Brief summaries
|
| 77 |
+
- URLs for full articles
|
| 78 |
+
|
| 79 |
+
## Output Format
|
| 80 |
+
|
| 81 |
+
Structure your response as:
|
| 82 |
+
|
| 83 |
+
**[News Category/Topic] - [Current Date]**
|
| 84 |
+
|
| 85 |
+
1. **[Headline]**
|
| 86 |
+
- Source: [News Source]
|
| 87 |
+
- Published: [Date/Time]
|
| 88 |
+
- Summary: [Brief description]
|
| 89 |
+
- Read more: [URL]
|
| 90 |
+
|
| 91 |
+
2. **[Next Headline]**
|
| 92 |
+
...
|
| 93 |
+
|
| 94 |
+
## Important Rules
|
| 95 |
+
|
| 96 |
+
- Always cite sources and include publication dates
|
| 97 |
+
- Prioritize recent news (within last 7 days unless specified otherwise)
|
| 98 |
+
- If API key is missing, inform the user and use the fallback tool
|
| 99 |
+
- Never fabricate news or sources
|
| 100 |
+
- Present news objectively without bias
|
| 101 |
+
- Include URLs so users can read full articles
|
| 102 |
+
- Use current_datetime to ensure temporal accuracy
|
| 103 |
+
""",
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
__all__ = ["news_agent", "get_top_headlines", "search_news", "get_news_by_category", "duckduckgo_search", "current_datetime"]
|
common/aagents/weather_agent.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Web search agent module for internet queries."""
|
| 2 |
+
import os
|
| 3 |
+
from agents import Agent
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
from mcp.tools.weather_tools import get_weather_forecast, search_weather_fallback_ddgs, search_weather_fallback_bs
|
| 7 |
+
from mcp.tools.time_tools import current_datetime
|
| 8 |
+
from agents import Agent, OpenAIChatCompletionsModel
|
| 9 |
+
from openai import AsyncOpenAI
|
| 10 |
+
|
| 11 |
+
# ---------------------------------------------------------
|
| 12 |
+
# Load environment variables
|
| 13 |
+
# ---------------------------------------------------------
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
################################
|
| 17 |
+
# Learning: gemini models struggles to construct the output_type when it's a Pydantic model.
|
| 18 |
+
# So we use list[dict] as output_type instead of list[searchResult].
|
| 19 |
+
# Then in the calling code, we can convert dicts back to searchResult models if needed.
|
| 20 |
+
################################
|
| 21 |
+
|
| 22 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 23 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
| 24 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 25 |
+
gemini_model = OpenAIChatCompletionsModel(model="gemini-flash-latest", openai_client=gemini_client)
|
| 26 |
+
|
| 27 |
+
GROQ_BASE_URL = "https://api.groq.com/openai/v1"
|
| 28 |
+
groq_api_key = os.getenv('GROQ_API_KEY')
|
| 29 |
+
groq_client = AsyncOpenAI(base_url=GROQ_BASE_URL, api_key=groq_api_key)
|
| 30 |
+
groq_model = OpenAIChatCompletionsModel(model="groq/compound", openai_client=groq_client)
|
| 31 |
+
|
| 32 |
+
weather_agent = Agent(
|
| 33 |
+
name="WeatherAgent",
|
| 34 |
+
model=gemini_model, #"gpt-4o-mini",
|
| 35 |
+
# description="An agent that can perform web searches using DuckDuckGo.",
|
| 36 |
+
tools=[current_datetime, get_weather_forecast, search_weather_fallback_ddgs, search_weather_fallback_bs],
|
| 37 |
+
instructions="""
|
| 38 |
+
You are a Weather Forecast agent who forecasts weather information ONLY.
|
| 39 |
+
You can use the 'current_datetime' tool to determine the current date as reference for the weather forecast.
|
| 40 |
+
When given a query, you use the 'get_weather_forecast' tool to retrieve weather data.
|
| 41 |
+
If the API key is missing or the API fails to get the forecast, you use the 'search_weather_fallback_ddgs' or 'search_weather_fallback_bs' as fallback tools to perform a web search for weather information.
|
| 42 |
+
Tool: get_weather_forecast Input:
|
| 43 |
+
A JSON object with the following structure:
|
| 44 |
+
{ "city": "The city name to get the weather for.",
|
| 45 |
+
"date": "Optional date in YYYY-MM-DD format to get the forecast for a specific day. If not provided, return the current weather."
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
Output the weather information MUST be in a JSON well-formatted form as below:
|
| 49 |
+
{
|
| 50 |
+
"city": "City name",
|
| 51 |
+
"forecasts": [
|
| 52 |
+
{
|
| 53 |
+
"date": "Date of the forecast in YYYY-MM-DD format",
|
| 54 |
+
"weather": {
|
| 55 |
+
|
| 56 |
+
"description": "Weather description",
|
| 57 |
+
"temperature": "Temperature in Fahrenheit. Report both the high and low temperatures.",
|
| 58 |
+
"humidity": "Humidity percentage",
|
| 59 |
+
"wind_speed": "Wind speed in Miles per Hour (MPH)"
|
| 60 |
+
}
|
| 61 |
+
}.
|
| 62 |
+
]
|
| 63 |
+
""",
|
| 64 |
+
# output_type=AgentOutputSchema(list[searchResult], strict_json_schema=False),
|
| 65 |
+
# output_type=list[dict], # safer than list[searchResult],
|
| 66 |
+
# output_type=list[searchResult],
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
__all__ = ["weather_agent", "get_weather_forecast", "search_weather_fallback_ddgs", "search_weather_fallback_bs"]
|
common/aagents/web_agent.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Web search agent module for internet queries."""
|
| 2 |
+
import os
|
| 3 |
+
from agents import AgentOutputSchema, function_tool, Agent
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
from mcp.tools.search_tools import duckduckgo_search, searchQuery, searchResult
|
| 7 |
+
from agents import Agent, OpenAIChatCompletionsModel
|
| 8 |
+
from openai import AsyncOpenAI
|
| 9 |
+
|
| 10 |
+
# ---------------------------------------------------------
|
| 11 |
+
# Load environment variables
|
| 12 |
+
# ---------------------------------------------------------
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
################################
|
| 16 |
+
# Learning: gemini models struggles to construct the output_type when it's a Pydantic model.
|
| 17 |
+
# So we use list[dict] as output_type instead of list[searchResult].
|
| 18 |
+
# Then in the calling code, we can convert dicts back to searchResult models if needed.
|
| 19 |
+
################################
|
| 20 |
+
|
| 21 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 22 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
| 23 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 24 |
+
gemini_model = OpenAIChatCompletionsModel(model="gemini-2.0-flash-exp", openai_client=gemini_client)
|
| 25 |
+
|
| 26 |
+
GROQ_BASE_URL = "https://api.groq.com/openai/v1"
|
| 27 |
+
groq_api_key = os.getenv('GROQ_API_KEY')
|
| 28 |
+
groq_client = AsyncOpenAI(base_url=GROQ_BASE_URL, api_key=groq_api_key)
|
| 29 |
+
groq_model = OpenAIChatCompletionsModel(model="groq/compound", openai_client=groq_client)
|
| 30 |
+
|
| 31 |
+
web_agent = Agent(
|
| 32 |
+
name="WebAgent",
|
| 33 |
+
model="gpt-4o-mini",
|
| 34 |
+
# description="An agent that can perform web searches using DuckDuckGo.",
|
| 35 |
+
tools=[duckduckgo_search],
|
| 36 |
+
instructions="""
|
| 37 |
+
You are a WebAgent that can perform web searches to find information on the internet.
|
| 38 |
+
When given a query, use the 'duckduckgo_search' tool to retrieve relevant search results.
|
| 39 |
+
Tool: duckduckgo_search Input:
|
| 40 |
+
A JSON object with the following structure:
|
| 41 |
+
{ "query": "The search query string.",
|
| 42 |
+
"max_results": "The maximum number of search results to return (default is 5).",
|
| 43 |
+
"search_type": "The type of search to perform. Options: 'text' (default) or 'news'. Use 'news' to get publication dates.",
|
| 44 |
+
"timelimit": "Time limit for search results. Options: 'd' (day), 'w' (week), 'm' (month), 'y' (year).",
|
| 45 |
+
"region": "Region for search results (e.g., 'us-en', 'uk-en'). Default is 'wt-wt' (world)."
|
| 46 |
+
}
|
| 47 |
+
""",
|
| 48 |
+
# output_type=AgentOutputSchema(list[searchResult], strict_json_schema=False),
|
| 49 |
+
# output_type=list[dict], # safer than list[searchResult],
|
| 50 |
+
output_type=list[searchResult],
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
__all__ = ["web_agent", "duckduckgo_search", "searchQuery", "searchResult"]
|
common/aagents/web_research_agent.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Web search agent module for internet queries."""
|
| 2 |
+
import os
|
| 3 |
+
from agents import AgentOutputSchema, function_tool, Agent
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
from mcp.tools.search_tools import duckduckgo_search, searchQuery, searchResult, fetch_page_content
|
| 7 |
+
from agents import Agent, OpenAIChatCompletionsModel
|
| 8 |
+
from openai import AsyncOpenAI
|
| 9 |
+
|
| 10 |
+
# ---------------------------------------------------------
|
| 11 |
+
# Load environment variables
|
| 12 |
+
# ---------------------------------------------------------
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
################################
|
| 16 |
+
# Learning: gemini models struggles to construct the output_type when it's a Pydantic model.
|
| 17 |
+
# So we use list[dict] as output_type instead of list[searchResult].
|
| 18 |
+
# Then in the calling code, we can convert dicts back to searchResult models if needed.
|
| 19 |
+
################################
|
| 20 |
+
|
| 21 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 22 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
| 23 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 24 |
+
gemini_model = OpenAIChatCompletionsModel(model="gemini-2.0-flash-exp", openai_client=gemini_client)
|
| 25 |
+
|
| 26 |
+
GROQ_BASE_URL = "https://api.groq.com/openai/v1"
|
| 27 |
+
groq_api_key = os.getenv('GROQ_API_KEY')
|
| 28 |
+
groq_client = AsyncOpenAI(base_url=GROQ_BASE_URL, api_key=groq_api_key)
|
| 29 |
+
groq_model = OpenAIChatCompletionsModel(model="groq/compound", openai_client=groq_client)
|
| 30 |
+
|
| 31 |
+
web_research_agent = Agent(
|
| 32 |
+
name="WebResearchAgent",
|
| 33 |
+
model="gpt-4o-mini",
|
| 34 |
+
# description="An agent that can perform web searches using DuckDuckGo.",
|
| 35 |
+
tools=[duckduckgo_search, fetch_page_content],
|
| 36 |
+
instructions="""
|
| 37 |
+
You are WebResearchAgent — an advanced internet research assistant with two core abilities:
|
| 38 |
+
|
| 39 |
+
1) Use the tool `duckduckgo_search` to discover relevant webpages for the user’s query.
|
| 40 |
+
2) Use the tool `fetch_page_content` to retrieve full text content from any webpage returned by the search tool.
|
| 41 |
+
|
| 42 |
+
===========================
|
| 43 |
+
AGENT RESPONSIBILITIES
|
| 44 |
+
===========================
|
| 45 |
+
|
| 46 |
+
• Always begin by invoking `duckduckgo_search` to gather an initial set of webpages relevant to the user's question.
|
| 47 |
+
|
| 48 |
+
• After receiving the search results, you MUST fetch the full content for *all result URLs* by invoking
|
| 49 |
+
`fetch_page_content` once per URL.
|
| 50 |
+
|
| 51 |
+
• These fetch calls should be made **in parallel**:
|
| 52 |
+
- Do NOT wait for one fetch call to finish before issuing the next.
|
| 53 |
+
- Issue all fetch calls immediately after you receive the search results.
|
| 54 |
+
|
| 55 |
+
• You MUST NOT wait more than 3 seconds for any individual page to respond.
|
| 56 |
+
If content is missing or a fetch fails, continue with what you have.
|
| 57 |
+
|
| 58 |
+
===========================
|
| 59 |
+
ANALYSIS & FINAL ANSWER
|
| 60 |
+
===========================
|
| 61 |
+
|
| 62 |
+
• After search and fetch operations complete, analyze:
|
| 63 |
+
– the snippets from the search results
|
| 64 |
+
– the full content from `fetch_page_content` (for pages that responded)
|
| 65 |
+
|
| 66 |
+
• Synthesize the collected information and provide a clear, factual, concise answer.
|
| 67 |
+
|
| 68 |
+
• Your final output MUST be a structured, easy-to-read Markdown summary.
|
| 69 |
+
|
| 70 |
+
===========================
|
| 71 |
+
IMPORTANT RULES
|
| 72 |
+
===========================
|
| 73 |
+
|
| 74 |
+
• Never fabricate URLs or content not returned by the tools.
|
| 75 |
+
• Never claim to have visited pages without using `fetch_page_content`.
|
| 76 |
+
• Use the tools exactly as required — search first, fetch after.
|
| 77 |
+
• The final response should answer the user’s query using the combined evidence.
|
| 78 |
+
• MUST provide references to the research.
|
| 79 |
+
"""
|
| 80 |
+
,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
__all__ = ["web_research_agent", "duckduckgo_search", "fetch_page_content", "searchQuery", "searchResult"]
|
common/aagents/yf_agent.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Yahoo Finance agent module for financial analysis and market research."""
|
| 2 |
+
import os
|
| 3 |
+
from agents import Agent, OpenAIChatCompletionsModel
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from mcp.tools.yf_tools import get_summary, get_market_sentiment, get_history
|
| 6 |
+
from mcp.tools.time_tools import current_datetime
|
| 7 |
+
from openai import AsyncOpenAI
|
| 8 |
+
|
| 9 |
+
# ---------------------------------------------------------
|
| 10 |
+
# Load environment variables
|
| 11 |
+
# ---------------------------------------------------------
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 15 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
| 16 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 17 |
+
gemini_model = OpenAIChatCompletionsModel(model="gemini-2.0-flash-exp", openai_client=gemini_client)
|
| 18 |
+
|
| 19 |
+
GROQ_BASE_URL = "https://api.groq.com/openai/v1"
|
| 20 |
+
groq_api_key = os.getenv('GROQ_API_KEY')
|
| 21 |
+
groq_client = AsyncOpenAI(base_url=GROQ_BASE_URL, api_key=groq_api_key)
|
| 22 |
+
groq_model = OpenAIChatCompletionsModel(model="groq/compound", openai_client=groq_client)
|
| 23 |
+
|
| 24 |
+
yf_agent = Agent(
|
| 25 |
+
name="YahooFinanceAgent",
|
| 26 |
+
model=gemini_model,
|
| 27 |
+
tools=[current_datetime, get_summary, get_market_sentiment, get_history],
|
| 28 |
+
instructions="""
|
| 29 |
+
You are a specialized **Financial Analysis Agent** 💰, expert in market research, financial data retrieval, and market analysis.
|
| 30 |
+
Your primary role is to provide *actionable*, *data-driven*, and *concise* financial reports based on the available tools.
|
| 31 |
+
|
| 32 |
+
## Core Directives & Priorities
|
| 33 |
+
|
| 34 |
+
1. **Time Sensitivity:** Always use the 'current_datetime' tool to ensure all analysis is contextually relevant to the current date and time.
|
| 35 |
+
Financial data is extremely time-sensitive.
|
| 36 |
+
|
| 37 |
+
2. **Financial Data Integrity:** Use the Yahoo Finance tools for specific stock/index data:
|
| 38 |
+
- 'get_summary': Get latest summary information and intraday price data for a ticker
|
| 39 |
+
- 'get_market_sentiment': Analyze recent price changes and provide market sentiment (Bullish/Bearish/Neutral)
|
| 40 |
+
- 'get_history': Fetch historical price data for a given ticker
|
| 41 |
+
|
| 42 |
+
Be precise about the date range and data source.
|
| 43 |
+
|
| 44 |
+
3. **Synthesis and Analysis:** Do not just list data. You must **synthesize** financial data (prices, volume, sentiment)
|
| 45 |
+
to provide a complete analytical perspective (e.g., "Stock X is up 5% today driven by strong market momentum").
|
| 46 |
+
|
| 47 |
+
4. **Professional Clarity:** Present information in a clear, professional, and structured format.
|
| 48 |
+
Use numerical data and financial terminology correctly.
|
| 49 |
+
|
| 50 |
+
5. **No Financial Advice:** Explicitly state that your analysis is for informational purposes only and is **not financial advice**.
|
| 51 |
+
|
| 52 |
+
6. **Tool Mandatory:** For any request involving a stock, index, or current market conditions, you **must** use
|
| 53 |
+
the appropriate tool(s) to verify data. **Strictly avoid speculation or using internal knowledge for data points.**
|
| 54 |
+
|
| 55 |
+
## Tool Usage Examples
|
| 56 |
+
|
| 57 |
+
Tool: current_datetime
|
| 58 |
+
Input: { "format": "natural" }
|
| 59 |
+
|
| 60 |
+
Tool: get_summary
|
| 61 |
+
Input: { "symbol": "AAPL", "period": "1d", "interval": "1h" }
|
| 62 |
+
|
| 63 |
+
Tool: get_market_sentiment
|
| 64 |
+
Input: { "symbol": "AAPL", "period": "1mo" }
|
| 65 |
+
|
| 66 |
+
Tool: get_history
|
| 67 |
+
Input: { "symbol": "AAPL", "period": "1mo" }
|
| 68 |
+
|
| 69 |
+
## Output Format Guidelines
|
| 70 |
+
|
| 71 |
+
* Use **bold** for key financial metrics (e.g., Stock Symbol, Price, Volume).
|
| 72 |
+
* Cite the tools used to obtain the data (e.g., "Data sourced from Yahoo Finance as of [Date]").
|
| 73 |
+
* If a symbol or data point cannot be found, clearly state "Data for [X] is unavailable or invalid."
|
| 74 |
+
* Always include a disclaimer: "This analysis is for informational purposes only and is not financial advice."
|
| 75 |
+
""",
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
__all__ = ["yf_agent", "get_summary", "get_market_sentiment", "get_history", "current_datetime"]
|
common/mcp/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MCP Tools Server
|
| 2 |
+
|
| 3 |
+
A Model Context Protocol (MCP) server that exposes all tools from the `tools/` folder via stdio transport.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- **Dynamic Tool Discovery**: Automatically discovers and registers all tools from the tools folder
|
| 8 |
+
- **Stdio Transport**: Compatible with Claude Desktop and other MCP clients
|
| 9 |
+
- **Comprehensive Tool Coverage**: Exposes ~13 tools across 6 categories:
|
| 10 |
+
- Google Search (google_tools)
|
| 11 |
+
- News API (news_tools)
|
| 12 |
+
- DuckDuckGo Search (search_tools)
|
| 13 |
+
- Time Utilities (time_tools)
|
| 14 |
+
- Weather Forecast (weather_tools)
|
| 15 |
+
- Yahoo Finance (yf_tools)
|
| 16 |
+
|
| 17 |
+
## Installation
|
| 18 |
+
|
| 19 |
+
1. Install required dependencies:
|
| 20 |
+
```bash
|
| 21 |
+
pip install mcp requests beautifulsoup4 ddgs yfinance python-dotenv pydantic
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
2. Set up environment variables in `.env`:
|
| 25 |
+
```bash
|
| 26 |
+
# Google Search (Serper.dev)
|
| 27 |
+
SERPER_API_KEY=your_serper_api_key
|
| 28 |
+
|
| 29 |
+
# News API
|
| 30 |
+
NEWS_API_KEY=your_news_api_key
|
| 31 |
+
|
| 32 |
+
# Weather API
|
| 33 |
+
OPENWEATHER_API_KEY=your_openweather_api_key
|
| 34 |
+
|
| 35 |
+
# Google AI (for agents)
|
| 36 |
+
GOOGLE_API_KEY=your_google_api_key
|
| 37 |
+
|
| 38 |
+
# Groq (for agents)
|
| 39 |
+
GROQ_API_KEY=your_groq_api_key
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## Usage
|
| 43 |
+
|
| 44 |
+
### Running the Server
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
cd common/mcp
|
| 48 |
+
python mcp_server.py
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
The server will:
|
| 52 |
+
1. Discover all tools from the `tools/` folder
|
| 53 |
+
2. Print registered tools to stderr
|
| 54 |
+
3. Start listening on stdio for MCP protocol messages
|
| 55 |
+
|
| 56 |
+
### Integrating with Claude Desktop
|
| 57 |
+
|
| 58 |
+
Add to your Claude Desktop config (`claude_desktop_config.json`):
|
| 59 |
+
|
| 60 |
+
```json
|
| 61 |
+
{
|
| 62 |
+
"mcpServers": {
|
| 63 |
+
"tools-server": {
|
| 64 |
+
"command": "python",
|
| 65 |
+
"args": ["/absolute/path/to/agenticaiprojects/common/mcp/mcp_server.py"],
|
| 66 |
+
"env": {
|
| 67 |
+
"SERPER_API_KEY": "your_key",
|
| 68 |
+
"NEWS_API_KEY": "your_key",
|
| 69 |
+
"OPENWEATHER_API_KEY": "your_key"
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
### Available Tools
|
| 77 |
+
|
| 78 |
+
The server exposes the following tools:
|
| 79 |
+
|
| 80 |
+
**Google Search:**
|
| 81 |
+
- `google_tools.google_search` - General Google search
|
| 82 |
+
- `google_tools.google_search_recent` - Time-filtered Google search
|
| 83 |
+
|
| 84 |
+
**News:**
|
| 85 |
+
- `news_tools.get_top_headlines` - Top headlines by country
|
| 86 |
+
- `news_tools.search_news` - Search news by topic
|
| 87 |
+
- `news_tools.get_news_by_category` - News by category
|
| 88 |
+
|
| 89 |
+
**Search & Content:**
|
| 90 |
+
- `search_tools.duckduckgo_search` - DuckDuckGo search
|
| 91 |
+
- `search_tools.fetch_page_content` - Extract page content
|
| 92 |
+
|
| 93 |
+
**Time:**
|
| 94 |
+
- `time_tools.current_datetime` - Get current date/time
|
| 95 |
+
|
| 96 |
+
**Weather:**
|
| 97 |
+
- `weather_tools.get_weather_forecast` - Weather forecast via API
|
| 98 |
+
- `weather_tools.search_weather_fallback_ddgs` - Weather via DuckDuckGo
|
| 99 |
+
- `weather_tools.search_weather_fallback_bs` - Weather via web scraping
|
| 100 |
+
|
| 101 |
+
**Finance:**
|
| 102 |
+
- `yf_tools.get_summary` - Stock summary
|
| 103 |
+
- `yf_tools.get_market_sentiment` - Market sentiment analysis
|
| 104 |
+
- `yf_tools.get_history` - Historical stock data
|
| 105 |
+
|
| 106 |
+
## Development
|
| 107 |
+
|
| 108 |
+
### Adding New Tools
|
| 109 |
+
|
| 110 |
+
1. Create a new file in `tools/` folder (e.g., `my_tools.py`)
|
| 111 |
+
2. Decorate functions with `@function_tool`
|
| 112 |
+
3. The server will automatically discover and register them on next restart
|
| 113 |
+
|
| 114 |
+
### Testing
|
| 115 |
+
|
| 116 |
+
```bash
|
| 117 |
+
# Test the server
|
| 118 |
+
cd common/mcp
|
| 119 |
+
python mcp_server.py
|
| 120 |
+
|
| 121 |
+
# In another terminal, you can send MCP protocol messages via stdin
|
| 122 |
+
# Or use an MCP client library to test
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
## Troubleshooting
|
| 126 |
+
|
| 127 |
+
**Tools not discovered:**
|
| 128 |
+
- Check that functions are decorated with `@function_tool`
|
| 129 |
+
- Verify the module is in the `tools/` folder
|
| 130 |
+
- Check stderr output for registration messages
|
| 131 |
+
|
| 132 |
+
**API errors:**
|
| 133 |
+
- Verify environment variables are set correctly
|
| 134 |
+
- Check API key validity
|
| 135 |
+
- Review tool-specific error messages in stderr
|
| 136 |
+
|
| 137 |
+
## License
|
| 138 |
+
|
| 139 |
+
Part of the agenticaiprojects repository.
|
common/mcp/__init__.py
ADDED
|
File without changes
|
common/mcp/mcp_server.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
MCP Server with stdio transport that exposes all tools from the tools folder.
|
| 4 |
+
"""
|
| 5 |
+
import asyncio
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
import inspect
|
| 9 |
+
import importlib
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any, Callable
|
| 12 |
+
|
| 13 |
+
# Add parent directory to path for imports
|
| 14 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 15 |
+
|
| 16 |
+
from mcp.server import Server
|
| 17 |
+
from mcp.server.stdio import stdio_server
|
| 18 |
+
from mcp.types import Tool, TextContent
|
| 19 |
+
|
| 20 |
+
# Initialize MCP server
|
| 21 |
+
app = Server("tools-server")
|
| 22 |
+
|
| 23 |
+
# Dictionary to store all discovered tools
|
| 24 |
+
TOOLS_REGISTRY: dict[str, Callable] = {}
|
| 25 |
+
|
| 26 |
+
def discover_tools():
|
| 27 |
+
"""
|
| 28 |
+
Dynamically discover all @function_tool decorated functions from the tools folder.
|
| 29 |
+
"""
|
| 30 |
+
tools_dir = Path(__file__).parent / "tools"
|
| 31 |
+
tool_modules = [
|
| 32 |
+
"google_tools",
|
| 33 |
+
"news_tools",
|
| 34 |
+
"search_tools",
|
| 35 |
+
"time_tools",
|
| 36 |
+
"weather_tools",
|
| 37 |
+
"yf_tools"
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
print(f"[MCP Server] Discovering tools from: {tools_dir}", file=sys.stderr)
|
| 41 |
+
|
| 42 |
+
for module_name in tool_modules:
|
| 43 |
+
try:
|
| 44 |
+
# Import the module
|
| 45 |
+
module = importlib.import_module(f"mcp.tools.{module_name}")
|
| 46 |
+
|
| 47 |
+
# Find all functions in the module
|
| 48 |
+
for name, obj in inspect.getmembers(module, inspect.isfunction):
|
| 49 |
+
# Check if it has the function_tool decorator
|
| 50 |
+
# The @function_tool decorator typically adds metadata to the function
|
| 51 |
+
if hasattr(obj, '__wrapped__') or name.startswith('_'):
|
| 52 |
+
continue
|
| 53 |
+
|
| 54 |
+
# Check if it's a tool by looking for common patterns
|
| 55 |
+
if callable(obj) and not name.startswith('_'):
|
| 56 |
+
# Register the tool
|
| 57 |
+
tool_name = f"{module_name}.{name}"
|
| 58 |
+
TOOLS_REGISTRY[tool_name] = obj
|
| 59 |
+
print(f"[MCP Server] Registered tool: {tool_name}", file=sys.stderr)
|
| 60 |
+
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"[MCP Server] Error loading module {module_name}: {e}", file=sys.stderr)
|
| 63 |
+
|
| 64 |
+
print(f"[MCP Server] Total tools registered: {len(TOOLS_REGISTRY)}", file=sys.stderr)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@app.list_tools()
|
| 68 |
+
async def list_tools() -> list[Tool]:
|
| 69 |
+
"""
|
| 70 |
+
List all available tools.
|
| 71 |
+
"""
|
| 72 |
+
tools = []
|
| 73 |
+
|
| 74 |
+
for tool_name, tool_func in TOOLS_REGISTRY.items():
|
| 75 |
+
# Extract function signature and docstring
|
| 76 |
+
sig = inspect.signature(tool_func)
|
| 77 |
+
doc = inspect.getdoc(tool_func) or "No description available"
|
| 78 |
+
|
| 79 |
+
# Build input schema from function parameters
|
| 80 |
+
properties = {}
|
| 81 |
+
required = []
|
| 82 |
+
|
| 83 |
+
for param_name, param in sig.parameters.items():
|
| 84 |
+
param_type = "string" # Default type
|
| 85 |
+
param_desc = ""
|
| 86 |
+
|
| 87 |
+
# Try to infer type from annotation
|
| 88 |
+
if param.annotation != inspect.Parameter.empty:
|
| 89 |
+
annotation = param.annotation
|
| 90 |
+
if annotation == int:
|
| 91 |
+
param_type = "integer"
|
| 92 |
+
elif annotation == bool:
|
| 93 |
+
param_type = "boolean"
|
| 94 |
+
elif annotation == float:
|
| 95 |
+
param_type = "number"
|
| 96 |
+
|
| 97 |
+
properties[param_name] = {
|
| 98 |
+
"type": param_type,
|
| 99 |
+
"description": param_desc or f"Parameter: {param_name}"
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
# Check if parameter is required (no default value)
|
| 103 |
+
if param.default == inspect.Parameter.empty:
|
| 104 |
+
required.append(param_name)
|
| 105 |
+
|
| 106 |
+
# Create tool definition
|
| 107 |
+
tool = Tool(
|
| 108 |
+
name=tool_name,
|
| 109 |
+
description=doc.split('\n')[0][:200], # First line, max 200 chars
|
| 110 |
+
inputSchema={
|
| 111 |
+
"type": "object",
|
| 112 |
+
"properties": properties,
|
| 113 |
+
"required": required
|
| 114 |
+
}
|
| 115 |
+
)
|
| 116 |
+
tools.append(tool)
|
| 117 |
+
|
| 118 |
+
return tools
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@app.call_tool()
|
| 122 |
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
| 123 |
+
"""
|
| 124 |
+
Execute a tool with the provided arguments.
|
| 125 |
+
"""
|
| 126 |
+
print(f"[MCP Server] Calling tool: {name} with args: {arguments}", file=sys.stderr)
|
| 127 |
+
|
| 128 |
+
if name not in TOOLS_REGISTRY:
|
| 129 |
+
raise ValueError(f"Tool not found: {name}")
|
| 130 |
+
|
| 131 |
+
tool_func = TOOLS_REGISTRY[name]
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
# Call the tool function
|
| 135 |
+
if inspect.iscoroutinefunction(tool_func):
|
| 136 |
+
result = await tool_func(**arguments)
|
| 137 |
+
else:
|
| 138 |
+
result = tool_func(**arguments)
|
| 139 |
+
|
| 140 |
+
# Convert result to string if needed
|
| 141 |
+
if not isinstance(result, str):
|
| 142 |
+
result = str(result)
|
| 143 |
+
|
| 144 |
+
return [TextContent(type="text", text=result)]
|
| 145 |
+
|
| 146 |
+
except Exception as e:
|
| 147 |
+
error_msg = f"Error executing tool {name}: {str(e)}"
|
| 148 |
+
print(f"[MCP Server] {error_msg}", file=sys.stderr)
|
| 149 |
+
return [TextContent(type="text", text=error_msg)]
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
async def main():
|
| 153 |
+
"""
|
| 154 |
+
Main entry point for the MCP server.
|
| 155 |
+
"""
|
| 156 |
+
# Discover all tools before starting the server
|
| 157 |
+
discover_tools()
|
| 158 |
+
|
| 159 |
+
print(f"[MCP Server] Starting MCP server with {len(TOOLS_REGISTRY)} tools", file=sys.stderr)
|
| 160 |
+
|
| 161 |
+
# Run the server with stdio transport
|
| 162 |
+
async with stdio_server() as (read_stream, write_stream):
|
| 163 |
+
await app.run(
|
| 164 |
+
read_stream,
|
| 165 |
+
write_stream,
|
| 166 |
+
app.create_initialization_options()
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
if __name__ == "__main__":
|
| 171 |
+
asyncio.run(main())
|
common/mcp/tools/__init__.py
ADDED
|
File without changes
|
common/mcp/tools/google_tools.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from agents import function_tool
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
# ---------------------------------------------------------
|
| 8 |
+
# Load environment variables
|
| 9 |
+
# ---------------------------------------------------------
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
# ============================================================
|
| 13 |
+
# 🔹 GOOGLE SEARCH TOOLSET (Serper.dev API)
|
| 14 |
+
# ============================================================
|
| 15 |
+
|
| 16 |
+
@function_tool
|
| 17 |
+
def google_search(query: str, num_results: int = 3) -> str:
|
| 18 |
+
"""
|
| 19 |
+
Perform a general Google search using Serper.dev API.
|
| 20 |
+
|
| 21 |
+
Parameters:
|
| 22 |
+
-----------
|
| 23 |
+
query : str
|
| 24 |
+
The search query string, e.g., "latest Tesla stock news".
|
| 25 |
+
num_results : int, optional (default=3)
|
| 26 |
+
Maximum number of search results to return.
|
| 27 |
+
|
| 28 |
+
Returns:
|
| 29 |
+
--------
|
| 30 |
+
str
|
| 31 |
+
Formatted string of top search results, each including:
|
| 32 |
+
- Title of the page
|
| 33 |
+
- URL link
|
| 34 |
+
- Snippet / description
|
| 35 |
+
If no results are found or API key is missing, returns an error message.
|
| 36 |
+
|
| 37 |
+
Example:
|
| 38 |
+
--------
|
| 39 |
+
google_search("AI in finance", num_results=2)
|
| 40 |
+
|
| 41 |
+
Output:
|
| 42 |
+
Title: How AI is Transforming Finance
|
| 43 |
+
Link: https://example.com/ai-finance
|
| 44 |
+
Snippet: AI is increasingly used for trading, risk management...
|
| 45 |
+
|
| 46 |
+
Title: AI Applications in Banking
|
| 47 |
+
Link: https://example.com/ai-banking
|
| 48 |
+
Snippet: Banks are leveraging AI for customer service, fraud detection...
|
| 49 |
+
"""
|
| 50 |
+
print(f"[DEBUG] google_search called with query='{query}', num_results={num_results}")
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
api_key = os.getenv("SERPER_API_KEY")
|
| 54 |
+
if not api_key:
|
| 55 |
+
return "Error: SERPER_API_KEY missing in environment variables."
|
| 56 |
+
|
| 57 |
+
url = "https://google.serper.dev/search"
|
| 58 |
+
headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
|
| 59 |
+
payload = {"q": query, "num": num_results, "tbs": "qdr:d"} # results from last 24h
|
| 60 |
+
|
| 61 |
+
response = requests.post(url, headers=headers, json=payload, timeout=10)
|
| 62 |
+
response.raise_for_status()
|
| 63 |
+
data = response.json()
|
| 64 |
+
|
| 65 |
+
if "organic" not in data or not data["organic"]:
|
| 66 |
+
return f"No results found for query: '{query}'"
|
| 67 |
+
|
| 68 |
+
formatted_results = [
|
| 69 |
+
f"Title: {item.get('title')}\n"
|
| 70 |
+
f"Link: {item.get('link')}\n"
|
| 71 |
+
f"Snippet: {item.get('snippet', '')}\n"
|
| 72 |
+
for item in data["organic"][:num_results]
|
| 73 |
+
]
|
| 74 |
+
return "\n".join(formatted_results)
|
| 75 |
+
|
| 76 |
+
except requests.exceptions.RequestException as e:
|
| 77 |
+
print(f"[DEBUG] Network error during Google search: {e}")
|
| 78 |
+
return f"Network error during Google search: {e}"
|
| 79 |
+
except Exception as e:
|
| 80 |
+
print(f"[DEBUG] Error performing Google search: {e}")
|
| 81 |
+
return f"Error performing Google search: {e}"
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@function_tool
|
| 85 |
+
def google_search_recent(query: str, num_results: int = 3, timeframe: str = "d") -> str:
|
| 86 |
+
"""
|
| 87 |
+
Perform a Google search with time-based filtering using Serper.dev API.
|
| 88 |
+
|
| 89 |
+
Parameters:
|
| 90 |
+
-----------
|
| 91 |
+
query : str
|
| 92 |
+
The search query string.
|
| 93 |
+
num_results : int, optional (default=3)
|
| 94 |
+
Maximum number of search results to return.
|
| 95 |
+
timeframe : str, optional (default="d")
|
| 96 |
+
Time range for results:
|
| 97 |
+
- "d" = past day
|
| 98 |
+
- "w" = past week
|
| 99 |
+
- "m" = past month
|
| 100 |
+
- "y" = past year
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
--------
|
| 104 |
+
str
|
| 105 |
+
Formatted string of recent search results.
|
| 106 |
+
"""
|
| 107 |
+
print(f"[DEBUG] google_search_recent called with query='{query}', timeframe={timeframe}")
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
api_key = os.getenv("SERPER_API_KEY")
|
| 111 |
+
if not api_key:
|
| 112 |
+
return "Error: SERPER_API_KEY missing in environment variables."
|
| 113 |
+
|
| 114 |
+
url = "https://google.serper.dev/search"
|
| 115 |
+
headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
|
| 116 |
+
payload = {"q": query, "num": num_results, "tbs": f"qdr:{timeframe}"}
|
| 117 |
+
|
| 118 |
+
response = requests.post(url, headers=headers, json=payload, timeout=10)
|
| 119 |
+
response.raise_for_status()
|
| 120 |
+
data = response.json()
|
| 121 |
+
|
| 122 |
+
if "organic" not in data or not data["organic"]:
|
| 123 |
+
return f"No recent results found for query: '{query}'"
|
| 124 |
+
|
| 125 |
+
formatted_results = [
|
| 126 |
+
f"Title: {item.get('title')}\n"
|
| 127 |
+
f"Link: {item.get('link')}\n"
|
| 128 |
+
f"Snippet: {item.get('snippet', '')}\n"
|
| 129 |
+
for item in data["organic"][:num_results]
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
return f"Recent results ({timeframe}):\n\n" + "\n".join(formatted_results)
|
| 133 |
+
|
| 134 |
+
except requests.exceptions.RequestException as e:
|
| 135 |
+
print(f"[DEBUG] Network error: {e}")
|
| 136 |
+
return f"Network error during Google search: {e}"
|
| 137 |
+
except Exception as e:
|
| 138 |
+
print(f"[DEBUG] Error: {e}")
|
| 139 |
+
return f"Error performing Google search: {e}"
|
common/mcp/tools/news_tools.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from agents import function_tool
|
| 5 |
+
from typing import Optional
|
| 6 |
+
import datetime
|
| 7 |
+
|
| 8 |
+
# ---------------------------------------------------------
|
| 9 |
+
# Load environment variables
|
| 10 |
+
# ---------------------------------------------------------
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
# ============================================================
|
| 14 |
+
# 🔹 NEWS TOOLSET (NewsAPI.org)
|
| 15 |
+
# ============================================================
|
| 16 |
+
|
| 17 |
+
@function_tool
|
| 18 |
+
def get_top_headlines(country: str = "us", num_results: int = 5) -> str:
|
| 19 |
+
"""
|
| 20 |
+
Fetch the latest top headlines for a country using NewsAPI.org.
|
| 21 |
+
|
| 22 |
+
Parameters:
|
| 23 |
+
-----------
|
| 24 |
+
country : str, optional (default="us")
|
| 25 |
+
Two-letter country code (e.g., "us", "gb", "in").
|
| 26 |
+
num_results : int, optional (default=5)
|
| 27 |
+
Number of articles to fetch.
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
--------
|
| 31 |
+
str
|
| 32 |
+
Formatted headlines with title, source, published date, and URL.
|
| 33 |
+
If API key is missing or no results found, returns an error message.
|
| 34 |
+
"""
|
| 35 |
+
print(f"[DEBUG] get_top_headlines called for country={country}, num_results={num_results}")
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
api_key = os.getenv("NEWS_API_KEY")
|
| 39 |
+
if not api_key:
|
| 40 |
+
return "Error: NEWS_API_KEY missing in environment variables."
|
| 41 |
+
|
| 42 |
+
url = "https://newsapi.org/v2/top-headlines"
|
| 43 |
+
params = {
|
| 44 |
+
"country": country,
|
| 45 |
+
"pageSize": num_results,
|
| 46 |
+
"apiKey": api_key
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
response = requests.get(url, params=params, timeout=10)
|
| 50 |
+
response.raise_for_status()
|
| 51 |
+
data = response.json()
|
| 52 |
+
|
| 53 |
+
if not data.get("articles"):
|
| 54 |
+
return f"No top headlines found for country: {country}"
|
| 55 |
+
|
| 56 |
+
formatted = []
|
| 57 |
+
for article in data["articles"][:num_results]:
|
| 58 |
+
formatted.append(
|
| 59 |
+
f"📰 {article.get('title')}\n"
|
| 60 |
+
f" Source: {article.get('source', {}).get('name')}\n"
|
| 61 |
+
f" Published: {article.get('publishedAt', 'N/A')}\n"
|
| 62 |
+
f" URL: {article.get('url')}\n"
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
return f"Top Headlines ({country.upper()}):\n\n" + "\n".join(formatted)
|
| 66 |
+
|
| 67 |
+
except requests.exceptions.RequestException as e:
|
| 68 |
+
print(f"[DEBUG] Network error: {e}")
|
| 69 |
+
return f"Network error while calling News API: {e}"
|
| 70 |
+
except Exception as e:
|
| 71 |
+
print(f"[DEBUG] Error: {e}")
|
| 72 |
+
return f"Unexpected error fetching news: {e}"
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@function_tool
|
| 76 |
+
def search_news(query: str, num_results: int = 5, days_back: int = 7) -> str:
|
| 77 |
+
"""
|
| 78 |
+
Search for recent news articles about a specific topic using NewsAPI.org.
|
| 79 |
+
|
| 80 |
+
Parameters:
|
| 81 |
+
-----------
|
| 82 |
+
query : str
|
| 83 |
+
Keyword or topic to search (e.g., "Tesla earnings", "AI healthcare").
|
| 84 |
+
num_results : int, optional (default=5)
|
| 85 |
+
Number of articles to fetch.
|
| 86 |
+
days_back : int, optional (default=7)
|
| 87 |
+
Number of days to look back for articles (1-30).
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
--------
|
| 91 |
+
str
|
| 92 |
+
Formatted news articles with title, source, published date, and URL.
|
| 93 |
+
If API key is missing or no results found, returns an error message.
|
| 94 |
+
"""
|
| 95 |
+
print(f"[DEBUG] search_news called with query='{query}', num_results={num_results}, days_back={days_back}")
|
| 96 |
+
|
| 97 |
+
try:
|
| 98 |
+
api_key = os.getenv("NEWS_API_KEY")
|
| 99 |
+
if not api_key:
|
| 100 |
+
return "Error: NEWS_API_KEY missing in environment variables."
|
| 101 |
+
|
| 102 |
+
# Calculate date range
|
| 103 |
+
today = datetime.datetime.utcnow()
|
| 104 |
+
from_date = (today - datetime.timedelta(days=days_back)).strftime('%Y-%m-%dT%H:%M:%SZ')
|
| 105 |
+
|
| 106 |
+
url = "https://newsapi.org/v2/everything"
|
| 107 |
+
params = {
|
| 108 |
+
"q": query,
|
| 109 |
+
"pageSize": num_results,
|
| 110 |
+
"apiKey": api_key,
|
| 111 |
+
"sortBy": "publishedAt",
|
| 112 |
+
"language": "en",
|
| 113 |
+
"from": from_date
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
response = requests.get(url, params=params, timeout=10)
|
| 117 |
+
response.raise_for_status()
|
| 118 |
+
data = response.json()
|
| 119 |
+
|
| 120 |
+
if not data.get("articles"):
|
| 121 |
+
return f"No news found for query: '{query}'"
|
| 122 |
+
|
| 123 |
+
formatted = []
|
| 124 |
+
for article in data["articles"][:num_results]:
|
| 125 |
+
formatted.append(
|
| 126 |
+
f"📰 {article.get('title')}\n"
|
| 127 |
+
f" Source: {article.get('source', {}).get('name')}\n"
|
| 128 |
+
f" Published: {article.get('publishedAt', 'N/A')}\n"
|
| 129 |
+
f" URL: {article.get('url')}\n"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
return f"News Search Results for '{query}' (last {days_back} days):\n\n" + "\n".join(formatted)
|
| 133 |
+
|
| 134 |
+
except requests.exceptions.RequestException as e:
|
| 135 |
+
print(f"[DEBUG] Network error: {e}")
|
| 136 |
+
return f"Network error while calling News API: {e}"
|
| 137 |
+
except Exception as e:
|
| 138 |
+
print(f"[DEBUG] Error: {e}")
|
| 139 |
+
return f"Unexpected error fetching news: {e}"
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
@function_tool
|
| 143 |
+
def get_news_by_category(category: str = "business", country: str = "us", num_results: int = 5) -> str:
|
| 144 |
+
"""
|
| 145 |
+
Fetch top headlines by category using NewsAPI.org.
|
| 146 |
+
|
| 147 |
+
Parameters:
|
| 148 |
+
-----------
|
| 149 |
+
category : str, optional (default="business")
|
| 150 |
+
News category: "business", "entertainment", "general", "health",
|
| 151 |
+
"science", "sports", "technology".
|
| 152 |
+
country : str, optional (default="us")
|
| 153 |
+
Two-letter country code.
|
| 154 |
+
num_results : int, optional (default=5)
|
| 155 |
+
Number of articles to fetch.
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
--------
|
| 159 |
+
str
|
| 160 |
+
Formatted headlines for the specified category.
|
| 161 |
+
"""
|
| 162 |
+
print(f"[DEBUG] get_news_by_category called for category={category}, country={country}")
|
| 163 |
+
|
| 164 |
+
try:
|
| 165 |
+
api_key = os.getenv("NEWS_API_KEY")
|
| 166 |
+
if not api_key:
|
| 167 |
+
return "Error: NEWS_API_KEY missing in environment variables."
|
| 168 |
+
|
| 169 |
+
url = "https://newsapi.org/v2/top-headlines"
|
| 170 |
+
params = {
|
| 171 |
+
"category": category,
|
| 172 |
+
"country": country,
|
| 173 |
+
"pageSize": num_results,
|
| 174 |
+
"apiKey": api_key
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
response = requests.get(url, params=params, timeout=10)
|
| 178 |
+
response.raise_for_status()
|
| 179 |
+
data = response.json()
|
| 180 |
+
|
| 181 |
+
if not data.get("articles"):
|
| 182 |
+
return f"No headlines found for category: {category}"
|
| 183 |
+
|
| 184 |
+
formatted = []
|
| 185 |
+
for article in data["articles"][:num_results]:
|
| 186 |
+
formatted.append(
|
| 187 |
+
f"📰 {article.get('title')}\n"
|
| 188 |
+
f" Source: {article.get('source', {}).get('name')}\n"
|
| 189 |
+
f" Published: {article.get('publishedAt', 'N/A')}\n"
|
| 190 |
+
f" URL: {article.get('url')}\n"
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
return f"Top {category.capitalize()} Headlines ({country.upper()}):\n\n" + "\n".join(formatted)
|
| 194 |
+
|
| 195 |
+
except requests.exceptions.RequestException as e:
|
| 196 |
+
print(f"[DEBUG] Network error: {e}")
|
| 197 |
+
return f"Network error while calling News API: {e}"
|
| 198 |
+
except Exception as e:
|
| 199 |
+
print(f"[DEBUG] Error: {e}")
|
| 200 |
+
return f"Unexpected error fetching news: {e}"
|
common/mcp/tools/rag_tool.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""RAG Search Tool - Search the local healthcare knowledge base"""
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from agents import function_tool, RunContextWrapper
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from rag.rag import Retriever
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass
|
| 11 |
+
class UserContext:
|
| 12 |
+
uid: str
|
| 13 |
+
db_path: str = ""
|
| 14 |
+
file_path: str = ""
|
| 15 |
+
similarity_threshold: float = 0.4 # FAISS L2 distance threshold for RAG relevance
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# ---------------------------------------------------------
|
| 19 |
+
# Load environment variables
|
| 20 |
+
# ---------------------------------------------------------
|
| 21 |
+
load_dotenv()
|
| 22 |
+
|
| 23 |
+
# ---------------------------------------------------------
|
| 24 |
+
# Initialize RAG Retriever
|
| 25 |
+
# ---------------------------------------------------------
|
| 26 |
+
# Get the healthcare-rag-chatbot directory path
|
| 27 |
+
# healthcare_dir = str(Path(__file__).parent.parent.parent)
|
| 28 |
+
# retriever = None
|
| 29 |
+
|
| 30 |
+
# ---------------------------------------------------------
|
| 31 |
+
# RAG Search Tool
|
| 32 |
+
# ---------------------------------------------------------
|
| 33 |
+
@function_tool
|
| 34 |
+
def rag_search(wrapper: RunContextWrapper[UserContext], query: str) -> str:
|
| 35 |
+
"""
|
| 36 |
+
Search the local healthcare knowledge base for relevant information.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
query: The medical question or topic to search for
|
| 40 |
+
|
| 41 |
+
Returns:
|
| 42 |
+
Relevant information from the healthcare knowledge base
|
| 43 |
+
"""
|
| 44 |
+
print(f"[DEBUG] RAG_SEARCH called with query: '{query}'")
|
| 45 |
+
|
| 46 |
+
# Get similarity threshold from user context
|
| 47 |
+
similarity_threshold = wrapper.context.similarity_threshold
|
| 48 |
+
print(f"[DEBUG] RAG_SEARCH: Using similarity threshold: {similarity_threshold}")
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
# Initialize retriever with user context
|
| 52 |
+
retriever = Retriever(
|
| 53 |
+
db_path=wrapper.context.db_path,
|
| 54 |
+
file_path=wrapper.context.file_path
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# Get results with similarity scores
|
| 58 |
+
results_with_scores = retriever.retrieve_with_scores(query, k=5) # Increased from 4 to 5
|
| 59 |
+
|
| 60 |
+
if not results_with_scores:
|
| 61 |
+
print("[DEBUG] RAG_SEARCH: No results found in knowledge base")
|
| 62 |
+
return "No relevant information found in the knowledge base."
|
| 63 |
+
|
| 64 |
+
print(f"[DEBUG] RAG_SEARCH: Found {len(results_with_scores)} results")
|
| 65 |
+
|
| 66 |
+
# Check if the best match meets the threshold
|
| 67 |
+
# FAISS returns (document, distance) where lower distance = better match
|
| 68 |
+
best_score = results_with_scores[0][1]
|
| 69 |
+
print(f"[DEBUG] RAG_SEARCH: Best similarity score (distance): {best_score:.4f} (threshold: {similarity_threshold})")
|
| 70 |
+
|
| 71 |
+
if best_score > similarity_threshold:
|
| 72 |
+
print(f"[DEBUG] RAG_SEARCH: Best match score {best_score:.4f} is above threshold {similarity_threshold}")
|
| 73 |
+
print("[DEBUG] RAG_SEARCH: Results not relevant enough, triggering web search fallback")
|
| 74 |
+
return "No relevant information found in the knowledge base."
|
| 75 |
+
|
| 76 |
+
print(f"[DEBUG] RAG_SEARCH: Results are relevant (score: {best_score:.4f} <= {similarity_threshold})")
|
| 77 |
+
|
| 78 |
+
# Log all scores for debugging
|
| 79 |
+
all_scores = [f"{score:.4f}" for _, score in results_with_scores]
|
| 80 |
+
print(f"[DEBUG] RAG_SEARCH: All scores: {', '.join(all_scores)}")
|
| 81 |
+
|
| 82 |
+
# Format results - only include documents that meet the similarity threshold
|
| 83 |
+
formatted_results = []
|
| 84 |
+
for i, (doc, score) in enumerate(results_with_scores[:5], 1): # Top 5 results
|
| 85 |
+
if score <= similarity_threshold:
|
| 86 |
+
content = doc.page_content.strip()
|
| 87 |
+
formatted_results.append(f"Result {i} (score: {score:.4f}):\n{content}\n")
|
| 88 |
+
|
| 89 |
+
if not formatted_results:
|
| 90 |
+
print("[DEBUG] RAG_SEARCH: No results met the similarity threshold")
|
| 91 |
+
print("[DEBUG] RAG_SEARCH: Triggering web search fallback")
|
| 92 |
+
return "No relevant information found in the knowledge base."
|
| 93 |
+
|
| 94 |
+
result_text = "\n".join(formatted_results)
|
| 95 |
+
print(f"[DEBUG] RAG_SEARCH: Returning {len(formatted_results)} results, total length: {len(result_text)} characters")
|
| 96 |
+
print(f"[DEBUG] RAG_SEARCH: First 300 chars: {result_text[:300]}...")
|
| 97 |
+
|
| 98 |
+
return result_text
|
| 99 |
+
|
| 100 |
+
except Exception as e:
|
| 101 |
+
print(f"[DEBUG] RAG_SEARCH: Error occurred - {str(e)}")
|
| 102 |
+
return f"Error retrieving from knowledge base: {str(e)}"
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
__all__ = ["rag_search", "retriever"]
|
common/mcp/tools/search_tools.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ddgs import DDGS
|
| 2 |
+
from agents import function_tool
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
import requests
|
| 6 |
+
from bs4 import BeautifulSoup
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
# ---------------------------------------------------------
|
| 10 |
+
# Load environment variables
|
| 11 |
+
# ---------------------------------------------------------
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
# ---------------------- MODELS ---------------------------
|
| 15 |
+
class searchQuery(BaseModel):
|
| 16 |
+
query: str = Field(..., description="The search query string.")
|
| 17 |
+
max_results: int = Field(5, description="The maximum number of search results to return.")
|
| 18 |
+
search_type: str = Field(
|
| 19 |
+
"text",
|
| 20 |
+
description="Search type: 'text' (default) or 'news'. Use 'news' to get publication dates."
|
| 21 |
+
)
|
| 22 |
+
timelimit: str = Field(
|
| 23 |
+
'd',
|
| 24 |
+
description="Time limit for search results: 'd' (day), 'w' (week), 'm' (month), 'y' (year)."
|
| 25 |
+
)
|
| 26 |
+
region: str = Field("us-en", description="Region for search results (e.g., 'us-en').")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class searchResult(BaseModel):
|
| 30 |
+
title: str
|
| 31 |
+
link: str
|
| 32 |
+
snippet: str
|
| 33 |
+
datetime: Optional[str] = None
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# ---------------------- PAGE FETCH TOOL ---------------------------
|
| 37 |
+
@function_tool
|
| 38 |
+
def fetch_page_content(url: str, timeout: int = 3) -> Optional[str]:
|
| 39 |
+
"""Fetch and extract text content from a web page."""
|
| 40 |
+
print(f"[DEBUG] fetch_page_content called with: {url} - timeout: {timeout}")
|
| 41 |
+
try:
|
| 42 |
+
headers = {
|
| 43 |
+
'User-Agent': (
|
| 44 |
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
|
| 45 |
+
'AppleWebKit/537.36 (KHTML, like Gecko) '
|
| 46 |
+
'Chrome/91.0.4472.124 Safari/537.36'
|
| 47 |
+
)
|
| 48 |
+
}
|
| 49 |
+
response = requests.get(url, headers=headers, timeout=timeout)
|
| 50 |
+
response.raise_for_status()
|
| 51 |
+
|
| 52 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
| 53 |
+
|
| 54 |
+
# Remove irrelevant elements
|
| 55 |
+
for tag in soup(["script", "style", "nav", "footer", "header"]):
|
| 56 |
+
tag.decompose()
|
| 57 |
+
|
| 58 |
+
# Extract text
|
| 59 |
+
text = soup.get_text(separator='\n', strip=True)
|
| 60 |
+
|
| 61 |
+
# Clean whitespace
|
| 62 |
+
lines = (line.strip() for line in text.splitlines())
|
| 63 |
+
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
| 64 |
+
text = '\n'.join(chunk for chunk in chunks if chunk)
|
| 65 |
+
|
| 66 |
+
return text
|
| 67 |
+
except Exception as e:
|
| 68 |
+
print(f"[WARNING] Failed to fetch content from {url}: {str(e)}")
|
| 69 |
+
return None
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# ---------------------- SEARCH TOOL ---------------------------
|
| 73 |
+
@function_tool
|
| 74 |
+
def duckduckgo_search(params: searchQuery) -> list[dict]:
|
| 75 |
+
"""Perform a DuckDuckGo search and return only snippets.
|
| 76 |
+
No page content fetched here."""
|
| 77 |
+
print(f"[DEBUG] duckduckgo_search called with: {params}")
|
| 78 |
+
|
| 79 |
+
results = []
|
| 80 |
+
with DDGS() as ddgs:
|
| 81 |
+
if params.search_type == "news":
|
| 82 |
+
search_results = ddgs.news(
|
| 83 |
+
params.query,
|
| 84 |
+
max_results=params.max_results,
|
| 85 |
+
timelimit=params.timelimit,
|
| 86 |
+
region=params.region
|
| 87 |
+
)
|
| 88 |
+
for result in search_results:
|
| 89 |
+
results.append(
|
| 90 |
+
searchResult(
|
| 91 |
+
title=result.get("title", ""),
|
| 92 |
+
link=result.get("url", ""),
|
| 93 |
+
snippet=result.get("body", ""),
|
| 94 |
+
datetime=result.get("date", "")
|
| 95 |
+
).model_dump()
|
| 96 |
+
)
|
| 97 |
+
else:
|
| 98 |
+
search_results = ddgs.text(
|
| 99 |
+
params.query,
|
| 100 |
+
max_results=params.max_results,
|
| 101 |
+
timelimit=params.timelimit,
|
| 102 |
+
region=params.region
|
| 103 |
+
)
|
| 104 |
+
for result in search_results:
|
| 105 |
+
results.append(
|
| 106 |
+
searchResult(
|
| 107 |
+
title=result.get("title", ""),
|
| 108 |
+
link=result.get("href", ""),
|
| 109 |
+
snippet=result.get("body", "")
|
| 110 |
+
).model_dump()
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
print(f"[DEBUG] duckduckgo_search returning {len(results)} results")
|
| 114 |
+
return results
|
| 115 |
+
|
common/mcp/tools/time_tools.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from agents import function_tool
|
| 3 |
+
# from ..common.utility.logger import log_call
|
| 4 |
+
|
| 5 |
+
@function_tool
|
| 6 |
+
# @log_call
|
| 7 |
+
def current_datetime(format: str = "natural") -> str:
|
| 8 |
+
"""
|
| 9 |
+
Returns the current date and time as a formatted string.
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
format (str): Format style for the datetime. Options:
|
| 13 |
+
- "natural" (default): "Saturday, December 7, 2025 at 3:59 PM"
|
| 14 |
+
- "natural_short": "Dec 7, 2025 at 3:59 PM"
|
| 15 |
+
- "natural_full": "Saturday, December 7, 2025 at 3:59:30 PM CST"
|
| 16 |
+
- Custom strftime format string (e.g., "%Y-%m-%d %H:%M:%S")
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
str: Current date and time in the specified format
|
| 20 |
+
"""
|
| 21 |
+
now = datetime.now()
|
| 22 |
+
|
| 23 |
+
# Natural format options
|
| 24 |
+
if format == "natural":
|
| 25 |
+
return now.strftime("%A, %B %d, %Y at %I:%M %p")
|
| 26 |
+
elif format == "natural_short":
|
| 27 |
+
return now.strftime("%b %d, %Y at %I:%M %p")
|
| 28 |
+
elif format == "natural_full":
|
| 29 |
+
return now.strftime("%A, %B %d, %Y at %I:%M:%S %p %Z")
|
| 30 |
+
else:
|
| 31 |
+
# Custom format string
|
| 32 |
+
return now.strftime(format)
|
common/mcp/tools/weather_tools.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import requests
|
| 4 |
+
import datetime
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
from ddgs import DDGS
|
| 9 |
+
from agents import function_tool
|
| 10 |
+
|
| 11 |
+
# ---------------------------------------------------------
|
| 12 |
+
# Load environment variables
|
| 13 |
+
# ---------------------------------------------------------
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
@function_tool
|
| 17 |
+
def get_weather_forecast(city: str, date: Optional[str] = None) -> str:
|
| 18 |
+
"""
|
| 19 |
+
PRIMARY TOOL: Fetch weather using OpenWeatherMap API.
|
| 20 |
+
"""
|
| 21 |
+
print(f"[DEBUG] Primary API get_weather_forecast called for city={city}")
|
| 22 |
+
|
| 23 |
+
api_key = os.getenv("OPENWEATHER_API_KEY")
|
| 24 |
+
if not api_key:
|
| 25 |
+
return "Error: OPENWEATHER_API_KEY missing. Please use the fallback search tool."
|
| 26 |
+
|
| 27 |
+
url = "https://api.openweathermap.org/data/2.5/forecast"
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
response = requests.get(
|
| 31 |
+
url,
|
| 32 |
+
params={"q": city, "appid": api_key, "units": "metric"},
|
| 33 |
+
timeout=5
|
| 34 |
+
)
|
| 35 |
+
data = response.json()
|
| 36 |
+
except Exception as e:
|
| 37 |
+
return f"Error calling weather API: {str(e)}"
|
| 38 |
+
|
| 39 |
+
if str(data.get("cod")) != "200":
|
| 40 |
+
return f"Error from API: {data.get('message', 'Unknown error')}"
|
| 41 |
+
|
| 42 |
+
# Build the report string
|
| 43 |
+
report_lines = []
|
| 44 |
+
found_date = False
|
| 45 |
+
|
| 46 |
+
for entry in data.get("list", []):
|
| 47 |
+
dt_txt = entry["dt_txt"].split(" ")[0]
|
| 48 |
+
|
| 49 |
+
if date and dt_txt != date:
|
| 50 |
+
continue
|
| 51 |
+
|
| 52 |
+
found_date = True
|
| 53 |
+
desc = entry['weather'][0]['description'].capitalize()
|
| 54 |
+
temp = entry['main']['temp']
|
| 55 |
+
hum = entry['main']['humidity']
|
| 56 |
+
wind = entry['wind']['speed']
|
| 57 |
+
|
| 58 |
+
report_lines.append(f"{dt_txt}: {desc}, Temp: {temp}°C, Humidity: {hum}%, Wind: {wind} m/s")
|
| 59 |
+
|
| 60 |
+
# Handle "Date not found" case
|
| 61 |
+
if date and not found_date:
|
| 62 |
+
return f"API valid, but date {date} is out of range (5-day limit). Try the search fallback tool."
|
| 63 |
+
|
| 64 |
+
final_report = "\n".join(report_lines)
|
| 65 |
+
|
| 66 |
+
return f"API Forecast for {city}:\n{final_report}"
|
| 67 |
+
|
| 68 |
+
# ---------------------------------------------------------
|
| 69 |
+
# Tool 2: Web Search Fallback (Secondary)
|
| 70 |
+
# ---------------------------------------------------------
|
| 71 |
+
|
| 72 |
+
@function_tool
|
| 73 |
+
def search_weather_fallback_ddgs(city: str, date: Optional[str] = None) -> str:
|
| 74 |
+
"""
|
| 75 |
+
SECONDARY TOOL: Search-based fallback that produces an API-like structured forecast.
|
| 76 |
+
"""
|
| 77 |
+
print(f"[DEBUG] Fallback API (DDGS) called for city={city}, date={date}")
|
| 78 |
+
|
| 79 |
+
# --- Build Query ---
|
| 80 |
+
try:
|
| 81 |
+
if date:
|
| 82 |
+
try:
|
| 83 |
+
dt_obj = datetime.strptime(date, "%Y-%m-%d")
|
| 84 |
+
natural_date = dt_obj.strftime("%B %d, %Y")
|
| 85 |
+
month_name = dt_obj.strftime("%B")
|
| 86 |
+
except ValueError:
|
| 87 |
+
natural_date = date
|
| 88 |
+
month_name = ""
|
| 89 |
+
else:
|
| 90 |
+
natural_date = datetime.now().strftime("%B %d, %Y")
|
| 91 |
+
month_name = natural_date.split()[0] # Month name
|
| 92 |
+
|
| 93 |
+
query = f"weather {city} {natural_date}"
|
| 94 |
+
print(f"[DEBUG] Search query: {query}")
|
| 95 |
+
|
| 96 |
+
# --- Perform Search ---
|
| 97 |
+
results = list(DDGS().text(query, max_results=3))
|
| 98 |
+
print(f"[DEBUG] Number of search results: {len(results)}")
|
| 99 |
+
|
| 100 |
+
if not results:
|
| 101 |
+
return f"Web Estimated Forecast for {city}:\nNo reliable search data found."
|
| 102 |
+
|
| 103 |
+
# --- Aggregate Text ---
|
| 104 |
+
full_text = " ".join([r.get("body", "") for r in results])
|
| 105 |
+
|
| 106 |
+
# --- Extract Values with Robust Regex ---
|
| 107 |
+
temp_match = re.findall(r'(-?\d+)\s*(?:°|deg|C|F)', full_text, re.I)
|
| 108 |
+
temperature = temp_match[0] if temp_match else "?"
|
| 109 |
+
|
| 110 |
+
humidity_match = re.findall(r'(\d+)\s*%', full_text)
|
| 111 |
+
humidity = humidity_match[0] if humidity_match else "?"
|
| 112 |
+
|
| 113 |
+
wind_match = re.findall(r'(\d+)\s*(?:mph|km/h|m/s)', full_text, re.I)
|
| 114 |
+
wind = wind_match[0] if wind_match else "?"
|
| 115 |
+
|
| 116 |
+
# --- Condition ---
|
| 117 |
+
# Take first word(s) of first title as best guess
|
| 118 |
+
condition_raw = results[0].get("title", "Unknown").split("-")[0].strip()
|
| 119 |
+
condition = condition_raw[0].upper() + condition_raw[1:] if condition_raw else "Unknown"
|
| 120 |
+
|
| 121 |
+
# --- Construct API-like Forecast ---
|
| 122 |
+
forecast = (
|
| 123 |
+
f"Web Estimated Forecast for {city}:\n"
|
| 124 |
+
f"{natural_date}: {condition}, Temp: {temperature}° (approx), "
|
| 125 |
+
f"Humidity: {humidity}%, Wind: {wind}\n"
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
# Optional: add raw snippets for debugging
|
| 129 |
+
# snippet_block = "\nSearch Snippets (Raw):\n" + "\n".join(
|
| 130 |
+
# f"- {r['title']}: {r['body']}" for r in results
|
| 131 |
+
# )
|
| 132 |
+
# return forecast + snippet_block
|
| 133 |
+
|
| 134 |
+
return forecast
|
| 135 |
+
|
| 136 |
+
except Exception as e:
|
| 137 |
+
print(f"[DEBUG] Error in fallback: {e}")
|
| 138 |
+
return f"Error performing web search: {str(e)}"
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
import requests
|
| 142 |
+
from bs4 import BeautifulSoup
|
| 143 |
+
import re
|
| 144 |
+
from typing import Optional
|
| 145 |
+
from agents import function_tool
|
| 146 |
+
from datetime import datetime
|
| 147 |
+
|
| 148 |
+
@function_tool
|
| 149 |
+
def search_weather_fallback_bs(city: str, date: Optional[str] = None) -> str:
|
| 150 |
+
"""
|
| 151 |
+
SECONDARY TOOL: Web-scraping fallback using BeautifulSoup.
|
| 152 |
+
Produces an API-like structured forecast.
|
| 153 |
+
"""
|
| 154 |
+
import requests
|
| 155 |
+
from bs4 import BeautifulSoup
|
| 156 |
+
import re
|
| 157 |
+
from datetime import datetime
|
| 158 |
+
|
| 159 |
+
print(f"[DEBUG] Fallback API (BeautifulSoup) called for city={city}, date={date}")
|
| 160 |
+
|
| 161 |
+
try:
|
| 162 |
+
# --- Build Query ---
|
| 163 |
+
if date:
|
| 164 |
+
try:
|
| 165 |
+
dt_obj = datetime.strptime(date, "%Y-%m-%d")
|
| 166 |
+
natural_date = dt_obj.strftime("%B %d, %Y")
|
| 167 |
+
except ValueError:
|
| 168 |
+
natural_date = date
|
| 169 |
+
else:
|
| 170 |
+
natural_date = datetime.now().strftime("%B %d, %Y")
|
| 171 |
+
|
| 172 |
+
query = f"weather {city} {natural_date}"
|
| 173 |
+
print(f"[DEBUG] Search query: {query}")
|
| 174 |
+
|
| 175 |
+
# --- DuckDuckGo Search ---
|
| 176 |
+
search_url = f"https://duckduckgo.com/html/?q={query.replace(' ', '+')}"
|
| 177 |
+
headers = {"User-Agent": "Mozilla/5.0"}
|
| 178 |
+
response = requests.get(search_url, headers=headers, timeout=5)
|
| 179 |
+
if response.status_code != 200:
|
| 180 |
+
return f"Error fetching search results: {response.status_code}"
|
| 181 |
+
|
| 182 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
| 183 |
+
results = []
|
| 184 |
+
for result in soup.select(".result__body"):
|
| 185 |
+
title_tag = result.select_one(".result__title a")
|
| 186 |
+
snippet_tag = result.select_one(".result__snippet")
|
| 187 |
+
if title_tag and snippet_tag:
|
| 188 |
+
results.append({
|
| 189 |
+
"title": title_tag.get_text(strip=True),
|
| 190 |
+
"body": snippet_tag.get_text(strip=True)
|
| 191 |
+
})
|
| 192 |
+
|
| 193 |
+
if not results:
|
| 194 |
+
return f"Web Estimated Forecast for {city}:\nNo reliable search data found."
|
| 195 |
+
|
| 196 |
+
# --- Aggregate Text ---
|
| 197 |
+
full_text = " ".join([r["body"] for r in results])
|
| 198 |
+
|
| 199 |
+
# --- Extract Temperature ---
|
| 200 |
+
temp_matches = re.findall(r'(-?\d{1,2})\s*(?:°|deg|C|F)', full_text, re.I)
|
| 201 |
+
temperature = temp_matches[0] if temp_matches else "?"
|
| 202 |
+
|
| 203 |
+
# --- Extract Humidity ---
|
| 204 |
+
humidity_matches = re.findall(r'(\d{1,3})\s*%', full_text)
|
| 205 |
+
humidity = humidity_matches[0] if humidity_matches else "?"
|
| 206 |
+
|
| 207 |
+
# --- Extract Wind ---
|
| 208 |
+
wind_matches = re.findall(r'(\d{1,3})\s*(?:mph|km/h|m/s)', full_text, re.I)
|
| 209 |
+
wind = wind_matches[0] if wind_matches else "?"
|
| 210 |
+
|
| 211 |
+
# --- Extract Condition ---
|
| 212 |
+
# Look in all results first, fallback to first title
|
| 213 |
+
condition = "Unknown"
|
| 214 |
+
for r in results:
|
| 215 |
+
m = re.search(r'(clear|sunny|cloudy|rain|snow|storm|fog|mist)', r["body"], re.I)
|
| 216 |
+
if m:
|
| 217 |
+
condition = m.group(1).capitalize()
|
| 218 |
+
break
|
| 219 |
+
if condition == "Unknown":
|
| 220 |
+
# Fallback
|
| 221 |
+
condition_raw = results[0]["title"].split("-")[0].strip()
|
| 222 |
+
condition = condition_raw[0].upper() + condition_raw[1:] if condition_raw else "Unknown"
|
| 223 |
+
|
| 224 |
+
# --- Build Forecast ---
|
| 225 |
+
forecast = (
|
| 226 |
+
f"Web Estimated Forecast for {city}:\n"
|
| 227 |
+
f"{natural_date}: {condition}, Temp: {temperature}° (approx), "
|
| 228 |
+
f"Humidity: {humidity}%, Wind: {wind}\n"
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
return forecast
|
| 232 |
+
|
| 233 |
+
except Exception as e:
|
| 234 |
+
print(f"[DEBUG] Error in fallback: {e}")
|
| 235 |
+
return f"Error performing web search: {str(e)}"
|
common/mcp/tools/yf_tools.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
import yfinance as yf
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from agents import function_tool
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
|
| 8 |
+
# Load environment variables
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# ============================================================
|
| 13 |
+
# 🔹 YAHOO FINANCE TOOLSET
|
| 14 |
+
# ============================================================
|
| 15 |
+
@function_tool
|
| 16 |
+
def get_summary(symbol: str, period: str = "1d", interval: str = "1h") -> str:
|
| 17 |
+
"""
|
| 18 |
+
Fetch the latest summary information and intraday price data for a given ticker.
|
| 19 |
+
Ensures recent data is retrieved by calculating start/end dates dynamically.
|
| 20 |
+
|
| 21 |
+
Parameters:
|
| 22 |
+
-----------
|
| 23 |
+
symbol : str
|
| 24 |
+
The ticker symbol (e.g., "AAPL", "GOOG", "BTC-USD").
|
| 25 |
+
period : str, optional (default="1d")
|
| 26 |
+
Time range for price data. Examples: "1d", "5d", "1mo", "3mo".
|
| 27 |
+
interval : str, optional (default="1h")
|
| 28 |
+
Granularity of the data. Examples: "1m", "5m", "1h", "1d".
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
--------
|
| 32 |
+
str
|
| 33 |
+
A formatted string containing:
|
| 34 |
+
- Company/ticker name
|
| 35 |
+
- Current price and change
|
| 36 |
+
- Open, High, Low prices
|
| 37 |
+
- Volume
|
| 38 |
+
- Period and interval used
|
| 39 |
+
"""
|
| 40 |
+
try:
|
| 41 |
+
ticker = yf.Ticker(symbol)
|
| 42 |
+
|
| 43 |
+
# Calculate start and end dates based on period
|
| 44 |
+
end_date = datetime.today()
|
| 45 |
+
if period.endswith("d"):
|
| 46 |
+
days = int(period[:-1])
|
| 47 |
+
elif period.endswith("mo"):
|
| 48 |
+
days = int(period[:-2]) * 30
|
| 49 |
+
elif period.endswith("y"):
|
| 50 |
+
days = int(period[:-1]) * 365
|
| 51 |
+
else:
|
| 52 |
+
days = 30 # default 1 month
|
| 53 |
+
start_date = end_date - timedelta(days=days)
|
| 54 |
+
|
| 55 |
+
# Fetch recent data explicitly
|
| 56 |
+
data = ticker.history(
|
| 57 |
+
start=start_date.strftime("%Y-%m-%d"),
|
| 58 |
+
end=end_date.strftime("%Y-%m-%d"),
|
| 59 |
+
interval=interval
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
if data.empty:
|
| 63 |
+
return f"No data found for symbol '{symbol}'."
|
| 64 |
+
|
| 65 |
+
latest = data.iloc[-1]
|
| 66 |
+
current_price = round(latest["Close"], 2)
|
| 67 |
+
open_price = round(latest["Open"], 2)
|
| 68 |
+
change = round(current_price - open_price, 2)
|
| 69 |
+
pct_change = round((change / open_price) * 100, 2)
|
| 70 |
+
|
| 71 |
+
info = ticker.info
|
| 72 |
+
long_name = info.get("longName", symbol)
|
| 73 |
+
currency = info.get("currency", "USD")
|
| 74 |
+
|
| 75 |
+
formatted = [
|
| 76 |
+
f"📈 {long_name} ({symbol})",
|
| 77 |
+
f"Current Price: {current_price} {currency}",
|
| 78 |
+
f"Change: {change} ({pct_change}%)",
|
| 79 |
+
f"Open: {open_price} | High: {round(latest['High'], 2)} | Low: {round(latest['Low'], 2)}",
|
| 80 |
+
f"Volume: {int(latest['Volume'])}",
|
| 81 |
+
f"Period: {period} | Interval: {interval}",
|
| 82 |
+
]
|
| 83 |
+
return "\n".join(formatted)
|
| 84 |
+
|
| 85 |
+
except Exception as e:
|
| 86 |
+
return f"Error fetching data for '{symbol}': {e}"
|
| 87 |
+
|
| 88 |
+
@function_tool
|
| 89 |
+
def get_market_sentiment(symbol: str, period: str = "1mo") -> str:
|
| 90 |
+
"""
|
| 91 |
+
Analyze recent price changes and provide a simple market sentiment.
|
| 92 |
+
Uses dynamic start/end dates to ensure recent data.
|
| 93 |
+
|
| 94 |
+
This tool computes the percentage change over the specified period and
|
| 95 |
+
classifies the sentiment as:
|
| 96 |
+
- Bullish (if price increased >2%)
|
| 97 |
+
- Bearish (if price decreased >2%)
|
| 98 |
+
- Neutral (otherwise)
|
| 99 |
+
|
| 100 |
+
Parameters:
|
| 101 |
+
-----------
|
| 102 |
+
symbol : str
|
| 103 |
+
The ticker symbol (e.g., "AAPL", "GOOG", "BTC-USD").
|
| 104 |
+
period : str, optional (default="1mo")
|
| 105 |
+
Time range to analyze. Examples: "7d", "1mo", "3mo".
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
--------
|
| 109 |
+
str
|
| 110 |
+
A human-readable sentiment string including percentage change.
|
| 111 |
+
"""
|
| 112 |
+
try:
|
| 113 |
+
ticker = yf.Ticker(symbol)
|
| 114 |
+
|
| 115 |
+
# Calculate start/end dynamically
|
| 116 |
+
end_date = datetime.today()
|
| 117 |
+
if period.endswith("d"):
|
| 118 |
+
days = int(period[:-1])
|
| 119 |
+
elif period.endswith("mo"):
|
| 120 |
+
days = int(period[:-2]) * 30
|
| 121 |
+
elif period.endswith("y"):
|
| 122 |
+
days = int(period[:-1]) * 365
|
| 123 |
+
else:
|
| 124 |
+
days = 30
|
| 125 |
+
start_date = end_date - timedelta(days=days)
|
| 126 |
+
|
| 127 |
+
data = ticker.history(
|
| 128 |
+
start=start_date.strftime("%Y-%m-%d"),
|
| 129 |
+
end=end_date.strftime("%Y-%m-%d")
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
if data.empty:
|
| 133 |
+
return f"No data for {symbol}."
|
| 134 |
+
|
| 135 |
+
recent_change = data["Close"].iloc[-1] - data["Close"].iloc[0]
|
| 136 |
+
pct_change = (recent_change / data["Close"].iloc[0]) * 100
|
| 137 |
+
|
| 138 |
+
sentiment = "Neutral"
|
| 139 |
+
if pct_change > 2:
|
| 140 |
+
sentiment = "Bullish"
|
| 141 |
+
elif pct_change < -2:
|
| 142 |
+
sentiment = "Bearish"
|
| 143 |
+
|
| 144 |
+
return f"{symbol} market sentiment ({period}): {sentiment} ({pct_change:.2f}% change)"
|
| 145 |
+
|
| 146 |
+
except Exception as e:
|
| 147 |
+
return f"Error fetching market sentiment for '{symbol}': {e}"
|
| 148 |
+
|
| 149 |
+
@function_tool
|
| 150 |
+
def get_history(symbol: str, period: str = "1mo") -> str:
|
| 151 |
+
"""
|
| 152 |
+
Fetch historical price data for a given ticker.
|
| 153 |
+
Ensures recent data is retrieved dynamically using start/end dates.
|
| 154 |
+
|
| 155 |
+
Parameters:
|
| 156 |
+
-----------
|
| 157 |
+
symbol : str
|
| 158 |
+
The ticker symbol (e.g., "AAPL", "GOOG", "BTC-USD").
|
| 159 |
+
period : str, optional (default="1mo")
|
| 160 |
+
The length of historical data to retrieve. Examples: "1d", "5d", "1mo", "3mo", "1y", "5y".
|
| 161 |
+
|
| 162 |
+
Returns:
|
| 163 |
+
--------
|
| 164 |
+
str
|
| 165 |
+
A formatted string showing the last 5 rows of historical prices (Open, High, Low, Close, Volume).
|
| 166 |
+
"""
|
| 167 |
+
try:
|
| 168 |
+
ticker = yf.Ticker(symbol)
|
| 169 |
+
|
| 170 |
+
# Calculate start/end dynamically
|
| 171 |
+
end_date = datetime.today()
|
| 172 |
+
if period.endswith("d"):
|
| 173 |
+
days = int(period[:-1])
|
| 174 |
+
elif period.endswith("mo"):
|
| 175 |
+
days = int(period[:-2]) * 30
|
| 176 |
+
elif period.endswith("y"):
|
| 177 |
+
days = int(period[:-1]) * 365
|
| 178 |
+
else:
|
| 179 |
+
days = 30
|
| 180 |
+
start_date = end_date - timedelta(days=days)
|
| 181 |
+
|
| 182 |
+
data = ticker.history(
|
| 183 |
+
start=start_date.strftime("%Y-%m-%d"),
|
| 184 |
+
end=end_date.strftime("%Y-%m-%d")
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
if data.empty:
|
| 188 |
+
return f"No historical data found for '{symbol}'."
|
| 189 |
+
return f"Historical data for {symbol} ({period}):\n{data.tail(5).to_string()}"
|
| 190 |
+
|
| 191 |
+
except Exception as e:
|
| 192 |
+
return f"Error fetching historical data for '{symbol}': {e}"
|
common/rag/rag.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import shutil
|
| 3 |
+
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
|
| 4 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 5 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 6 |
+
from langchain_community.vectorstores import FAISS
|
| 7 |
+
|
| 8 |
+
DB_NAME = 'healthcare_db'
|
| 9 |
+
DIRECTORY_NAME = "healthcare"
|
| 10 |
+
|
| 11 |
+
class Retriever:
|
| 12 |
+
def __init__(self,
|
| 13 |
+
file_path:str = os.path.join(os.getcwd(), "data"),
|
| 14 |
+
db_path:str = os.path.join(os.getcwd(), "db") ):
|
| 15 |
+
self.directory_path = os.path.join(file_path, DIRECTORY_NAME)
|
| 16 |
+
self.db_path = os.path.join(db_path, DB_NAME)
|
| 17 |
+
self.embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
|
| 18 |
+
self.text_splitter = RecursiveCharacterTextSplitter(
|
| 19 |
+
chunk_size=1024,
|
| 20 |
+
chunk_overlap=200,
|
| 21 |
+
length_function=len,
|
| 22 |
+
# separators=["\n\n", "\n", ".", "!", "?", ",", " ", ""],
|
| 23 |
+
is_separator_regex=False,
|
| 24 |
+
)
|
| 25 |
+
self.retriever = None
|
| 26 |
+
|
| 27 |
+
def load_knowledge_base(self):
|
| 28 |
+
if os.path.exists(self.db_path):
|
| 29 |
+
self.retriever = FAISS.load_local(
|
| 30 |
+
self.db_path,
|
| 31 |
+
self.embeddings,
|
| 32 |
+
allow_dangerous_deserialization=True
|
| 33 |
+
).as_retriever()
|
| 34 |
+
else:
|
| 35 |
+
self.retriever = self._create_knowledge_base()
|
| 36 |
+
|
| 37 |
+
def _create_knowledge_base(self):
|
| 38 |
+
documents = self._load_documents()
|
| 39 |
+
chunks = self._split_documents(documents)
|
| 40 |
+
# embeddings = self._embed_documents(texts)
|
| 41 |
+
vectorstore = FAISS.from_documents(chunks, self.embeddings)
|
| 42 |
+
vectorstore.save_local(self.db_path)
|
| 43 |
+
return vectorstore.as_retriever()
|
| 44 |
+
|
| 45 |
+
def _load_documents(self):
|
| 46 |
+
documents = []
|
| 47 |
+
loader = DirectoryLoader(
|
| 48 |
+
self.directory_path,
|
| 49 |
+
glob="**/*.pdf",
|
| 50 |
+
loader_cls=PyPDFLoader,
|
| 51 |
+
show_progress=True
|
| 52 |
+
)
|
| 53 |
+
documents = loader.load()
|
| 54 |
+
return documents
|
| 55 |
+
|
| 56 |
+
def _split_documents(self, documents):
|
| 57 |
+
chunks = []
|
| 58 |
+
for doc in documents:
|
| 59 |
+
chunks.extend(self.text_splitter.split_documents([doc]))
|
| 60 |
+
return chunks
|
| 61 |
+
|
| 62 |
+
# def _embed_documents(self, texts):
|
| 63 |
+
# return [self.embeddings.embed_query(text.page_content) for text in texts]
|
| 64 |
+
|
| 65 |
+
def retrieve(self, query, k=4):
|
| 66 |
+
"""Retrieve documents without scores (backward compatible)"""
|
| 67 |
+
if not self.retriever:
|
| 68 |
+
self.load_knowledge_base()
|
| 69 |
+
return self.retriever.invoke(query)
|
| 70 |
+
|
| 71 |
+
def retrieve_with_scores(self, query, k=4):
|
| 72 |
+
"""Retrieve documents with similarity scores"""
|
| 73 |
+
if not self.retriever:
|
| 74 |
+
self.load_knowledge_base()
|
| 75 |
+
|
| 76 |
+
# Get the underlying vectorstore from the retriever
|
| 77 |
+
vectorstore = self.retriever.vectorstore
|
| 78 |
+
|
| 79 |
+
# Use similarity_search_with_score to get scores
|
| 80 |
+
# Note: FAISS returns L2 distance, lower is better
|
| 81 |
+
results = vectorstore.similarity_search_with_score(query, k=k)
|
| 82 |
+
|
| 83 |
+
return results
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def update_knowledge_base(self):
|
| 87 |
+
self._create_knowledge_base()
|
| 88 |
+
|
| 89 |
+
def delete_knowledge_base(self):
|
| 90 |
+
if os.path.exists(self.db_path):
|
| 91 |
+
shutil.rmtree(self.db_path)
|
| 92 |
+
|
| 93 |
+
# No cleanup needed for VectorStoreRetriever
|
| 94 |
+
|
common/utility/__init__.py
ADDED
|
File without changes
|
common/utility/embedding_factory.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Union
|
| 3 |
+
# from azure.identity import DefaultAzureCredential
|
| 4 |
+
from langchain_openai import AzureOpenAIEmbeddings, OpenAIEmbeddings
|
| 5 |
+
from langchain_ollama import OllamaEmbeddings
|
| 6 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class EmbeddingFactory:
|
| 10 |
+
"""
|
| 11 |
+
A static utility class to create and return LLM Embedding instances based on the input type.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
@staticmethod
|
| 15 |
+
def get_llm(llm_type: str) -> Union[AzureOpenAIEmbeddings, OpenAIEmbeddings]:
|
| 16 |
+
"""
|
| 17 |
+
Returns an LLM instance based on the specified type.
|
| 18 |
+
|
| 19 |
+
Parameters:
|
| 20 |
+
llm_type (str): The type of LLM to return. Valid values are 'azure' or 'openai'.
|
| 21 |
+
|
| 22 |
+
Returns:
|
| 23 |
+
Union[AzureOpenAIEmbeddings, OpenAIEmbeddings]: The LLM instance.
|
| 24 |
+
"""
|
| 25 |
+
if llm_type.lower() == "azure":
|
| 26 |
+
# Get the Azure Credential
|
| 27 |
+
# credential = DefaultAzureCredential()
|
| 28 |
+
# token=credential.get_token("https://cognitiveservices.azure.com/.default").token
|
| 29 |
+
|
| 30 |
+
# if not token:
|
| 31 |
+
# raise ValueError("Token is required for AzureOpenAIEmbeddings.")
|
| 32 |
+
# return AzureOpenAIEmbeddings(
|
| 33 |
+
# azure_endpoint=os.environ["AZURE_OPENAI_API_URI"],
|
| 34 |
+
# azure_deployment="text-embedding-3-small", #os.environ["AZURE_OPENAI_API_BASE_MODEL"],
|
| 35 |
+
# api_version=os.environ["AZURE_OPENAI_API_VERSION"],
|
| 36 |
+
# api_key=token
|
| 37 |
+
# )
|
| 38 |
+
pass
|
| 39 |
+
elif llm_type.lower() == "openai":
|
| 40 |
+
return OpenAIEmbeddings(
|
| 41 |
+
api_key=os.environ["OPENAI_API_KEY"],
|
| 42 |
+
model="text-embedding-3-large"
|
| 43 |
+
)
|
| 44 |
+
elif llm_type.lower() == "ollama": # must have ollama running locally with the following model
|
| 45 |
+
return OllamaEmbeddings(model="gemma:2b")
|
| 46 |
+
elif llm_type.lower() == "hf": # must have key update in env:HF_TOKEN
|
| 47 |
+
return HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
| 48 |
+
else:
|
| 49 |
+
raise ValueError("Invalid llm_type. Use 'azure' or 'openai'.")
|
common/utility/llm_factory.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tiktoken
|
| 3 |
+
from typing import Any
|
| 4 |
+
from langchain_openai.chat_models import ChatOpenAI, AzureChatOpenAI
|
| 5 |
+
from langchain_openai.embeddings import AzureOpenAIEmbeddings, OpenAIEmbeddings
|
| 6 |
+
# from azure.identity import DefaultAzureCredential
|
| 7 |
+
from huggingface_hub import login
|
| 8 |
+
from langchain_huggingface import ChatHuggingFace, HuggingFaceEmbeddings
|
| 9 |
+
from langchain_ollama import ChatOllama, OllamaEmbeddings
|
| 10 |
+
from langchain_groq import ChatGroq
|
| 11 |
+
# from langchain_openai import OpenAIEmbeddings
|
| 12 |
+
|
| 13 |
+
class LLMFactory:
|
| 14 |
+
"""
|
| 15 |
+
Factory class to provide LLM and embedding model instances for different providers.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
@staticmethod
|
| 19 |
+
def get_llm(provider: str, **kwargs) -> Any:
|
| 20 |
+
"""
|
| 21 |
+
Returns a chat/completion LLM instance based on the provider.
|
| 22 |
+
Supported providers: openai, azureopenai, huggingface, ollama, groq
|
| 23 |
+
"""
|
| 24 |
+
if provider == "openai":
|
| 25 |
+
# OpenAI Chat Model
|
| 26 |
+
return ChatOpenAI(
|
| 27 |
+
openai_api_key=kwargs.get("api_key", os.environ.get("OPENAI_API_KEY")),
|
| 28 |
+
model_name=kwargs.get("model_name", "gpt-4")
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# elif provider == "azureopenai":
|
| 32 |
+
# # Azure OpenAI Chat Model using Azure Identity for token
|
| 33 |
+
# credential = DefaultAzureCredential()
|
| 34 |
+
# token = credential.get_token("https://cognitiveservices.azure.com/.default").token
|
| 35 |
+
# if not token:
|
| 36 |
+
# raise ValueError("Token is required for AzureChatOpenAI.")
|
| 37 |
+
# return AzureChatOpenAI(
|
| 38 |
+
# azure_endpoint=kwargs["endpoint"],
|
| 39 |
+
# azure_deployment=kwargs.get("deployment_name", "gpt-4"),
|
| 40 |
+
# api_version=kwargs["api_version"],
|
| 41 |
+
# api_key=token
|
| 42 |
+
# )
|
| 43 |
+
|
| 44 |
+
# pip install langchain langchain-huggingface huggingface_hub
|
| 45 |
+
elif provider == "huggingface":
|
| 46 |
+
# If using a private model or endpoint, authenticate
|
| 47 |
+
login(token=kwargs.get("api_key", os.environ.get("HF_TOKEN")))
|
| 48 |
+
|
| 49 |
+
return ChatHuggingFace(
|
| 50 |
+
repo_id=kwargs.get("model_name", "mistralai/Mistral-Nemo-Instruct-2407"), # Or any other chat-friendly model
|
| 51 |
+
task="text-generation",
|
| 52 |
+
model_kwargs={
|
| 53 |
+
"temperature": 0.7,
|
| 54 |
+
"max_new_tokens": 256
|
| 55 |
+
}
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
elif provider == "ollama":
|
| 59 |
+
# Ollama local model
|
| 60 |
+
return ChatOllama(
|
| 61 |
+
model=kwargs.get("model_name", "gemma:2b"),
|
| 62 |
+
temperature=0
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
elif provider == "groq":
|
| 66 |
+
# Groq LLM
|
| 67 |
+
return ChatGroq(
|
| 68 |
+
model=kwargs.get("model_name", "Gemma2-9b-It"),
|
| 69 |
+
max_tokens=512,
|
| 70 |
+
api_key=kwargs.get("api_key", os.environ.get("GROQ_API_KEY"))
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
else:
|
| 74 |
+
raise ValueError(f"Unsupported provider: {provider}")
|
| 75 |
+
|
| 76 |
+
@staticmethod
|
| 77 |
+
def get_embedding_model(provider: str, **kwargs) -> Any:
|
| 78 |
+
"""
|
| 79 |
+
Returns an embedding model instance based on the provider.
|
| 80 |
+
Supported providers: openai, huggingface
|
| 81 |
+
"""
|
| 82 |
+
if provider == "openai":
|
| 83 |
+
return OpenAIEmbeddings(
|
| 84 |
+
model=kwargs.get("model_name", "text-embedding-3-large"),
|
| 85 |
+
openai_api_key=kwargs.get("api_key", os.environ.get("OPENAI_API_KEY"))
|
| 86 |
+
)
|
| 87 |
+
# if provider == "azureopenai":
|
| 88 |
+
# # Get the Azure Credential
|
| 89 |
+
# credential = DefaultAzureCredential()
|
| 90 |
+
# token=credential.get_token("https://cognitiveservices.azure.com/.default").token
|
| 91 |
+
|
| 92 |
+
# if not token:
|
| 93 |
+
# raise ValueError("Token is required for AzureOpenAIEmbeddings.")
|
| 94 |
+
# return AzureOpenAIEmbeddings(
|
| 95 |
+
# azure_endpoint=os.environ["AZURE_OPENAI_API_URI"],
|
| 96 |
+
# azure_deployment=kwargs.get("azure_deployment", "text-embedding-3-large"),
|
| 97 |
+
# api_version=os.environ["AZURE_OPENAI_API_VERSION"],
|
| 98 |
+
# api_key=token
|
| 99 |
+
# )
|
| 100 |
+
elif provider == "huggingface":
|
| 101 |
+
# If using a private model or endpoint, authenticate
|
| 102 |
+
login(token=kwargs.get("api_key", os.environ.get("HF_TOKEN")))
|
| 103 |
+
|
| 104 |
+
return HuggingFaceEmbeddings(
|
| 105 |
+
model_name=kwargs.get("model_name", "all-MiniLM-L6-v2")
|
| 106 |
+
)
|
| 107 |
+
elif provider == "groq":
|
| 108 |
+
raise ValueError(f"No embedding support from the provider: {provider}")
|
| 109 |
+
elif provider == "ollama":
|
| 110 |
+
return OllamaEmbeddings(model=kwargs.get("model_name", "gemma:2b"))
|
| 111 |
+
else:
|
| 112 |
+
raise ValueError(f"Unsupported embedding provider: {provider}")
|
| 113 |
+
|
| 114 |
+
@staticmethod
|
| 115 |
+
def num_tokens_from_messages(messages) -> int:
|
| 116 |
+
"""
|
| 117 |
+
Return the number of tokens used by a list of messages.
|
| 118 |
+
Adapted from the OpenAI cookbook token counter.
|
| 119 |
+
"""
|
| 120 |
+
encoding = tiktoken.encoding_for_model("gpt-3.5-turbo")
|
| 121 |
+
tokens_per_message = 3 # <|start|>, role, <|end|>
|
| 122 |
+
num_tokens = 0
|
| 123 |
+
|
| 124 |
+
for message in messages:
|
| 125 |
+
num_tokens += tokens_per_message
|
| 126 |
+
for key, value in message.items():
|
| 127 |
+
num_tokens += len(encoding.encode(value))
|
| 128 |
+
|
| 129 |
+
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
|
| 130 |
+
return num_tokens
|
common/utility/llm_factory2.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tiktoken
|
| 3 |
+
from typing import Union
|
| 4 |
+
# from azure.identity import DefaultAzureCredential
|
| 5 |
+
from langchain_openai.chat_models import AzureChatOpenAI, ChatOpenAI
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class LLMFactory:
|
| 9 |
+
"""
|
| 10 |
+
A static utility class to create and return LLM instances based on the input type.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
@staticmethod
|
| 14 |
+
def get_llm(llm_type: str) -> Union[AzureChatOpenAI, ChatOpenAI]:
|
| 15 |
+
"""
|
| 16 |
+
Returns an LLM instance based on the specified type.
|
| 17 |
+
|
| 18 |
+
Parameters:
|
| 19 |
+
llm_type (str): The type of LLM to return. Valid values are 'azure' or 'openai'.
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
Union[AzureChatOpenAI, ChatOpenAI]: The LLM instance.
|
| 23 |
+
"""
|
| 24 |
+
if llm_type.lower() == "azure":
|
| 25 |
+
# # Get the Azure Credential
|
| 26 |
+
# credential = DefaultAzureCredential()
|
| 27 |
+
# token=credential.get_token("https://cognitiveservices.azure.com/.default").token
|
| 28 |
+
|
| 29 |
+
# if not token:
|
| 30 |
+
# raise ValueError("Token is required for AzureChatOpenAI.")
|
| 31 |
+
# return AzureChatOpenAI(
|
| 32 |
+
# azure_endpoint=os.environ["AZURE_OPENAI_API_URI"],
|
| 33 |
+
# azure_deployment=os.environ["AZURE_OPENAI_API_BASE_MODEL"],
|
| 34 |
+
# api_version=os.environ["AZURE_OPENAI_API_VERSION"],
|
| 35 |
+
# api_key=token
|
| 36 |
+
# )
|
| 37 |
+
pass
|
| 38 |
+
elif llm_type.lower() == "openai":
|
| 39 |
+
return ChatOpenAI(
|
| 40 |
+
api_key=os.environ["OPENAI_API_KEY"],
|
| 41 |
+
model_name="gpt-4"
|
| 42 |
+
)
|
| 43 |
+
elif llm_type.lower() == "openai_chat":
|
| 44 |
+
return ChatOpenAI(
|
| 45 |
+
api_key=os.environ["OPENAI_API_KEY"],
|
| 46 |
+
model_name="gpt-4"
|
| 47 |
+
)
|
| 48 |
+
else:
|
| 49 |
+
raise ValueError("Invalid llm_type. Use 'azure' or 'openai'.")
|
| 50 |
+
|
| 51 |
+
@staticmethod
|
| 52 |
+
def num_tokens_from_messages(messages):
|
| 53 |
+
|
| 54 |
+
"""
|
| 55 |
+
Return the number of tokens used by a list of messages.
|
| 56 |
+
Adapted from the Open AI cookbook token counter
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
encoding = tiktoken.encoding_for_model("gpt-3.5-turbo")
|
| 60 |
+
|
| 61 |
+
# Each message is sandwiched with <|start|>role and <|end|>
|
| 62 |
+
# Hence, messages look like: <|start|>system or user or assistant{message}<|end|>
|
| 63 |
+
|
| 64 |
+
tokens_per_message = 3 # token1:<|start|>, token2:system(or user or assistant), token3:<|end|>
|
| 65 |
+
|
| 66 |
+
num_tokens = 0
|
| 67 |
+
|
| 68 |
+
for message in messages:
|
| 69 |
+
num_tokens += tokens_per_message
|
| 70 |
+
for key, value in message.items():
|
| 71 |
+
num_tokens += len(encoding.encode(value))
|
| 72 |
+
|
| 73 |
+
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
|
| 74 |
+
|
| 75 |
+
return num_tokens
|
common/utility/logger.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import datetime
|
| 3 |
+
|
| 4 |
+
def log_call(func):
|
| 5 |
+
"""
|
| 6 |
+
A decorator that logs when a function is called and when it finishes.
|
| 7 |
+
"""
|
| 8 |
+
@functools.wraps(func)
|
| 9 |
+
def wrapper(*args, **kwargs):
|
| 10 |
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 11 |
+
arg_list = ", ".join(
|
| 12 |
+
[repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
|
| 13 |
+
)
|
| 14 |
+
print(f"[{timestamp}] 🚀 Calling: {func.__name__}({arg_list})")
|
| 15 |
+
try:
|
| 16 |
+
result = func(*args, **kwargs)
|
| 17 |
+
print(f"[{timestamp}] ✅ Finished: {func.__name__}")
|
| 18 |
+
return result
|
| 19 |
+
except Exception as e:
|
| 20 |
+
print(f"[{timestamp}] ❌ Error in {func.__name__}: {e}")
|
| 21 |
+
raise
|
| 22 |
+
return wrapper
|
pyproject.toml
CHANGED
|
@@ -67,6 +67,7 @@ dependencies = [
|
|
| 67 |
"logfire",
|
| 68 |
"serpapi",
|
| 69 |
"smithery>=0.4.4",
|
|
|
|
| 70 |
|
| 71 |
# =======================
|
| 72 |
# WEB SCRAPING
|
|
@@ -100,6 +101,7 @@ dependencies = [
|
|
| 100 |
# =======================
|
| 101 |
"scikit-learn>=1.7.2",
|
| 102 |
"huggingface_hub<=1.1.4",
|
|
|
|
| 103 |
|
| 104 |
# =======================
|
| 105 |
# IPYNB SUPPORT
|
|
|
|
| 67 |
"logfire",
|
| 68 |
"serpapi",
|
| 69 |
"smithery>=0.4.4",
|
| 70 |
+
"sendgrid",
|
| 71 |
|
| 72 |
# =======================
|
| 73 |
# WEB SCRAPING
|
|
|
|
| 101 |
# =======================
|
| 102 |
"scikit-learn>=1.7.2",
|
| 103 |
"huggingface_hub<=1.1.4",
|
| 104 |
+
"datasets>=4.4.1",
|
| 105 |
|
| 106 |
# =======================
|
| 107 |
# IPYNB SUPPORT
|
run.py
CHANGED
|
@@ -1,11 +1,215 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Universal App Launcher for AgenticAI Projects
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
python run.py <app_name> [--port PORT] [--help]
|
| 7 |
+
|
| 8 |
+
Examples:
|
| 9 |
+
python run.py healthcare
|
| 10 |
+
python run.py deep-research --port 8502
|
| 11 |
+
python run.py stock-advisor
|
| 12 |
+
python run.py --list
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import sys
|
| 16 |
+
import os
|
| 17 |
+
import subprocess
|
| 18 |
+
import argparse
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Dict, Optional
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# App registry - maps app names to their paths and entry points
|
| 24 |
+
APP_REGISTRY: Dict[str, Dict[str, str]] = {
|
| 25 |
+
"healthcare": {
|
| 26 |
+
"path": "src/healthcare-assistant",
|
| 27 |
+
"entry": "app.py",
|
| 28 |
+
"description": "Healthcare Assistant - Medical information with RAG and web search"
|
| 29 |
+
},
|
| 30 |
+
"deep-research": {
|
| 31 |
+
"path": "src/deep-research",
|
| 32 |
+
"entry": "app.py",
|
| 33 |
+
"description": "Deep Research AI - Comprehensive research assistant"
|
| 34 |
+
},
|
| 35 |
+
"stock-advisor": {
|
| 36 |
+
"path": "src/stock-advisor",
|
| 37 |
+
"entry": "app.py",
|
| 38 |
+
"description": "Stock Advisor - Financial analysis and stock recommendations"
|
| 39 |
+
},
|
| 40 |
+
"travel-agent": {
|
| 41 |
+
"path": "src/travel-agent",
|
| 42 |
+
"entry": "app.py",
|
| 43 |
+
"description": "Travel Agent - Trip planning and travel recommendations"
|
| 44 |
+
},
|
| 45 |
+
"trip-planner": {
|
| 46 |
+
"path": "src/trip-planner",
|
| 47 |
+
"entry": "app.py",
|
| 48 |
+
"description": "Trip Planner - Detailed trip itinerary planning"
|
| 49 |
+
},
|
| 50 |
+
"chatbot": {
|
| 51 |
+
"path": "src/chatbot",
|
| 52 |
+
"entry": "app.py",
|
| 53 |
+
"description": "General Chatbot - Multi-purpose conversational AI"
|
| 54 |
+
},
|
| 55 |
+
"accessibility": {
|
| 56 |
+
"path": "src/accessibility",
|
| 57 |
+
"entry": "app.py",
|
| 58 |
+
"description": "Accessibility Tools - Assistive technology applications"
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def print_banner():
|
| 64 |
+
"""Print a nice banner."""
|
| 65 |
+
print("=" * 70)
|
| 66 |
+
print("🚀 AgenticAI Projects Launcher".center(70))
|
| 67 |
+
print("=" * 70)
|
| 68 |
+
print()
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def list_apps():
|
| 72 |
+
"""List all available apps."""
|
| 73 |
+
print_banner()
|
| 74 |
+
print("Available Applications:\n")
|
| 75 |
+
|
| 76 |
+
max_name_len = max(len(name) for name in APP_REGISTRY.keys())
|
| 77 |
+
|
| 78 |
+
for name, config in sorted(APP_REGISTRY.items()):
|
| 79 |
+
print(f" {name.ljust(max_name_len + 2)} - {config['description']}")
|
| 80 |
+
|
| 81 |
+
print("\n" + "=" * 70)
|
| 82 |
+
print("\nUsage: python run.py <app_name> [--port PORT]")
|
| 83 |
+
print("Example: python run.py healthcare --port 8501\n")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def validate_app(app_name: str) -> Optional[Dict[str, str]]:
|
| 87 |
+
"""
|
| 88 |
+
Validate that the app exists and its files are present.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
app_name: Name of the app to validate
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
App configuration dict if valid, None otherwise
|
| 95 |
+
"""
|
| 96 |
+
if app_name not in APP_REGISTRY:
|
| 97 |
+
print(f"❌ Error: Unknown app '{app_name}'")
|
| 98 |
+
print(f"\nAvailable apps: {', '.join(sorted(APP_REGISTRY.keys()))}")
|
| 99 |
+
print("\nRun 'python run.py --list' to see all available apps.")
|
| 100 |
+
return None
|
| 101 |
+
|
| 102 |
+
config = APP_REGISTRY[app_name]
|
| 103 |
+
project_root = Path(__file__).parent
|
| 104 |
+
app_path = project_root / config["path"] / config["entry"]
|
| 105 |
+
|
| 106 |
+
if not app_path.exists():
|
| 107 |
+
print(f"❌ Error: App file not found at {app_path}")
|
| 108 |
+
return None
|
| 109 |
+
|
| 110 |
+
return config
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def launch_app(app_name: str, port: Optional[int] = None):
|
| 114 |
+
"""
|
| 115 |
+
Launch a Streamlit app.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
app_name: Name of the app to launch
|
| 119 |
+
port: Optional port number (default: 8501)
|
| 120 |
+
"""
|
| 121 |
+
config = validate_app(app_name)
|
| 122 |
+
if not config:
|
| 123 |
+
sys.exit(1)
|
| 124 |
+
|
| 125 |
+
project_root = Path(__file__).parent
|
| 126 |
+
app_dir = project_root / config["path"]
|
| 127 |
+
app_file = config["entry"]
|
| 128 |
+
|
| 129 |
+
print_banner()
|
| 130 |
+
print(f"📱 Launching: {config['description']}")
|
| 131 |
+
print(f"📂 Location: {config['path']}")
|
| 132 |
+
print(f"🌐 Entry Point: {app_file}")
|
| 133 |
+
|
| 134 |
+
# Build streamlit command
|
| 135 |
+
cmd = ["streamlit", "run", app_file]
|
| 136 |
+
|
| 137 |
+
# Add port if specified
|
| 138 |
+
if port:
|
| 139 |
+
cmd.extend(["--server.port", str(port)])
|
| 140 |
+
print(f"🔌 Port: {port}")
|
| 141 |
+
else:
|
| 142 |
+
print(f"🔌 Port: 8501 (default)")
|
| 143 |
+
|
| 144 |
+
print("\n" + "=" * 70)
|
| 145 |
+
print("\n🎯 Starting application...\n")
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
# Change to app directory and run
|
| 149 |
+
os.chdir(app_dir)
|
| 150 |
+
subprocess.run(cmd)
|
| 151 |
+
except KeyboardInterrupt:
|
| 152 |
+
print("\n\n👋 Application stopped by user")
|
| 153 |
+
except FileNotFoundError:
|
| 154 |
+
print("\n❌ Error: Streamlit not found. Please install it:")
|
| 155 |
+
print(" pip install streamlit")
|
| 156 |
+
sys.exit(1)
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print(f"\n❌ Error launching app: {e}")
|
| 159 |
+
sys.exit(1)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def main():
|
| 163 |
+
"""Main entry point."""
|
| 164 |
+
parser = argparse.ArgumentParser(
|
| 165 |
+
description="Universal launcher for AgenticAI project applications",
|
| 166 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 167 |
+
epilog="""
|
| 168 |
+
Examples:
|
| 169 |
+
python run.py healthcare # Launch healthcare chatbot
|
| 170 |
+
python run.py deep-research --port 8502 # Launch on custom port
|
| 171 |
+
python run.py --list # List all available apps
|
| 172 |
+
|
| 173 |
+
Available Apps:
|
| 174 |
+
""" + "\n ".join(f"{name}: {config['description']}"
|
| 175 |
+
for name, config in sorted(APP_REGISTRY.items()))
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
parser.add_argument(
|
| 179 |
+
"app_name",
|
| 180 |
+
nargs="?",
|
| 181 |
+
help="Name of the app to launch"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
parser.add_argument(
|
| 185 |
+
"--port",
|
| 186 |
+
type=int,
|
| 187 |
+
help="Port number for Streamlit server (default: 8501)"
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
parser.add_argument(
|
| 191 |
+
"--list",
|
| 192 |
+
action="store_true",
|
| 193 |
+
help="List all available apps"
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
args = parser.parse_args()
|
| 197 |
+
|
| 198 |
+
# Handle --list flag
|
| 199 |
+
if args.list:
|
| 200 |
+
list_apps()
|
| 201 |
+
return
|
| 202 |
+
|
| 203 |
+
# Require app name if not listing
|
| 204 |
+
if not args.app_name:
|
| 205 |
+
parser.print_help()
|
| 206 |
+
print("\n")
|
| 207 |
+
list_apps()
|
| 208 |
+
return
|
| 209 |
+
|
| 210 |
+
# Launch the app
|
| 211 |
+
launch_app(args.app_name, args.port)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
if __name__ == "__main__":
|
| 215 |
+
main()
|
src/chatbot/Dockerfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 4 |
+
DEBIAN_FRONTEND=noninteractive \
|
| 5 |
+
PYTHONPATH=/app:/app/common:$PYTHONPATH
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
# System deps
|
| 10 |
+
RUN apt-get update && apt-get install -y \
|
| 11 |
+
git build-essential curl \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
# Install uv
|
| 15 |
+
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 16 |
+
ENV PATH="/root/.local/bin:$PATH"
|
| 17 |
+
|
| 18 |
+
# Copy project metadata
|
| 19 |
+
COPY pyproject.toml .
|
| 20 |
+
COPY uv.lock .
|
| 21 |
+
|
| 22 |
+
# Copy required folders
|
| 23 |
+
COPY common/ ./common/
|
| 24 |
+
COPY src/chatbot/ ./src/chatbot/
|
| 25 |
+
|
| 26 |
+
# Install dependencies using uv, then export and install with pip to system
|
| 27 |
+
RUN uv sync --frozen --no-dev && \
|
| 28 |
+
uv pip install -e . --system
|
| 29 |
+
|
| 30 |
+
# Copy entry point
|
| 31 |
+
COPY run.py .
|
| 32 |
+
|
| 33 |
+
EXPOSE 7860
|
| 34 |
+
|
| 35 |
+
CMD ["python", "run.py", "chatbot", "--port", "7860"]
|
src/chatbot/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AI Chatbot
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
sdk_version: "0.0.1"
|
| 8 |
+
app_file: ui/app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# AI Chatbot
|
| 13 |
+
|
| 14 |
+
This is an experimental chatbot for chatting with AI. It is equipped with agents & tools to give you realtime data from the web. It uses **OpenAI - SDK** and **OpenAI - Agents**.
|
| 15 |
+
|
| 16 |
+
## Features
|
| 17 |
+
- Predefined prompts for quick analysis
|
| 18 |
+
- Chat interface with AI responses
|
| 19 |
+
- Enter key support and responsive design
|
| 20 |
+
- Latest messages appear on top
|
| 21 |
+
|
| 22 |
+
## Usage
|
| 23 |
+
1. Type a message or select a predefined prompt
|
| 24 |
+
2. Press **Enter** or click **Send**
|
| 25 |
+
3. AI responses appear instantly in the chat interface
|
| 26 |
+
|
| 27 |
+
## Supported APIs
|
| 28 |
+
- OpenAI
|
| 29 |
+
- Google
|
| 30 |
+
- GROQ
|
| 31 |
+
- SERPER
|
| 32 |
+
- News API
|
| 33 |
+
|
| 34 |
+
## Notes
|
| 35 |
+
- Make sure your API keys are configured in the Space secrets
|
| 36 |
+
- Built using Streamlit and deployed as a Docker Space
|
| 37 |
+
|
| 38 |
+
## References
|
| 39 |
+
|
| 40 |
+
https://openai.github.io/openai-agents-python/
|
| 41 |
+
|
| 42 |
+
https://github.com/openai/openai-agents-python/tree/main/examples/mcp
|
| 43 |
+
|
| 44 |
+
## Project Folder Structure
|
| 45 |
+
|
| 46 |
+
```
|
| 47 |
+
chatbot/
|
| 48 |
+
├── ui/
|
| 49 |
+
│ ├── __init__.py # Package initialization
|
| 50 |
+
│ └── app.py # Main Streamlit chatbot interface
|
| 51 |
+
├── appagents/
|
| 52 |
+
│ ├── __init__.py # Package initialization
|
| 53 |
+
│ ├── OrchestratorAgent.py # Main orchestrator - coordinates all agents
|
| 54 |
+
│ ├── FinancialAgent.py # Financial data and analysis agent
|
| 55 |
+
│ ├── NewsAgent.py # News retrieval and summarization agent
|
| 56 |
+
│ ├── SearchAgent.py # General web search agent
|
| 57 |
+
│ └── InputValidationAgent.py # Input validation and sanitization agent
|
| 58 |
+
├── core/
|
| 59 |
+
│ ├── __init__.py # Package initialization
|
| 60 |
+
│ └── logger.py # Centralized logging configuration
|
| 61 |
+
├── tools/
|
| 62 |
+
│ ├── __init__.py # Package initialization
|
| 63 |
+
│ ├── google_tools.py # Google search API wrapper
|
| 64 |
+
│ ├── yahoo_tools.py # Yahoo Finance API wrapper
|
| 65 |
+
│ ├── news_tools.py # News API wrapper
|
| 66 |
+
│ └── time_tools.py # Time-related utility functions
|
| 67 |
+
├── prompts/
|
| 68 |
+
│ ├── economic_news.txt # Prompt for economic news analysis
|
| 69 |
+
│ ├── market_sentiment.txt # Prompt for market sentiment analysis
|
| 70 |
+
│ ├── news_headlines.txt # Prompt for news headline summarization
|
| 71 |
+
│ ├── trade_recommendation.txt # Prompt for trade recommendations
|
| 72 |
+
│ └── upcoming_earnings.txt # Prompt for upcoming earnings alerts
|
| 73 |
+
├── Dockerfile # Docker configuration for container deployment
|
| 74 |
+
├── pyproject.toml # Project metadata and dependencies (copied from root)
|
| 75 |
+
├── uv.lock # Locked dependency versions (copied from root)
|
| 76 |
+
├── README.md # Project documentation
|
| 77 |
+
└── run.py # Script to run the application locally
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
## File Descriptions
|
| 81 |
+
|
| 82 |
+
### UI Layer (`ui/`)
|
| 83 |
+
- **app.py** - Main Streamlit chatbot interface that provides:
|
| 84 |
+
- Chat message display with user and AI messages
|
| 85 |
+
- Text input for user queries
|
| 86 |
+
- Predefined prompt buttons for quick analysis
|
| 87 |
+
- Real-time AI responses
|
| 88 |
+
- Support for Enter key submission
|
| 89 |
+
- Responsive design with latest messages appearing first
|
| 90 |
+
|
| 91 |
+
### Agents (`appagents/`)
|
| 92 |
+
- **OrchestratorAgent.py** - Main orchestrator that:
|
| 93 |
+
- Coordinates communication between all specialized agents
|
| 94 |
+
- Routes user queries to appropriate agents
|
| 95 |
+
- Manages conversation flow and context
|
| 96 |
+
- Integrates tool responses
|
| 97 |
+
|
| 98 |
+
- **FinancialAgent.py** - Financial data and analysis:
|
| 99 |
+
- Retrieves stock prices and financial metrics
|
| 100 |
+
- Performs financial analysis using Yahoo Finance API
|
| 101 |
+
- Provides market insights and recommendations
|
| 102 |
+
- Integrates with yahoo_tools for data fetching
|
| 103 |
+
|
| 104 |
+
- **NewsAgent.py** - News retrieval and summarization:
|
| 105 |
+
- Fetches latest news articles
|
| 106 |
+
- Summarizes news content
|
| 107 |
+
- Integrates with News API for real-time updates
|
| 108 |
+
- Provides news-based market insights
|
| 109 |
+
|
| 110 |
+
- **SearchAgent.py** - General web search:
|
| 111 |
+
- Performs web searches for general queries
|
| 112 |
+
- Integrates with Google Search / Serper API
|
| 113 |
+
- Returns relevant search results
|
| 114 |
+
- Supports multi-source data gathering
|
| 115 |
+
|
| 116 |
+
- **InputValidationAgent.py** - Input validation:
|
| 117 |
+
- Sanitizes user input
|
| 118 |
+
- Validates query format and content
|
| 119 |
+
- Prevents malicious inputs
|
| 120 |
+
- Ensures appropriate content
|
| 121 |
+
|
| 122 |
+
### Core Utilities (`core/`)
|
| 123 |
+
- **logger.py** - Centralized logging configuration:
|
| 124 |
+
- Provides consistent logging across agents
|
| 125 |
+
- Handles different log levels
|
| 126 |
+
- Formats log messages for clarity
|
| 127 |
+
|
| 128 |
+
### Tools (`tools/`)
|
| 129 |
+
- **google_tools.py** - Google Search API wrapper:
|
| 130 |
+
- Executes web searches via Google Search / Serper API
|
| 131 |
+
- Parses and returns search results
|
| 132 |
+
- Handles API authentication
|
| 133 |
+
|
| 134 |
+
- **yahoo_tools.py** - Yahoo Finance API integration:
|
| 135 |
+
- Retrieves stock price data
|
| 136 |
+
- Fetches financial metrics and indicators
|
| 137 |
+
- Provides historical price data
|
| 138 |
+
- Returns earnings information
|
| 139 |
+
|
| 140 |
+
- **news_tools.py** - News API integration:
|
| 141 |
+
- Fetches latest news articles
|
| 142 |
+
- Filters news by category and keywords
|
| 143 |
+
- Returns news headlines and summaries
|
| 144 |
+
- Provides market-related news feeds
|
| 145 |
+
|
| 146 |
+
- **time_tools.py** - Time utility functions:
|
| 147 |
+
- Provides current time information
|
| 148 |
+
- Formats timestamps
|
| 149 |
+
- Handles timezone conversions
|
| 150 |
+
|
| 151 |
+
### Prompts (`prompts/`)
|
| 152 |
+
Predefined prompts for specialized analysis:
|
| 153 |
+
- **economic_news.txt** - Analyzes economic news and implications
|
| 154 |
+
- **market_sentiment.txt** - Analyzes market sentiment trends
|
| 155 |
+
- **news_headlines.txt** - Summarizes and explains news headlines
|
| 156 |
+
- **trade_recommendation.txt** - Provides trading recommendations
|
| 157 |
+
- **upcoming_earnings.txt** - Alerts about upcoming earnings reports
|
| 158 |
+
|
| 159 |
+
### Configuration Files
|
| 160 |
+
- **Dockerfile** - Container deployment:
|
| 161 |
+
- Builds Docker image with Python 3.12
|
| 162 |
+
- Installs dependencies using `uv`
|
| 163 |
+
- Sets up Streamlit server on port 8501
|
| 164 |
+
- Configures PYTHONPATH for module imports
|
| 165 |
+
|
| 166 |
+
- **pyproject.toml** - Project metadata:
|
| 167 |
+
- Package name: "agents"
|
| 168 |
+
- Python version requirement: 3.12
|
| 169 |
+
- Lists all dependencies (OpenAI, LangChain, Streamlit, etc.)
|
| 170 |
+
|
| 171 |
+
- **uv.lock** - Dependency lock file:
|
| 172 |
+
- Ensures reproducible builds
|
| 173 |
+
- Pins exact versions of all dependencies
|
| 174 |
+
|
| 175 |
+
## Key Technologies
|
| 176 |
+
|
| 177 |
+
| Component | Technology | Purpose |
|
| 178 |
+
|-----------|-----------|---------|
|
| 179 |
+
| LLM Framework | OpenAI Agents | Multi-agent orchestration |
|
| 180 |
+
| Chat Interface | Streamlit | User interaction and display |
|
| 181 |
+
| Web Search | Google Search / Serper API | Web search results |
|
| 182 |
+
| Financial Data | Yahoo Finance API | Stock prices and metrics |
|
| 183 |
+
| News Data | News API | Latest news articles |
|
| 184 |
+
| Async Operations | AsyncIO | Parallel agent execution |
|
| 185 |
+
| Dependencies | UV | Fast Python package management |
|
| 186 |
+
| Containerization | Docker | Cloud deployment |
|
| 187 |
+
|
| 188 |
+
## Predefined Prompts
|
| 189 |
+
|
| 190 |
+
The chatbot includes quick-access buttons for common analysis:
|
| 191 |
+
|
| 192 |
+
1. **Economic News** - Analyzes current economic trends and news
|
| 193 |
+
2. **Market Sentiment** - Provides market sentiment analysis
|
| 194 |
+
3. **News Headlines** - Summarizes latest news headlines
|
| 195 |
+
4. **Trade Recommendation** - Suggests trading strategies
|
| 196 |
+
5. **Upcoming Earnings** - Lists upcoming company earnings
|
| 197 |
+
|
| 198 |
+
## Running Locally
|
| 199 |
+
|
| 200 |
+
```bash
|
| 201 |
+
# Install dependencies
|
| 202 |
+
uv sync
|
| 203 |
+
|
| 204 |
+
# Set environment variables defined in .env.name file
|
| 205 |
+
export OPENAI_API_KEY="your-key"
|
| 206 |
+
export SERPER_API_KEY="your-key"
|
| 207 |
+
export NEWS_API_KEY="your-key"
|
| 208 |
+
|
| 209 |
+
# Run the Streamlit app
|
| 210 |
+
python run.py
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
## Deployment
|
| 214 |
+
|
| 215 |
+
The project is deployed on Hugging Face Spaces as a Docker container:
|
| 216 |
+
- **Space**: https://huggingface.co/spaces/mishrabp/chatbot-app
|
| 217 |
+
- **URL**: https://mishrabp-chatbot-app.hf.space
|
| 218 |
+
- **Trigger**: Automatic deployment on push to `main` branch
|
| 219 |
+
- **Configuration**: `.github/workflows/chatbot-app-hf.yml`
|
src/chatbot/app.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import os
|
| 3 |
+
import glob
|
| 4 |
+
import asyncio
|
| 5 |
+
import sys
|
| 6 |
+
import uuid
|
| 7 |
+
|
| 8 |
+
# Add project root
|
| 9 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".")))
|
| 10 |
+
|
| 11 |
+
from appagents.OrchestratorAgent import OrchestratorAgent
|
| 12 |
+
from agents import Runner, trace, SQLiteSession
|
| 13 |
+
from agents.exceptions import InputGuardrailTripwireTriggered
|
| 14 |
+
|
| 15 |
+
# -----------------------------
|
| 16 |
+
# Configuration & Utils
|
| 17 |
+
# -----------------------------
|
| 18 |
+
st.set_page_config(
|
| 19 |
+
page_title="AI Assistant",
|
| 20 |
+
layout="wide",
|
| 21 |
+
page_icon="🤖"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
def load_prompts(folder="prompts"):
|
| 25 |
+
prompts = []
|
| 26 |
+
prompt_labels = []
|
| 27 |
+
if os.path.exists(folder):
|
| 28 |
+
for file_path in glob.glob(os.path.join(folder, "*.txt")):
|
| 29 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 30 |
+
content = f.read().strip()
|
| 31 |
+
if content:
|
| 32 |
+
prompts.append(content)
|
| 33 |
+
prompt_labels.append(os.path.basename(file_path).replace("_", " ").replace(".txt", "").title())
|
| 34 |
+
return prompts, prompt_labels
|
| 35 |
+
|
| 36 |
+
prompts, prompt_labels = load_prompts()
|
| 37 |
+
|
| 38 |
+
# -----------------------------
|
| 39 |
+
# Session State
|
| 40 |
+
# -----------------------------
|
| 41 |
+
if "messages" not in st.session_state:
|
| 42 |
+
st.session_state.messages = []
|
| 43 |
+
|
| 44 |
+
if "ai_session_id" not in st.session_state:
|
| 45 |
+
st.session_state.ai_session_id = str(uuid.uuid4())
|
| 46 |
+
|
| 47 |
+
# Persistent SQLite session
|
| 48 |
+
if "ai_session" not in st.session_state:
|
| 49 |
+
st.session_state.ai_session = SQLiteSession(f"conversation_{st.session_state.ai_session_id}.db")
|
| 50 |
+
|
| 51 |
+
session = st.session_state.ai_session
|
| 52 |
+
|
| 53 |
+
# -----------------------------
|
| 54 |
+
# Premium Styling
|
| 55 |
+
# -----------------------------
|
| 56 |
+
st.markdown("""
|
| 57 |
+
<style>
|
| 58 |
+
/* Global Cleanliness */
|
| 59 |
+
.stApp {
|
| 60 |
+
background-color: #f8f9fa;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.block-container {
|
| 64 |
+
padding-top: 1rem !important;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* Remove default header decoration */
|
| 68 |
+
header[data-testid="stHeader"] {
|
| 69 |
+
background-color: transparent;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/* Typography */
|
| 73 |
+
h1, h2, h3 {
|
| 74 |
+
font-family: 'Inter', sans-serif;
|
| 75 |
+
font-weight: 600;
|
| 76 |
+
color: #1a1a1a;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/* Hero Section */
|
| 80 |
+
.hero-container {
|
| 81 |
+
position: sticky;
|
| 82 |
+
top: 0;
|
| 83 |
+
z-index: 1000;
|
| 84 |
+
padding: 2rem 0;
|
| 85 |
+
text-align: center;
|
| 86 |
+
margin-bottom: 2rem;
|
| 87 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 88 |
+
border-radius: 0 0 16px 16px; /* Rounded only at bottom to look like a header */
|
| 89 |
+
color: white;
|
| 90 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
| 91 |
+
width: auto;
|
| 92 |
+
margin-left: -5rem;
|
| 93 |
+
margin-right: -5rem;
|
| 94 |
+
margin-top: -3rem;
|
| 95 |
+
}
|
| 96 |
+
.hero-title {
|
| 97 |
+
font-size: 2rem;
|
| 98 |
+
margin-bottom: 0.5rem;
|
| 99 |
+
font-weight: 700;
|
| 100 |
+
}
|
| 101 |
+
.hero-subtitle {
|
| 102 |
+
font-size: 1rem;
|
| 103 |
+
opacity: 0.9;
|
| 104 |
+
font-weight: 400;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/* Spacer to prevent content form being hidden under sticky header initially if needed,
|
| 108 |
+
but standard flow usually puts it after. */
|
| 109 |
+
|
| 110 |
+
/* Chat Bubbles */
|
| 111 |
+
.stChatMessage {
|
| 112 |
+
background-color: transparent;
|
| 113 |
+
border-radius: 10px;
|
| 114 |
+
padding: 1rem;
|
| 115 |
+
margin-bottom: 0.5rem;
|
| 116 |
+
}
|
| 117 |
+
div[data-testid="stChatMessageContent"] {
|
| 118 |
+
font-size: 1.05rem;
|
| 119 |
+
line-height: 1.6;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/* Sidebar Styling */
|
| 123 |
+
section[data-testid="stSidebar"] {
|
| 124 |
+
background-color: #ffffff;
|
| 125 |
+
border-right: 1px solid #eaeaea;
|
| 126 |
+
}
|
| 127 |
+
.suggestion-btn {
|
| 128 |
+
width: 100%;
|
| 129 |
+
text-align: left;
|
| 130 |
+
padding: 0.75rem 1rem;
|
| 131 |
+
margin-bottom: 0.5rem;
|
| 132 |
+
background-color: #f8f9fa;
|
| 133 |
+
border: 1px solid #e9ecef;
|
| 134 |
+
border-radius: 8px;
|
| 135 |
+
color: #495057;
|
| 136 |
+
font-size: 0.95rem;
|
| 137 |
+
transition: all 0.2s ease;
|
| 138 |
+
cursor: pointer;
|
| 139 |
+
display: block;
|
| 140 |
+
text-decoration: none;
|
| 141 |
+
}
|
| 142 |
+
.suggestion-btn:hover {
|
| 143 |
+
background-color: #e2e6ea;
|
| 144 |
+
border-color: #dae0e5;
|
| 145 |
+
text-decoration: none;
|
| 146 |
+
color: #212529;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
</style>
|
| 150 |
+
""", unsafe_allow_html=True)
|
| 151 |
+
|
| 152 |
+
# -----------------------------
|
| 153 |
+
# Logic
|
| 154 |
+
# -----------------------------
|
| 155 |
+
async def get_ai_response(prompt: str) -> str:
|
| 156 |
+
try:
|
| 157 |
+
agent = OrchestratorAgent.create()
|
| 158 |
+
# Ensure session is valid
|
| 159 |
+
current_session = st.session_state.ai_session
|
| 160 |
+
with trace("Chatbot Agent Run"):
|
| 161 |
+
# Run agent
|
| 162 |
+
result = await Runner.run(agent, prompt, session=current_session)
|
| 163 |
+
return result.final_output
|
| 164 |
+
except InputGuardrailTripwireTriggered as e:
|
| 165 |
+
reasoning = getattr(e, "reasoning", None) \
|
| 166 |
+
or getattr(getattr(e, "output", None), "reasoning", None) \
|
| 167 |
+
or getattr(getattr(e, "guardrail_output", None), "reasoning", None) \
|
| 168 |
+
or "Guardrail triggered, but no reasoning provided."
|
| 169 |
+
return f"⚠️ **Guardrail Blocked Input**\n\n{reasoning}"
|
| 170 |
+
except Exception as e:
|
| 171 |
+
return f"❌ **Error**: {str(e)}"
|
| 172 |
+
|
| 173 |
+
# -----------------------------
|
| 174 |
+
# Sidebar - Quick Actions
|
| 175 |
+
# -----------------------------
|
| 176 |
+
with st.sidebar:
|
| 177 |
+
st.markdown("### ⚡ Quick Starters")
|
| 178 |
+
st.markdown("Select a prompt to start:")
|
| 179 |
+
|
| 180 |
+
# We use a trick with st.button to act as input triggers
|
| 181 |
+
# If a button is clicked, we'll handle it in the main loop logic
|
| 182 |
+
selected_prompt = None
|
| 183 |
+
for idx, prompt_text in enumerate(prompts):
|
| 184 |
+
label = prompt_labels[idx] if idx < len(prompt_labels) else f"Prompt {idx+1}"
|
| 185 |
+
if st.button(label, key=f"sidebar_btn_{idx}", use_container_width=True):
|
| 186 |
+
selected_prompt = prompt_text
|
| 187 |
+
|
| 188 |
+
st.markdown("---")
|
| 189 |
+
if st.button("🗑️ Clear Conversation", use_container_width=True):
|
| 190 |
+
st.session_state.messages = []
|
| 191 |
+
st.rerun()
|
| 192 |
+
|
| 193 |
+
# -----------------------------
|
| 194 |
+
# Main Content
|
| 195 |
+
# -----------------------------
|
| 196 |
+
|
| 197 |
+
# Hero Banner (Always visible & Sticky)
|
| 198 |
+
st.markdown("""
|
| 199 |
+
<div class="hero-container">
|
| 200 |
+
<div class="hero-title">🤖 AI Companion</div>
|
| 201 |
+
<div class="hero-subtitle">Your intelligent partner for research, analysis, and more.</div>
|
| 202 |
+
</div>
|
| 203 |
+
""", unsafe_allow_html=True)
|
| 204 |
+
|
| 205 |
+
# Display Chat History
|
| 206 |
+
for message in st.session_state.messages:
|
| 207 |
+
with st.chat_message(message["role"]):
|
| 208 |
+
st.markdown(message["content"])
|
| 209 |
+
|
| 210 |
+
# Chat Input Handling
|
| 211 |
+
# We handle both the chat input widget and the sidebar selection here
|
| 212 |
+
if prompt := (st.chat_input("Type your message...") or selected_prompt):
|
| 213 |
+
# User Message
|
| 214 |
+
st.session_state.messages.append({"role": "user", "content": prompt})
|
| 215 |
+
with st.chat_message("user"):
|
| 216 |
+
st.markdown(prompt)
|
| 217 |
+
|
| 218 |
+
# Assistant Response
|
| 219 |
+
with st.chat_message("assistant"):
|
| 220 |
+
with st.spinner("Thinking..."):
|
| 221 |
+
response_text = asyncio.run(get_ai_response(prompt))
|
| 222 |
+
st.markdown(response_text)
|
| 223 |
+
|
| 224 |
+
st.session_state.messages.append({"role": "assistant", "content": response_text})
|
| 225 |
+
|
| 226 |
+
# If it was a sidebar click, we need to rerun to clear the selection state potentially,
|
| 227 |
+
# but st.chat_input usually handles focus. With buttons, a rerun happens automatically
|
| 228 |
+
# but we want to make sure the input box is cleared (which 'selected_prompt' doesn't use).
|
| 229 |
+
if selected_prompt:
|
| 230 |
+
st.rerun()
|
src/chatbot/appagents/FinancialAgent.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from tools.yahoo_tools import FinanceTools
|
| 2 |
+
from tools.time_tools import TimeTools
|
| 3 |
+
from tools.google_tools import GoogleTools
|
| 4 |
+
import os
|
| 5 |
+
from agents import Agent, OpenAIChatCompletionsModel
|
| 6 |
+
from openai import AsyncOpenAI
|
| 7 |
+
|
| 8 |
+
class FinancialAgent:
|
| 9 |
+
"""
|
| 10 |
+
Encapsulates the AI agent definition for financial analysis and market research.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
@staticmethod
|
| 14 |
+
def create():
|
| 15 |
+
"""
|
| 16 |
+
Returns a configured Agent instance ready for use.
|
| 17 |
+
"""
|
| 18 |
+
# Included all relevant tools
|
| 19 |
+
tools = [
|
| 20 |
+
TimeTools.current_datetime,
|
| 21 |
+
FinanceTools.get_market_sentiment,
|
| 22 |
+
FinanceTools.get_history,
|
| 23 |
+
GoogleTools.search
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
instructions = """
|
| 27 |
+
You are a specialized **Financial Analysis Agent** 💰, expert in market research, financial data retrieval, and news correlation. Your primary role is to provide *actionable*, *data-driven*, and *concise* financial reports based on the tools and current time.
|
| 28 |
+
|
| 29 |
+
## Core Directives & Priorities
|
| 30 |
+
1. **Time Sensitivity (TimeTools):** Always use the **TimeTools.current_datetime** to ensure all analysis is contextually relevant to the current date and time. Financial data is extremely time-sensitive.
|
| 31 |
+
2. **Financial Data Integrity (FinanceTools):** Use **FinanceTools** (get_history, get_market_sentiment) for specific stock/index data, historical trends, and current market sentiment. Be precise about the date range and data source.
|
| 32 |
+
3. **Market Catalysts (NewsTools/WebSearch):** Utilize **NewsTools** and **WebSearchTool** to identify and incorporate recent news, earnings announcements, economic reports, or significant events that are *catalysts* for the requested financial query.
|
| 33 |
+
4. **Synthesis and Analysis:** Do not just list data. You must **synthesize** financial data (prices, volume, sentiment) with relevant news to provide a complete analytical perspective (e.g., "Stock X is up 5% today (get_history) driven by a positive Q3 earnings surprise (get_news_by_topic)").
|
| 34 |
+
5. **Professional Clarity:** Present information in a clear, professional, and structured format. Use numerical data and financial terminology correctly.
|
| 35 |
+
6. **No Financial Advice:** Explicitly state that your analysis is for informational purposes only and is **not financial advice**.
|
| 36 |
+
7. **Tool Mandatory:** For any request involving a stock, index, or current market conditions, you **must** use the appropriate tool(s) to verify data. **Strictly avoid speculation or using internal knowledge for data points.**
|
| 37 |
+
|
| 38 |
+
## Output Format Guidelines
|
| 39 |
+
* Use **bold** for key financial metrics (e.g., Stock Symbol, Price, Volume).
|
| 40 |
+
* Cite the tools used to obtain the data (e.g., Data sourced from FinanceTools (Yahoo) as of [Date]).
|
| 41 |
+
* If a symbol or data point cannot be found, clearly state "Data for [X] is unavailable or invalid."
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 46 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
| 47 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 48 |
+
gemini_model = OpenAIChatCompletionsModel(model="gemini-2.0-flash", openai_client=gemini_client)
|
| 49 |
+
|
| 50 |
+
agent = Agent(
|
| 51 |
+
name="Financial Analysis Agent",
|
| 52 |
+
tools=tools,
|
| 53 |
+
instructions=instructions,
|
| 54 |
+
model=gemini_model
|
| 55 |
+
)
|
| 56 |
+
return agent
|
src/chatbot/appagents/InputValidationAgent.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from agents import Agent, OpenAIChatCompletionsModel, Runner, GuardrailFunctionOutput
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
import json
|
| 5 |
+
from openai import AsyncOpenAI
|
| 6 |
+
|
| 7 |
+
class ValidatedOutput(BaseModel):
|
| 8 |
+
is_valid: bool
|
| 9 |
+
reasoning: str
|
| 10 |
+
|
| 11 |
+
class InputValidationAgent:
|
| 12 |
+
"""
|
| 13 |
+
Encapsulates the AI agent definition for conducting comprehensive web searches and synthesizing information.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
@staticmethod
|
| 17 |
+
def create():
|
| 18 |
+
"""
|
| 19 |
+
Returns a configured Agent instance ready for use.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
instructions = """
|
| 23 |
+
You are a highly efficient and specialized **Agent** 🌐. Your sole function is to validate the user inputs.
|
| 24 |
+
|
| 25 |
+
## Core Directives & Priorities
|
| 26 |
+
1. You should flag if the user uses unparaliamentary language ONLY.
|
| 27 |
+
2. You MUST give reasoning for the same.
|
| 28 |
+
|
| 29 |
+
## Rules
|
| 30 |
+
- If it contains any of these, mark `"is_valid": false` and explain **why** in `"reasoning"`.
|
| 31 |
+
- Otherwise, mark `"is_valid": true` with reasoning like "The input follows respectful communication guidelines."
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
## Output Format (MANDATORY)
|
| 35 |
+
* Return a JSON object with the following structure:
|
| 36 |
+
{
|
| 37 |
+
"is_valid": <boolean>,
|
| 38 |
+
"reasoning": <string>
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 45 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
| 46 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 47 |
+
gemini_model = OpenAIChatCompletionsModel(model="gemini-2.0-flash", openai_client=gemini_client)
|
| 48 |
+
|
| 49 |
+
agent = Agent(
|
| 50 |
+
name="Guardrail Input Validation Agent",
|
| 51 |
+
instructions=instructions,
|
| 52 |
+
model=gemini_model,
|
| 53 |
+
output_type=ValidatedOutput,
|
| 54 |
+
)
|
| 55 |
+
return agent
|
| 56 |
+
|
| 57 |
+
async def input_validation_guardrail(ctx, agent, input_data):
|
| 58 |
+
result = await Runner.run(InputValidationAgent.create(), input_data, context=ctx.context)
|
| 59 |
+
raw_output = result.final_output
|
| 60 |
+
|
| 61 |
+
# print("Raw Output from Guardrail Model:", raw_output)
|
| 62 |
+
|
| 63 |
+
# Handle different return shapes gracefully
|
| 64 |
+
if isinstance(raw_output, ValidatedOutput):
|
| 65 |
+
final_output = raw_output
|
| 66 |
+
print("Parsed ValidatedOutput:", final_output)
|
| 67 |
+
else:
|
| 68 |
+
final_output = ValidatedOutput(
|
| 69 |
+
is_valid=False,
|
| 70 |
+
reasoning=f"Unexpected output type: {type(raw_output)}"
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
return GuardrailFunctionOutput(
|
| 74 |
+
output_info=final_output,
|
| 75 |
+
tripwire_triggered=not final_output.is_valid,
|
| 76 |
+
)
|
src/chatbot/appagents/NewsAgent.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from tools.news_tools import NewsTools
|
| 2 |
+
from tools.time_tools import TimeTools
|
| 3 |
+
from tools.google_tools import GoogleTools
|
| 4 |
+
# from tools.yahoo_tools import FinanceTools # Removed: Not needed for a pure News Agent
|
| 5 |
+
import os
|
| 6 |
+
from agents import Agent, OpenAIChatCompletionsModel
|
| 7 |
+
from openai import AsyncOpenAI
|
| 8 |
+
|
| 9 |
+
class NewsAgent:
|
| 10 |
+
"""
|
| 11 |
+
Encapsulates the AI agent definition for real-time news gathering, summarization, and reporting.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
@staticmethod
|
| 15 |
+
def create():
|
| 16 |
+
"""
|
| 17 |
+
Returns a configured Agent instance ready for use.
|
| 18 |
+
"""
|
| 19 |
+
# Corrected tool list: removed FinanceTools, added WebSearchTool
|
| 20 |
+
tools = [
|
| 21 |
+
TimeTools.current_datetime,
|
| 22 |
+
NewsTools.top_headlines,
|
| 23 |
+
NewsTools.search_news,
|
| 24 |
+
GoogleTools.search
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
instructions = """
|
| 28 |
+
You are a specialized **News Reporting Agent** 📰, expert in retrieving, summarizing, and synthesizing current events and information from various sources. Your primary role is to deliver a concise, objective, and timely news digest or report.
|
| 29 |
+
|
| 30 |
+
## Core Directives & Priorities
|
| 31 |
+
1. **Immediacy and Context (TimeTools):** Always use **TimeTools.current_datetime** to contextualize all reports. Clearly indicate when the information was retrieved (e.g., "As of [Date/Time]").
|
| 32 |
+
2. **News Retrieval (NewsTools):**
|
| 33 |
+
* For general awareness, use **NewsTools.top_headlines**.
|
| 34 |
+
* For specific topics, use **NewsTools.search_news**.
|
| 35 |
+
* Prioritize the most recent articles.
|
| 36 |
+
3. **Comprehensive Search (WebSearchTool):** Use the **WebSearchTool (Google Search)** for:
|
| 37 |
+
* Verifying facts or statistics found in news articles.
|
| 38 |
+
* Gathering background context on a complex story.
|
| 39 |
+
* Finding information not covered by the dedicated news API.
|
| 40 |
+
4. **Objective Synthesis:** Do not express opinions or speculate. You must **synthesize** information from multiple articles or tools to provide a **balanced and neutral summary**. Avoid sensationalism.
|
| 41 |
+
5. **Attribution and Transparency:** Every piece of reported information must be sourced. Cite the original publication or the tool used (e.g., "The New York Times reported...", "According to data found via WebSearch..."). Provide links where available.
|
| 42 |
+
6. **Structured Reporting:** Present the information clearly. For complex topics, use bullet points, subheadings, and a brief introductory summary.
|
| 43 |
+
7. **Data Gaps:** If a requested topic yields no recent or verifiable information, explicitly state: **"No verifiable recent news could be found on [Topic]."**
|
| 44 |
+
|
| 45 |
+
## Output Format Guidelines
|
| 46 |
+
* Start with a brief, high-level summary of the answer.
|
| 47 |
+
* Use **bold** for key names, dates, and locations.
|
| 48 |
+
* List news sources clearly with the article title and, if possible, the link/publication name.
|
| 49 |
+
* Maintain a professional, journalistic tone.
|
| 50 |
+
|
| 51 |
+
**Strictly adhere to verifiable facts and avoid making up any information.**
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 56 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
| 57 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 58 |
+
gemini_model = OpenAIChatCompletionsModel(model="gemini-2.0-flash", openai_client=gemini_client)
|
| 59 |
+
|
| 60 |
+
agent = Agent(
|
| 61 |
+
name="News Reporting Agent",
|
| 62 |
+
tools=tools,
|
| 63 |
+
instructions=instructions,
|
| 64 |
+
model=gemini_model #"gpt-4o-mini"
|
| 65 |
+
)
|
| 66 |
+
return agent
|
src/chatbot/appagents/OrchestratorAgent.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import asyncio
|
| 3 |
+
from appagents.FinancialAgent import FinancialAgent
|
| 4 |
+
from appagents.NewsAgent import NewsAgent
|
| 5 |
+
from appagents.SearchAgent import SearchAgent
|
| 6 |
+
from appagents.InputValidationAgent import input_validation_guardrail
|
| 7 |
+
from agents import Agent, OpenAIChatCompletionsModel, InputGuardrail
|
| 8 |
+
from openai import AsyncOpenAI
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class OrchestratorAgent:
|
| 12 |
+
"""
|
| 13 |
+
The OrchestratorAgent coordinates multiple specialized sub-agents
|
| 14 |
+
(Financial, News, and Search) to provide accurate, up-to-date,
|
| 15 |
+
and well-routed market research insights.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
MAX_RETRIES = 2
|
| 19 |
+
|
| 20 |
+
# ----------------------------------------------------------
|
| 21 |
+
# MAIN CREATION METHOD
|
| 22 |
+
# ----------------------------------------------------------
|
| 23 |
+
@staticmethod
|
| 24 |
+
def create(model: str = "gpt-4o-mini"):
|
| 25 |
+
"""
|
| 26 |
+
Creates and returns a configured Orchestrator agent.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
# --- Sub-agent setup ---
|
| 30 |
+
handoffs = [
|
| 31 |
+
FinancialAgent.create(),
|
| 32 |
+
NewsAgent.create(),
|
| 33 |
+
SearchAgent.create(),
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
# --- Behavioral instructions ---
|
| 37 |
+
instructions = """
|
| 38 |
+
You are the Orchestrator Agent responsible for coordinating specialized sub-agents
|
| 39 |
+
to generate accurate and well-rounded market research responses.
|
| 40 |
+
|
| 41 |
+
**Your Core Responsibilities**
|
| 42 |
+
1. **Task Routing:** Determine which sub-agent (Financial, News, or Search) is best suited
|
| 43 |
+
to handle each user query based on intent and context.
|
| 44 |
+
2. **Delegation:** Forward the request to the appropriate sub-agent and wait for its result.
|
| 45 |
+
3. **Synthesis:** When multiple agents provide responses, summarize and merge their findings
|
| 46 |
+
into a clear, concise, and accurate overall answer.
|
| 47 |
+
4. **Recency and Accuracy:** Prioritize the most up-to-date, verifiable data from sub-agents.
|
| 48 |
+
5. **Transparency:** Clearly identify which insights came from which sub-agent when relevant.
|
| 49 |
+
6. **Error Handling:** If a sub-agent fails or provides insufficient data, attempt fallback
|
| 50 |
+
strategies such as rerouting the query or notifying the user.
|
| 51 |
+
7. **Clarity:** Always present the final response in a professional, well-structured,
|
| 52 |
+
and easy-to-understand format.
|
| 53 |
+
|
| 54 |
+
⚠️ Do **not** perform the underlying data analysis or external lookup yourself —
|
| 55 |
+
ALWAYS delegate those tasks to the respective sub-agents.
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
# --- Model setup ---
|
| 59 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 60 |
+
google_api_key = os.getenv("GOOGLE_API_KEY")
|
| 61 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 62 |
+
gemini_model = OpenAIChatCompletionsModel(
|
| 63 |
+
model="gemini-2.0-flash",
|
| 64 |
+
openai_client=gemini_client
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
# --- Create orchestrator agent ---
|
| 68 |
+
agent = Agent(
|
| 69 |
+
name="AI Market Research Assistant",
|
| 70 |
+
handoffs=handoffs,
|
| 71 |
+
instructions=instructions.strip(),
|
| 72 |
+
model=gemini_model,
|
| 73 |
+
# input_guardrails=[
|
| 74 |
+
# InputGuardrail(
|
| 75 |
+
# name="Input Validation Guardrail",
|
| 76 |
+
# guardrail_function=input_validation_guardrail,
|
| 77 |
+
# )
|
| 78 |
+
# ],
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
# Attach orchestration logic
|
| 82 |
+
agent.respond = lambda prompt: OrchestratorAgent.respond(prompt, handoffs, gemini_model)
|
| 83 |
+
return agent
|
| 84 |
+
|
| 85 |
+
# ----------------------------------------------------------
|
| 86 |
+
# RESPONSE HANDLING + SELF-CORRECTION
|
| 87 |
+
# ----------------------------------------------------------
|
| 88 |
+
@staticmethod
|
| 89 |
+
async def respond(prompt: str, handoffs: list, model) -> str:
|
| 90 |
+
"""
|
| 91 |
+
Routes prompt to the most relevant agent, retries if output seems irrelevant.
|
| 92 |
+
"""
|
| 93 |
+
attempted_agents = set()
|
| 94 |
+
|
| 95 |
+
for attempt in range(OrchestratorAgent.MAX_RETRIES):
|
| 96 |
+
# Step 1: Route intelligently
|
| 97 |
+
chosen_agent = await OrchestratorAgent._route_to_agent(prompt, handoffs, attempted_agents)
|
| 98 |
+
if not chosen_agent:
|
| 99 |
+
return "⚠️ No available agent could handle this query."
|
| 100 |
+
|
| 101 |
+
print(f"🤖 Attempt {attempt+1}: Sending query to {chosen_agent.name}")
|
| 102 |
+
|
| 103 |
+
# Step 2: Run agent
|
| 104 |
+
try:
|
| 105 |
+
response = await chosen_agent.run(prompt)
|
| 106 |
+
except Exception as e:
|
| 107 |
+
print(f"⚠️ Agent {chosen_agent.name} failed: {e}")
|
| 108 |
+
attempted_agents.add(chosen_agent.name)
|
| 109 |
+
continue
|
| 110 |
+
|
| 111 |
+
# Step 3: Evaluate if relevant
|
| 112 |
+
if await OrchestratorAgent._is_relevant(prompt, response, model):
|
| 113 |
+
return f"✅ {chosen_agent.name} handled this successfully:\n\n{response}"
|
| 114 |
+
|
| 115 |
+
print(f"🔁 {chosen_agent.name}'s response deemed irrelevant. Re-routing...")
|
| 116 |
+
attempted_agents.add(chosen_agent.name)
|
| 117 |
+
|
| 118 |
+
return "⚠️ Could not find a relevant answer after multiple attempts."
|
| 119 |
+
|
| 120 |
+
# ----------------------------------------------------------
|
| 121 |
+
# ROUTING LOGIC
|
| 122 |
+
# ----------------------------------------------------------
|
| 123 |
+
@staticmethod
|
| 124 |
+
async def _route_to_agent(prompt: str, handoffs: list, attempted_agents: set):
|
| 125 |
+
"""
|
| 126 |
+
Determines the best-fit agent for the given prompt.
|
| 127 |
+
Avoids previously tried agents.
|
| 128 |
+
"""
|
| 129 |
+
lowered = prompt.lower()
|
| 130 |
+
available = [a for a in handoffs if a.name not in attempted_agents]
|
| 131 |
+
|
| 132 |
+
if not available:
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
if any(k in lowered for k in ["finance", "stock", "market", "earnings"]):
|
| 136 |
+
return next((a for a in available if "financial" in a.name.lower()), available[0])
|
| 137 |
+
elif any(k in lowered for k in ["news", "headline", "press release"]):
|
| 138 |
+
return next((a for a in available if "news" in a.name.lower()), available[0])
|
| 139 |
+
elif any(k in lowered for k in ["search", "find", "lookup", "discover"]):
|
| 140 |
+
return next((a for a in available if "search" in a.name.lower()), available[0])
|
| 141 |
+
else:
|
| 142 |
+
# fallback — first available agent
|
| 143 |
+
return available[0]
|
| 144 |
+
|
| 145 |
+
# ----------------------------------------------------------
|
| 146 |
+
# LLM-BASED EVALUATOR
|
| 147 |
+
# ----------------------------------------------------------
|
| 148 |
+
@staticmethod
|
| 149 |
+
async def _is_relevant(prompt: str, response: str, model) -> bool:
|
| 150 |
+
"""
|
| 151 |
+
Uses the model itself to check if the response matches the prompt intent.
|
| 152 |
+
"""
|
| 153 |
+
eval_prompt = f"""
|
| 154 |
+
You are an evaluator checking multi-agent responses.
|
| 155 |
+
User asked: "{prompt}"
|
| 156 |
+
Agent responded: "{response}"
|
| 157 |
+
|
| 158 |
+
Does this response accurately and completely answer the user's intent?
|
| 159 |
+
Reply with only 'yes' or 'no'.
|
| 160 |
+
"""
|
| 161 |
+
try:
|
| 162 |
+
eval_result = await model.run(eval_prompt)
|
| 163 |
+
print(f"🧠 Evaluation result: {eval_result}")
|
| 164 |
+
return "yes" in eval_result.lower()
|
| 165 |
+
except Exception as e:
|
| 166 |
+
print(f"⚠️ Evaluation failed: {e}")
|
| 167 |
+
return False
|
src/chatbot/appagents/SearchAgent.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from tools.google_tools import GoogleTools
|
| 2 |
+
from tools.time_tools import TimeTools
|
| 3 |
+
import os
|
| 4 |
+
from agents import Agent, OpenAIChatCompletionsModel
|
| 5 |
+
from openai import AsyncOpenAI
|
| 6 |
+
|
| 7 |
+
class SearchAgent:
|
| 8 |
+
"""
|
| 9 |
+
Encapsulates the AI agent definition for conducting comprehensive web searches and synthesizing information.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
@staticmethod
|
| 13 |
+
def create():
|
| 14 |
+
"""
|
| 15 |
+
Returns a configured Agent instance ready for use.
|
| 16 |
+
"""
|
| 17 |
+
# The tool list is correct for a pure search agent
|
| 18 |
+
tools = [
|
| 19 |
+
TimeTools.current_datetime,
|
| 20 |
+
GoogleTools.search,
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
instructions = """
|
| 24 |
+
You are a highly efficient and specialized **Web Search Agent** 🌐. Your sole function is to retrieve and analyze information from the internet using the **GoogleTools.search** function. You must act as a digital librarian and researcher, providing synthesized, cited, and up-to-date answers.
|
| 25 |
+
|
| 26 |
+
## Core Directives & Priorities
|
| 27 |
+
1. **Search First:** For virtually *every* factual query, you must invoke **GoogleTools.search** before responding. Your primary source of truth is the current web search results.
|
| 28 |
+
2. **Query Optimization:** Before calling the tool, analyze the user's request and construct the most effective, concise, and targeted search queries (1-3 queries max). Use specific keywords, dates, or phrases to ensure relevant results.
|
| 29 |
+
3. **Time Sensitivity (TimeTools):** Use **TimeTools.current_datetime** to contextualize time-sensitive queries. Results must reflect the current state of information.
|
| 30 |
+
4. **Verification and Synthesis:**
|
| 31 |
+
* **Verification:** Corroborate facts by checking multiple search results if necessary. If results conflict, report the disagreement and the most common or reputable finding.
|
| 32 |
+
* **Synthesis:** Aggregate the key findings from the search snippets into a single, comprehensive, and easy-to-read answer. Do not simply list the search results.
|
| 33 |
+
5. **Source Transparency (Citation Mandatory):** You **must** provide clear citation for all facts. At the end of your response, list the source titles and URLs from the Google Search results used to construct the answer.
|
| 34 |
+
6. **Clarity and Brevity:** Use professional, plain language. Structure the response using headings and bullet points for complex topics. Avoid filler text or unnecessary detail.
|
| 35 |
+
7. **Data Gaps:** If no relevant or conclusive information is found via the search tool, explicitly state: **"A conclusive answer could not be verified by current web search results."**
|
| 36 |
+
|
| 37 |
+
## Output Format Guidelines
|
| 38 |
+
* Begin with a direct answer to the user's question.
|
| 39 |
+
* Use **bold** for key facts, names, dates, or statistics.
|
| 40 |
+
* Conclude the response with a separate "Sources" section citing the search results.
|
| 41 |
+
|
| 42 |
+
**Crucially, never fabricate information or provide an answer without grounding it in the search results.**
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 47 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
| 48 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 49 |
+
gemini_model = OpenAIChatCompletionsModel(model="gemini-2.0-flash", openai_client=gemini_client)
|
| 50 |
+
|
| 51 |
+
agent = Agent(
|
| 52 |
+
name="Web Search Agent",
|
| 53 |
+
tools=tools,
|
| 54 |
+
instructions=instructions,
|
| 55 |
+
model=gemini_model
|
| 56 |
+
)
|
| 57 |
+
return agent
|
src/chatbot/appagents/__init__.py
ADDED
|
File without changes
|
src/chatbot/core/__init__.py
ADDED
|
File without changes
|
src/chatbot/core/logger.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import datetime
|
| 3 |
+
|
| 4 |
+
def log_call(func):
|
| 5 |
+
"""
|
| 6 |
+
A decorator that logs when a function is called and when it finishes.
|
| 7 |
+
"""
|
| 8 |
+
@functools.wraps(func)
|
| 9 |
+
def wrapper(*args, **kwargs):
|
| 10 |
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 11 |
+
arg_list = ", ".join(
|
| 12 |
+
[repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
|
| 13 |
+
)
|
| 14 |
+
print(f"[{timestamp}] 🚀 Calling: {func.__name__}({arg_list})")
|
| 15 |
+
try:
|
| 16 |
+
result = func(*args, **kwargs)
|
| 17 |
+
print(f"[{timestamp}] ✅ Finished: {func.__name__}")
|
| 18 |
+
return result
|
| 19 |
+
except Exception as e:
|
| 20 |
+
print(f"[{timestamp}] ❌ Error in {func.__name__}: {e}")
|
| 21 |
+
raise
|
| 22 |
+
return wrapper
|
src/chatbot/prompts/economic_news.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Provide an update on major economic indicators released today.
|
| 2 |
+
Include interest rates, unemployment data, and other relevant statistics.
|
src/chatbot/prompts/market_sentiment.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
What is the current market sentiment based on the news and market index?
|
src/chatbot/prompts/news_headlines.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Tell me the top 3 world headlines. Present me the headlines in the following way:
|
| 2 |
+
- Show the title in Bold
|
| 3 |
+
- Summerize the headline in 3 lines.
|
| 4 |
+
- show me the headline publish date and time.
|
| 5 |
+
- give me a link to the exct source url.
|
src/chatbot/prompts/trade_recommendation.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Recommend 3 option spreades which has more than 80% likelihood of profiting me.
|
| 2 |
+
You must do a **thorough analysis** of the stocks 3 months trend and study the market sentiment to conclude into the answer.
|
| 3 |
+
Explain each spread legs with the strike price, exact expiry date and premium entry.
|
| 4 |
+
Provide a rational of your recommendation and likelyhood of winning it in %.
|
src/chatbot/prompts/upcoming_earnings.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Provide me the upcoming critical earnings in next 2 weeks.
|
src/chatbot/tools/__init__.py
ADDED
|
File without changes
|
src/chatbot/tools/google_tools.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from agents import function_tool
|
| 5 |
+
from core.logger import log_call
|
| 6 |
+
|
| 7 |
+
# Load environment variables once
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# ============================================================
|
| 12 |
+
# 🔹 GOOGLE SEARCH TOOLSET (Serper.dev API)
|
| 13 |
+
# ============================================================
|
| 14 |
+
class GoogleTools:
|
| 15 |
+
"""
|
| 16 |
+
GoogleTools provides function tools to perform web searches
|
| 17 |
+
using the Serper.dev API (Google Search). I am a fallback for
|
| 18 |
+
retrieving recent information from the web.
|
| 19 |
+
|
| 20 |
+
Features:
|
| 21 |
+
- Search for recent web pages.
|
| 22 |
+
- Limit number of results.
|
| 23 |
+
- Returns formatted title, link, date, and snippet for each result.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
@staticmethod
|
| 27 |
+
@function_tool
|
| 28 |
+
@log_call
|
| 29 |
+
def search(query: str, num_results: int = 3) -> str:
|
| 30 |
+
"""
|
| 31 |
+
Perform a general Google search using Serper.dev API.
|
| 32 |
+
|
| 33 |
+
Parameters:
|
| 34 |
+
-----------
|
| 35 |
+
query : str
|
| 36 |
+
The search query string, e.g., "latest Tesla stock news".
|
| 37 |
+
num_results : int, optional (default=3)
|
| 38 |
+
Maximum number of search results to return.
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
--------
|
| 42 |
+
str
|
| 43 |
+
Formatted string of top search results, each including:
|
| 44 |
+
- Title of the page
|
| 45 |
+
- URL link
|
| 46 |
+
- Published date
|
| 47 |
+
- Snippet / description
|
| 48 |
+
If no results are found or API key is missing, returns an error message.
|
| 49 |
+
|
| 50 |
+
Example:
|
| 51 |
+
--------
|
| 52 |
+
search("AI in finance", num_results=2)
|
| 53 |
+
|
| 54 |
+
Output:
|
| 55 |
+
Title: How AI is Transforming Finance
|
| 56 |
+
Link: https://example.com/ai-finance
|
| 57 |
+
Published: 2024-06-15
|
| 58 |
+
Snippet: AI is increasingly used for trading, risk management...
|
| 59 |
+
|
| 60 |
+
Title: AI Applications in Banking
|
| 61 |
+
Link: https://example.com/ai-banking
|
| 62 |
+
Published: 2024-06-10
|
| 63 |
+
Snippet: Banks are leveraging AI for customer service, fraud detection...
|
| 64 |
+
"""
|
| 65 |
+
try:
|
| 66 |
+
api_key = os.getenv("SERPER_API_KEY")
|
| 67 |
+
if not api_key:
|
| 68 |
+
return "Missing SERPER_API_KEY in environment variables."
|
| 69 |
+
|
| 70 |
+
url = "https://google.serper.dev/search"
|
| 71 |
+
headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
|
| 72 |
+
payload = {"q": query, "num": num_results, "tbs": "qdr:d"} # results from last 24h
|
| 73 |
+
|
| 74 |
+
response = requests.post(url, headers=headers, json=payload)
|
| 75 |
+
response.raise_for_status()
|
| 76 |
+
data = response.json()
|
| 77 |
+
|
| 78 |
+
if "organic" not in data or not data["organic"]:
|
| 79 |
+
return "No results found."
|
| 80 |
+
|
| 81 |
+
formatted_results = [
|
| 82 |
+
f"Title: {item.get('title')}\n"
|
| 83 |
+
f"Link: {item.get('link')}\n"
|
| 84 |
+
f"Snippet: {item.get('snippet', '')}\n"
|
| 85 |
+
for item in data["organic"][:num_results]
|
| 86 |
+
]
|
| 87 |
+
return "\n".join(formatted_results)
|
| 88 |
+
|
| 89 |
+
except requests.exceptions.RequestException as e:
|
| 90 |
+
return f"Network error during Google search: {e}"
|
| 91 |
+
except Exception as e:
|
| 92 |
+
return f"Error performing Google search: {e}"
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# ============================================================
|
| 96 |
+
# 🔹 OPENAI & OTHER MODEL TOOLS
|
| 97 |
+
# ============================================================
|
| 98 |
+
class ModelTools:
|
| 99 |
+
"""
|
| 100 |
+
ModelTools provides function tools to interact with LLM APIs
|
| 101 |
+
such as OpenAI, Gemini, or Groq.
|
| 102 |
+
|
| 103 |
+
Features:
|
| 104 |
+
- Send prompts to a language model.
|
| 105 |
+
- Receive structured text completions.
|
| 106 |
+
- Can be extended to support multiple LLM providers.
|
| 107 |
+
"""
|
| 108 |
+
|
| 109 |
+
@staticmethod
|
| 110 |
+
@function_tool
|
| 111 |
+
def query_openai(prompt: str, model: str = "gpt-4o-mini") -> str:
|
| 112 |
+
"""
|
| 113 |
+
Query an OpenAI language model with a prompt.
|
| 114 |
+
|
| 115 |
+
Parameters:
|
| 116 |
+
-----------
|
| 117 |
+
prompt : str
|
| 118 |
+
User-provided prompt for the model.
|
| 119 |
+
model : str, optional (default="gpt-4o-mini")
|
| 120 |
+
Model name to query (e.g., "gpt-4o-mini", "gpt-4").
|
| 121 |
+
|
| 122 |
+
Returns:
|
| 123 |
+
--------
|
| 124 |
+
str
|
| 125 |
+
Model's response content as text.
|
| 126 |
+
If an error occurs (network/API), returns an error message.
|
| 127 |
+
|
| 128 |
+
Example:
|
| 129 |
+
--------
|
| 130 |
+
query_openai("Explain AI in finance")
|
| 131 |
+
|
| 132 |
+
Output:
|
| 133 |
+
"AI in finance refers to the use of machine learning and natural language
|
| 134 |
+
processing techniques to automate trading, risk assessment, and customer service..."
|
| 135 |
+
"""
|
| 136 |
+
try:
|
| 137 |
+
from openai import OpenAI # delayed import
|
| 138 |
+
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
| 139 |
+
response = client.chat.completions.create(
|
| 140 |
+
model=model,
|
| 141 |
+
messages=[{"role": "user", "content": prompt}],
|
| 142 |
+
)
|
| 143 |
+
return response.choices[0].message.content
|
| 144 |
+
except Exception as e:
|
| 145 |
+
return f"Error querying OpenAI API: {e}"
|
src/chatbot/tools/news_tools.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from agents import function_tool
|
| 5 |
+
from core.logger import log_call
|
| 6 |
+
import datetime
|
| 7 |
+
|
| 8 |
+
# Load environment variables once
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# ============================================================
|
| 13 |
+
# 🔹 NEWS TOOLSET (NewsAPI.org)
|
| 14 |
+
# ============================================================
|
| 15 |
+
class NewsTools:
|
| 16 |
+
"""
|
| 17 |
+
NewsTools provides function tools to fetch recent news headlines or
|
| 18 |
+
topic-specific articles from NewsAPI.org. Always returns recent data.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
@staticmethod
|
| 22 |
+
@function_tool
|
| 23 |
+
@log_call
|
| 24 |
+
def top_headlines(country: str = "us", num_results: int = 5) -> str:
|
| 25 |
+
"""
|
| 26 |
+
Fetch the latest top headlines for a country.
|
| 27 |
+
|
| 28 |
+
Parameters:
|
| 29 |
+
-----------
|
| 30 |
+
country : str, optional (default="us")
|
| 31 |
+
Two-letter country code.
|
| 32 |
+
num_results : int, optional (default=5)
|
| 33 |
+
Number of articles to fetch.
|
| 34 |
+
|
| 35 |
+
Returns:
|
| 36 |
+
--------
|
| 37 |
+
str
|
| 38 |
+
Formatted headlines with title, source, and URL.
|
| 39 |
+
"""
|
| 40 |
+
return NewsTools._fetch_news(query="", country=country, num_results=num_results)
|
| 41 |
+
|
| 42 |
+
@staticmethod
|
| 43 |
+
@function_tool
|
| 44 |
+
@log_call
|
| 45 |
+
def search_news(query: str, num_results: int = 5) -> str:
|
| 46 |
+
"""
|
| 47 |
+
Search for recent news articles about a specific topic.
|
| 48 |
+
|
| 49 |
+
Parameters:
|
| 50 |
+
-----------
|
| 51 |
+
query : str
|
| 52 |
+
Keyword or topic to search (e.g., "Tesla earnings").
|
| 53 |
+
num_results : int, optional (default=5)
|
| 54 |
+
Number of articles to fetch.
|
| 55 |
+
|
| 56 |
+
Returns:
|
| 57 |
+
--------
|
| 58 |
+
str
|
| 59 |
+
Formatted news articles with title, source, and URL.
|
| 60 |
+
"""
|
| 61 |
+
return NewsTools._fetch_news(query=query, country="", num_results=num_results)
|
| 62 |
+
|
| 63 |
+
@staticmethod
|
| 64 |
+
@log_call
|
| 65 |
+
def _fetch_news(query: str, country: str, num_results: int) -> str:
|
| 66 |
+
"""
|
| 67 |
+
Internal helper to fetch news from NewsAPI.org.
|
| 68 |
+
|
| 69 |
+
Ensures always recent news (last 7 days).
|
| 70 |
+
|
| 71 |
+
Parameters:
|
| 72 |
+
-----------
|
| 73 |
+
query : str
|
| 74 |
+
Search query (leave empty for top headlines).
|
| 75 |
+
country : str
|
| 76 |
+
Two-letter country code (ignored if query provided).
|
| 77 |
+
num_results : int
|
| 78 |
+
Max articles to return.
|
| 79 |
+
|
| 80 |
+
Returns:
|
| 81 |
+
--------
|
| 82 |
+
str
|
| 83 |
+
Formatted articles or error message.
|
| 84 |
+
"""
|
| 85 |
+
try:
|
| 86 |
+
api_key = os.getenv("NEWS_API_KEY")
|
| 87 |
+
if not api_key:
|
| 88 |
+
return "Missing NEWS_API_KEY in environment variables."
|
| 89 |
+
|
| 90 |
+
today = datetime.datetime.utcnow()
|
| 91 |
+
from_date = (today - datetime.timedelta(days=7)).strftime('%Y-%m-%dT%H:%M:%SZ')
|
| 92 |
+
|
| 93 |
+
if query:
|
| 94 |
+
url = "https://newsapi.org/v2/everything"
|
| 95 |
+
params = {
|
| 96 |
+
"q": query,
|
| 97 |
+
"pageSize": num_results,
|
| 98 |
+
"apiKey": api_key,
|
| 99 |
+
"sortBy": "publishedAt",
|
| 100 |
+
"language": "en",
|
| 101 |
+
"from": from_date
|
| 102 |
+
}
|
| 103 |
+
else:
|
| 104 |
+
url = "https://newsapi.org/v2/top-headlines"
|
| 105 |
+
params = {
|
| 106 |
+
"country": country or "us",
|
| 107 |
+
"pageSize": num_results,
|
| 108 |
+
"apiKey": api_key
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
response = requests.get(url, params=params)
|
| 112 |
+
response.raise_for_status()
|
| 113 |
+
data = response.json()
|
| 114 |
+
|
| 115 |
+
if not data.get("articles"):
|
| 116 |
+
return f"No news found for '{query or country}'."
|
| 117 |
+
|
| 118 |
+
formatted = [
|
| 119 |
+
f"📰 {a.get('title')}\n"
|
| 120 |
+
f" Source: {a.get('source', {}).get('name')}\n"
|
| 121 |
+
f" URL: {a.get('url')}\n"
|
| 122 |
+
for a in data["articles"][:num_results]
|
| 123 |
+
]
|
| 124 |
+
return "\n".join(formatted)
|
| 125 |
+
|
| 126 |
+
except requests.exceptions.RequestException as e:
|
| 127 |
+
return f"Network error while calling News API: {e}"
|
| 128 |
+
except Exception as e:
|
| 129 |
+
return f"Unexpected error fetching news: {e}"
|
src/chatbot/tools/time_tools.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from agents import function_tool
|
| 3 |
+
from core.logger import log_call
|
| 4 |
+
|
| 5 |
+
class TimeTools:
|
| 6 |
+
"""Provides tools related to current date and time."""
|
| 7 |
+
|
| 8 |
+
@staticmethod
|
| 9 |
+
@function_tool
|
| 10 |
+
@log_call
|
| 11 |
+
def current_datetime(format: str = "%Y-%m-%d %H:%M:%S") -> str:
|
| 12 |
+
"""
|
| 13 |
+
Returns the current date and time as a formatted string.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
format (str): Optional datetime format (default: "YYYY-MM-DD HH:MM:SS")
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
str: Current date and time in the specified format
|
| 20 |
+
"""
|
| 21 |
+
now = datetime.now()
|
| 22 |
+
return now.strftime(format)
|
src/chatbot/tools/yahoo_tools.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
import yfinance as yf
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from agents import function_tool
|
| 6 |
+
from core.logger import log_call
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
|
| 9 |
+
# Load environment variables
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# ============================================================
|
| 14 |
+
# 🔹 YAHOO FINANCE TOOLSET
|
| 15 |
+
# ============================================================
|
| 16 |
+
class FinanceTools:
|
| 17 |
+
"""
|
| 18 |
+
FinanceTools provides a set of function tools for interacting with Yahoo Finance for **Financial Data** only.
|
| 19 |
+
These tools can be used by an AI agent to fetch stock, ETF, or crypto data,
|
| 20 |
+
analyze recent market trends, market sentiments, and generate market insights.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
@staticmethod
|
| 24 |
+
@function_tool
|
| 25 |
+
@log_call
|
| 26 |
+
def get_summary(symbol: str, period: str = "1d", interval: str = "1h") -> str:
|
| 27 |
+
"""
|
| 28 |
+
Fetch the latest summary information and intraday price data for a given ticker.
|
| 29 |
+
Ensures recent data is retrieved by calculating start/end dates dynamically.
|
| 30 |
+
|
| 31 |
+
Parameters:
|
| 32 |
+
-----------
|
| 33 |
+
symbol : str
|
| 34 |
+
The ticker symbol (e.g., "AAPL", "GOOG", "BTC-USD").
|
| 35 |
+
period : str, optional (default="1d")
|
| 36 |
+
Time range for price data. Examples: "1d", "5d", "1mo", "3mo".
|
| 37 |
+
interval : str, optional (default="1h")
|
| 38 |
+
Granularity of the data. Examples: "1m", "5m", "1h", "1d".
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
--------
|
| 42 |
+
str
|
| 43 |
+
A formatted string containing:
|
| 44 |
+
- Company/ticker name
|
| 45 |
+
- Current price and change
|
| 46 |
+
- Open, High, Low prices
|
| 47 |
+
- Volume
|
| 48 |
+
- Period and interval used
|
| 49 |
+
"""
|
| 50 |
+
try:
|
| 51 |
+
ticker = yf.Ticker(symbol)
|
| 52 |
+
|
| 53 |
+
# Calculate start and end dates based on period
|
| 54 |
+
end_date = datetime.today()
|
| 55 |
+
if period.endswith("d"):
|
| 56 |
+
days = int(period[:-1])
|
| 57 |
+
elif period.endswith("mo"):
|
| 58 |
+
days = int(period[:-2]) * 30
|
| 59 |
+
elif period.endswith("y"):
|
| 60 |
+
days = int(period[:-1]) * 365
|
| 61 |
+
else:
|
| 62 |
+
days = 30 # default 1 month
|
| 63 |
+
start_date = end_date - timedelta(days=days)
|
| 64 |
+
|
| 65 |
+
# Fetch recent data explicitly
|
| 66 |
+
data = ticker.history(
|
| 67 |
+
start=start_date.strftime("%Y-%m-%d"),
|
| 68 |
+
end=end_date.strftime("%Y-%m-%d"),
|
| 69 |
+
interval=interval
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
if data.empty:
|
| 73 |
+
return f"No data found for symbol '{symbol}'."
|
| 74 |
+
|
| 75 |
+
latest = data.iloc[-1]
|
| 76 |
+
current_price = round(latest["Close"], 2)
|
| 77 |
+
open_price = round(latest["Open"], 2)
|
| 78 |
+
change = round(current_price - open_price, 2)
|
| 79 |
+
pct_change = round((change / open_price) * 100, 2)
|
| 80 |
+
|
| 81 |
+
info = ticker.info
|
| 82 |
+
long_name = info.get("longName", symbol)
|
| 83 |
+
currency = info.get("currency", "USD")
|
| 84 |
+
|
| 85 |
+
formatted = [
|
| 86 |
+
f"📈 {long_name} ({symbol})",
|
| 87 |
+
f"Current Price: {current_price} {currency}",
|
| 88 |
+
f"Change: {change} ({pct_change}%)",
|
| 89 |
+
f"Open: {open_price} | High: {round(latest['High'], 2)} | Low: {round(latest['Low'], 2)}",
|
| 90 |
+
f"Volume: {int(latest['Volume'])}",
|
| 91 |
+
f"Period: {period} | Interval: {interval}",
|
| 92 |
+
]
|
| 93 |
+
return "\n".join(formatted)
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
return f"Error fetching data for '{symbol}': {e}"
|
| 97 |
+
|
| 98 |
+
@staticmethod
|
| 99 |
+
@function_tool
|
| 100 |
+
@log_call
|
| 101 |
+
def get_market_sentiment(symbol: str, period: str = "1mo") -> str:
|
| 102 |
+
"""
|
| 103 |
+
Analyze recent price changes and provide a simple market sentiment.
|
| 104 |
+
Uses dynamic start/end dates to ensure recent data.
|
| 105 |
+
|
| 106 |
+
This tool computes the percentage change over the specified period and
|
| 107 |
+
classifies the sentiment as:
|
| 108 |
+
- Bullish (if price increased >2%)
|
| 109 |
+
- Bearish (if price decreased >2%)
|
| 110 |
+
- Neutral (otherwise)
|
| 111 |
+
|
| 112 |
+
Parameters:
|
| 113 |
+
-----------
|
| 114 |
+
symbol : str
|
| 115 |
+
The ticker symbol (e.g., "AAPL", "GOOG", "BTC-USD").
|
| 116 |
+
period : str, optional (default="1mo")
|
| 117 |
+
Time range to analyze. Examples: "7d", "1mo", "3mo".
|
| 118 |
+
|
| 119 |
+
Returns:
|
| 120 |
+
--------
|
| 121 |
+
str
|
| 122 |
+
A human-readable sentiment string including percentage change.
|
| 123 |
+
"""
|
| 124 |
+
try:
|
| 125 |
+
ticker = yf.Ticker(symbol)
|
| 126 |
+
|
| 127 |
+
# Calculate start/end dynamically
|
| 128 |
+
end_date = datetime.today()
|
| 129 |
+
if period.endswith("d"):
|
| 130 |
+
days = int(period[:-1])
|
| 131 |
+
elif period.endswith("mo"):
|
| 132 |
+
days = int(period[:-2]) * 30
|
| 133 |
+
elif period.endswith("y"):
|
| 134 |
+
days = int(period[:-1]) * 365
|
| 135 |
+
else:
|
| 136 |
+
days = 30
|
| 137 |
+
start_date = end_date - timedelta(days=days)
|
| 138 |
+
|
| 139 |
+
data = ticker.history(
|
| 140 |
+
start=start_date.strftime("%Y-%m-%d"),
|
| 141 |
+
end=end_date.strftime("%Y-%m-%d")
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
if data.empty:
|
| 145 |
+
return f"No data for {symbol}."
|
| 146 |
+
|
| 147 |
+
recent_change = data["Close"].iloc[-1] - data["Close"].iloc[0]
|
| 148 |
+
pct_change = (recent_change / data["Close"].iloc[0]) * 100
|
| 149 |
+
|
| 150 |
+
sentiment = "Neutral"
|
| 151 |
+
if pct_change > 2:
|
| 152 |
+
sentiment = "Bullish"
|
| 153 |
+
elif pct_change < -2:
|
| 154 |
+
sentiment = "Bearish"
|
| 155 |
+
|
| 156 |
+
return f"{symbol} market sentiment ({period}): {sentiment} ({pct_change:.2f}% change)"
|
| 157 |
+
|
| 158 |
+
except Exception as e:
|
| 159 |
+
return f"Error fetching market sentiment for '{symbol}': {e}"
|
| 160 |
+
|
| 161 |
+
@staticmethod
|
| 162 |
+
@function_tool
|
| 163 |
+
@log_call
|
| 164 |
+
def get_history(symbol: str, period: str = "1mo") -> str:
|
| 165 |
+
"""
|
| 166 |
+
Fetch historical price data for a given ticker.
|
| 167 |
+
Ensures recent data is retrieved dynamically using start/end dates.
|
| 168 |
+
|
| 169 |
+
Parameters:
|
| 170 |
+
-----------
|
| 171 |
+
symbol : str
|
| 172 |
+
The ticker symbol (e.g., "AAPL", "GOOG", "BTC-USD").
|
| 173 |
+
period : str, optional (default="1mo")
|
| 174 |
+
The length of historical data to retrieve. Examples: "1d", "5d", "1mo", "3mo", "1y", "5y".
|
| 175 |
+
|
| 176 |
+
Returns:
|
| 177 |
+
--------
|
| 178 |
+
str
|
| 179 |
+
A formatted string showing the last 5 rows of historical prices (Open, High, Low, Close, Volume).
|
| 180 |
+
"""
|
| 181 |
+
try:
|
| 182 |
+
ticker = yf.Ticker(symbol)
|
| 183 |
+
|
| 184 |
+
# Calculate start/end dynamically
|
| 185 |
+
end_date = datetime.today()
|
| 186 |
+
if period.endswith("d"):
|
| 187 |
+
days = int(period[:-1])
|
| 188 |
+
elif period.endswith("mo"):
|
| 189 |
+
days = int(period[:-2]) * 30
|
| 190 |
+
elif period.endswith("y"):
|
| 191 |
+
days = int(period[:-1]) * 365
|
| 192 |
+
else:
|
| 193 |
+
days = 30
|
| 194 |
+
start_date = end_date - timedelta(days=days)
|
| 195 |
+
|
| 196 |
+
data = ticker.history(
|
| 197 |
+
start=start_date.strftime("%Y-%m-%d"),
|
| 198 |
+
end=end_date.strftime("%Y-%m-%d")
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
if data.empty:
|
| 202 |
+
return f"No historical data found for '{symbol}'."
|
| 203 |
+
return f"Historical data for {symbol} ({period}):\n{data.tail(5).to_string()}"
|
| 204 |
+
|
| 205 |
+
except Exception as e:
|
| 206 |
+
return f"Error fetching historical data for '{symbol}': {e}"
|