Spaces:
Sleeping
Sleeping
Upload 8 files
Browse files- Dockerfile +30 -0
- README.md +19 -0
- arithmetic_server.py +30 -0
- backend.py +257 -0
- frontend.py +218 -0
- huggingface.yaml +6 -0
- requirements.txt +9 -0
- stock_server.py +63 -0
Dockerfile
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -------- base
|
2 |
+
FROM python:3.10-slim
|
3 |
+
|
4 |
+
ENV PYTHONUNBUFFERED=1 \
|
5 |
+
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
6 |
+
PORT=7860
|
7 |
+
|
8 |
+
# -------- system deps (curl for healthcheck)
|
9 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
10 |
+
curl \
|
11 |
+
&& rm -rf /var/lib/apt/lists/*
|
12 |
+
|
13 |
+
# -------- workdir
|
14 |
+
WORKDIR /app
|
15 |
+
|
16 |
+
# -------- python deps
|
17 |
+
COPY requirements.txt /app/
|
18 |
+
RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
|
19 |
+
|
20 |
+
# -------- app code
|
21 |
+
# (Put ALL your python files in the image)
|
22 |
+
COPY . /app
|
23 |
+
|
24 |
+
# -------- healthcheck & run
|
25 |
+
# Hugging Face probes container health; probing "/" is the simplest + most robust for Streamlit
|
26 |
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=5 \
|
27 |
+
CMD curl --fail http://127.0.0.1:${PORT}/ || exit 1
|
28 |
+
|
29 |
+
# Streamlit must bind to 0.0.0.0 and HF's provided $PORT
|
30 |
+
CMD ["streamlit", "run", "frontend.py", "--server.port=${PORT}", "--server.address=0.0.0.0"]
|
README.md
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Mcp Stock Math
|
3 |
+
emoji: π
|
4 |
+
colorFrom: red
|
5 |
+
colorTo: red
|
6 |
+
sdk: docker
|
7 |
+
app_port: 8501
|
8 |
+
tags:
|
9 |
+
- streamlit
|
10 |
+
pinned: false
|
11 |
+
short_description: mcp-agent-chat for live stock prices and math
|
12 |
+
---
|
13 |
+
|
14 |
+
# Welcome to Streamlit!
|
15 |
+
|
16 |
+
Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
|
17 |
+
|
18 |
+
If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
|
19 |
+
forums](https://discuss.streamlit.io).
|
arithmetic_server.py
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from mcp.server.fastmcp import FastMCP
|
2 |
+
|
3 |
+
# Creates a server named "Arithmetic"
|
4 |
+
mcp = FastMCP("Arithmetic")
|
5 |
+
|
6 |
+
@mcp.tool()
|
7 |
+
def add(a: int, b: int) -> int:
|
8 |
+
"""Add two numbers"""
|
9 |
+
return a + b
|
10 |
+
|
11 |
+
@mcp.tool()
|
12 |
+
def multiply(a: int, b: int) -> int:
|
13 |
+
"""Multiply two numbers"""
|
14 |
+
return a * b
|
15 |
+
|
16 |
+
@mcp.tool()
|
17 |
+
def minus(a: int, b: int) -> int:
|
18 |
+
"""Subtract two numbers (a - b)"""
|
19 |
+
return a - b
|
20 |
+
|
21 |
+
@mcp.tool()
|
22 |
+
def divide(a: int, b: int) -> float:
|
23 |
+
"""Divide two numbers (a / b). Returns a float. Raises ValueError on division by zero."""
|
24 |
+
if b == 0:
|
25 |
+
raise ValueError("Division by zero is not allowed.")
|
26 |
+
return a / b
|
27 |
+
|
28 |
+
|
29 |
+
if __name__ == "__main__":
|
30 |
+
mcp.run(transport="stdio")
|
backend.py
ADDED
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# backend.py
|
2 |
+
"""
|
3 |
+
Backend module for MCP Agent
|
4 |
+
Handles all the MCP server connections, LLM setup, and agent logic
|
5 |
+
"""
|
6 |
+
import sys
|
7 |
+
import os
|
8 |
+
import re
|
9 |
+
import asyncio
|
10 |
+
from dotenv import load_dotenv
|
11 |
+
from typing import Optional, Dict, List, Any
|
12 |
+
|
13 |
+
from pathlib import Path # already imported
|
14 |
+
here = Path(__file__).parent.resolve()
|
15 |
+
|
16 |
+
|
17 |
+
|
18 |
+
################### --- Auth setup --- ###################
|
19 |
+
##########################################################
|
20 |
+
HF = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN")
|
21 |
+
if not HF:
|
22 |
+
print("WARNING: HF_TOKEN not set. The app will start, but model calls may fail.")
|
23 |
+
else:
|
24 |
+
os.environ["HF_TOKEN"] = HF
|
25 |
+
os.environ["HUGGINGFACEHUB_API_TOKEN"] = HF
|
26 |
+
try:
|
27 |
+
from huggingface_hub import login
|
28 |
+
login(token=HF)
|
29 |
+
except Exception:
|
30 |
+
pass
|
31 |
+
|
32 |
+
|
33 |
+
# --- LangChain / MCP ---
|
34 |
+
from langgraph.prebuilt import create_react_agent
|
35 |
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
36 |
+
from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace
|
37 |
+
from langchain_core.messages import HumanMessage
|
38 |
+
|
39 |
+
# First choice, then free/tiny fallbacks
|
40 |
+
CANDIDATE_MODELS = [
|
41 |
+
os.getenv("HF_MODEL_ID", "Qwen/Qwen2.5-7B-Instruct"),
|
42 |
+
"HuggingFaceTB/SmolLM3-3B-Instruct",
|
43 |
+
"Qwen/Qwen2.5-1.5B-Instruct",
|
44 |
+
"microsoft/Phi-3-mini-4k-instruct",
|
45 |
+
]
|
46 |
+
|
47 |
+
SYSTEM_PROMPT = (
|
48 |
+
"You are an AI assistant with tools.\n"
|
49 |
+
"- Use the arithmetic tools (`add`, `minus`, `multiply`, `divide`) for arithmetic or multi-step calculations.\n"
|
50 |
+
"- Use the stock tools (`get_stock_price`, `get_market_summary`, `get_company_news`) for financial/market queries.\n"
|
51 |
+
"- Otherwise, answer directly with your own knowledge.\n"
|
52 |
+
"Be concise and accurate. Only call tools when they clearly help."
|
53 |
+
)
|
54 |
+
|
55 |
+
################### --- ROUTER helpers --- ###################
|
56 |
+
##############################################################
|
57 |
+
|
58 |
+
# Detect stock/financial intent
|
59 |
+
def is_stock_query(q: str) -> bool:
|
60 |
+
"""Check if query is about stocks, markets, or financial data."""
|
61 |
+
stock_patterns = [
|
62 |
+
r"\b(stock|share|price|ticker|market|nasdaq|dow|s&p|spy|qqq)\b",
|
63 |
+
r"\b(AAPL|GOOGL|MSFT|TSLA|AMZN|META|NVDA|AMD)\b", # Common tickers
|
64 |
+
r"\$[A-Z]{1,5}\b", # $SYMBOL format
|
65 |
+
r"\b(trading|invest|portfolio|earnings|dividend)\b",
|
66 |
+
r"\b(bull|bear|rally|crash|volatility)\b",
|
67 |
+
]
|
68 |
+
return any(re.search(pattern, q, re.I) for pattern in stock_patterns)
|
69 |
+
|
70 |
+
# Extract ticker symbol from query
|
71 |
+
def extract_ticker(q: str) -> str:
|
72 |
+
"""Extract stock ticker from query."""
|
73 |
+
# Check for $SYMBOL format first
|
74 |
+
dollar_match = re.search(r"\$([A-Z]{1,5})\b", q, re.I)
|
75 |
+
if dollar_match:
|
76 |
+
return dollar_match.group(1).upper()
|
77 |
+
|
78 |
+
# Check for common patterns like "price of AAPL" or "AAPL stock"
|
79 |
+
patterns = [
|
80 |
+
r"(?:price of|stock price of|quote for)\s+([A-Z]{1,5})\b",
|
81 |
+
r"\b([A-Z]{1,5})\s+(?:stock|share|price|quote)",
|
82 |
+
r"(?:what is|what's|get)\s+([A-Z]{1,5})\b",
|
83 |
+
]
|
84 |
+
|
85 |
+
for pattern in patterns:
|
86 |
+
match = re.search(pattern, q, re.I)
|
87 |
+
if match:
|
88 |
+
return match.group(1).upper()
|
89 |
+
|
90 |
+
# Look for standalone uppercase tickers
|
91 |
+
words = q.split()
|
92 |
+
for word in words:
|
93 |
+
clean_word = word.strip(".,!?")
|
94 |
+
if 2 <= len(clean_word) <= 5 and clean_word.isupper():
|
95 |
+
return clean_word
|
96 |
+
|
97 |
+
return None
|
98 |
+
|
99 |
+
# Check if asking for market summary
|
100 |
+
def wants_market_summary(q: str) -> bool:
|
101 |
+
"""Check if user wants overall market summary."""
|
102 |
+
patterns = [
|
103 |
+
r"\bmarket\s+(?:summary|overview|today|status)\b",
|
104 |
+
r"\bhow(?:'s| is) the market\b",
|
105 |
+
r"\b(?:dow|nasdaq|s&p)\s+(?:today|now)\b",
|
106 |
+
r"\bmarket indices\b",
|
107 |
+
]
|
108 |
+
return any(re.search(pattern, q, re.I) for pattern in patterns)
|
109 |
+
|
110 |
+
# Check if asking for news
|
111 |
+
def wants_news(q: str) -> bool:
|
112 |
+
"""Check if user wants company news."""
|
113 |
+
return bool(re.search(r"\b(news|headline|announcement|update)\b", q, re.I))
|
114 |
+
|
115 |
+
def build_tool_map(tools):
|
116 |
+
mp = {t.name: t for t in tools}
|
117 |
+
return mp
|
118 |
+
|
119 |
+
def find_tool(name: str, tool_map: dict):
|
120 |
+
name = name.lower()
|
121 |
+
for k, t in tool_map.items():
|
122 |
+
kl = k.lower()
|
123 |
+
if kl == name or kl.endswith("/" + name):
|
124 |
+
return t
|
125 |
+
return None
|
126 |
+
|
127 |
+
async def build_chat_llm_with_fallback():
|
128 |
+
"""
|
129 |
+
Try each candidate model. For each:
|
130 |
+
- create HuggingFaceEndpoint + ChatHuggingFace
|
131 |
+
- do a tiny 'ping' with a proper LC message to trigger routing
|
132 |
+
On 402/Payment Required (or other errors), fall through to next.
|
133 |
+
"""
|
134 |
+
last_err = None
|
135 |
+
for mid in CANDIDATE_MODELS:
|
136 |
+
try:
|
137 |
+
llm = HuggingFaceEndpoint(
|
138 |
+
repo_id=mid,
|
139 |
+
huggingfacehub_api_token=HF,
|
140 |
+
temperature=0.1,
|
141 |
+
max_new_tokens=256, # Increased for better responses
|
142 |
+
)
|
143 |
+
model = ChatHuggingFace(llm=llm)
|
144 |
+
# PROBE with a valid message type
|
145 |
+
_ = await model.ainvoke([HumanMessage(content="ping")])
|
146 |
+
print(f"[LLM] Using: {mid}")
|
147 |
+
return model
|
148 |
+
except Exception as e:
|
149 |
+
msg = str(e)
|
150 |
+
if "402" in msg or "Payment Required" in msg:
|
151 |
+
print(f"[LLM] {mid} requires payment; trying next...")
|
152 |
+
last_err = e
|
153 |
+
continue
|
154 |
+
print(f"[LLM] {mid} error: {e}; trying next...")
|
155 |
+
last_err = e
|
156 |
+
continue
|
157 |
+
raise RuntimeError(f"Could not initialize any candidate model. Last error: {last_err}")
|
158 |
+
|
159 |
+
# NEW: Class to manage the MCP Agent (moved from main function)
|
160 |
+
class MCPAgent:
|
161 |
+
def __init__(self):
|
162 |
+
self.client = None
|
163 |
+
self.agent = None
|
164 |
+
self.tool_map = None
|
165 |
+
self.tools = None
|
166 |
+
self.model = None
|
167 |
+
self.initialized = False
|
168 |
+
|
169 |
+
async def initialize(self):
|
170 |
+
"""Initialize the MCP client and agent"""
|
171 |
+
if self.initialized:
|
172 |
+
return
|
173 |
+
|
174 |
+
# Start the Stock server separately first: `python stockserver.py`
|
175 |
+
self.client = MultiServerMCPClient({
|
176 |
+
"arithmetic": {
|
177 |
+
"command": sys.executable,
|
178 |
+
"args": [str(here / "arithmetic_server.py")],
|
179 |
+
"transport": "stdio",
|
180 |
+
},
|
181 |
+
"stocks": {
|
182 |
+
"command": sys.executable,
|
183 |
+
"args": [str(here / "stock_server.py")],
|
184 |
+
"transport": "stdio",
|
185 |
+
},
|
186 |
+
}
|
187 |
+
)
|
188 |
+
|
189 |
+
# 1. MCP client + tools
|
190 |
+
self.tools = await self.client.get_tools()
|
191 |
+
self.tool_map = build_tool_map(self.tools)
|
192 |
+
|
193 |
+
# 2. Build LLM with auto-fallback
|
194 |
+
self.model = await build_chat_llm_with_fallback()
|
195 |
+
|
196 |
+
# Build the ReAct agent with MCP tools
|
197 |
+
self.agent = create_react_agent(self.model, self.tools)
|
198 |
+
|
199 |
+
self.initialized = True
|
200 |
+
return list(self.tool_map.keys()) # Return available tools
|
201 |
+
|
202 |
+
async def process_message(self, user_text: str, history: List[Dict]) -> str:
|
203 |
+
"""Process a single message with the agent"""
|
204 |
+
if not self.initialized:
|
205 |
+
await self.initialize()
|
206 |
+
|
207 |
+
# Try direct stock tool routing first
|
208 |
+
if is_stock_query(user_text):
|
209 |
+
if wants_market_summary(user_text):
|
210 |
+
market_tool = find_tool("get_market_summary", self.tool_map)
|
211 |
+
if market_tool:
|
212 |
+
return await market_tool.ainvoke({})
|
213 |
+
|
214 |
+
elif wants_news(user_text):
|
215 |
+
ticker = extract_ticker(user_text)
|
216 |
+
if ticker:
|
217 |
+
news_tool = find_tool("get_company_news", self.tool_map)
|
218 |
+
if news_tool:
|
219 |
+
return await news_tool.ainvoke({"symbol": ticker, "limit": 3})
|
220 |
+
|
221 |
+
else:
|
222 |
+
ticker = extract_ticker(user_text)
|
223 |
+
if ticker:
|
224 |
+
price_tool = find_tool("get_stock_price", self.tool_map)
|
225 |
+
if price_tool:
|
226 |
+
return await price_tool.ainvoke({"symbol": ticker})
|
227 |
+
|
228 |
+
# Fall back to agent for everything else
|
229 |
+
messages = [{"role": "system", "content": SYSTEM_PROMPT}] + history + [
|
230 |
+
{"role": "user", "content": user_text}
|
231 |
+
]
|
232 |
+
result = await self.agent.ainvoke({"messages": messages})
|
233 |
+
return result["messages"][-1].content
|
234 |
+
|
235 |
+
async def cleanup(self):
|
236 |
+
"""Clean up resources"""
|
237 |
+
if self.client:
|
238 |
+
close = getattr(self.client, "close", None)
|
239 |
+
if callable(close):
|
240 |
+
res = close()
|
241 |
+
if asyncio.iscoroutine(res):
|
242 |
+
await res
|
243 |
+
|
244 |
+
|
245 |
+
|
246 |
+
|
247 |
+
|
248 |
+
|
249 |
+
# NEW: Singleton instance for the agent
|
250 |
+
_agent_instance = None
|
251 |
+
|
252 |
+
def get_agent() -> MCPAgent:
|
253 |
+
"""Get or create the singleton agent instance"""
|
254 |
+
global _agent_instance
|
255 |
+
if _agent_instance is None:
|
256 |
+
_agent_instance = MCPAgent()
|
257 |
+
return _agent_instance
|
frontend.py
ADDED
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# frontend.py
|
2 |
+
"""
|
3 |
+
Streamlit frontend for MCP Agent
|
4 |
+
Provides a chat interface for interacting with the MCP backend
|
5 |
+
"""
|
6 |
+
|
7 |
+
import streamlit as st
|
8 |
+
import asyncio
|
9 |
+
from backend import get_agent, MCPAgent
|
10 |
+
import time
|
11 |
+
|
12 |
+
# Page configuration
|
13 |
+
st.set_page_config(
|
14 |
+
page_title="MCP Agent Chat",
|
15 |
+
page_icon="π€",
|
16 |
+
layout="wide",
|
17 |
+
initial_sidebar_state="expanded"
|
18 |
+
)
|
19 |
+
|
20 |
+
# Custom CSS for better chat appearance
|
21 |
+
st.markdown("""
|
22 |
+
<style>
|
23 |
+
.stChatMessage {
|
24 |
+
padding: 1rem;
|
25 |
+
border-radius: 0.5rem;
|
26 |
+
margin-bottom: 1rem;
|
27 |
+
}
|
28 |
+
.user-message {
|
29 |
+
background-color: #e3f2fd;
|
30 |
+
}
|
31 |
+
.assistant-message {
|
32 |
+
background-color: #f5f5f5;
|
33 |
+
}
|
34 |
+
.sidebar-info {
|
35 |
+
padding: 1rem;
|
36 |
+
background-color: #f0f2f6;
|
37 |
+
border-radius: 0.5rem;
|
38 |
+
margin-bottom: 1rem;
|
39 |
+
}
|
40 |
+
</style>
|
41 |
+
""", unsafe_allow_html=True)
|
42 |
+
|
43 |
+
# Initialize session state
|
44 |
+
if "messages" not in st.session_state:
|
45 |
+
st.session_state.messages = []
|
46 |
+
st.session_state.history = [] # For agent history
|
47 |
+
st.session_state.agent_initialized = False
|
48 |
+
st.session_state.available_tools = []
|
49 |
+
st.session_state.pending_query = None # For example queries
|
50 |
+
|
51 |
+
# Sidebar
|
52 |
+
with st.sidebar:
|
53 |
+
st.title("π€ MCP Agent")
|
54 |
+
st.markdown("---")
|
55 |
+
|
56 |
+
# Status indicator
|
57 |
+
if st.session_state.agent_initialized:
|
58 |
+
st.success("β
Agent Connected")
|
59 |
+
|
60 |
+
# Show available tools
|
61 |
+
st.markdown("### π οΈ Available Tools")
|
62 |
+
for tool in st.session_state.available_tools:
|
63 |
+
st.markdown(f"β’ `{tool}`")
|
64 |
+
else:
|
65 |
+
st.info("β³ Agent Initializing...")
|
66 |
+
|
67 |
+
st.markdown("---")
|
68 |
+
|
69 |
+
# Example queries
|
70 |
+
st.markdown("### π‘ Example Queries")
|
71 |
+
example_queries = [
|
72 |
+
"What's the price of AAPL?",
|
73 |
+
"Show me the market summary",
|
74 |
+
"Get news about TSLA",
|
75 |
+
"Calculate 25 * 4",
|
76 |
+
"What's 100 divided by 7?",
|
77 |
+
"Add 456 and 789"
|
78 |
+
]
|
79 |
+
|
80 |
+
for query in example_queries:
|
81 |
+
if st.button(query, key=f"example_{query}"):
|
82 |
+
st.session_state.pending_query = query
|
83 |
+
st.rerun()
|
84 |
+
|
85 |
+
st.markdown("---")
|
86 |
+
|
87 |
+
# Clear chat button
|
88 |
+
if st.button("ποΈ Clear Chat", type="secondary"):
|
89 |
+
st.session_state.messages = []
|
90 |
+
st.session_state.history = []
|
91 |
+
st.rerun()
|
92 |
+
|
93 |
+
# Info section
|
94 |
+
st.markdown("### βΉοΈ About")
|
95 |
+
st.markdown("""
|
96 |
+
This chat interface connects to:
|
97 |
+
- **Math Server**: Basic arithmetic operations
|
98 |
+
- **Stock Server**: Real-time market data
|
99 |
+
|
100 |
+
The agent uses LangChain and MCP to intelligently route your queries to the appropriate tools.
|
101 |
+
""")
|
102 |
+
|
103 |
+
# Main chat interface
|
104 |
+
st.title("π¬ MCP Agent Chat")
|
105 |
+
st.markdown("Ask me about stocks, math calculations, or general questions!")
|
106 |
+
|
107 |
+
# Initialize agent asynchronously
|
108 |
+
async def initialize_agent():
|
109 |
+
"""Initialize the agent if not already done"""
|
110 |
+
if not st.session_state.agent_initialized:
|
111 |
+
agent = get_agent()
|
112 |
+
with st.spinner("π§ Initializing MCP servers..."):
|
113 |
+
try:
|
114 |
+
tools = await agent.initialize()
|
115 |
+
st.session_state.available_tools = tools
|
116 |
+
st.session_state.agent_initialized = True
|
117 |
+
return True
|
118 |
+
except Exception as e:
|
119 |
+
st.error(f"Failed to initialize agent: {str(e)}")
|
120 |
+
st.info("Please make sure the stock server is running: `python stockserver.py`")
|
121 |
+
return False
|
122 |
+
return True
|
123 |
+
|
124 |
+
# Process user message
|
125 |
+
async def process_user_message(user_input: str):
|
126 |
+
"""Process the user's message and get response from agent"""
|
127 |
+
agent = get_agent()
|
128 |
+
|
129 |
+
# Add user message to history
|
130 |
+
st.session_state.history.append({"role": "user", "content": user_input})
|
131 |
+
|
132 |
+
try:
|
133 |
+
# Get response from agent
|
134 |
+
response = await agent.process_message(user_input, st.session_state.history)
|
135 |
+
|
136 |
+
# Add assistant response to history
|
137 |
+
st.session_state.history.append({"role": "assistant", "content": response})
|
138 |
+
|
139 |
+
return response
|
140 |
+
except Exception as e:
|
141 |
+
return f"Error: {str(e)}"
|
142 |
+
|
143 |
+
# Display chat messages
|
144 |
+
for message in st.session_state.messages:
|
145 |
+
with st.chat_message(message["role"]):
|
146 |
+
st.markdown(message["content"])
|
147 |
+
|
148 |
+
# Process pending query from example buttons
|
149 |
+
if st.session_state.pending_query:
|
150 |
+
query = st.session_state.pending_query
|
151 |
+
st.session_state.pending_query = None # Clear it
|
152 |
+
|
153 |
+
# Add to messages
|
154 |
+
st.session_state.messages.append({"role": "user", "content": query})
|
155 |
+
|
156 |
+
# Process the query
|
157 |
+
async def process_example():
|
158 |
+
if not st.session_state.agent_initialized:
|
159 |
+
if not await initialize_agent():
|
160 |
+
return "Failed to initialize agent. Please check the servers."
|
161 |
+
return await process_user_message(query)
|
162 |
+
|
163 |
+
# Get response
|
164 |
+
with st.spinner("Processing..."):
|
165 |
+
response = asyncio.run(process_example())
|
166 |
+
|
167 |
+
# Add response to messages
|
168 |
+
st.session_state.messages.append({"role": "assistant", "content": response})
|
169 |
+
|
170 |
+
# Rerun to display the new messages
|
171 |
+
st.rerun()
|
172 |
+
|
173 |
+
# Chat input
|
174 |
+
if prompt := st.chat_input("Type your message here..."):
|
175 |
+
# Add user message to chat
|
176 |
+
st.session_state.messages.append({"role": "user", "content": prompt})
|
177 |
+
|
178 |
+
# Display user message
|
179 |
+
with st.chat_message("user"):
|
180 |
+
st.markdown(prompt)
|
181 |
+
|
182 |
+
# Get and display assistant response
|
183 |
+
with st.chat_message("assistant"):
|
184 |
+
message_placeholder = st.empty()
|
185 |
+
|
186 |
+
# Run async function
|
187 |
+
async def get_response():
|
188 |
+
# Initialize agent if needed
|
189 |
+
if not st.session_state.agent_initialized:
|
190 |
+
if not await initialize_agent():
|
191 |
+
return "Failed to initialize agent. Please check the servers."
|
192 |
+
|
193 |
+
# Process message
|
194 |
+
return await process_user_message(prompt)
|
195 |
+
|
196 |
+
# Execute async function
|
197 |
+
with st.spinner("Thinking..."):
|
198 |
+
response = asyncio.run(get_response())
|
199 |
+
|
200 |
+
message_placeholder.markdown(response)
|
201 |
+
st.session_state.messages.append({"role": "assistant", "content": response})
|
202 |
+
|
203 |
+
# Initialize agent on first load
|
204 |
+
# Optional: manual connect so page renders fast
|
205 |
+
if not st.session_state.agent_initialized:
|
206 |
+
if st.sidebar.button("π Connect agent"):
|
207 |
+
asyncio.run(initialize_agent())
|
208 |
+
|
209 |
+
# Footer
|
210 |
+
st.markdown("---")
|
211 |
+
st.markdown(
|
212 |
+
"""
|
213 |
+
<div style='text-align: center; color: #666;'>
|
214 |
+
Powered by LangChain, MCP, and Hugging Face π€
|
215 |
+
</div>
|
216 |
+
""",
|
217 |
+
unsafe_allow_html=True
|
218 |
+
)
|
huggingface.yaml
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# huggingface.yaml
|
2 |
+
|
3 |
+
title: mcp-stock-math
|
4 |
+
sdk: streamlit
|
5 |
+
app_file: frontend.py
|
6 |
+
python_version: "3.10"
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
langchain-mcp-adapters==0.1.9
|
2 |
+
mcp==1.12.3
|
3 |
+
langgraph==0.6.3
|
4 |
+
langchain-huggingface==0.3.1
|
5 |
+
aiohttp==3.12.15
|
6 |
+
streamlit==1.48.0
|
7 |
+
python-dotenv
|
8 |
+
huggingface-hub
|
9 |
+
fastmcp
|
stock_server.py
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from mcp.server.fastmcp import FastMCP
|
2 |
+
import aiohttp
|
3 |
+
import json
|
4 |
+
from typing import Optional
|
5 |
+
|
6 |
+
mcp = FastMCP("StockMarket")
|
7 |
+
|
8 |
+
@mcp.tool()
|
9 |
+
async def get_stock_price(symbol: str) -> str:
|
10 |
+
"""
|
11 |
+
Get current stock price and info for a given ticker symbol.
|
12 |
+
Uses Alpha Vantage free API (or Yahoo Finance fallback).
|
13 |
+
"""
|
14 |
+
# Using a mock response for demo - replace with actual API call
|
15 |
+
# For real implementation, use yfinance or Alpha Vantage API
|
16 |
+
mock_prices = {
|
17 |
+
"AAPL": {"price": 195.89, "change": "+2.34", "percent": "+1.21%"},
|
18 |
+
"GOOGL": {"price": 142.57, "change": "-0.89", "percent": "-0.62%"},
|
19 |
+
"MSFT": {"price": 378.91, "change": "+5.12", "percent": "+1.37%"},
|
20 |
+
"TSLA": {"price": 238.45, "change": "-3.21", "percent": "-1.33%"},
|
21 |
+
}
|
22 |
+
|
23 |
+
symbol = symbol.upper()
|
24 |
+
if symbol in mock_prices:
|
25 |
+
data = mock_prices[symbol]
|
26 |
+
return f"{symbol}: ${data['price']} ({data['change']}, {data['percent']})"
|
27 |
+
|
28 |
+
# For production, uncomment and use real API:
|
29 |
+
# try:
|
30 |
+
# import yfinance as yf
|
31 |
+
# ticker = yf.Ticker(symbol)
|
32 |
+
# info = ticker.info
|
33 |
+
# price = info.get('currentPrice', info.get('regularMarketPrice', 'N/A'))
|
34 |
+
# return f"{symbol}: ${price}"
|
35 |
+
# except:
|
36 |
+
# return f"Could not fetch data for {symbol}"
|
37 |
+
|
38 |
+
return f"Unknown symbol: {symbol}"
|
39 |
+
|
40 |
+
@mcp.tool()
|
41 |
+
async def get_market_summary() -> str:
|
42 |
+
"""Get a summary of major market indices."""
|
43 |
+
# Mock data - replace with real API calls
|
44 |
+
return """Market Summary:
|
45 |
+
π S&P 500: 4,783.45 (+0.73%)
|
46 |
+
π NASDAQ: 15,123.68 (+1.18%)
|
47 |
+
π DOW: 37,863.80 (-0.31%)
|
48 |
+
π± USD/EUR: 0.9234
|
49 |
+
πͺ Bitcoin: $43,521.00 (+2.4%)
|
50 |
+
π’οΈ Oil (WTI): $73.41/barrel"""
|
51 |
+
|
52 |
+
@mcp.tool()
|
53 |
+
async def get_company_news(symbol: str, limit: int = 3) -> str:
|
54 |
+
"""Get latest news headlines for a company."""
|
55 |
+
# Mock news - in production, use NewsAPI or similar
|
56 |
+
symbol = symbol.upper()
|
57 |
+
return f"""Latest news for {symbol}:
|
58 |
+
1. {symbol} announces Q4 earnings beat expectations
|
59 |
+
2. Analysts upgrade {symbol} to 'Buy' rating
|
60 |
+
3. {symbol} unveils new product line for 2025"""
|
61 |
+
|
62 |
+
if __name__ == "__main__":
|
63 |
+
mcp.run(transport="stdio")
|