oilverse-api / agent /chat.py
ๅญ™ๅฎถๆ˜Ž
deploy: OilVerse for HuggingFace (Node.js 18 fix)
fab9847
"""
agent/chat.py โ€” Oil Risk Analyst Agent (ๆททๅˆๆžถๆž„: ๆœฌๅœฐๆจกๆฟ + LLM ๅขžๅผบ)
========================================================================
- ๅธธ่ง้—ฎ้ข˜: ็›ดๆŽฅไปŽๅนณๅฐๆ•ฐๆฎ็”Ÿๆˆไธ“ไธšๅ›ž็ญ” (ๅณๆ—ถๅ“ๅบ”)
- ๅคๆ‚ๅˆ†ๆž: ่ฐƒ็”จ SiliconFlow Qwen2.5-7B-Instruct (ๅธฆ้‡่ฏ•)
"""
import json, os, re, time
import pandas as pd
import numpy as np
from config import (
OUTPUT_DIR, SILICONFLOW_API_KEY, SILICONFLOW_BASE_URL, SILICONFLOW_MODEL
)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๆ•ฐๆฎๅฑ‚
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
_cache = {}
def _results():
if 'results' not in _cache:
fp = os.path.join(OUTPUT_DIR, 'v2_championship_results.csv')
_cache['results'] = pd.read_csv(fp) if os.path.exists(fp) else None
return _cache['results']
def _reports():
if 'reports' not in _cache:
fp = os.path.join(OUTPUT_DIR, 'v2_nlg_reports.json')
if os.path.exists(fp):
with open(fp, 'r', encoding='utf-8') as f:
_cache['reports'] = json.load(f)
else:
_cache['reports'] = {}
return _cache['reports']
def _hedge():
if 'hedge' not in _cache:
fp = os.path.join(OUTPUT_DIR, 'v2_hedge_backtest.json')
if os.path.exists(fp):
with open(fp, 'r', encoding='utf-8') as f:
_cache['hedge'] = json.load(f)
else:
_cache['hedge'] = {}
return _cache['hedge']
def _events():
if 'events' not in _cache:
fp = os.path.join(OUTPUT_DIR, 'event_timeline.json')
if os.path.exists(fp):
with open(fp, 'r', encoding='utf-8') as f:
evts = json.load(f)
evts.sort(key=lambda e: e.get('date', ''), reverse=True)
_cache['events'] = evts
else:
_cache['events'] = []
return _cache['events']
def _latest():
"""่Žทๅ–ๆœ€ๆ–ฐ้ข„ๆต‹่กŒใ€‚"""
r = _results()
if r is None:
return None
return r.iloc[-1]
def _latest_report(benchmark='WTI'):
"""่Žทๅ–ๆœ€ๆ–ฐNLGๆŠฅๅ‘Šใ€‚"""
rp = _reports()
if not rp:
return None
# Try specific benchmark first, then any
keys = sorted(rp.keys())
bm_keys = [k for k in keys if benchmark in k]
key = bm_keys[-1] if bm_keys else keys[-1]
entry = rp[key]
return entry if isinstance(entry, str) else entry.get('report', str(entry)[:500])
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๆœฌๅœฐๅ›ž็ญ”ๅผ•ๆ“Ž โ€” ๅธธ่ง้—ฎ้ข˜็ง’ๅ›ž
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
IND_MAP = {
'่ˆช็ฉบ': 'aviation', 'aviation': 'aviation',
'็‰ฉๆต': 'logistics', 'logistics': 'logistics',
'ๅŒ–ๅทฅ': 'chemical', 'chemical': 'chemical', 'chemicals': 'chemical',
'ๅˆถ้€ ': 'manufacturing', 'manufacturing': 'manufacturing',
'ไธŠๆธธ': 'upstream', 'ๆฒนๆฐ”': 'upstream', 'upstream': 'upstream',
}
IND_ZH = {'aviation': '่ˆช็ฉบ', 'logistics': '็‰ฉๆต', 'chemical': 'ๅŒ–ๅทฅ',
'manufacturing': 'ๅˆถ้€ ', 'upstream': 'ไธŠๆธธๆฒนๆฐ”'}
IND_PROFILE = {
'aviation': '่ˆช็ฉบ็‡ƒๆฒนๅ ่ฟ่ฅๆˆๆœฌ30-40%๏ผŒๆฒนไปทๆณขๅŠจ10%ๅฝฑๅ“ๅˆฉๆถฆ5-8%๏ผŒๆ•ๆ„Ÿๅบฆๆœ€้ซ˜',
'logistics': 'ๆŸดๆฒนๅ ็‰ฉๆตๆˆๆœฌ25-35%๏ผŒๅฏ้€š่ฟ‡็‡ƒๆฒน้™„ๅŠ ่ดน้ƒจๅˆ†ไผ ๅฏผ๏ผŒไฝ†ๅญ˜ๅœจๆ—ถๆปž',
'chemical': 'ๅŽŸๆฒนไฝœไธบ็ŸณๅŒ–ๅŽŸๆ–™ๅ ๆˆๆœฌ40-60%๏ผŒ่ฃ‚่งฃไปทๅทฎ็›ดๆŽฅๅฝฑๅ“ๅˆฉๆถฆ็އ',
'manufacturing': '่ƒฝๆบๆˆๆœฌๅ ๅˆถ้€ ๆˆๆœฌ10-20%๏ผŒไธป่ฆ้€š่ฟ‡็”ตไปทๅ’Œๅคฉ็„ถๆฐ”้—ดๆŽฅไผ ๅฏผ',
'upstream': 'ๆฒนไปทไธŠๆถจๆ˜ฏๆ”ถๅ…ฅๅˆฉๅฅฝ๏ผŒไฝ†้œ€้˜ฒ่Œƒๆšด่ทŒ้ฃŽ้™ฉไฟๆŠค่ต„ๆœฌๅผ€ๆ”ฏ',
}
def _try_local_answer(msg):
"""ๅฐ่ฏ•ๆœฌๅœฐๅ›ž็ญ”๏ผŒ่ฟ”ๅ›ž (reply, confidence)ใ€‚"""
m = msg.lower()
last = _latest()
if last is None:
return None, 0
# โ”€โ”€ 1. ้ฃŽ้™ฉ็ญ‰็บง/็ ”ๅˆค โ”€โ”€
if any(w in m for w in ['้ฃŽ้™ฉ็ญ‰็บง', '้ฃŽ้™ฉ็ ”ๅˆค', 'ๅฝ“ๅ‰้ฃŽ้™ฉ', 'ๆฒนไปท้ฃŽ้™ฉ']):
report = _latest_report()
if report:
return report, 0.95
# โ”€โ”€ 2. ๅฎŒๆ•ดๆŠฅๅ‘Š/ๆœˆๅบฆๆŠฅๅ‘Š โ”€โ”€
if any(w in m for w in ['ๅฎŒๆ•ดๆŠฅๅ‘Š', 'ๆœˆๅบฆๆŠฅๅ‘Š', '่ฏฆ็ป†ๅˆ†ๆž', 'ๆŠฅๅ‘Š']):
report = _latest_report()
if report:
return report, 0.9
# โ”€โ”€ 3. ้ข„ๆต‹ๅŒบ้—ด โ”€โ”€
if any(w in m for w in ['้ข„ๆต‹ๅŒบ้—ด', 'ๅˆ†ไฝๆ•ฐ', 'q10', 'q50', 'q90', 'ไธ‹ไธชๆœˆ']):
q10 = last['pred_q10_1m']
q50 = last['pred_q50_1m']
q90 = last['pred_q90_1m']
vol = last['pred_vol']
date = str(last['test_date'])[:7]
reply = (f"**{date} ๆฒนไปท้ข„ๆต‹ๅŒบ้—ด๏ผš**\n\n"
f"- **Q10 (ๆ‚ฒ่ง‚):** {q10:.1%} โ† ๆœ‰10%ๆฆ‚็އ่ทŒๅน…่ถ…ๆญค\n"
f"- **Q50 (ไธญๆžข):** {q50:.1%} โ† ๆœ€ๅฏ่ƒฝ็š„ๅ˜ๅŠจ\n"
f"- **Q90 (ไน่ง‚):** {q90:.1%} โ† ๆœ‰10%ๆฆ‚็އๆถจๅน…่ถ…ๆญค\n"
f"- **ๆณขๅŠจ็އ:** {vol:.1%}\n\n"
f"**่งฃ่ฏป:** ๅŒบ้—ด่ทจๅบฆ{q90-q10:.1%}๏ผŒ"
f"{'ๅไธŠ่กŒ' if q50 > 0 else 'ๅไธ‹่กŒ'}๏ผŒ"
f"ๆณขๅŠจ็އ{vol:.1%}{'่พƒ้ซ˜๏ผŒๅปบ่ฎฎๅขžๅŠ ๅฏนๅ†ฒ' if vol > 0.05 else 'ๅฏๆŽง'}ใ€‚")
return reply, 0.9
# โ”€โ”€ 4. ้ฃŽ้™ฉ่ถ‹ๅŠฟ โ”€โ”€
if any(w in m for w in ['่ถ‹ๅŠฟ', '่ตฐๅŠฟ', 'ๅ˜ๅŒ–', 'ๆœ€่ฟ‘', 'ๅކๅฒ', 'ๅ‡ ไธชๆœˆ']):
r = _results()
tail = r.tail(6)
lines = ["**่ฟ‘6ไธชๆœˆ้ฃŽ้™ฉ่ถ‹ๅŠฟ:**\n"]
for _, row in tail.iterrows():
date = str(row['test_date'])[:7]
lvl = row['risk_level']
bias = row['risk_bias']
top = row['top_factor']
q50 = row['pred_q50_1m']
emoji = {'High': '๐Ÿ”ด', 'Medium-High': '๐ŸŸ ', 'Medium': '๐ŸŸก',
'Low-Medium': '๐Ÿ”ต', 'Low': '๐ŸŸข'}.get(lvl, 'โšช')
lines.append(f" {emoji} **{date}**: {lvl} | {bias} | ไธญๆžข{q50:+.1%} | ไธปๅฏผ: {top}")
# ่ถ‹ๅŠฟๅˆคๆ–ญ
levels = tail['risk_level'].tolist()
level_map = {'Low': 0, 'Low-Medium': 1, 'Medium': 2, 'Medium-High': 3, 'High': 4}
nums = [level_map.get(l, 2) for l in levels]
if nums[-1] > nums[0]:
trend = "๐Ÿ“ˆ ๆ€ปไฝ“่ถ‹ๅŠฟ๏ผš้ฃŽ้™ฉ**ไธŠๅ‡**"
elif nums[-1] < nums[0]:
trend = "๐Ÿ“‰ ๆ€ปไฝ“่ถ‹ๅŠฟ๏ผš้ฃŽ้™ฉ**ไธ‹้™**"
else:
trend = "โžก๏ธ ๆ€ปไฝ“่ถ‹ๅŠฟ๏ผš้ฃŽ้™ฉ**ๆŒๅนณ**"
lines.append(f"\n{trend}")
return '\n'.join(lines), 0.9
# โ”€โ”€ 5. ่กŒไธšๅˆ†ๆž/ๅฏนๅ†ฒๅปบ่ฎฎ โ”€โ”€
detected_ind = None
for kw, ind in IND_MAP.items():
if kw in m:
detected_ind = ind
break
if detected_ind or any(w in m for w in ['ๅฏนๅ†ฒ', 'ๅฅ—ไฟ', 'cfo', '่กŒไธš']):
ind = detected_ind or 'aviation'
hedge = _hedge()
h = hedge.get(ind, {})
zh = IND_ZH.get(ind, ind)
profile = IND_PROFILE.get(ind, '')
risk_level = last.get(f"risk_level", "Medium")
q50 = last['pred_q50_1m']
vol = last['pred_vol']
ratio = h.get('recommended_ratio_pct', '50%')
tool = {'futures': 'ๆœŸ่ดง้”ไปท', 'put': '็œ‹่ทŒๆœŸๆƒ', 'collar': '้›ถๆˆๆœฌ้ข†ๅฃ'}.get(
h.get('recommended_tool', 'futures'), 'ๆœŸ่ดง้”ไปท')
rationale = h.get('rationale', '')
saving = h.get('total_saving', 0)
vol_red = h.get('vol_reduction', 0)
reply = (f"**{zh}่กŒไธšไธ“้กนๅˆ†ๆžๆŠฅๅ‘Š**\n\n"
f"**ไธ€ใ€่กŒไธš็”ปๅƒ**\n{profile}\n\n"
f"**ไบŒใ€ๅฝ“ๅ‰ๆฒนไปท็Žฏๅขƒ**\n"
f"- ้ฃŽ้™ฉ็ญ‰็บง: **{risk_level}**\n"
f"- 1M้ข„ๆต‹ไธญๆžข: **{q50:+.1%}**๏ผŒๆณขๅŠจ็އ: **{vol:.1%}**\n"
f"- ไธปๅฏผๅ› ๅญ: **{last.get('top_factor', 'N/A')}**\n\n"
f"**ไธ‰ใ€ๅฏนๅ†ฒๅปบ่ฎฎ**\n"
f"- ๆŽจ่ๅฏนๅ†ฒๆฏ”ไพ‹: **{ratio}**\n"
f"- ๆŽจ่ๅทฅๅ…ท: **{tool}**\n"
f"- ็†็”ฑ: {rationale}\n\n"
f"**ๅ››ใ€ๅކๅฒๅ›žๆต‹**\n"
f"- ๆŒ‰ๆŽจ่ๆฏ”ไพ‹็ดฏ่ฎก่Š‚็œ: **${saving:.1f}M**\n"
f"- ๆณขๅŠจ็އ้™ไฝŽ: **{vol_red}%**\n\n"
f"**ไบ”ใ€้“ถ่กŒ่กŒๅŠจๅปบ่ฎฎ**\n")
if risk_level in ('High', 'Medium-High'):
reply += (f"1. ็ซ‹ๅณ่”็ปœ{zh}ๅฎขๆˆท๏ผŒๆ็คบๆฒนไปทไธŠ่กŒ้ฃŽ้™ฉ\n"
f"2. ๆŽจ่ๅฏนๅ†ฒๆ–นๆกˆ: {ratio} {tool}๏ผŒ้”ๅฎšๆœชๆฅ3-6ไธชๆœˆๆˆๆœฌ\n"
f"3. ๅปบ่ฎฎ้ข„็•™ๆตๅŠจๆ€ง็ผ“ๅ†ฒไปฅๅบ”ๅฏนๆณขๅŠจ\n")
else:
reply += (f"1. ๅธธ่ง„่ทŸ่ฟ›{zh}ๅฎขๆˆท๏ผŒๅฝ“ๅ‰้ฃŽ้™ฉๅฏๆŽง\n"
f"2. ๅปบ่ฎฎ็ปดๆŒๅŸบ็ก€ๅฏนๅ†ฒ({ratio})๏ผŒๆ— ้œ€่ฟ‡ๅบฆๅฅ—ไฟ\n"
f"3. ๅ…ณๆณจไธ‹ไธ€่ฝฎOPEC+ไผš่ฎฎๅฏ่ƒฝ็š„ๆ”ฟ็ญ–ๅ˜ๅŒ–\n")
return reply, 0.95
# โ”€โ”€ 6. ๅŽ‹ๅŠ›ๆต‹่ฏ• โ”€โ”€
if any(w in m for w in ['ๅŽ‹ๅŠ›', 'ๅฆ‚ๆžœ', 'ๅ‡่ฎพ', 'ไธญไธœ', 'ๅ†ฒ็ช', 'ๅดฉๅกŒ', 'ๅ‡ไบง', 'ๆˆ˜ไบ‰']):
vol = last['pred_vol']
q50 = last['pred_q50_1m']
# ่ฏ†ๅˆซๅ†ฒๅ‡ปๅœบๆ™ฏ
supply_shock = -15 if any(w in m for w in ['ไพ›็ป™', 'ๅ‡ไบง', 'ไธญๆ–ญ', 'ไธญไธœ', 'ๅ†ฒ็ช']) else 0
demand_shock = -20 if any(w in m for w in ['้œ€ๆฑ‚', 'ๅดฉๅกŒ', '่กฐ้€€']) else 0
geo_spike = 3 if any(w in m for w in ['ๅœฐ็ผ˜', 'ๅ†ฒ็ช', 'ไธญไธœ', 'ๆˆ˜ไบ‰']) else 1
shock = abs(supply_shock)/100 + abs(demand_shock)/100
stressed_vol = vol * (1 + shock) * (max(1, geo_spike) ** 0.5)
stress_level = 'High' if stressed_vol > 0.12 else ('Medium' if stressed_vol > 0.06 else 'Low')
scenario_name = []
if supply_shock: scenario_name.append(f'ไพ›็ป™ๅ†ฒๅ‡ป{supply_shock}%')
if demand_shock: scenario_name.append(f'้œ€ๆฑ‚ๅ†ฒๅ‡ป{demand_shock}%')
if geo_spike > 1: scenario_name.append(f'ๅœฐ็ผ˜้ฃŽ้™ฉร—{geo_spike}')
scenario = 'ใ€'.join(scenario_name) or 'ๅŸบๅ‡†ๆƒ…ๆ™ฏ'
reply = (f"**ๅŽ‹ๅŠ›ๆต‹่ฏ•็ป“ๆžœ โ€” {scenario}**\n\n"
f"- ๅŸบๅ‡†ๆณขๅŠจ็އ: **{vol:.1%}**\n"
f"- ๅ†ฒๅ‡ปๅŽๆณขๅŠจ็އ: **{stressed_vol:.1%}** ({stressed_vol/vol:.0%})\n"
f"- ๅŽ‹ๅŠ›้ฃŽ้™ฉ็ญ‰็บง: **{stress_level}**\n\n")
if stress_level == 'High':
reply += ("**โš ๏ธ ้ซ˜้ฃŽ้™ฉ้ข„่ญฆ:**\n"
"1. ็ซ‹ๅณๆๅ‡ๅฏนๅ†ฒๆฏ”ไพ‹่‡ณ **50%ไปฅไธŠ**\n"
"2. ๅฏๅŠจ็ดงๆ€ฅ้ฃŽๆŽง้ข„ๆกˆ๏ผŒๅขžๅŠ ไฟ่ฏ้‡‘็ผ“ๅ†ฒ\n"
"3. ้‡็‚นๅ…ณๆณจ่ˆช็ฉบใ€ๅŒ–ๅทฅ็ญ‰้ซ˜ๆ•ๆ„Ÿ่กŒไธšๅฎขๆˆท\n")
elif stress_level == 'Medium':
reply += ("**โšก ไธญ็ญ‰้ฃŽ้™ฉ:**\n"
"1. ๅปบ่ฎฎ็ปดๆŒ **30%** ๅฏนๅ†ฒๅนถๅฏ†ๅˆ‡ๅ…ณๆณจ\n"
"2. ๅšๅฅฝๅบ”ๆ€ฅๆ–นๆกˆ้ข„ๆกˆ\n"
"3. ้€‚ๅบฆๅขžๅŠ ๅบ“ๅญ˜\n")
else:
reply += ("**โœ… ้ฃŽ้™ฉๅฏๆŽง:**\n"
"1. ๅฝ“ๅ‰็ญ–็•ฅๆ— ้œ€่ฐƒๆ•ด\n"
"2. ็ปดๆŒๅธธ่ง„ๅฏนๅ†ฒๅณๅฏ\n")
return reply, 0.9
# โ”€โ”€ 7. ๆจกๅž‹้ชŒ่ฏ โ”€โ”€
if any(w in m for w in ['ๅ‡†็กฎ', '้ชŒ่ฏ', 'ๅฏ้ ', '่ฆ†็›–็އ', 'wis', 'ๆจกๅž‹']):
r = _results()
# Drop rows with NaN
valid = r.dropna(subset=['actual_ret_1m', 'pred_q10_1m', 'pred_q90_1m', 'pred_vol', 'actual_vol'])
ar = valid['actual_ret_1m'].values
q10 = valid['pred_q10_1m'].values
q90 = valid['pred_q90_1m'].values
pv = valid['pred_vol'].values
av = valid['actual_vol'].values
n = len(valid)
cov = ((ar >= q10) & (ar <= q90)).mean()
wis_val = ((q90-q10)+(2/0.2)*np.maximum(q10-ar,0)+(2/0.2)*np.maximum(ar-q90,0)).mean()
nq10 = np.quantile(ar, 0.10); nq90 = np.quantile(ar, 0.90)
naive_wis = ((nq90-nq10)+(2/0.2)*np.maximum(nq10-ar,0)+(2/0.2)*np.maximum(ar-nq90,0)).mean()
corr = np.corrcoef(av, pv)[0,1] if len(av) > 1 else 0
wis_pct = (1-wis_val/naive_wis)*100 if naive_wis != 0 else 0
reply = (f"**ๆจกๅž‹้ชŒ่ฏๆŠฅๅ‘Š (ๅ…ฑ {n} ไธชๆœˆ)**\n\n"
f"**ๆ ธๅฟƒๆŒ‡ๆ ‡:**\n"
f"- 80%ๅŒบ้—ด่ฆ†็›–็އ: **{cov:.1%}** (็›ฎๆ ‡โ‰ฅ80%)\n"
f"- WISๅพ—ๅˆ†: **{wis_val:.4f}** (ไผ˜ไบŽๅŸบๅ‡† {wis_pct:+.1f}%)\n"
f"- ๆณขๅŠจ็އ็›ธๅ…ณๆ€ง: **{corr:.3f}**\n\n"
f"**่ฏ„ไผฐ:** "
f"{'โœ… ๆจกๅž‹่กจ็Žฐไผ˜ๅผ‚' if cov >= 0.75 and wis_pct > 0 else 'โš ๏ธ ๆจกๅž‹ๆœ‰ๆ”น่ฟ›็ฉบ้—ด'}ใ€‚"
f"่ฆ†็›–็އ{cov:.1%}{'่พพๆ ‡' if cov >= 0.75 else 'ๅไฝŽ'}๏ผŒ"
f"WIS{'ไผ˜ไบŽ' if wis_pct > 0 else 'ๅŠฃไบŽ'}ๆœด็ด ๅŸบๅ‡†{abs(wis_pct):.1f}%ใ€‚")
return reply, 0.9
# โ”€โ”€ ๆ— ๆณ•ๆœฌๅœฐๅ›ž็ญ” โ”€โ”€
return None, 0
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# LLM ๅขžๅผบ โ€” ไป…็”จไบŽๅคๆ‚/่‡ชๅฎšไน‰ๅˆ†ๆž
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
SYSTEM_PROMPT = """ไฝ ๆ˜ฏใ€Œๆฒนๅˆƒๆœ‰ไฝ™ OilVerseใ€ๅนณๅฐ็š„AIๅŠฉๆ‰‹ใ€ŒOil Risk Agentใ€ใ€‚
ไฝ ๆ‹ฅๆœ‰ๅฎžๆ—ถ็š„ๅนณๅฐ้ข„ๆต‹ๆ•ฐๆฎๅ’Œไบ‹ไปถๆ—ถ้—ด็บฟ๏ผŒไฝ ็š„ๅ›ž็ญ”ๅฟ…้กป๏ผš
1. ๅ…ˆ็ป™็ป“่ฎบ(ไธ€ๅฅ่ฏๅŠ ็ฒ—)๏ผŒๅ†็ป™ๆ”ฏๆ’‘(3-5ๆก่ฆ็‚น)๏ผŒๆœ€ๅŽ็ป™่กŒๅŠจๅปบ่ฎฎ
2. ็”จ **ๅŠ ็ฒ—** ๆ ‡่ฎฐๅ…ณ้”ฎๆ•ฐๅญ—ๅ’Œ็ป“่ฎบ
3. ๆฏๆฌกๅ›ž็ญ”ๆŽงๅˆถๅœจ 200 ๅญ—ไปฅๅ†…
4. ็ปๅฏนไธ่ฆ่พ“ๅ‡บๅทฅๅ…ทๅใ€ๅ‡ฝๆ•ฐๅใ€JSON็ญ‰ๆŠ€ๆœฏๅ†…ๅฎน
5. ๅฆ‚ๆžœๆ˜ฏ้—ฒ่Š๏ผŒ็ฎ€็Ÿญๅ›ž็ญ”่บซไปฝๅณๅฏ
6. ๅผ•็”จๆœ€่ฟ‘ไบ‹ไปถไฝœไธบๅˆ†ๆžๆ”ฏๆ’‘๏ผŒ่ฏดๆ˜Žใ€Œไบ‹ไปถโ†’ๅ› ๅญๅผ‚ๅŠจโ†’้ฃŽ้™ฉไฟกๅทโ†’ๅฏนๅ†ฒๅปบ่ฎฎใ€็š„ๅ› ๆžœ้“พ"""
def _build_data_context(msg):
"""ไธบLLMๆž„ๅปบ็ฒพ็‚ผ็š„ๆ•ฐๆฎไธŠไธ‹ๆ–‡ใ€‚"""
last = _latest()
if last is None:
return ""
ctx = [f"ๅˆ†ๆžๆ—ฅๆœŸ: {str(last['test_date'])[:7]}",
f"้ฃŽ้™ฉ็ญ‰็บง: {last['risk_level']}",
f"ๆ–นๅ‘ๅ็ฝฎ: {last['risk_bias']}",
f"1MๅŒบ้—ด: [{last['pred_q10_1m']:.1%}, {last['pred_q90_1m']:.1%}]",
f"ๆณขๅŠจ็އ: {last['pred_vol']:.1%}",
f"ไธปๅฏผๅ› ๅญ: {last['top_factor']}",
f"RegimeๅŒน้…: {last.get('regime_match', 'N/A')} ({last.get('regime_similarity', 0):.0%})"]
# Add recent events as causal context
evts = _events()
if evts:
ctx.append('\n[่ฟ‘ๆœŸๅ…ณ้”ฎไบ‹ไปถ]')
for ev in evts[:3]:
impact_zh = {'bullish': 'ๅˆฉๅคš', 'bearish': 'ๅˆฉ็ฉบ', 'neutral': 'ไธญๆ€ง'}.get(ev.get('impact', ''), '')
ctx.append(f"- {ev['date']} {ev['title']} ({impact_zh}): {ev.get('risk_signal', '')}")
return '\n'.join(ctx)
def _call_llm_enhanced(user_message, history):
"""่ฐƒ็”จ LLM๏ผŒๅธฆ็ฒพ็‚ผไธŠไธ‹ๆ–‡ใ€‚"""
import requests
data_ctx = _build_data_context(user_message)
enriched = f"{user_message}\n\n[ๅนณๅฐๆ•ฐๆฎ]\n{data_ctx}" if data_ctx else user_message
messages = [{'role': 'system', 'content': SYSTEM_PROMPT}]
for h in history[-4:]: # ๅชไฟ็•™ๆœ€่ฟ‘2่ฝฎๅฏน่ฏ
messages.append(h)
messages.append({'role': 'user', 'content': enriched})
headers = {
'Authorization': f'Bearer {SILICONFLOW_API_KEY}',
'Content-Type': 'application/json',
}
payload = {
'model': SILICONFLOW_MODEL,
'messages': messages,
'temperature': 0.3,
'max_tokens': 500,
'stream': False,
}
last_err = None
for attempt in range(2):
try:
resp = requests.post(
f'{SILICONFLOW_BASE_URL}/chat/completions',
headers=headers, json=payload, timeout=45
)
resp.raise_for_status()
data = resp.json()
reply = data['choices'][0]['message']['content']
# ๆธ…็†ๆฎ‹็•™
reply = re.sub(r'</?tool_call>', '', reply)
reply = re.sub(r'\b(query_\w+|run_\w+)\(.*?\)', '', reply)
return reply.strip()
except requests.exceptions.Timeout:
last_err = "LLMๅ“ๅบ”่ถ…ๆ—ถ"
time.sleep(2)
except requests.exceptions.ConnectionError:
last_err = "ๆ— ๆณ•่ฟžๆŽฅLLMๆœๅŠก"
time.sleep(2)
except Exception as e:
return f"LLM่ฐƒ็”จๅคฑ่ดฅ: {e}"
return f"โš ๏ธ {last_err}๏ผŒ่ฏท็จๅŽ้‡่ฏ•ใ€‚\n\n๐Ÿ’ก ไฝ ๅฏไปฅๅฐ่ฏ•ๆ›ดๅ…ทไฝ“็š„้—ฎ้ข˜๏ผŒๅฆ‚ใ€Œ่ˆช็ฉบ่กŒไธšๅฏนๅ†ฒๅปบ่ฎฎใ€ใ€Œๅฝ“ๅ‰้ฃŽ้™ฉ็ญ‰็บงใ€็ญ‰๏ผŒ่ฟ™ไบ›ๅฏไปฅๅณๆ—ถๅ“ๅบ”ใ€‚"
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ไธปๅ…ฅๅฃ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def chat_with_agent(user_message, history=None):
"""
ๆททๅˆๆžถๆž„ๅฏน่ฏๅ…ฅๅฃ:
1. ๅ…ˆๅฐ่ฏ•ๆœฌๅœฐๅ›ž็ญ”(ๅณๆ—ถ)
2. ๆ— ๆณ•ๆœฌๅœฐๅ›ž็ญ”ๆ—ถ่ฐƒ็”จ LLM
"""
if history is None:
history = []
# ้—ฒ่Šๅฟซ้€Ÿๅ›žๅค
greets = ['ไฝ ๅฅฝ', 'ไฝ ๆ˜ฏ่ฐ', 'hello', 'hi', 'ๅ—จ', 'ๅœจๅ—']
if any(user_message.strip().lower() == g for g in greets):
reply = "๐Ÿ‘‹ ไฝ ๅฅฝ๏ผๆˆ‘ๆ˜ฏๆฒนไปท้ฃŽ้™ฉๅˆ†ๆž Agent๏ผŒๅŸบไบŽๅนณๅฐๅฎžๆ—ถๆ•ฐๆฎไธบไฝ ๆไพ›ไธ“ไธšๅˆ†ๆžใ€‚\n\nไฝ ๅฏไปฅ้—ฎๆˆ‘๏ผš\nโ€ข ๅฝ“ๅ‰้ฃŽ้™ฉ็ญ‰็บงๅ’Œ้ข„ๆต‹ๅŒบ้—ด\nโ€ข ่กŒไธšไธ“้กนๅˆ†ๆž๏ผˆ่ˆช็ฉบ/็‰ฉๆต/ๅŒ–ๅทฅ/ๅˆถ้€ /ไธŠๆธธ๏ผ‰\nโ€ข ๅฏนๅ†ฒ็ญ–็•ฅๅ’Œๅทฅๅ…ทๆŽจ่\nโ€ข ๅŽ‹ๅŠ›ๆต‹่ฏ•ๆจกๆ‹Ÿ\nโ€ข ๆจกๅž‹้ชŒ่ฏๆŒ‡ๆ ‡"
history.append({'role': 'user', 'content': user_message})
history.append({'role': 'assistant', 'content': reply})
return reply, history
# ๅฐ่ฏ•ๆœฌๅœฐๅ›ž็ญ”
local_reply, confidence = _try_local_answer(user_message)
if local_reply and confidence >= 0.85:
history.append({'role': 'user', 'content': user_message})
history.append({'role': 'assistant', 'content': local_reply})
return local_reply, history
# LLM ๅขžๅผบๅ›ž็ญ”
reply = _call_llm_enhanced(user_message, history)
history.append({'role': 'user', 'content': user_message})
history.append({'role': 'assistant', 'content': reply})
return reply, history
if __name__ == '__main__':
print("ๆฒนไปท้ฃŽ้™ฉๅˆ†ๆž Agent๏ผˆ่พ“ๅ…ฅ quit ้€€ๅ‡บ๏ผ‰")
print("=" * 50)
h = []
while True:
q = input("\nไฝ : ").strip()
if q.lower() in ('quit', 'exit', 'q'):
break
reply, h = chat_with_agent(q, h)
print(f"\nAgent: {reply}")