aria-code 4.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. workspace/verify.py +113 -0
intent_classifier.py ADDED
@@ -0,0 +1,358 @@
1
+ """
2
+ intent_classifier.py — Aria intent classification using Prelude model or keyword fallback.
3
+
4
+ Two-tier design:
5
+ Tier 1 (fast, accurate): aria-prelude via Ollama — the model trained specifically
6
+ for intent routing (adapters: intent-route-control,
7
+ intent-rag_gate, intent-upgrade_gate, intent-clarify).
8
+ Tier 2 (instant, offline): keyword regex — exact same logic as before, used when
9
+ Ollama / aria-prelude is not available.
10
+
11
+ Intent labels (matching CODING_SYSTEM_PROMPT routing in aria_cli.py):
12
+ "coding" → code generation, backtest scripts, chart scripts
13
+ "analysis" → stock/macro analysis, technical/fundamental research
14
+ "realtime" → live price / quote / market data queries (needs tool)
15
+ "general" → conceptual / educational finance questions (no tools needed)
16
+ "finance" → default finance chat with tool access
17
+
18
+ Usage::
19
+
20
+ from intent_classifier import classify_intent, INTENT_CODING, INTENT_ANALYSIS
21
+
22
+ intent = await classify_intent_async("写一个 AAPL 动量策略", ollama_url)
23
+ # → "coding"
24
+
25
+ intent = classify_intent_sync("什么是夏普比率")
26
+ # → "general"
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import asyncio
32
+ import json
33
+ import urllib.request
34
+ from typing import Optional
35
+
36
+ # ── Intent constants ──────────────────────────────────────────────────────────
37
+ INTENT_CODING = "coding"
38
+ INTENT_ANALYSIS = "analysis"
39
+ INTENT_REALTIME = "realtime"
40
+ INTENT_GENERAL = "general"
41
+ INTENT_FINANCE = "finance" # default / catch-all
42
+
43
+ # ── Prelude model name ────────────────────────────────────────────────────────
44
+ _PRELUDE_MODEL = "aria-prelude"
45
+ _PRELUDE_TIMEOUT = 3.0 # seconds — must be fast; fallback on timeout
46
+
47
+ # ── Prelude system prompt (mirrors the adapter training format) ───────────────
48
+ _PRELUDE_SYSTEM = (
49
+ "You are an intent classifier for a quantitative finance AI assistant.\n"
50
+ "Classify the user message into EXACTLY ONE of these labels:\n"
51
+ " coding — code generation, script writing, backtest, chart plotting\n"
52
+ " analysis — stock analysis, market research, technical/fundamental analysis\n"
53
+ " realtime — live price, current quote, today's market data\n"
54
+ " general — conceptual/educational finance question, no live data needed\n"
55
+ " finance — other finance chat, portfolio, risk, strategy discussion\n"
56
+ "Reply with ONLY the label word, nothing else."
57
+ )
58
+
59
+
60
+ # ── Keyword fallback (tier-2, instant) ───────────────────────────────────────
61
+
62
+ _CODING_KW = (
63
+ "write", "generate", "create", "script", "code", "plot", "backtest",
64
+ "策略", "代码", "回测", "编写", "生成", "k线", "k-line", "kline",
65
+ "python", "dashboard", "写一个", "生成代码", "写代码", "编写代码",
66
+ "analyze and save", "analysis script",
67
+ )
68
+
69
+ _VISUAL_ARTIFACT_KW = (
70
+ "图表", "走势图", "k线图", "k线", "k-line", "kline", "candlestick",
71
+ "chart", "plot", "dashboard", "看板", "晨报", "日报", "周报", "月报",
72
+ "report", "热力图", "heatmap",
73
+ )
74
+
75
+ _VISUAL_MARKET_CONTEXT_KW = (
76
+ "股票", "股价", "行情", "市场", "美股", "港股", "a股", "指数",
77
+ "持仓", "portfolio", "回测", "财报", "earnings", "基金", "etf",
78
+ "资产", "组合", "市场数据", "market data",
79
+ )
80
+ _ANALYSIS_KW = (
81
+ "analyze", "analysis", "分析", "研究", "评估", "研判",
82
+ "技术面", "基本面", "走势", "趋势", "行情",
83
+ "stock analysis", "technical analysis", "fundamental",
84
+ "valuation", "estimate", "outlook", "投资建议", "买入", "卖出",
85
+ )
86
+
87
+ # Topics that must NOT be classified as stock technical "analysis" even if they
88
+ # contain analysis keywords. They get routed to "finance" (general chat) instead,
89
+ # because the stock-analysis prompt requires injected market data that doesn't
90
+ # exist for real estate or pure macroeconomic questions.
91
+ _NON_STOCK_ANALYSIS_TOPICS = (
92
+ # Real-estate
93
+ "房价", "楼市", "房产", "房地产", "租金", "二手房", "折旧价", "商铺",
94
+ # Pure macro — "宏观角度分析" should be finance chat, not stock template
95
+ "宏观", "宏观经济", "宏观政策", "宏观角度",
96
+ "货币政策", "财政政策", "gdp", "通胀", "通货膨胀", "cpi", "ppi",
97
+ # Non-chartable commodities / currencies
98
+ "黄金走势", "原油走势", "汇率走势", "美元指数",
99
+ )
100
+
101
+ # Pure macro/conceptual topics — no live market data needed, route to GENERAL
102
+ # (no tools invoked). More specific than _NON_STOCK_ANALYSIS_TOPICS: real-estate
103
+ # still goes to FINANCE because the user might want live data, but "宏观角度" is
104
+ # clearly a discussion question, not a quote lookup.
105
+ _MACRO_GENERAL_TOPICS = (
106
+ "宏观", "宏观经济", "宏观政策", "宏观角度", "宏观分析",
107
+ "货币政策", "财政政策", "gdp", "通胀", "通货膨胀", "cpi", "ppi",
108
+ "值得投资吗", "应该投资吗", "是否值得", "投资逻辑",
109
+ "长期展望", "未来前景", "宏观前景",
110
+ # Interest rate / bond macro concepts
111
+ "利率", "加息", "降息", "美联储政策", "央行政策",
112
+ "债券市场", "利差", "收益率曲线", "国债收益",
113
+ # Structural/sector macro
114
+ "产业政策", "行业监管", "政策影响", "监管政策",
115
+ )
116
+ _REALTIME_KW = (
117
+ "今天", "today", "现在", "now", "current", "latest", "最新",
118
+ "市值", "price", "股价", "quote", "行情", "涨跌", "涨幅",
119
+ "market cap", "how much", "what is the price",
120
+ "是多少", "多少钱", "多少点", "多少美元", "多少港元",
121
+ )
122
+
123
+ # Question words that, when combined with finance concept terms, mean "explain X"
124
+ # rather than "look up X" — should route to general, not finance.
125
+ _QUESTION_PREFIX = (
126
+ "什么是", "什么叫", "how does", "what is", "what are",
127
+ "explain", "define", "解释", "定义", "概念", "原理", "介绍",
128
+ "是什么", "为什么", "区别", "difference", "如何理解", "怎么理解",
129
+ )
130
+
131
+ # Finance-metric terms that combined with realtime words → live lookup
132
+ _METRIC_KW = (
133
+ "pe", "pb", "ps", "市盈率", "市净率", "市销率",
134
+ "eps", "净利润", "营收", "市值", "股息", "分红",
135
+ "ebitda", "利润率", "毛利率", "roe", "roa",
136
+ )
137
+
138
+ # File path extensions — presence means it's a document/code task, not stock analysis
139
+ _FILE_EXT_RE = r'\S+\.(?:docx|pdf|xlsx|pptx|txt|csv|json|py|md|log)\b'
140
+
141
+ # Specific financial entity signals — must be present for "分析" to route as stock analysis.
142
+ # Keep to SPECIFIC company names and ticker symbols only.
143
+ # Generic market categories ("美股", "债券", "股市") must NOT be here — they appear in
144
+ # macro conceptual questions and would incorrectly block the → general route.
145
+ _FIN_ENTITY_KW = (
146
+ # Company names (CN)
147
+ "苹果", "谷歌", "英伟达", "微软", "特斯拉", "亚马逊", "腾讯",
148
+ "阿里", "百度", "比亚迪", "茅台", "招商银行", "中国平安", "恒生指数",
149
+ "华为", "小米", "美团", "京东", "字节", "滴滴",
150
+ # Company names (EN)
151
+ "apple", "google", "nvidia", "microsoft", "tesla", "amazon",
152
+ "meta", "netflix", "palantir", "snowflake",
153
+ # Common tickers (lowercase)
154
+ "aapl", "nvda", "msft", "tsla", "amzn", "googl", "meta", "baba",
155
+ "spy", "qqq", "iwm", "dia", "gld", "uso",
156
+ # Specific crypto coins (named assets, not generic "加密货币")
157
+ "比特币", "以太坊", "bitcoin", "ethereum", "btc", "eth", "sol", "bnb",
158
+ # Named indices (specific, not generic "指数")
159
+ "纳斯达克", "标普500", "道琼斯", "沪深300", "中证500",
160
+ )
161
+
162
+ # Broader market terms (generic) — used only to check for stock-analysis context,
163
+ # NOT used to block macro-general classification (avoid false negatives on conceptual Qs)
164
+ _MARKET_GENERAL_KW = (
165
+ "股票", "股市", "股价", "美股", "港股", "a股",
166
+ "etf", "基金", "指数", "期货", "期权", "加密", "数字货币",
167
+ )
168
+ _GENERAL_KW = (
169
+ "什么是", "what is", "what are", "how does", "explain", "define",
170
+ "解释", "定义", "概念", "原理", "介绍", "怎么", "如何理解",
171
+ "是什么", "为什么", "区别", "difference between",
172
+ "tell me about", "describe", "how to", "举例", "example",
173
+ )
174
+ _FINANCE_CONCEPT_KW = (
175
+ "dcf", "pe", "pb", "ps", "ev", "ebitda", "wacc", "capm",
176
+ "beta", "alpha", "sharpe", "sortino", "var", "cvar", "drawdown",
177
+ "black-scholes", "期权", "期货", "衍生品", "套利",
178
+ "量化", "quant", "回测", "因子", "ic值", "ir值",
179
+ "market cap", "市盈率", "市净率", "净利润", "估值", "valuation",
180
+ )
181
+
182
+
183
+ def is_visual_market_artifact_request(message: str) -> bool:
184
+ """Return True for finance-adjacent visual artifact requests.
185
+
186
+ These should prefer chart/dashboard/report workflows instead of generic
187
+ market-data prefetch.
188
+ """
189
+ low = message.lower().strip()
190
+ if low.startswith(("/chart", "/dashboard", "/report")):
191
+ return True
192
+ if not any(k in low for k in _VISUAL_ARTIFACT_KW):
193
+ return False
194
+ if any(k in low for k in _VISUAL_MARKET_CONTEXT_KW):
195
+ return True
196
+ if any(k in low for k in _MARKET_GENERAL_KW):
197
+ return True
198
+ if any(e in low for e in _FIN_ENTITY_KW):
199
+ return True
200
+ return any(k in low for k in ("公司", "集团", "股份", "科技", "银行", "证券", "能源", "汽车"))
201
+
202
+
203
+ def classify_intent_sync(message: str) -> str:
204
+ """
205
+ Tier-2 keyword-based classification (synchronous, always available).
206
+ Returns one of the INTENT_* constants.
207
+ """
208
+ import re as _re
209
+ low = message.lower().strip()
210
+
211
+ if is_visual_market_artifact_request(message):
212
+ return INTENT_CODING
213
+
214
+ # Bug ⑥ — file path present → document/code task, never stock analysis template
215
+ if _re.search(_FILE_EXT_RE, low):
216
+ if any(k in low for k in _CODING_KW):
217
+ return INTENT_CODING # "帮我写一个解析这个 csv 的脚本"
218
+ return INTENT_FINANCE # "分析这个文件的可行性 loads/x.docx"
219
+
220
+ has_coding = any(k in low for k in _CODING_KW)
221
+ has_realtime = any(k in low for k in _REALTIME_KW)
222
+ has_question = any(q in low for q in _QUESTION_PREFIX)
223
+
224
+ # Coding intent — but skip if phrased as a conceptual question ("X的核心是什么")
225
+ if has_coding and not has_question:
226
+ return INTENT_CODING
227
+
228
+ # Bug ⑤ — "metric是多少/how much" → realtime lookup, not finance concept chat
229
+ if any(m in low for m in _METRIC_KW) and any(k in low for k in _REALTIME_KW):
230
+ return INTENT_REALTIME
231
+
232
+ # Live-data terms must win over broad analysis words. "分析苹果今天的市场"
233
+ # contains both "分析" and "今天"; routing it as generic analysis lets the
234
+ # model answer from memory instead of using market data.
235
+ if has_realtime:
236
+ return INTENT_REALTIME
237
+
238
+ # Bug ② — "什么是X" + finance concept → general (explain, not look up)
239
+ if any(q in low for q in _QUESTION_PREFIX) and any(k in low for k in _FINANCE_CONCEPT_KW):
240
+ return INTENT_GENERAL
241
+
242
+ # Stock/market analysis — but only if a financial entity is present.
243
+ if any(k in low for k in _ANALYSIS_KW):
244
+ # Bug ③ — macro topics check (also works independently below)
245
+ if any(t in low for t in _MACRO_GENERAL_TOPICS):
246
+ return INTENT_GENERAL
247
+ if any(t in low for t in _NON_STOCK_ANALYSIS_TOPICS):
248
+ return INTENT_FINANCE
249
+ # Bug ① — "分析" without a financial entity = general task (project, doc, etc.)
250
+ if not any(e in low for e in _FIN_ENTITY_KW):
251
+ # Check for uppercase ticker pattern (e.g. AAPL, BTC)
252
+ if not _re.search(r'\b[A-Z]{2,5}\b', message):
253
+ return INTENT_FINANCE
254
+ # Bug ④ — recommendation phrasing ("应该买入吗") → finance chat, not chart analysis
255
+ _rec_phrases = ("应该", "是否值得", "要不要", "该不该", "值不值", "建议买", "建议卖")
256
+ if any(p in low for p in _rec_phrases):
257
+ return INTENT_FINANCE
258
+ return INTENT_ANALYSIS
259
+
260
+ # Bug ③ (standalone) — macro conceptual topics, unless a SPECIFIC entity is named.
261
+ # Generic market terms like "股市"/"美股" don't block this — "宏观经济对美股的影响"
262
+ # is still a macro discussion, not a specific stock query.
263
+ if any(t in low for t in _MACRO_GENERAL_TOPICS):
264
+ _has_specific = (
265
+ any(e in low for e in _FIN_ENTITY_KW)
266
+ or _re.search(r'\b[A-Z]{2,5}\b', message)
267
+ )
268
+ if not _has_specific:
269
+ return INTENT_GENERAL
270
+
271
+ # Finance concept terms → keep full finance context, not general
272
+ if any(k in low for k in _FINANCE_CONCEPT_KW):
273
+ return INTENT_FINANCE
274
+
275
+ if any(k in low for k in _GENERAL_KW):
276
+ return INTENT_GENERAL
277
+
278
+ return INTENT_FINANCE
279
+
280
+
281
+ def _prelude_available(ollama_url: str) -> bool:
282
+ """Quick sync check: is aria-prelude loaded in Ollama?"""
283
+ try:
284
+ with urllib.request.urlopen(
285
+ ollama_url.rstrip("/") + "/api/tags", timeout=1
286
+ ) as r:
287
+ data = json.loads(r.read())
288
+ models = [m["name"] for m in data.get("models", [])]
289
+ return any(m.startswith(_PRELUDE_MODEL) for m in models)
290
+ except Exception:
291
+ return False
292
+
293
+
294
+ async def classify_intent_async(
295
+ message: str,
296
+ ollama_url: str = "http://localhost:11434",
297
+ *,
298
+ timeout: float = _PRELUDE_TIMEOUT,
299
+ ) -> str:
300
+ """
301
+ Tier-1 classification using aria-prelude via Ollama.
302
+ Falls back to tier-2 keyword classification on any error or timeout.
303
+ """
304
+ try:
305
+ import aiohttp
306
+ except ImportError:
307
+ return classify_intent_sync(message)
308
+
309
+ if is_visual_market_artifact_request(message):
310
+ return INTENT_CODING
311
+
312
+ if not _prelude_available(ollama_url):
313
+ return classify_intent_sync(message)
314
+
315
+ payload = {
316
+ "model": _PRELUDE_MODEL,
317
+ "messages": [
318
+ {"role": "system", "content": _PRELUDE_SYSTEM},
319
+ {"role": "user", "content": message},
320
+ ],
321
+ "stream": False,
322
+ "options": {"num_predict": 8, "temperature": 0.0},
323
+ }
324
+ url = ollama_url.rstrip("/") + "/api/chat"
325
+ try:
326
+ async with aiohttp.ClientSession() as sess:
327
+ async with sess.post(
328
+ url, json=payload,
329
+ timeout=aiohttp.ClientTimeout(total=timeout),
330
+ ) as resp:
331
+ if resp.status != 200:
332
+ return classify_intent_sync(message)
333
+ data = await resp.json()
334
+ raw = data.get("message", {}).get("content", "").strip().lower()
335
+ # Accept only known labels
336
+ for label in (INTENT_CODING, INTENT_ANALYSIS, INTENT_REALTIME,
337
+ INTENT_GENERAL, INTENT_FINANCE):
338
+ if label in raw:
339
+ if is_visual_market_artifact_request(message):
340
+ return INTENT_CODING
341
+ # Post-override: even if the prelude model says "analysis" or "finance",
342
+ # non-stock topics must not get the stock-analysis template.
343
+ if label == INTENT_ANALYSIS:
344
+ low_msg = message.lower()
345
+ if any(t in low_msg for t in _MACRO_GENERAL_TOPICS):
346
+ return INTENT_GENERAL # macro → educational, no tools
347
+ if any(t in low_msg for t in _NON_STOCK_ANALYSIS_TOPICS):
348
+ return INTENT_FINANCE # real-estate etc. → finance chat
349
+ if label == INTENT_FINANCE:
350
+ # Even a "finance" label from the model shouldn't invoke live tools
351
+ # for pure macro conceptual questions.
352
+ low_msg = message.lower()
353
+ if any(t in low_msg for t in _MACRO_GENERAL_TOPICS):
354
+ return INTENT_GENERAL
355
+ return label
356
+ except Exception:
357
+ pass
358
+ return classify_intent_sync(message)