mishrabp commited on
Commit
eedb75f
Β·
verified Β·
1 Parent(s): 98fa1b7

Upload folder using huggingface_hub

Browse files
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
- If you cannot verify an answer, explicitly state that the data could not be confirmed or is unavailable.
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 stock 3 months trend and study the market sentiment to conclude into the answer.
 
 
 
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
- """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")
@@ -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
- # # πŸ”Ή 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}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- """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 = {
@@ -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 once
9
  load_dotenv()
10
 
11
 
@@ -13,16 +14,60 @@ load_dotenv()
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}'."
@@ -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
- """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}"
 
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 to sys.path
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
- prompt_lables = []
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
- prompt_lables.append(os.path.basename(file_path).replace("_", " ").replace(".txt", "").title())
27
- return prompts, prompt_lables
28
 
29
- # Assuming 'prompts' folder exists relative to where this script is run
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 (Fancier, Premium UI & Mobile Fixes)
44
  # -----------------------------
45
  st.markdown("""
46
- <style>
47
- /* 1. Global & Layout Reset */
48
- .stApp { background-color: #F7F9FC; /* Very light cool gray */ font-family: 'Inter', sans-serif; }
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
- /* 2. Accent Color & Typography */
58
- :root {
59
- --accent-color: #5B48CC; /* Deep Violet/Purple */
60
- --accent-light: #7E6DD3;
61
- --text-color: #1A1A2E;
62
- --secondary-text: #4C4C66;
63
- --card-bg: #ffffff;
64
- --border-color: #E0E4EB;
65
- --shadow-strong: 0 10px 30px rgba(0, 0, 0, 0.15);
66
- }
67
-
68
- /* 3. Sidebar (Quick Prompts Navigation) */
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
- /* 4. Main Header/Title */
100
- .app-title-container {
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
- /* 6. AI Response - Premium Card Styles */
153
- .ai-response-card {
154
- background-color: var(--card-bg);
155
- border: none;
156
- border-radius: 20px;
157
- padding: 30px;
158
- margin-bottom: 40px;
159
- box-shadow: var(--shadow-strong); /* Stronger shadow for depth */
160
- line-height: 1.65;
161
- color: var(--text-color);
162
- }
163
- .ai-response-card h4 {
164
- color: var(--accent-color);
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
- /* --- MOBILE FLOATING SIDEBAR FIX (Bidirectional Nav) --- */
174
- @media (max-width: 768px) {
175
- /* Force fixed position and high z-index on the sidebar content wrapper when open */
176
- [data-testid="stSidebar"] {
177
- z-index: 10000 !important; /* Extremely high z-index */
178
- }
179
- [data-testid="stSidebarContent"] {
180
- position: fixed !important;
181
- top: 0 !important;
182
- left: 0 !important;
183
- width: 85% !important; /* Good width for mobile access */
184
- height: 100vh !important;
185
- box-shadow: 4px 0 20px rgba(0,0,0,0.5); /* Strong shadow when open */
186
- transition: transform 0.3s ease;
187
- }
188
- }
189
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 "send_triggered" not in st.session_state:
202
- st.session_state.send_triggered = False
203
 
204
- # -----------------------------
205
- # Function to fetch AI response (Mocked for runnable code)
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
- global MarketResearchAgent, Runner, trace
244
- MarketResearchAgent = MockAgent()
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
- try:
249
- agent = MarketResearchAgent.create()
250
- with trace("Chatting with AI"):
251
- result = await Runner.run(agent, prompt)
252
- return result.final_output
253
- except Exception as e:
254
- return f"Sorry, the analysis engine encountered an error: {e}"
 
255
 
256
  # -----------------------------
257
- # Sidebar with prompts (Bidirectional Left Navigation)
258
  # -----------------------------
259
- with st.sidebar:
260
- st.title("πŸ’‘ Quick Prompts")
261
- st.markdown('<p style="font-size:0.9rem; color:var(--secondary-text); margin-bottom: 20px;">Jumpstart your research with these pre-defined queries.</p>', unsafe_allow_html=True)
262
  for idx, prompt_text in enumerate(prompts):
263
- if st.button(prompt_labels[idx], key=f"prompt_{idx}", help=prompt_text):
264
- st.session_state.input_value = prompt_text
265
- st.session_state.send_triggered = True
 
266
 
267
  # -----------------------------
268
- # Main Header/Title
 
 
 
 
 
 
 
 
 
 
 
 
269
  # -----------------------------
270
  st.markdown("""
271
- <div class="app-title-container">
272
- <div class="app-title">Hello, how can I help you today?</div>
273
- <div class="app-subtitle">Your strategic partner for market analysis and next-level insights.</div>
 
274
  </div>
 
275
  """, unsafe_allow_html=True)
276
 
277
  # -----------------------------
278
- # Chat history display (FIXED to render correctly)
279
  # -----------------------------
280
- # Displaying directly in the main app flow for reliability
281
- if st.session_state.chat_history:
282
- for chat in st.session_state.chat_history:
283
- if chat["role"] == "user":
284
- st.markdown(
285
- f"""
286
- <div style="margin-top: 30px;"></div>
287
- <div class="user-message-block">
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
- # Chat input area (The central search bar)
315
  # -----------------------------
316
- with st.form(key="chat_form", clear_on_submit=True):
317
- col1, col2 = st.columns([1, 0.12])
318
- with col1:
319
- user_input = st.text_input(
320
- "Enter your prompt:",
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 sending message
331
  # -----------------------------
332
- message_to_send = ""
333
- if send_button and user_input:
334
- message_to_send = user_input.strip()
335
- elif st.session_state.send_triggered and st.session_state.input_value:
336
- message_to_send = st.session_state.input_value.strip()
 
 
337
 
338
- if message_to_send:
339
- # 1. Add user message to history
340
- st.session_state.chat_history.insert(0, {"role": "user", "message": message_to_send})
341
-
342
- # 2. Get and add AI response
343
- with st.spinner("⏳ Analyzing market data... Generating detailed insight report..."):
344
  try:
345
- response = asyncio.run(get_ai_response(message_to_send))
346
- st.session_state.chat_history.insert(0, {"role": "assistant", "message": response})
347
  except Exception as e:
348
- st.error(f"An unexpected error occurred during processing: {e}")
349
- st.session_state.chat_history.insert(0, {"role": "assistant", "message": "Sorry, I encountered an internal error and couldn't complete the analysis."})
350
-
351
- # 3. Clear state and force a rerun (FIXED: using st.rerun())
352
- st.session_state.input_value = ""
353
- st.session_state.send_triggered = False
354
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ )