DasbootU9607 commited on
Commit
f4bdc38
·
1 Parent(s): 3e039e1

Integrate TradingAgents backend adapter

Browse files
Files changed (4) hide show
  1. .env.example +12 -0
  2. agent_service.py +406 -0
  3. requirements.txt +1 -0
  4. web_app.py +9 -27
.env.example CHANGED
@@ -22,6 +22,18 @@ DEEPSEEK_API_KEY=replace_with_your_deepseek_api_key
22
  DEEPSEEK_BASE_URL=https://api.deepseek.com
23
  DEEPSEEK_MODEL=deepseek-chat
24
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  # Persistent runtime paths
26
  DATA_DIR=./data
27
  CHECKPOINTS_DB_PATH=./data/checkpoints.sqlite
 
22
  DEEPSEEK_BASE_URL=https://api.deepseek.com
23
  DEEPSEEK_MODEL=deepseek-chat
24
 
25
+ # Agent backend selection
26
+ AGENT_BACKEND=auto
27
+
28
+ # Optional TradingAgents overrides
29
+ TRADINGAGENTS_PROVIDER=openai
30
+ TRADINGAGENTS_BACKEND_URL=https://api.deepseek.com
31
+ TRADINGAGENTS_DEEP_MODEL=deepseek-chat
32
+ TRADINGAGENTS_QUICK_MODEL=deepseek-chat
33
+ TRADINGAGENTS_DATA_VENDOR=yfinance
34
+ TRADINGAGENTS_MAX_DEBATE_ROUNDS=1
35
+ TRADINGAGENTS_MAX_RISK_ROUNDS=1
36
+
37
  # Persistent runtime paths
38
  DATA_DIR=./data
39
  CHECKPOINTS_DB_PATH=./data/checkpoints.sqlite
agent_service.py ADDED
@@ -0,0 +1,406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ from datetime import datetime, timedelta
4
+ from typing import Any, Dict, List, Optional, Tuple
5
+
6
+ from langchain_core.messages import HumanMessage
7
+
8
+ from market_demo import MARKET_META
9
+ from runtime_config import DATA_DIR
10
+
11
+ TICKER_STOPWORDS = {
12
+ "A",
13
+ "AI",
14
+ "AM",
15
+ "API",
16
+ "ARE",
17
+ "ETF",
18
+ "ETFS",
19
+ "FOR",
20
+ "I",
21
+ "IS",
22
+ "JSON",
23
+ "NOW",
24
+ "OF",
25
+ "ON",
26
+ "PE",
27
+ "PM",
28
+ "RSI",
29
+ "THE",
30
+ "TO",
31
+ "U2",
32
+ "USD",
33
+ }
34
+
35
+ KNOWN_SYMBOLS = sorted(MARKET_META.keys(), key=len, reverse=True)
36
+ KNOWN_NAMES = {
37
+ meta["name"].lower(): symbol
38
+ for symbol, meta in MARKET_META.items()
39
+ }
40
+ TICKER_PATTERN = re.compile(r"\$?([A-Z]{1,5}(?:-[A-Z]{2,5}|(?:\.[A-Z]{1,4}))?)\b")
41
+ DATE_PATTERN = re.compile(r"\b(20\d{2})[-/](\d{2})[-/](\d{2})\b")
42
+
43
+
44
+ def get_agent_backend_mode() -> str:
45
+ return os.getenv("AGENT_BACKEND", "auto").strip().lower() or "auto"
46
+
47
+
48
+ def get_tradingagents_provider() -> str:
49
+ return os.getenv("TRADINGAGENTS_PROVIDER", "openai").strip().lower() or "openai"
50
+
51
+
52
+ def agent_is_configured() -> bool:
53
+ backend = get_agent_backend_mode()
54
+
55
+ if backend == "legacy":
56
+ return bool(os.getenv("DEEPSEEK_API_KEY"))
57
+
58
+ if backend == "tradingagents":
59
+ return _tradingagents_is_configured()
60
+
61
+ if backend == "auto":
62
+ return _tradingagents_is_configured() or bool(os.getenv("DEEPSEEK_API_KEY"))
63
+
64
+ return bool(os.getenv("DEEPSEEK_API_KEY"))
65
+
66
+
67
+ def run_agent_message(user_message: str, session_id: str) -> Tuple[str, List[Dict[str, Any]], str]:
68
+ backend = get_agent_backend_mode()
69
+
70
+ if backend in {"tradingagents", "auto"}:
71
+ trading_request = resolve_tradingagents_request(user_message)
72
+
73
+ if backend == "tradingagents" or trading_request:
74
+ try:
75
+ response, tools_used = run_tradingagents_message(
76
+ user_message,
77
+ trading_request=trading_request,
78
+ )
79
+ return response, tools_used, "tradingagents"
80
+ except Exception as error:
81
+ if backend == "tradingagents":
82
+ raise RuntimeError(f"TradingAgents backend failed: {error}") from error
83
+
84
+ print(f"TradingAgents fallback triggered: {error}")
85
+
86
+ response, tools_used = run_legacy_message(user_message, session_id)
87
+ return response, tools_used, "legacy"
88
+
89
+
90
+ def run_legacy_message(user_message: str, session_id: str) -> Tuple[str, List[Dict[str, Any]]]:
91
+ if not os.getenv("DEEPSEEK_API_KEY"):
92
+ raise RuntimeError(
93
+ "Legacy agent is not configured. Set DEEPSEEK_API_KEY in your environment."
94
+ )
95
+
96
+ from agent_graph import stock_agent_app
97
+
98
+ config = {"configurable": {"thread_id": session_id}}
99
+ initial_state = {"messages": [HumanMessage(content=user_message)]}
100
+
101
+ response_content = ""
102
+ tool_results: List[Dict[str, Any]] = []
103
+
104
+ for event in stock_agent_app.stream(initial_state, config):
105
+ for output in event.values():
106
+ if "messages" not in output:
107
+ continue
108
+
109
+ last_msg = output["messages"][-1]
110
+ if hasattr(last_msg, "content") and last_msg.content:
111
+ response_content = last_msg.content
112
+
113
+ if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
114
+ for tool_call in last_msg.tool_calls:
115
+ tool_results.append(
116
+ {
117
+ "tool": tool_call.get("name", "unknown"),
118
+ "args": tool_call.get("args", {}),
119
+ }
120
+ )
121
+
122
+ return response_content or "Please rephrase your question.", tool_results
123
+
124
+
125
+ def run_tradingagents_message(
126
+ user_message: str,
127
+ trading_request: Optional[Dict[str, str]] = None,
128
+ ) -> Tuple[str, List[Dict[str, Any]]]:
129
+ trading_request = trading_request or resolve_tradingagents_request(user_message)
130
+ if not trading_request:
131
+ raise RuntimeError(
132
+ "TradingAgents needs a stock ticker or company name in the message."
133
+ )
134
+
135
+ _prime_tradingagents_env()
136
+
137
+ from tradingagents.default_config import DEFAULT_CONFIG
138
+ from tradingagents.graph.trading_graph import TradingAgentsGraph
139
+
140
+ config = DEFAULT_CONFIG.copy()
141
+ config["llm_provider"] = get_tradingagents_provider()
142
+ config["deep_think_llm"] = _resolve_tradingagents_model(
143
+ override_name="TRADINGAGENTS_DEEP_MODEL",
144
+ default_model=config.get("deep_think_llm", "gpt-5.4"),
145
+ )
146
+ config["quick_think_llm"] = _resolve_tradingagents_model(
147
+ override_name="TRADINGAGENTS_QUICK_MODEL",
148
+ default_model=config.get("quick_think_llm", "gpt-5.4-mini"),
149
+ )
150
+
151
+ backend_url = _resolve_tradingagents_backend_url(
152
+ default_backend_url=config.get("backend_url", "https://api.openai.com/v1"),
153
+ )
154
+ if backend_url:
155
+ config["backend_url"] = backend_url
156
+
157
+ config["max_debate_rounds"] = _get_positive_int_env("TRADINGAGENTS_MAX_DEBATE_ROUNDS", 1)
158
+ config["max_risk_discuss_rounds"] = _get_positive_int_env(
159
+ "TRADINGAGENTS_MAX_RISK_ROUNDS",
160
+ 1,
161
+ )
162
+ config["output_language"] = os.getenv("TRADINGAGENTS_OUTPUT_LANGUAGE", "English")
163
+ config["results_dir"] = str(DATA_DIR / "tradingagents-logs")
164
+ config["data_cache_dir"] = str(DATA_DIR / "tradingagents-cache")
165
+
166
+ data_vendor = os.getenv("TRADINGAGENTS_DATA_VENDOR", "yfinance").strip().lower() or "yfinance"
167
+ config["data_vendors"] = {
168
+ "core_stock_apis": data_vendor,
169
+ "technical_indicators": data_vendor,
170
+ "fundamental_data": data_vendor,
171
+ "news_data": data_vendor,
172
+ }
173
+
174
+ selected_analysts = [
175
+ analyst.strip()
176
+ for analyst in os.getenv(
177
+ "TRADINGAGENTS_SELECTED_ANALYSTS",
178
+ "market,social,news,fundamentals",
179
+ ).split(",")
180
+ if analyst.strip()
181
+ ]
182
+
183
+ trading_graph = TradingAgentsGraph(
184
+ selected_analysts=selected_analysts,
185
+ debug=False,
186
+ config=config,
187
+ )
188
+
189
+ full_state, decision = trading_graph.propagate(
190
+ trading_request["symbol"],
191
+ trading_request["trade_date"],
192
+ )
193
+
194
+ response = build_tradingagents_response(
195
+ symbol=trading_request["symbol"],
196
+ trade_date=trading_request["trade_date"],
197
+ decision=decision,
198
+ full_state=full_state,
199
+ )
200
+
201
+ tools_used = [
202
+ {
203
+ "tool": "tradingagents",
204
+ "args": {
205
+ "symbol": trading_request["symbol"],
206
+ "trade_date": trading_request["trade_date"],
207
+ "llm_provider": config["llm_provider"],
208
+ "data_vendor": data_vendor,
209
+ },
210
+ }
211
+ ]
212
+
213
+ return response, tools_used
214
+
215
+
216
+ def resolve_tradingagents_request(user_message: str) -> Optional[Dict[str, str]]:
217
+ normalized_message = user_message.strip()
218
+ if not normalized_message:
219
+ return None
220
+
221
+ symbol = (
222
+ extract_focus_stock(normalized_message)
223
+ or extract_known_symbol(normalized_message)
224
+ or extract_known_company(normalized_message)
225
+ or extract_generic_ticker(normalized_message)
226
+ )
227
+ if not symbol:
228
+ return None
229
+
230
+ return {
231
+ "symbol": symbol,
232
+ "trade_date": extract_trade_date(normalized_message),
233
+ }
234
+
235
+
236
+ def extract_focus_stock(message: str) -> Optional[str]:
237
+ match = re.search(r"focus stocks:\s*(.+)$", message, flags=re.IGNORECASE | re.DOTALL)
238
+ if not match:
239
+ return None
240
+
241
+ stock_list = [
242
+ item.strip().upper()
243
+ for item in re.split(r"[,/\n]", match.group(1))
244
+ if item.strip()
245
+ ]
246
+ for item in stock_list:
247
+ if item in MARKET_META:
248
+ return item
249
+
250
+ return None
251
+
252
+
253
+ def extract_known_symbol(message: str) -> Optional[str]:
254
+ upper_message = message.upper()
255
+ for symbol in KNOWN_SYMBOLS:
256
+ if re.search(rf"(?<![A-Z0-9]){re.escape(symbol)}(?![A-Z0-9])", upper_message):
257
+ return symbol
258
+ return None
259
+
260
+
261
+ def extract_known_company(message: str) -> Optional[str]:
262
+ lower_message = message.lower()
263
+ for company_name, symbol in KNOWN_NAMES.items():
264
+ if re.search(rf"\b{re.escape(company_name)}\b", lower_message):
265
+ return symbol
266
+ return None
267
+
268
+
269
+ def extract_generic_ticker(message: str) -> Optional[str]:
270
+ for match in TICKER_PATTERN.finditer(message.upper()):
271
+ candidate = match.group(1).strip("$")
272
+ if candidate in TICKER_STOPWORDS:
273
+ continue
274
+ return candidate
275
+ return None
276
+
277
+
278
+ def extract_trade_date(message: str) -> str:
279
+ explicit_date = DATE_PATTERN.search(message)
280
+ if explicit_date:
281
+ return f"{explicit_date.group(1)}-{explicit_date.group(2)}-{explicit_date.group(3)}"
282
+
283
+ lower_message = message.lower()
284
+ today = datetime.utcnow().date()
285
+ if "yesterday" in lower_message:
286
+ return (today - timedelta(days=1)).isoformat()
287
+
288
+ return today.isoformat()
289
+
290
+
291
+ def build_tradingagents_response(
292
+ symbol: str,
293
+ trade_date: str,
294
+ decision: str,
295
+ full_state: Dict[str, Any],
296
+ ) -> str:
297
+ sections = [
298
+ "### TradingAgents Decision",
299
+ f"- Symbol: {symbol}",
300
+ f"- Analysis date: {trade_date}",
301
+ f"- Final rating: {decision}",
302
+ "",
303
+ "### Portfolio Manager",
304
+ _truncate_text(full_state.get("final_trade_decision")),
305
+ "",
306
+ "### Investment Plan",
307
+ _truncate_text(full_state.get("investment_plan")),
308
+ "",
309
+ "### Analyst Highlights",
310
+ f"- Market: {_summarize_text(full_state.get('market_report'))}",
311
+ f"- Sentiment: {_summarize_text(full_state.get('sentiment_report'))}",
312
+ f"- News: {_summarize_text(full_state.get('news_report'))}",
313
+ f"- Fundamentals: {_summarize_text(full_state.get('fundamentals_report'))}",
314
+ ]
315
+
316
+ return "\n".join(line for line in sections if line is not None and line != "")
317
+
318
+
319
+ def _truncate_text(text: Any, limit: int = 1800) -> str:
320
+ cleaned = _clean_text(text)
321
+ if not cleaned:
322
+ return "No detailed portfolio-manager report was returned."
323
+
324
+ if len(cleaned) <= limit:
325
+ return cleaned
326
+
327
+ return cleaned[: limit - 3].rstrip() + "..."
328
+
329
+
330
+ def _summarize_text(text: Any, limit: int = 240) -> str:
331
+ cleaned = _clean_text(text)
332
+ if not cleaned:
333
+ return "No analyst report returned."
334
+
335
+ if len(cleaned) <= limit:
336
+ return cleaned
337
+
338
+ return cleaned[: limit - 3].rstrip() + "..."
339
+
340
+
341
+ def _clean_text(text: Any) -> str:
342
+ if text is None:
343
+ return ""
344
+ return re.sub(r"\s+", " ", str(text)).strip()
345
+
346
+
347
+ def _prime_tradingagents_env() -> None:
348
+ provider = get_tradingagents_provider()
349
+
350
+ if provider == "openai":
351
+ if not os.getenv("OPENAI_API_KEY") and os.getenv("DEEPSEEK_API_KEY"):
352
+ os.environ["OPENAI_API_KEY"] = os.getenv("DEEPSEEK_API_KEY", "")
353
+ if not os.getenv("OPENAI_API_KEY"):
354
+ raise RuntimeError(
355
+ "TradingAgents openai-compatible provider needs OPENAI_API_KEY or DEEPSEEK_API_KEY."
356
+ )
357
+
358
+
359
+ def _tradingagents_is_configured() -> bool:
360
+ provider = get_tradingagents_provider()
361
+
362
+ if provider == "openai":
363
+ return bool(os.getenv("OPENAI_API_KEY") or os.getenv("DEEPSEEK_API_KEY"))
364
+
365
+ provider_key_map = {
366
+ "google": "GOOGLE_API_KEY",
367
+ "anthropic": "ANTHROPIC_API_KEY",
368
+ "xai": "XAI_API_KEY",
369
+ "openrouter": "OPENROUTER_API_KEY",
370
+ "ollama": "OLLAMA_HOST",
371
+ }
372
+ required_key = provider_key_map.get(provider)
373
+ return bool(required_key and os.getenv(required_key))
374
+
375
+
376
+ def _get_positive_int_env(name: str, default: int) -> int:
377
+ raw_value = os.getenv(name, "").strip()
378
+ if not raw_value:
379
+ return default
380
+
381
+ try:
382
+ parsed = int(raw_value)
383
+ except ValueError:
384
+ return default
385
+
386
+ return parsed if parsed > 0 else default
387
+
388
+
389
+ def _resolve_tradingagents_model(override_name: str, default_model: str) -> str:
390
+ if os.getenv(override_name):
391
+ return os.getenv(override_name, "").strip()
392
+
393
+ if os.getenv("DEEPSEEK_API_KEY"):
394
+ return os.getenv("DEEPSEEK_MODEL", "deepseek-chat").strip()
395
+
396
+ return default_model
397
+
398
+
399
+ def _resolve_tradingagents_backend_url(default_backend_url: str) -> str:
400
+ if os.getenv("TRADINGAGENTS_BACKEND_URL"):
401
+ return os.getenv("TRADINGAGENTS_BACKEND_URL", "").strip()
402
+
403
+ if os.getenv("DEEPSEEK_API_KEY"):
404
+ return os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com").strip()
405
+
406
+ return default_backend_url
requirements.txt CHANGED
@@ -30,3 +30,4 @@ requests>=2.31.0
30
  beautifulsoup4>=4.12.2
31
  langgraph-checkpoint-sqlite
32
  gunicorn>=21.2.0
 
 
30
  beautifulsoup4>=4.12.2
31
  langgraph-checkpoint-sqlite
32
  gunicorn>=21.2.0
33
+ tradingagents @ git+https://github.com/TauricResearch/TradingAgents.git@v0.2.3
web_app.py CHANGED
@@ -12,6 +12,7 @@ from flask_cors import CORS
12
  from dotenv import load_dotenv
13
  from collections import defaultdict
14
  from werkzeug.middleware.proxy_fix import ProxyFix
 
15
  from market_demo import MARKET_POOL, generate_kline, generate_news, generate_quotes
16
  from runtime_config import (
17
  CHROMA_DB_DIR,
@@ -268,7 +269,7 @@ def health_check():
268
  "status": "ok" if writable else "degraded",
269
  "service": "u2invest",
270
  "timestamp": datetime.utcnow().isoformat() + "Z",
271
- "agent_configured": bool(os.getenv("DEEPSEEK_API_KEY")),
272
  "storage": {
273
  "data_dir": str(DATA_DIR),
274
  "checkpoints_db": str(CHECKPOINTS_DB_PATH),
@@ -573,13 +574,13 @@ def agent_chat():
573
  }
574
 
575
  current_session = CHAT_SESSIONS[user_id][session_id]
576
- if not os.getenv('DEEPSEEK_API_KEY'):
577
  current_session["messages"].append({
578
  "role": "user",
579
  "content": user_message,
580
  "timestamp": datetime.now().isoformat()
581
  })
582
- error_msg = "DeepSeek is not configured. Set DEEPSEEK_API_KEY in .env or in your cloud service environment."
583
  current_session["messages"].append({
584
  "role": "assistant",
585
  "content": error_msg,
@@ -598,29 +599,10 @@ def agent_chat():
598
 
599
  # Try real agent
600
  try:
601
- from agent_graph import stock_agent_app
602
- from langchain_core.messages import HumanMessage
603
-
604
- # Use session_id as thread_id for LangGraph persistence
605
- config = {"configurable": {"thread_id": session_id}}
606
- initial_state = {"messages": [HumanMessage(content=user_message)]}
607
-
608
- response_content = ""
609
- tool_results = []
610
-
611
- for event in stock_agent_app.stream(initial_state, config):
612
- for node_name, output in event.items():
613
- if "messages" in output:
614
- last_msg = output["messages"][-1]
615
- if hasattr(last_msg, 'content') and last_msg.content:
616
- response_content = last_msg.content
617
-
618
- if hasattr(last_msg, 'tool_calls') and last_msg.tool_calls:
619
- for tool_call in last_msg.tool_calls:
620
- tool_results.append({
621
- "tool": tool_call.get('name', 'unknown'),
622
- "args": tool_call.get('args', {})
623
- })
624
 
625
  current_session["messages"].append({
626
  "role": "assistant",
@@ -629,7 +611,7 @@ def agent_chat():
629
  "tools_used": tool_results
630
  })
631
 
632
- print(f"鉁?Agent response: {response_content[:100]}...")
633
  return jsonify({
634
  "status": "success",
635
  "session_id": session_id,
 
12
  from dotenv import load_dotenv
13
  from collections import defaultdict
14
  from werkzeug.middleware.proxy_fix import ProxyFix
15
+ from agent_service import agent_is_configured, run_agent_message
16
  from market_demo import MARKET_POOL, generate_kline, generate_news, generate_quotes
17
  from runtime_config import (
18
  CHROMA_DB_DIR,
 
269
  "status": "ok" if writable else "degraded",
270
  "service": "u2invest",
271
  "timestamp": datetime.utcnow().isoformat() + "Z",
272
+ "agent_configured": agent_is_configured(),
273
  "storage": {
274
  "data_dir": str(DATA_DIR),
275
  "checkpoints_db": str(CHECKPOINTS_DB_PATH),
 
574
  }
575
 
576
  current_session = CHAT_SESSIONS[user_id][session_id]
577
+ if not agent_is_configured():
578
  current_session["messages"].append({
579
  "role": "user",
580
  "content": user_message,
581
  "timestamp": datetime.now().isoformat()
582
  })
583
+ error_msg = "The agent backend is not configured. Set the required API key in your cloud service environment."
584
  current_session["messages"].append({
585
  "role": "assistant",
586
  "content": error_msg,
 
599
 
600
  # Try real agent
601
  try:
602
+ response_content, tool_results, backend_used = run_agent_message(
603
+ user_message,
604
+ session_id,
605
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
 
607
  current_session["messages"].append({
608
  "role": "assistant",
 
611
  "tools_used": tool_results
612
  })
613
 
614
+ print(f"鉁?Agent response ({backend_used}): {response_content[:100]}...")
615
  return jsonify({
616
  "status": "success",
617
  "session_id": session_id,