Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- appagents/research_agent.py +4 -1
- prompts/trade_recommendation.txt +3 -1
- tools/google_tools.py +95 -28
- tools/news_tools.py +59 -6
- tools/yahoo_tools.py +148 -7
- ui/app.py +193 -286
appagents/research_agent.py
CHANGED
|
@@ -18,6 +18,7 @@ class MarketResearchAgent:
|
|
| 18 |
TimeTools.current_datetime,
|
| 19 |
GoogleTools.search,
|
| 20 |
FinanceTools.get_summary,
|
|
|
|
| 21 |
FinanceTools.get_history,
|
| 22 |
# NewsTools.top_headlines,
|
| 23 |
# NewsTools.search_news,
|
|
@@ -33,8 +34,10 @@ class MarketResearchAgent:
|
|
| 33 |
3. **Synthesis:** When multiple tools provide information, synthesize and summarize results into a coherent, easy-to-understand response.
|
| 34 |
4. **Source Transparency:** Always cite your information sources clearly, preferably with links or named publications.
|
| 35 |
5. **Clarity and Brevity:** Use plain, professional language. Avoid speculation, filler text, or unnecessary verbosity.
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
|
| 38 |
"""
|
| 39 |
|
| 40 |
|
|
|
|
| 18 |
TimeTools.current_datetime,
|
| 19 |
GoogleTools.search,
|
| 20 |
FinanceTools.get_summary,
|
| 21 |
+
FinanceTools.get_market_sentiment,
|
| 22 |
FinanceTools.get_history,
|
| 23 |
# NewsTools.top_headlines,
|
| 24 |
# NewsTools.search_news,
|
|
|
|
| 34 |
3. **Synthesis:** When multiple tools provide information, synthesize and summarize results into a coherent, easy-to-understand response.
|
| 35 |
4. **Source Transparency:** Always cite your information sources clearly, preferably with links or named publications.
|
| 36 |
5. **Clarity and Brevity:** Use plain, professional language. Avoid speculation, filler text, or unnecessary verbosity.
|
| 37 |
+
6. **Simplification:** Break down complex topics into simpler terms that a general audience can understand.
|
| 38 |
+
7. **Verification:**: Verify an answer, explicitly state that the data could not be confirmed or is unavailable
|
| 39 |
|
| 40 |
+
Strictly avoid fabricating information.
|
| 41 |
"""
|
| 42 |
|
| 43 |
|
prompts/trade_recommendation.txt
CHANGED
|
@@ -1,2 +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
|
|
|
|
|
|
|
|
|
| 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 %.
|
tools/google_tools.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 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
|
|
@@ -13,21 +12,55 @@ load_dotenv()
|
|
| 13 |
# πΉ GOOGLE SEARCH TOOLSET (Serper.dev API)
|
| 14 |
# ============================================================
|
| 15 |
class GoogleTools:
|
| 16 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
Returns:
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
"""
|
| 32 |
try:
|
| 33 |
api_key = os.getenv("SERPER_API_KEY")
|
|
@@ -36,13 +69,13 @@ class GoogleTools:
|
|
| 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, "tbs": "qdr:d"}
|
| 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 = [
|
|
@@ -53,26 +86,60 @@ class GoogleTools:
|
|
| 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 |
-
#
|
| 62 |
-
#
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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")
|
|
|
|
| 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 = [
|
|
|
|
| 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}"
|
tools/news_tools.py
CHANGED
|
@@ -13,31 +13,83 @@ load_dotenv()
|
|
| 13 |
# πΉ NEWS TOOLSET (NewsAPI.org)
|
| 14 |
# ============================================================
|
| 15 |
class NewsTools:
|
| 16 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
@staticmethod
|
| 19 |
@function_tool
|
| 20 |
@log_call
|
| 21 |
def top_headlines(country: str = "us", num_results: int = 5) -> str:
|
| 22 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = {
|
|
@@ -45,7 +97,8 @@ class NewsTools:
|
|
| 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"
|
|
@@ -73,4 +126,4 @@ class NewsTools:
|
|
| 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}"
|
|
|
|
| 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 = {
|
|
|
|
| 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"
|
|
|
|
| 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}"
|
tools/yahoo_tools.py
CHANGED
|
@@ -4,8 +4,9 @@ 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
|
| 9 |
load_dotenv()
|
| 10 |
|
| 11 |
|
|
@@ -13,16 +14,60 @@ load_dotenv()
|
|
| 13 |
# πΉ YAHOO FINANCE TOOLSET
|
| 14 |
# ============================================================
|
| 15 |
class FinanceTools:
|
| 16 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
@staticmethod
|
| 19 |
@function_tool
|
| 20 |
@log_call
|
| 21 |
def get_summary(symbol: str, period: str = "1d", interval: str = "1h") -> str:
|
| 22 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
try:
|
| 24 |
ticker = yf.Ticker(symbol)
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
if data.empty:
|
| 28 |
return f"No data found for symbol '{symbol}'."
|
|
@@ -50,16 +95,112 @@ class FinanceTools:
|
|
| 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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
try:
|
| 59 |
ticker = yf.Ticker(symbol)
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}"
|
|
|
|
| 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 |
|
|
|
|
| 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}'."
|
|
|
|
| 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}"
|
ui/app.py
CHANGED
|
@@ -3,10 +3,8 @@ import os
|
|
| 3 |
import glob
|
| 4 |
import asyncio
|
| 5 |
import sys
|
| 6 |
-
import textwrap
|
| 7 |
|
| 8 |
-
# Add project root
|
| 9 |
-
# Retaining original path logic for environment compatibility
|
| 10 |
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 11 |
|
| 12 |
from appagents.research_agent import MarketResearchAgent
|
|
@@ -17,22 +15,16 @@ from agents import Runner, trace
|
|
| 17 |
# -----------------------------
|
| 18 |
def load_prompts(folder="prompts"):
|
| 19 |
prompts = []
|
| 20 |
-
|
| 21 |
for file_path in glob.glob(os.path.join(folder, "*.txt")):
|
| 22 |
-
with open(file_path, "r") as f:
|
| 23 |
content = f.read().strip()
|
| 24 |
if content:
|
| 25 |
prompts.append(content)
|
| 26 |
-
|
| 27 |
-
return prompts,
|
| 28 |
|
| 29 |
-
|
| 30 |
-
try:
|
| 31 |
-
prompts, prompt_labels = load_prompts()
|
| 32 |
-
except:
|
| 33 |
-
# Fallback if prompts folder is not available
|
| 34 |
-
prompts = ["What are the top 3 market trends in AI?", "Analyze the competitive landscape for EV startups.", "Suggest a strategic pivot for a struggling e-commerce company."]
|
| 35 |
-
prompt_labels = ["AI Trends Analysis", "EV Competitive Map", "E-commerce Strategy"]
|
| 36 |
|
| 37 |
# -----------------------------
|
| 38 |
# Streamlit page config
|
|
@@ -40,315 +32,230 @@ except:
|
|
| 40 |
st.set_page_config(page_title="AI Chat", layout="wide")
|
| 41 |
|
| 42 |
# -----------------------------
|
| 43 |
-
# Custom CSS (
|
| 44 |
# -----------------------------
|
| 45 |
st.markdown("""
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
.block-container {
|
| 50 |
-
padding-top: 2rem !important;
|
| 51 |
-
padding-bottom: 5rem;
|
| 52 |
-
max-width: 1100px; /* Slightly wider content area */
|
| 53 |
-
margin: 0 auto;
|
| 54 |
-
}
|
| 55 |
-
header[data-testid="stHeader"] { display: none !important; }
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
section[data-testid="stSidebar"] {
|
| 70 |
-
background-color: var(--card-bg);
|
| 71 |
-
border-right: 1px solid var(--border-color);
|
| 72 |
-
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05); /* Subtle lift */
|
| 73 |
-
padding-top: 2.5rem !important;
|
| 74 |
-
}
|
| 75 |
-
[data-testid="stSidebar"] h2 {
|
| 76 |
-
color: var(--accent-color);
|
| 77 |
-
font-weight: 700;
|
| 78 |
-
margin-bottom: 1.5rem;
|
| 79 |
-
}
|
| 80 |
-
[data-testid="stSidebar"] .stButton button {
|
| 81 |
-
/* Style the prompt button as a 'chip' */
|
| 82 |
-
text-align: left;
|
| 83 |
-
color: var(--secondary-text);
|
| 84 |
-
background-color: #f7f9fc;
|
| 85 |
-
border: 1px solid #DCE0E6;
|
| 86 |
-
border-radius: 8px;
|
| 87 |
-
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
| 88 |
-
padding: 12px 15px;
|
| 89 |
-
margin-bottom: 8px;
|
| 90 |
-
transition: all 0.2s ease;
|
| 91 |
-
}
|
| 92 |
-
[data-testid="stSidebar"] .stButton button:hover {
|
| 93 |
-
background-color: #E8EBF0;
|
| 94 |
-
border-color: var(--accent-color);
|
| 95 |
-
color: var(--text-color);
|
| 96 |
-
box-shadow: 0 3px 8px rgba(0,0,0,0.1);
|
| 97 |
-
}
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
text-align: center;
|
| 102 |
-
padding: 1rem 0 2rem 0;
|
| 103 |
-
}
|
| 104 |
-
.app-title {
|
| 105 |
-
font-size: 2.8rem;
|
| 106 |
-
font-weight: 800;
|
| 107 |
-
color: var(--text-color);
|
| 108 |
-
letter-spacing: -1px;
|
| 109 |
-
}
|
| 110 |
-
.app-subtitle {
|
| 111 |
-
font-size: 1.2rem;
|
| 112 |
-
color: var(--secondary-text);
|
| 113 |
-
margin-top: 0.5rem;
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
/* 5. Input Form (The Fancier Search Bar) */
|
| 117 |
-
[data-testid="stForm"] {
|
| 118 |
-
padding: 1.5rem 0;
|
| 119 |
-
position: sticky;
|
| 120 |
-
top: 0;
|
| 121 |
-
background-color: #f7f9fc;
|
| 122 |
-
z-index: 10;
|
| 123 |
-
}
|
| 124 |
-
[data-testid="stForm"] input {
|
| 125 |
-
border-radius: 16px; /* Fancier radius */
|
| 126 |
-
height: 65px;
|
| 127 |
-
font-size: 1.15rem;
|
| 128 |
-
border: 1px solid var(--border-color) !important;
|
| 129 |
-
box-shadow: 0 6px 20px rgba(0,0,0,0.08); /* Softer initial shadow */
|
| 130 |
-
transition: border-color 0.3s, box-shadow 0.3s;
|
| 131 |
-
}
|
| 132 |
-
[data-testid="stForm"] input:focus {
|
| 133 |
-
border-color: var(--accent-light) !important;
|
| 134 |
-
box-shadow: 0 0 0 3px rgba(91, 72, 204, 0.2), 0 6px 20px rgba(0,0,0,0.15); /* Accent glow on focus */
|
| 135 |
-
}
|
| 136 |
-
[data-testid="stForm"] button {
|
| 137 |
-
/* Gradient Button with strong shadow */
|
| 138 |
-
background: linear-gradient(45deg, var(--accent-light), var(--accent-color));
|
| 139 |
-
color: white;
|
| 140 |
-
border-radius: 16px;
|
| 141 |
-
font-weight: 700;
|
| 142 |
-
height: 65px;
|
| 143 |
-
margin-left: 15px;
|
| 144 |
-
box-shadow: 0 5px 20px rgba(91, 72, 204, 0.4);
|
| 145 |
-
transition: all 0.2s;
|
| 146 |
-
}
|
| 147 |
-
[data-testid="stForm"] button:hover {
|
| 148 |
-
background: linear-gradient(45deg, var(--accent-color), var(--accent-light));
|
| 149 |
-
box-shadow: 0 8px 25px rgba(91, 72, 204, 0.55);
|
| 150 |
-
}
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
font-size: 1.4rem;
|
| 166 |
-
margin-top: 0;
|
| 167 |
-
margin-bottom: 20px;
|
| 168 |
-
padding-bottom: 10px;
|
| 169 |
-
border-bottom: 2px solid #F0F2F6; /* Clean separator */
|
| 170 |
-
font-weight: 700;
|
| 171 |
-
}
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
""", unsafe_allow_html=True)
|
| 191 |
|
| 192 |
# -----------------------------
|
| 193 |
-
# Session state
|
| 194 |
# -----------------------------
|
| 195 |
if "chat_history" not in st.session_state:
|
| 196 |
-
st.session_state.chat_history = []
|
| 197 |
|
| 198 |
if "input_value" not in st.session_state:
|
| 199 |
st.session_state.input_value = ""
|
| 200 |
|
| 201 |
-
if "
|
| 202 |
-
st.session_state.
|
| 203 |
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
# -----------------------------
|
| 207 |
-
async def get_ai_response(prompt):
|
| 208 |
-
# Mocking the agent response since the actual agent dependencies are unavailable
|
| 209 |
-
if "agent" not in locals():
|
| 210 |
-
class MockAgent:
|
| 211 |
-
def create(self): return self
|
| 212 |
-
async def run(self, prompt):
|
| 213 |
-
class MockResult:
|
| 214 |
-
def __init__(self, output): self.final_output = output
|
| 215 |
-
|
| 216 |
-
if "trends" in prompt.lower():
|
| 217 |
-
return MockResult(textwrap.dedent("""
|
| 218 |
-
#### Global AI Market Trends Summary
|
| 219 |
-
The AI landscape is rapidly accelerating across three primary vectors:
|
| 220 |
-
|
| 221 |
-
* **Hyper-Personalization at Scale:** New models leverage real-time behavioral data to offer truly individualized experiences in e-commerce, education, and healthcare.
|
| 222 |
-
* **Synthetic Data Generation:** Due to privacy concerns and high costs of real-world data, the use of high-fidelity synthetic data for training complex models is skyrocketing.
|
| 223 |
-
* **Governance and Regulation Focus:** Significant R&D and policy focus is shifting toward verifiable, explainable, and ethically compliant systems (e.g., EU AI Act compliance).
|
| 224 |
-
"""))
|
| 225 |
-
elif "competitive landscape" in prompt.lower():
|
| 226 |
-
return MockResult(textwrap.dedent("""
|
| 227 |
-
#### The Competitive Map for EV Startups
|
| 228 |
-
The EV market competition is fierce, centered on two core strategic areas:
|
| 229 |
-
|
| 230 |
-
1. **Proprietary Software Ecosystems:** Winning long-term customers relies on creating unique in-vehicle software experiences and robust integration with personal digital devices.
|
| 231 |
-
2. **Next-Generation Charging Solutions:** Companies pioneering extreme fast-charging (over 350kW) or battery-swap models are gaining strategic advantages in logistics and consumer convenience.
|
| 232 |
-
"""))
|
| 233 |
-
else:
|
| 234 |
-
return MockResult(textwrap.dedent(f"""
|
| 235 |
-
#### Strategic Recommendation for '{prompt}'
|
| 236 |
-
For a company seeking a strategic pivot, three actions are critical:
|
| 237 |
-
|
| 238 |
-
* **De-Risking Supply Chains:** Shift sourcing and manufacturing to multi-regional supply hubs to mitigate geopolitical and logistics vulnerabilities.
|
| 239 |
-
* **Subscription-Based Revenue Model:** Introduce a tier of subscription services for core offerings, creating recurring, high-margin revenue streams that stabilize cash flow.
|
| 240 |
-
* **Talent Recalibration:** Invest heavily in upskilling existing teams in AI/ML and data science, moving away from legacy skill sets to future-proof the organization's technological capacity.
|
| 241 |
-
"""))
|
| 242 |
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
Runner = type('Runner', (object,), {'run': MarketResearchAgent.run})
|
| 246 |
-
trace = lambda name: type('trace', (object,), {'__enter__': lambda s: None, '__exit__': lambda s,e,t: None})
|
| 247 |
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
|
|
|
| 255 |
|
| 256 |
# -----------------------------
|
| 257 |
-
# Sidebar
|
| 258 |
# -----------------------------
|
| 259 |
-
with st.sidebar:
|
| 260 |
-
st.
|
| 261 |
-
st.
|
| 262 |
for idx, prompt_text in enumerate(prompts):
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
st.session_state.
|
|
|
|
| 266 |
|
| 267 |
# -----------------------------
|
| 268 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
# -----------------------------
|
| 270 |
st.markdown("""
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
<div class="
|
|
|
|
| 274 |
</div>
|
|
|
|
| 275 |
""", unsafe_allow_html=True)
|
| 276 |
|
| 277 |
# -----------------------------
|
| 278 |
-
# Chat
|
| 279 |
# -----------------------------
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
<span style='font-size:2.2rem;'>π‘</span>
|
| 289 |
-
<div class="user-message-text">{chat['message']}</div>
|
| 290 |
-
</div>
|
| 291 |
-
""",
|
| 292 |
-
unsafe_allow_html=True
|
| 293 |
-
)
|
| 294 |
-
else:
|
| 295 |
-
# The AI card is structured to allow the content (chat['message'])
|
| 296 |
-
# which contains markdown, to be printed and styled inside the card.
|
| 297 |
-
st.markdown(
|
| 298 |
-
f"""
|
| 299 |
-
<div class="ai-response-card">
|
| 300 |
-
<h4 style='display: flex; align-items: center;'>
|
| 301 |
-
<span style='margin-right: 12px; font-size: 1.4em;'>π€</span> Insight Report
|
| 302 |
-
</h4>
|
| 303 |
-
{chat['message']}
|
| 304 |
-
</div>
|
| 305 |
-
""",
|
| 306 |
-
unsafe_allow_html=True
|
| 307 |
-
)
|
| 308 |
-
elif not st.session_state.send_triggered:
|
| 309 |
-
# Initial state message when no chat history exists
|
| 310 |
-
st.info("Start by asking a complex question about market trends, competitors, or strategy. Use the sidebar for quick ideas!")
|
| 311 |
-
|
| 312 |
|
| 313 |
# -----------------------------
|
| 314 |
-
#
|
| 315 |
# -----------------------------
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
value=st.session_state.input_value,
|
| 322 |
-
placeholder="E.g., What are the key regulatory hurdles for carbon capture technologies in Europe?",
|
| 323 |
-
label_visibility="collapsed",
|
| 324 |
-
key="chat_input"
|
| 325 |
-
)
|
| 326 |
-
with col2:
|
| 327 |
-
send_button = st.form_submit_button("Analyze")
|
| 328 |
|
| 329 |
# -----------------------------
|
| 330 |
-
# Handle
|
| 331 |
# -----------------------------
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
|
|
|
|
|
|
| 337 |
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
with st.spinner("β³ Analyzing market data... Generating detailed insight report..."):
|
| 344 |
try:
|
| 345 |
-
|
| 346 |
-
st.session_state.chat_history.insert(0, {"role": "assistant", "message": response})
|
| 347 |
except Exception as e:
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import glob
|
| 4 |
import asyncio
|
| 5 |
import sys
|
|
|
|
| 6 |
|
| 7 |
+
# Add project root
|
|
|
|
| 8 |
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 9 |
|
| 10 |
from appagents.research_agent import MarketResearchAgent
|
|
|
|
| 15 |
# -----------------------------
|
| 16 |
def load_prompts(folder="prompts"):
|
| 17 |
prompts = []
|
| 18 |
+
prompt_labels = []
|
| 19 |
for file_path in glob.glob(os.path.join(folder, "*.txt")):
|
| 20 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 21 |
content = f.read().strip()
|
| 22 |
if content:
|
| 23 |
prompts.append(content)
|
| 24 |
+
prompt_labels.append(os.path.basename(file_path).replace("_", " ").replace(".txt", "").title())
|
| 25 |
+
return prompts, prompt_labels
|
| 26 |
|
| 27 |
+
prompts, prompt_labels = load_prompts()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
# -----------------------------
|
| 30 |
# Streamlit page config
|
|
|
|
| 32 |
st.set_page_config(page_title="AI Chat", layout="wide")
|
| 33 |
|
| 34 |
# -----------------------------
|
| 35 |
+
# Custom CSS (chat, hero banner, input)
|
| 36 |
# -----------------------------
|
| 37 |
st.markdown("""
|
| 38 |
+
<style>
|
| 39 |
+
header[data-testid="stHeader"] {display: none !important;}
|
| 40 |
+
.block-container {padding-top: 0 !important; margin-top: -2rem !important;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
/* Hero banner */
|
| 43 |
+
.hero-banner {
|
| 44 |
+
width: 100%;
|
| 45 |
+
background: linear-gradient(90deg, #343541 0%, #444654 100%);
|
| 46 |
+
color: white;
|
| 47 |
+
padding: 1.5rem 2rem;
|
| 48 |
+
border-radius: 0 0 12px 12px;
|
| 49 |
+
margin-bottom: 1rem;
|
| 50 |
+
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
|
| 51 |
+
}
|
| 52 |
+
.hero-text { font-size: 1.8rem; font-weight: 700; }
|
| 53 |
+
.hero-subtext { font-size: 1rem; opacity: 0.8; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
/* Sidebar */
|
| 56 |
+
section[data-testid="stSidebar"] {padding-top: 0.5rem !important;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
+
/* Chat container */
|
| 59 |
+
.chat-container {
|
| 60 |
+
display: flex;
|
| 61 |
+
flex-direction: column;
|
| 62 |
+
background-color: #ffffff;
|
| 63 |
+
padding: 1.5rem;
|
| 64 |
+
border-radius: 12px;
|
| 65 |
+
height: 70vh;
|
| 66 |
+
overflow-y: auto;
|
| 67 |
+
border: 1px solid #ddd;
|
| 68 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.05);
|
| 69 |
+
margin-top: 1rem;
|
| 70 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
+
/* Chat bubbles */
|
| 73 |
+
.user-bubble, .ai-bubble {
|
| 74 |
+
padding: 12px 16px;
|
| 75 |
+
border-radius: 12px;
|
| 76 |
+
max-width: 75%;
|
| 77 |
+
margin: 6px 0;
|
| 78 |
+
word-wrap: break-word;
|
| 79 |
+
font-size: 15px;
|
| 80 |
+
line-height: 1.4;
|
| 81 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 82 |
+
}
|
| 83 |
+
.user-bubble {
|
| 84 |
+
background-color: #DCF8C6;
|
| 85 |
+
color: #111;
|
| 86 |
+
align-self: flex-end;
|
| 87 |
+
}
|
| 88 |
+
.ai-bubble {
|
| 89 |
+
background-color: #f0f0f0;
|
| 90 |
+
color: #111;
|
| 91 |
+
align-self: flex-start;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* Icons */
|
| 95 |
+
.icon {
|
| 96 |
+
font-size: 28px;
|
| 97 |
+
margin: 4px;
|
| 98 |
+
opacity: 0.8;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/* Input area */
|
| 102 |
+
.stTextInput input {
|
| 103 |
+
border-radius: 10px;
|
| 104 |
+
border: 1px solid #ccc;
|
| 105 |
+
padding: 12px;
|
| 106 |
+
font-size: 15px;
|
| 107 |
+
}
|
| 108 |
+
.stForm button {
|
| 109 |
+
border-radius: 10px;
|
| 110 |
+
font-size: 15px;
|
| 111 |
+
background-color: #10a37f !important;
|
| 112 |
+
color: white !important;
|
| 113 |
+
border: none !important;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/* Mobile-specific Quick Prompts at top */
|
| 117 |
+
@media (max-width: 768px) {
|
| 118 |
+
.mobile-prompts { display: block; margin-bottom: 1rem; }
|
| 119 |
+
.desktop-sidebar { display: none; }
|
| 120 |
+
.chat-container { height: 60vh; background-color: #f5f5f5; }
|
| 121 |
+
.user-bubble, .ai-bubble { font-size: 14px; }
|
| 122 |
+
}
|
| 123 |
+
@media (min-width: 769px) {
|
| 124 |
+
.mobile-prompts { display: none; }
|
| 125 |
+
.desktop-sidebar { display: block; }
|
| 126 |
+
}
|
| 127 |
+
</style>
|
| 128 |
""", unsafe_allow_html=True)
|
| 129 |
|
| 130 |
# -----------------------------
|
| 131 |
+
# Session state defaults
|
| 132 |
# -----------------------------
|
| 133 |
if "chat_history" not in st.session_state:
|
| 134 |
+
st.session_state.chat_history = [] # newest-first
|
| 135 |
|
| 136 |
if "input_value" not in st.session_state:
|
| 137 |
st.session_state.input_value = ""
|
| 138 |
|
| 139 |
+
if "pending_response" not in st.session_state:
|
| 140 |
+
st.session_state.pending_response = False
|
| 141 |
|
| 142 |
+
if "pending_message" not in st.session_state:
|
| 143 |
+
st.session_state.pending_message = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
+
if "auto_send_prompt" not in st.session_state:
|
| 146 |
+
st.session_state.auto_send_prompt = None
|
|
|
|
|
|
|
| 147 |
|
| 148 |
+
# -----------------------------
|
| 149 |
+
# Async AI response
|
| 150 |
+
# -----------------------------
|
| 151 |
+
async def get_ai_response(prompt: str) -> str:
|
| 152 |
+
agent = MarketResearchAgent.create()
|
| 153 |
+
with trace("Chatting with AI"):
|
| 154 |
+
result = await Runner.run(agent, prompt)
|
| 155 |
+
return result.final_output
|
| 156 |
|
| 157 |
# -----------------------------
|
| 158 |
+
# Desktop Sidebar Quick Prompts
|
| 159 |
# -----------------------------
|
| 160 |
+
with st.sidebar.container() if st.config.get_option("server.headless") is False else st.container() as container:
|
| 161 |
+
st.markdown('<div class="desktop-sidebar">', unsafe_allow_html=True)
|
| 162 |
+
st.sidebar.title("π‘ Quick Prompts")
|
| 163 |
for idx, prompt_text in enumerate(prompts):
|
| 164 |
+
label = prompt_labels[idx] if idx < len(prompt_labels) else f"Prompt {idx+1}"
|
| 165 |
+
if st.sidebar.button(label, key=f"prompt_{idx}"):
|
| 166 |
+
st.session_state.auto_send_prompt = prompt_text
|
| 167 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 168 |
|
| 169 |
# -----------------------------
|
| 170 |
+
# Mobile Quick Prompts
|
| 171 |
+
# -----------------------------
|
| 172 |
+
with st.container():
|
| 173 |
+
st.markdown('<div class="mobile-prompts">', unsafe_allow_html=True)
|
| 174 |
+
with st.expander("π‘ Quick Prompts"):
|
| 175 |
+
for idx, prompt_text in enumerate(prompts):
|
| 176 |
+
label = prompt_labels[idx] if idx < len(prompt_labels) else f"Prompt {idx+1}"
|
| 177 |
+
if st.button(label, key=f"mobile_prompt_{idx}"):
|
| 178 |
+
st.session_state.auto_send_prompt = prompt_text
|
| 179 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 180 |
+
|
| 181 |
+
# -----------------------------
|
| 182 |
+
# Hero banner
|
| 183 |
# -----------------------------
|
| 184 |
st.markdown("""
|
| 185 |
+
<div class="hero-banner">
|
| 186 |
+
<div>
|
| 187 |
+
<div class="hero-text">π€ AI Chatbot</div>
|
| 188 |
+
<div class="hero-subtext">Your intelligent assistant for insights, trends, and strategy exploration.</div>
|
| 189 |
</div>
|
| 190 |
+
</div>
|
| 191 |
""", unsafe_allow_html=True)
|
| 192 |
|
| 193 |
# -----------------------------
|
| 194 |
+
# Chat input area
|
| 195 |
# -----------------------------
|
| 196 |
+
with st.form(key="chat_form", clear_on_submit=False):
|
| 197 |
+
user_input = st.text_input(
|
| 198 |
+
"Type your message here:",
|
| 199 |
+
value=st.session_state.input_value,
|
| 200 |
+
placeholder="Send a message...",
|
| 201 |
+
key="chat_input"
|
| 202 |
+
)
|
| 203 |
+
send_button = st.form_submit_button("Send")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
# -----------------------------
|
| 206 |
+
# Helper to insert user message immediately
|
| 207 |
# -----------------------------
|
| 208 |
+
def send_user_message(msg):
|
| 209 |
+
st.session_state.chat_history.insert(0, {"role": "user", "message": msg})
|
| 210 |
+
st.session_state.pending_message = msg
|
| 211 |
+
st.session_state.pending_response = True
|
| 212 |
+
st.session_state.input_value = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
# -----------------------------
|
| 215 |
+
# Handle normal send
|
| 216 |
# -----------------------------
|
| 217 |
+
if send_button and user_input.strip():
|
| 218 |
+
send_user_message(user_input.strip())
|
| 219 |
+
|
| 220 |
+
# Handle sidebar/mobile prompt auto-send
|
| 221 |
+
if st.session_state.auto_send_prompt:
|
| 222 |
+
send_user_message(st.session_state.auto_send_prompt)
|
| 223 |
+
st.session_state.auto_send_prompt = None
|
| 224 |
|
| 225 |
+
# -----------------------------
|
| 226 |
+
# Handle AI response asynchronously
|
| 227 |
+
# -----------------------------
|
| 228 |
+
if st.session_state.pending_response and st.session_state.pending_message:
|
| 229 |
+
with st.spinner("π€ Thinking..."):
|
|
|
|
| 230 |
try:
|
| 231 |
+
ai_response = asyncio.run(get_ai_response(st.session_state.pending_message))
|
|
|
|
| 232 |
except Exception as e:
|
| 233 |
+
ai_response = f"[Error generating response: {e}]"
|
| 234 |
+
st.session_state.chat_history.insert(0, {"role": "assistant", "message": ai_response})
|
| 235 |
+
st.session_state.pending_response = False
|
| 236 |
+
st.session_state.pending_message = None
|
| 237 |
+
|
| 238 |
+
# -----------------------------
|
| 239 |
+
# Display chat history with Markdown in AI bubbles
|
| 240 |
+
# -----------------------------
|
| 241 |
+
for chat in st.session_state.chat_history:
|
| 242 |
+
if chat["role"] == "user":
|
| 243 |
+
msg_html = chat["message"].replace("\n","<br>")
|
| 244 |
+
st.markdown(
|
| 245 |
+
f"<div style='display:flex; justify-content:flex-end; align-items:flex-start;'>"
|
| 246 |
+
f"<div class='user-bubble'>{msg_html}</div>"
|
| 247 |
+
f"<span class='icon'>π€</span>"
|
| 248 |
+
f"</div>", unsafe_allow_html=True
|
| 249 |
+
)
|
| 250 |
+
else:
|
| 251 |
+
st.markdown(
|
| 252 |
+
f"""
|
| 253 |
+
<div style='display:flex; justify-content:flex-start; align-items:flex-start;'>
|
| 254 |
+
<span class='icon'>π€</span>
|
| 255 |
+
<div class='ai-bubble'>
|
| 256 |
+
{chat['message']}
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
""",
|
| 260 |
+
unsafe_allow_html=True
|
| 261 |
+
)
|