mishrabp commited on
Commit
6b50ab8
·
verified ·
1 Parent(s): c4db792

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +8 -4
  2. common/__init__.py +0 -0
  3. common/aagents/__init__.py +0 -0
  4. common/aagents/google_agent.py +139 -0
  5. common/aagents/healthcare_agent.py +100 -0
  6. common/aagents/news_agent.py +106 -0
  7. common/aagents/weather_agent.py +69 -0
  8. common/aagents/web_agent.py +53 -0
  9. common/aagents/web_research_agent.py +83 -0
  10. common/aagents/yf_agent.py +78 -0
  11. common/mcp/README.md +139 -0
  12. common/mcp/__init__.py +0 -0
  13. common/mcp/mcp_server.py +171 -0
  14. common/mcp/tools/__init__.py +0 -0
  15. common/mcp/tools/google_tools.py +139 -0
  16. common/mcp/tools/news_tools.py +200 -0
  17. common/mcp/tools/rag_tool.py +106 -0
  18. common/mcp/tools/search_tools.py +115 -0
  19. common/mcp/tools/time_tools.py +32 -0
  20. common/mcp/tools/weather_tools.py +235 -0
  21. common/mcp/tools/yf_tools.py +192 -0
  22. common/rag/rag.py +94 -0
  23. common/utility/__init__.py +0 -0
  24. common/utility/embedding_factory.py +49 -0
  25. common/utility/llm_factory.py +130 -0
  26. common/utility/llm_factory2.py +75 -0
  27. common/utility/logger.py +22 -0
  28. pyproject.toml +2 -0
  29. run.py +215 -11
  30. src/chatbot/Dockerfile +35 -0
  31. src/chatbot/README.md +219 -0
  32. src/chatbot/app.py +230 -0
  33. src/chatbot/appagents/FinancialAgent.py +56 -0
  34. src/chatbot/appagents/InputValidationAgent.py +76 -0
  35. src/chatbot/appagents/NewsAgent.py +66 -0
  36. src/chatbot/appagents/OrchestratorAgent.py +167 -0
  37. src/chatbot/appagents/SearchAgent.py +57 -0
  38. src/chatbot/appagents/__init__.py +0 -0
  39. src/chatbot/core/__init__.py +0 -0
  40. src/chatbot/core/logger.py +22 -0
  41. src/chatbot/prompts/economic_news.txt +2 -0
  42. src/chatbot/prompts/market_sentiment.txt +1 -0
  43. src/chatbot/prompts/news_headlines.txt +5 -0
  44. src/chatbot/prompts/trade_recommendation.txt +4 -0
  45. src/chatbot/prompts/upcoming_earnings.txt +1 -0
  46. src/chatbot/tools/__init__.py +0 -0
  47. src/chatbot/tools/google_tools.py +145 -0
  48. src/chatbot/tools/news_tools.py +129 -0
  49. src/chatbot/tools/time_tools.py +22 -0
  50. 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 your source code
27
- COPY . .
28
 
29
  EXPOSE 7860
30
 
31
- CMD ["streamlit", "run", "ui/app.py", "--server.port=7860", "--server.address=0.0.0.0", "--server.headless=true"]
 
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
- import os
2
- import subprocess
3
- import sys
4
-
5
- # Use module execution to guarantee Streamlit runs inside the current interpreter
6
- subprocess.run([
7
- sys.executable, "-m", "streamlit",
8
- "run",
9
- os.path.join("ui", "app.py"),
10
- "--server.runOnSave", "true"
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}"