Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Dockerfile +33 -0
- README.md +18 -10
- SETUP.md +18 -0
- appagents/__init__.py +0 -0
- appagents/research_agent.py +38 -0
- core/__init__.py +0 -0
- core/logger.py +22 -0
- prompts/company_analysis.txt +2 -0
- prompts/crypto_update.txt +2 -0
- prompts/economic_news.txt +2 -0
- prompts/market_overview.txt +2 -0
- prompts/market_sentiment.txt +2 -0
- prompts/tech_news.txt +2 -0
- requirements.txt +20 -0
- run.py +10 -0
- tools/__init__.py +0 -0
- tools/google_tools.py +78 -0
- tools/news_tools.py +76 -0
- tools/yahoo_tools.py +65 -0
- ui/app.py +135 -0
Dockerfile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use official Python slim image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Set environment variables
|
| 5 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 6 |
+
PIP_NO_CACHE_DIR=1 \
|
| 7 |
+
DEBIAN_FRONTEND=noninteractive
|
| 8 |
+
|
| 9 |
+
# Set working directory
|
| 10 |
+
WORKDIR /app
|
| 11 |
+
|
| 12 |
+
# Install system dependencies
|
| 13 |
+
RUN apt-get update && apt-get install -y \
|
| 14 |
+
git \
|
| 15 |
+
build-essential \
|
| 16 |
+
curl \
|
| 17 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 18 |
+
|
| 19 |
+
# Copy requirements file
|
| 20 |
+
COPY requirements.txt .
|
| 21 |
+
|
| 22 |
+
# Install Python dependencies
|
| 23 |
+
RUN pip install --upgrade pip
|
| 24 |
+
RUN pip install -r requirements.txt
|
| 25 |
+
|
| 26 |
+
# Copy the rest of the app
|
| 27 |
+
COPY . .
|
| 28 |
+
|
| 29 |
+
# Expose port for Streamlit
|
| 30 |
+
EXPOSE 7860
|
| 31 |
+
|
| 32 |
+
# Command to run the Streamlit app
|
| 33 |
+
CMD ["streamlit", "run", "ui/app.py", "--server.port=7860", "--server.address=0.0.0.0", "--server.headless=true"]
|
README.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Market Research AI Chatbot
|
| 2 |
+
|
| 3 |
+
This is a Streamlit-based AI chatbot for market research. It supports:
|
| 4 |
+
|
| 5 |
+
- Predefined prompts in the sidebar (with truncated display and tooltip for full text)
|
| 6 |
+
- Chat interface with latest messages at the top
|
| 7 |
+
- Enter key submission for messages
|
| 8 |
+
- AI responses using `MarketResearchAgent`
|
| 9 |
+
|
| 10 |
+
## 🚀 How to Run Locally
|
| 11 |
+
|
| 12 |
+
1. Clone the repo:
|
| 13 |
+
|
| 14 |
+
```bash
|
| 15 |
+
git clone <repo_url>
|
| 16 |
+
cd <repo_folder>
|
| 17 |
+
python run.py
|
| 18 |
+
```
|
SETUP.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
### Setting up .venv
|
| 2 |
+
```bash
|
| 3 |
+
conda create --prefix /home/azureuser/ws/agenticai/projects/chatbot/.venv python=3.11 -y
|
| 4 |
+
|
| 5 |
+
conda activate /home/azureuser/ws/agenticai/projects/chatbot/.venv
|
| 6 |
+
|
| 7 |
+
conda deactivate
|
| 8 |
+
|
| 9 |
+
uv pip install -r requirements.txt
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
### Run Unit Tests
|
| 13 |
+
```bash
|
| 14 |
+
pytest -v tests/test_data_agent.py
|
| 15 |
+
|
| 16 |
+
python -m pytest -v
|
| 17 |
+
|
| 18 |
+
```
|
appagents/__init__.py
ADDED
|
File without changes
|
appagents/research_agent.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from tools.google_tools import GoogleTools
|
| 2 |
+
from tools.news_tools import NewsTools
|
| 3 |
+
from tools.yahoo_tools import FinanceTools
|
| 4 |
+
from agents import Agent
|
| 5 |
+
|
| 6 |
+
class MarketResearchAgent:
|
| 7 |
+
"""
|
| 8 |
+
Encapsulates the AI agent definition for market research.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
@staticmethod
|
| 12 |
+
def create():
|
| 13 |
+
"""
|
| 14 |
+
Returns a configured Agent instance ready for use.
|
| 15 |
+
"""
|
| 16 |
+
tools = [
|
| 17 |
+
GoogleTools.search,
|
| 18 |
+
FinanceTools.get_summary,
|
| 19 |
+
FinanceTools.get_history,
|
| 20 |
+
NewsTools.top_headlines,
|
| 21 |
+
NewsTools.search_news,
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
instructions = """
|
| 25 |
+
You are a research assistant. Your primary goal is to give accurate, concise, and well-sourced answers.
|
| 26 |
+
|
| 27 |
+
If the question involves current events, factual data, or recent updates, always call the provided tools (like Google or News) to ensure information is current.
|
| 28 |
+
|
| 29 |
+
Prefer factual precision over opinion. Summarize retrieved results into clear, user-friendly responses.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
agent = Agent(
|
| 33 |
+
name="AI Assistant",
|
| 34 |
+
tools=tools,
|
| 35 |
+
instructions=instructions,
|
| 36 |
+
model="gpt-4o-mini"
|
| 37 |
+
)
|
| 38 |
+
return agent
|
core/__init__.py
ADDED
|
File without changes
|
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
|
prompts/company_analysis.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Analyze the recent performance of Apple Inc. (AAPL).
|
| 2 |
+
Include stock price trends, recent news, and any important announcements.
|
prompts/crypto_update.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Give a summary of the current cryptocurrency market.
|
| 2 |
+
Include top performers, losers, and overall market sentiment.
|
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.
|
prompts/market_overview.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Provide a concise overview of the current stock market trends and sentiment today.
|
| 2 |
+
Include any notable market-moving news or events.
|
prompts/market_sentiment.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
What is the current market sentiment based on news and financial reports?
|
| 2 |
+
Provide a brief analysis of investor confidence and market outlook.
|
prompts/tech_news.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Summarize the latest technology news from the past 24 hours.
|
| 2 |
+
Highlight any major product launches, acquisitions, or trends.
|
requirements.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openai==1.85.0
|
| 2 |
+
# via
|
| 3 |
+
# agents (pyproject.toml)
|
| 4 |
+
# autogen-ext
|
| 5 |
+
# langchain-openai
|
| 6 |
+
# openai-agents
|
| 7 |
+
# semantic-kernel
|
| 8 |
+
openai-agents==0.0.17
|
| 9 |
+
# via agents (pyproject.toml)
|
| 10 |
+
python-dotenv>=1.0.1
|
| 11 |
+
requests>=2.31.0
|
| 12 |
+
# via
|
| 13 |
+
# agents (pyproject.toml)
|
| 14 |
+
# autogen-ext
|
| 15 |
+
# langchain-openai
|
| 16 |
+
# openai-agents
|
| 17 |
+
# semantic-kernel
|
| 18 |
+
yfinance>=0.2.27
|
| 19 |
+
# via tools/news_tools.py, tools/yahoo_tools.py
|
| 20 |
+
streamlit
|
run.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import subprocess
|
| 3 |
+
|
| 4 |
+
# Run the Streamlit app with automatic reload on file changes
|
| 5 |
+
subprocess.run([
|
| 6 |
+
"streamlit",
|
| 7 |
+
"run",
|
| 8 |
+
os.path.join("ui", "app.py"),
|
| 9 |
+
"--server.runOnSave", "true"
|
| 10 |
+
])
|
tools/__init__.py
ADDED
|
File without changes
|
tools/google_tools.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 8 |
+
# Load environment variables once
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# ============================================================
|
| 13 |
+
# 🔹 GOOGLE SEARCH TOOLSET (Serper.dev API)
|
| 14 |
+
# ============================================================
|
| 15 |
+
class GoogleTools:
|
| 16 |
+
"""Provides tools for web search using Serper.dev (Google Search API)."""
|
| 17 |
+
|
| 18 |
+
@staticmethod
|
| 19 |
+
@function_tool
|
| 20 |
+
@log_call
|
| 21 |
+
def search(query: str, num_results: int = 3) -> str:
|
| 22 |
+
"""
|
| 23 |
+
Perform a general Google search using the Serper.dev API.
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
query (str): The search query string.
|
| 27 |
+
num_results (int): Number of results to return.
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
str: Formatted search results.
|
| 31 |
+
"""
|
| 32 |
+
try:
|
| 33 |
+
api_key = os.getenv("SERPER_API_KEY")
|
| 34 |
+
if not api_key:
|
| 35 |
+
return "Missing SERPER_API_KEY in environment variables."
|
| 36 |
+
|
| 37 |
+
url = "https://google.serper.dev/search"
|
| 38 |
+
headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
|
| 39 |
+
payload = {"q": query, "num": num_results}
|
| 40 |
+
|
| 41 |
+
response = requests.post(url, headers=headers, json=payload)
|
| 42 |
+
response.raise_for_status()
|
| 43 |
+
data = response.json()
|
| 44 |
+
|
| 45 |
+
if "organic" not in data:
|
| 46 |
+
return "No results found."
|
| 47 |
+
|
| 48 |
+
formatted_results = [
|
| 49 |
+
f"Title: {item.get('title')}\n"
|
| 50 |
+
f"Link: {item.get('link')}\n"
|
| 51 |
+
f"Snippet: {item.get('snippet', '')}\n"
|
| 52 |
+
for item in data["organic"][:num_results]
|
| 53 |
+
]
|
| 54 |
+
return "\n".join(formatted_results)
|
| 55 |
+
|
| 56 |
+
except Exception as e:
|
| 57 |
+
return f"Error performing Google search: {e}"
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# # ============================================================
|
| 61 |
+
# # 🔹 OPENAI & OTHER MODEL APIs (optional future grouping)
|
| 62 |
+
# # ============================================================
|
| 63 |
+
# class ModelTools:
|
| 64 |
+
# """Provides access to LLM APIs like OpenAI, Gemini, or Groq."""
|
| 65 |
+
|
| 66 |
+
# @staticmethod
|
| 67 |
+
# @function_tool
|
| 68 |
+
# def query_openai(prompt: str, model: str = "gpt-4o-mini") -> str:
|
| 69 |
+
# """Query OpenAI model."""
|
| 70 |
+
# try:
|
| 71 |
+
# client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
| 72 |
+
# response = client.chat.completions.create(
|
| 73 |
+
# model=model,
|
| 74 |
+
# messages=[{"role": "user", "content": prompt}],
|
| 75 |
+
# )
|
| 76 |
+
# return response.choices[0].message.content
|
| 77 |
+
# except Exception as e:
|
| 78 |
+
# return f"Error querying OpenAI API: {e}"
|
tools/news_tools.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 8 |
+
# Load environment variables once
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# ============================================================
|
| 13 |
+
# 🔹 NEWS TOOLSET (NewsAPI.org)
|
| 14 |
+
# ============================================================
|
| 15 |
+
class NewsTools:
|
| 16 |
+
"""Provides tools to fetch top headlines and topic-based news."""
|
| 17 |
+
|
| 18 |
+
@staticmethod
|
| 19 |
+
@function_tool
|
| 20 |
+
@log_call
|
| 21 |
+
def top_headlines(country: str = "us", num_results: int = 5) -> str:
|
| 22 |
+
"""Fetch top headlines for a given country."""
|
| 23 |
+
return NewsTools._fetch_news(query="", country=country, num_results=num_results)
|
| 24 |
+
|
| 25 |
+
@staticmethod
|
| 26 |
+
@function_tool
|
| 27 |
+
@log_call
|
| 28 |
+
def search_news(query: str, num_results: int = 5) -> str:
|
| 29 |
+
"""Search for recent news articles about a specific topic."""
|
| 30 |
+
return NewsTools._fetch_news(query=query, country="", num_results=num_results)
|
| 31 |
+
|
| 32 |
+
@staticmethod
|
| 33 |
+
@log_call
|
| 34 |
+
def _fetch_news(query: str, country: str, num_results: int) -> str:
|
| 35 |
+
"""Internal helper for NewsAPI.org."""
|
| 36 |
+
try:
|
| 37 |
+
api_key = os.getenv("NEWS_API_KEY")
|
| 38 |
+
if not api_key:
|
| 39 |
+
return "Missing NEWS_API_KEY in environment variables."
|
| 40 |
+
|
| 41 |
+
if query:
|
| 42 |
+
url = "https://newsapi.org/v2/everything"
|
| 43 |
+
params = {
|
| 44 |
+
"q": query,
|
| 45 |
+
"pageSize": num_results,
|
| 46 |
+
"apiKey": api_key,
|
| 47 |
+
"sortBy": "publishedAt",
|
| 48 |
+
"language": "en"
|
| 49 |
+
}
|
| 50 |
+
else:
|
| 51 |
+
url = "https://newsapi.org/v2/top-headlines"
|
| 52 |
+
params = {
|
| 53 |
+
"country": country or "us",
|
| 54 |
+
"pageSize": num_results,
|
| 55 |
+
"apiKey": api_key
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
response = requests.get(url, params=params)
|
| 59 |
+
response.raise_for_status()
|
| 60 |
+
data = response.json()
|
| 61 |
+
|
| 62 |
+
if not data.get("articles"):
|
| 63 |
+
return f"No news found for '{query or country}'."
|
| 64 |
+
|
| 65 |
+
formatted = [
|
| 66 |
+
f"📰 {a.get('title')}\n"
|
| 67 |
+
f" Source: {a.get('source', {}).get('name')}\n"
|
| 68 |
+
f" URL: {a.get('url')}\n"
|
| 69 |
+
for a in data["articles"][:num_results]
|
| 70 |
+
]
|
| 71 |
+
return "\n".join(formatted)
|
| 72 |
+
|
| 73 |
+
except requests.exceptions.RequestException as e:
|
| 74 |
+
return f"Network error while calling News API: {e}"
|
| 75 |
+
except Exception as e:
|
| 76 |
+
return f"Unexpected error fetching news: {e}"
|
tools/yahoo_tools.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 8 |
+
# Load environment variables once
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# ============================================================
|
| 13 |
+
# 🔹 YAHOO FINANCE TOOLSET
|
| 14 |
+
# ============================================================
|
| 15 |
+
class FinanceTools:
|
| 16 |
+
"""Provides tools for fetching stock, crypto, or ETF data from Yahoo Finance."""
|
| 17 |
+
|
| 18 |
+
@staticmethod
|
| 19 |
+
@function_tool
|
| 20 |
+
@log_call
|
| 21 |
+
def get_summary(symbol: str, period: str = "1d", interval: str = "1h") -> str:
|
| 22 |
+
"""Fetch summary and price data for a ticker."""
|
| 23 |
+
try:
|
| 24 |
+
ticker = yf.Ticker(symbol)
|
| 25 |
+
data = ticker.history(period=period, interval=interval)
|
| 26 |
+
|
| 27 |
+
if data.empty:
|
| 28 |
+
return f"No data found for symbol '{symbol}'."
|
| 29 |
+
|
| 30 |
+
latest = data.iloc[-1]
|
| 31 |
+
current_price = round(latest["Close"], 2)
|
| 32 |
+
open_price = round(latest["Open"], 2)
|
| 33 |
+
change = round(current_price - open_price, 2)
|
| 34 |
+
pct_change = round((change / open_price) * 100, 2)
|
| 35 |
+
|
| 36 |
+
info = ticker.info
|
| 37 |
+
long_name = info.get("longName", symbol)
|
| 38 |
+
currency = info.get("currency", "USD")
|
| 39 |
+
|
| 40 |
+
formatted = [
|
| 41 |
+
f"📈 {long_name} ({symbol})",
|
| 42 |
+
f"Current Price: {current_price} {currency}",
|
| 43 |
+
f"Change: {change} ({pct_change}%)",
|
| 44 |
+
f"Open: {open_price} | High: {round(latest['High'], 2)} | Low: {round(latest['Low'], 2)}",
|
| 45 |
+
f"Volume: {int(latest['Volume'])}",
|
| 46 |
+
f"Period: {period} | Interval: {interval}",
|
| 47 |
+
]
|
| 48 |
+
return "\n".join(formatted)
|
| 49 |
+
|
| 50 |
+
except Exception as e:
|
| 51 |
+
return f"Error fetching data for '{symbol}': {e}"
|
| 52 |
+
|
| 53 |
+
@staticmethod
|
| 54 |
+
@function_tool
|
| 55 |
+
@log_call
|
| 56 |
+
def get_history(symbol: str, period: str = "1mo") -> str:
|
| 57 |
+
"""Fetch historical data for a given ticker."""
|
| 58 |
+
try:
|
| 59 |
+
ticker = yf.Ticker(symbol)
|
| 60 |
+
data = ticker.history(period=period)
|
| 61 |
+
if data.empty:
|
| 62 |
+
return f"No historical data found for '{symbol}'."
|
| 63 |
+
return f"Historical data for {symbol} ({period}):\n{data.tail(5).to_string()}"
|
| 64 |
+
except Exception as e:
|
| 65 |
+
return f"Error fetching historical data: {e}"
|
ui/app.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import os
|
| 3 |
+
import glob
|
| 4 |
+
import asyncio
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
# Add project root to sys.path
|
| 8 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 9 |
+
|
| 10 |
+
from appagents.research_agent import MarketResearchAgent
|
| 11 |
+
from agents import Runner, trace
|
| 12 |
+
|
| 13 |
+
# -----------------------------
|
| 14 |
+
# Load predefined prompts
|
| 15 |
+
# -----------------------------
|
| 16 |
+
def load_prompts(folder="prompts"):
|
| 17 |
+
prompts = []
|
| 18 |
+
for file_path in glob.glob(os.path.join(folder, "*.txt")):
|
| 19 |
+
with open(file_path, "r") as f:
|
| 20 |
+
content = f.read().strip()
|
| 21 |
+
if content:
|
| 22 |
+
prompts.append(content)
|
| 23 |
+
return prompts
|
| 24 |
+
|
| 25 |
+
prompts = load_prompts()
|
| 26 |
+
|
| 27 |
+
# -----------------------------
|
| 28 |
+
# Streamlit page config
|
| 29 |
+
# -----------------------------
|
| 30 |
+
st.set_page_config(page_title="Market Research AI", layout="wide")
|
| 31 |
+
|
| 32 |
+
# -----------------------------
|
| 33 |
+
# Session state
|
| 34 |
+
# -----------------------------
|
| 35 |
+
if "chat_history" not in st.session_state:
|
| 36 |
+
st.session_state.chat_history = []
|
| 37 |
+
|
| 38 |
+
if "input_value" not in st.session_state:
|
| 39 |
+
st.session_state.input_value = ""
|
| 40 |
+
|
| 41 |
+
# -----------------------------
|
| 42 |
+
# Function to fetch AI response
|
| 43 |
+
# -----------------------------
|
| 44 |
+
async def get_ai_response(prompt):
|
| 45 |
+
agent = MarketResearchAgent.create()
|
| 46 |
+
with trace("Chatting with AI"):
|
| 47 |
+
result = await Runner.run(agent, prompt)
|
| 48 |
+
return result.final_output
|
| 49 |
+
|
| 50 |
+
# -----------------------------
|
| 51 |
+
# Sidebar prompts (2-line truncation with tooltip)
|
| 52 |
+
# -----------------------------
|
| 53 |
+
st.sidebar.title("Predefined Prompts")
|
| 54 |
+
|
| 55 |
+
for idx, prompt_text in enumerate(prompts):
|
| 56 |
+
# Truncate to 2 lines (~80 characters per line)
|
| 57 |
+
truncated = prompt_text
|
| 58 |
+
if len(prompt_text) > 160:
|
| 59 |
+
truncated = prompt_text[:157] + "..."
|
| 60 |
+
# Use a container for each button
|
| 61 |
+
with st.sidebar.container():
|
| 62 |
+
# Show the truncated prompt as a button
|
| 63 |
+
if st.button(truncated, key=f"prompt_{idx}"):
|
| 64 |
+
# Add user message
|
| 65 |
+
st.session_state.chat_history.insert(0, {"role": "user", "message": prompt_text})
|
| 66 |
+
# Fetch AI response
|
| 67 |
+
response = asyncio.run(get_ai_response(prompt_text))
|
| 68 |
+
st.session_state.chat_history.insert(0, {"role": "assistant", "message": response})
|
| 69 |
+
# Show tooltip below (hover shows full prompt)
|
| 70 |
+
st.markdown(f"<span title='{prompt_text}' style='font-size:10px;color:gray;'>Hover to see full prompt</span>", unsafe_allow_html=True)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# -----------------------------
|
| 74 |
+
# Main chat area
|
| 75 |
+
# -----------------------------
|
| 76 |
+
st.title("Market Research AI Chat")
|
| 77 |
+
|
| 78 |
+
# -----------------------------
|
| 79 |
+
# Chat input with Enter key submit (using st.form)
|
| 80 |
+
# -----------------------------
|
| 81 |
+
with st.form(key="chat_form", clear_on_submit=True):
|
| 82 |
+
user_input = st.text_input(
|
| 83 |
+
"Type your message here:",
|
| 84 |
+
value=st.session_state.input_value,
|
| 85 |
+
placeholder="Write a message...",
|
| 86 |
+
key="chat_input"
|
| 87 |
+
)
|
| 88 |
+
send_button = st.form_submit_button("Send")
|
| 89 |
+
|
| 90 |
+
if send_button and user_input.strip():
|
| 91 |
+
message = user_input.strip()
|
| 92 |
+
# Add user message
|
| 93 |
+
st.session_state.chat_history.insert(0, {"role": "user", "message": message})
|
| 94 |
+
# Get AI response
|
| 95 |
+
response = asyncio.run(get_ai_response(message))
|
| 96 |
+
st.session_state.chat_history.insert(0, {"role": "assistant", "message": response})
|
| 97 |
+
st.session_state.input_value = "" # input will be cleared automatically by form
|
| 98 |
+
|
| 99 |
+
# -----------------------------
|
| 100 |
+
# Display chat history
|
| 101 |
+
# -----------------------------
|
| 102 |
+
if st.session_state.chat_history:
|
| 103 |
+
chat_style = """
|
| 104 |
+
<style>
|
| 105 |
+
.chat-container {
|
| 106 |
+
display: flex;
|
| 107 |
+
flex-direction: column; /* latest messages on top */
|
| 108 |
+
border: 1px solid #ccc;
|
| 109 |
+
padding: 10px;
|
| 110 |
+
border-radius: 8px;
|
| 111 |
+
background-color: #fafafa;
|
| 112 |
+
}
|
| 113 |
+
</style>
|
| 114 |
+
"""
|
| 115 |
+
st.markdown(chat_style, unsafe_allow_html=True)
|
| 116 |
+
|
| 117 |
+
chat_html = '<div class="chat-container">'
|
| 118 |
+
for chat in st.session_state.chat_history:
|
| 119 |
+
if chat["role"] == "user":
|
| 120 |
+
chat_html += (
|
| 121 |
+
f"<div style='display:flex; align-items:center; justify-content:flex-end; margin:5px;'>"
|
| 122 |
+
f"<div style='background-color: #daf1fc; padding:10px; border-radius:10px; max-width:70%;'>{chat['message']}</div>"
|
| 123 |
+
f"<span style='font-size:28px; margin-left:8px;'>👤</span>"
|
| 124 |
+
f"</div>"
|
| 125 |
+
)
|
| 126 |
+
else:
|
| 127 |
+
chat_html += (
|
| 128 |
+
f"<div style='display:flex; align-items:center; justify-content:flex-start; margin:5px;'>"
|
| 129 |
+
f"<span style='font-size:28px; margin-right:8px;'>🤖</span>"
|
| 130 |
+
f"<div style='background-color: #f1f0f0; padding:10px; border-radius:10px; max-width:70%;'>{chat['message']}</div>"
|
| 131 |
+
f"</div>"
|
| 132 |
+
)
|
| 133 |
+
chat_html += '</div>'
|
| 134 |
+
|
| 135 |
+
st.markdown(chat_html, unsafe_allow_html=True)
|