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
@@ -0,0 +1,265 @@
1
+ """Market symbol resolution across A-shares, HK, global tickers, FX and futures.
2
+
3
+ The resolver is intentionally cache-first. Static aliases cover common global
4
+ assets and critical names; optional akshare loaders populate full A-share/HK
5
+ name tables into a local cache when available.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ import re
14
+ import time
15
+ from typing import Callable, Iterable
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class MarketSymbol:
20
+ name: str
21
+ symbol: str
22
+ market: str
23
+ source: str = "static"
24
+
25
+
26
+ STATIC_MARKET_ALIASES: dict[str, MarketSymbol] = {
27
+ # A-share fixes and high-frequency names not guaranteed in older static maps
28
+ "斯迪克": MarketSymbol("斯迪克", "300806", "CN", "static"),
29
+ # China / HK indices
30
+ "上证指数": MarketSymbol("上证指数", "000001.SS", "INDEX", "static"),
31
+ "上证": MarketSymbol("上证指数", "000001.SS", "INDEX", "static"),
32
+ "深证成指": MarketSymbol("深证成指", "399001.SZ", "INDEX", "static"),
33
+ "创业板指": MarketSymbol("创业板指", "399006.SZ", "INDEX", "static"),
34
+ "沪深300": MarketSymbol("沪深300", "000300.SS", "INDEX", "static"),
35
+ "中证500": MarketSymbol("中证500", "000905.SS", "INDEX", "static"),
36
+ "恒生指数": MarketSymbol("恒生指数", "^HSI", "INDEX", "static"),
37
+ "恒指": MarketSymbol("恒生指数", "^HSI", "INDEX", "static"),
38
+ # US/global indices
39
+ "标普500": MarketSymbol("标普500", "^GSPC", "INDEX", "static"),
40
+ "标普": MarketSymbol("标普500", "^GSPC", "INDEX", "static"),
41
+ "纳斯达克": MarketSymbol("纳斯达克综合", "^IXIC", "INDEX", "static"),
42
+ "纳指": MarketSymbol("纳斯达克综合", "^IXIC", "INDEX", "static"),
43
+ "道琼斯": MarketSymbol("道琼斯工业指数", "^DJI", "INDEX", "static"),
44
+ "道指": MarketSymbol("道琼斯工业指数", "^DJI", "INDEX", "static"),
45
+ "罗素2000": MarketSymbol("罗素2000", "^RUT", "INDEX", "static"),
46
+ "恐慌指数": MarketSymbol("VIX", "^VIX", "INDEX", "static"),
47
+ "vix": MarketSymbol("VIX", "^VIX", "INDEX", "static"),
48
+ "富时100": MarketSymbol("FTSE 100", "^FTSE", "INDEX", "static"),
49
+ "德国dax": MarketSymbol("DAX", "^GDAXI", "INDEX", "static"),
50
+ "dax": MarketSymbol("DAX", "^GDAXI", "INDEX", "static"),
51
+ "法国cac": MarketSymbol("CAC 40", "^FCHI", "INDEX", "static"),
52
+ "日经225": MarketSymbol("Nikkei 225", "^N225", "INDEX", "static"),
53
+ # Europe equities / brands frequently asked by name rather than ticker.
54
+ "lvmh": MarketSymbol("LVMH Moet Hennessy Louis Vuitton SE", "MC.PA", "EU", "static"),
55
+ "路易威登": MarketSymbol("LVMH Moet Hennessy Louis Vuitton SE", "MC.PA", "EU", "static"),
56
+ "路易斯威登": MarketSymbol("LVMH Moet Hennessy Louis Vuitton SE", "MC.PA", "EU", "static"),
57
+ "louis vuitton": MarketSymbol("LVMH Moet Hennessy Louis Vuitton SE", "MC.PA", "EU", "static"),
58
+ "爱马仕": MarketSymbol("Hermes International SCA", "RMS.PA", "EU", "static"),
59
+ "开云集团": MarketSymbol("Kering SA", "KER.PA", "EU", "static"),
60
+ "古驰": MarketSymbol("Kering SA", "KER.PA", "EU", "static"),
61
+ # Crypto
62
+ "比特币": MarketSymbol("比特币", "BTC-USD", "CRYPTO", "static"),
63
+ "btc": MarketSymbol("Bitcoin", "BTC-USD", "CRYPTO", "static"),
64
+ "以太坊": MarketSymbol("以太坊", "ETH-USD", "CRYPTO", "static"),
65
+ "eth": MarketSymbol("Ethereum", "ETH-USD", "CRYPTO", "static"),
66
+ "狗狗币": MarketSymbol("Dogecoin", "DOGE-USD", "CRYPTO", "static"),
67
+ "sol": MarketSymbol("Solana", "SOL-USD", "CRYPTO", "static"),
68
+ "索拉纳": MarketSymbol("Solana", "SOL-USD", "CRYPTO", "static"),
69
+ # FX
70
+ "美元人民币": MarketSymbol("USD/CNY", "CNY=X", "FX", "static"),
71
+ "人民币汇率": MarketSymbol("USD/CNY", "CNY=X", "FX", "static"),
72
+ "美元兑人民币": MarketSymbol("USD/CNY", "CNY=X", "FX", "static"),
73
+ "美元指数": MarketSymbol("美元指数", "DX-Y.NYB", "FX", "static"),
74
+ "欧元美元": MarketSymbol("EUR/USD", "EURUSD=X", "FX", "static"),
75
+ "欧元兑美元": MarketSymbol("EUR/USD", "EURUSD=X", "FX", "static"),
76
+ "美元日元": MarketSymbol("USD/JPY", "JPY=X", "FX", "static"),
77
+ "英镑美元": MarketSymbol("GBP/USD", "GBPUSD=X", "FX", "static"),
78
+ # Futures / commodities via Yahoo continuous futures
79
+ "黄金": MarketSymbol("黄金期货", "GC=F", "FUTURES", "static"),
80
+ "白银": MarketSymbol("白银期货", "SI=F", "FUTURES", "static"),
81
+ "原油": MarketSymbol("WTI原油期货", "CL=F", "FUTURES", "static"),
82
+ "wti": MarketSymbol("WTI原油期货", "CL=F", "FUTURES", "static"),
83
+ "布伦特": MarketSymbol("布伦特原油期货", "BZ=F", "FUTURES", "static"),
84
+ "铜": MarketSymbol("铜期货", "HG=F", "FUTURES", "static"),
85
+ "天然气": MarketSymbol("天然气期货", "NG=F", "FUTURES", "static"),
86
+ "玉米": MarketSymbol("玉米期货", "ZC=F", "FUTURES", "static"),
87
+ "大豆": MarketSymbol("大豆期货", "ZS=F", "FUTURES", "static"),
88
+ }
89
+
90
+
91
+ def _cache_path() -> Path:
92
+ root = Path(os.getenv("ARIA_CACHE_DIR") or (Path.home() / ".aria" / "cache"))
93
+ return root / "market_universe.json"
94
+
95
+
96
+ def _load_cache(path: Path | None = None, *, max_age_seconds: int = 7 * 86400) -> list[MarketSymbol]:
97
+ path = path or _cache_path()
98
+ try:
99
+ if not path.exists() or time.time() - path.stat().st_mtime > max_age_seconds:
100
+ return []
101
+ payload = json.loads(path.read_text(encoding="utf-8"))
102
+ return [
103
+ MarketSymbol(
104
+ name=str(item.get("name") or ""),
105
+ symbol=str(item.get("symbol") or ""),
106
+ market=str(item.get("market") or ""),
107
+ source=str(item.get("source") or "cache"),
108
+ )
109
+ for item in payload.get("symbols", [])
110
+ if item.get("name") and item.get("symbol")
111
+ ]
112
+ except Exception:
113
+ return []
114
+
115
+
116
+ def _write_cache(symbols: Iterable[MarketSymbol], path: Path | None = None) -> None:
117
+ path = path or _cache_path()
118
+ try:
119
+ path.parent.mkdir(parents=True, exist_ok=True)
120
+ data = {
121
+ "updated_at": int(time.time()),
122
+ "symbols": [s.__dict__ for s in symbols if s.name and s.symbol],
123
+ }
124
+ path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
125
+ except Exception:
126
+ pass
127
+
128
+
129
+ def _iter_static_symbols() -> list[MarketSymbol]:
130
+ seen: set[tuple[str, str]] = set()
131
+ out: list[MarketSymbol] = []
132
+ for item in STATIC_MARKET_ALIASES.values():
133
+ key = (item.name, item.symbol)
134
+ if key not in seen:
135
+ out.append(item)
136
+ seen.add(key)
137
+ return out
138
+
139
+
140
+ def _symbols_from_frame(frame, *, name_cols: tuple[str, ...], code_cols: tuple[str, ...], market: str, source: str) -> list[MarketSymbol]:
141
+ out: list[MarketSymbol] = []
142
+ try:
143
+ columns = {str(c).lower(): c for c in frame.columns}
144
+ name_col = next((columns[c.lower()] for c in name_cols if c.lower() in columns), None)
145
+ code_col = next((columns[c.lower()] for c in code_cols if c.lower() in columns), None)
146
+ if name_col is None or code_col is None:
147
+ return []
148
+ for _, row in frame.iterrows():
149
+ name = str(row.get(name_col, "")).strip()
150
+ code = str(row.get(code_col, "")).strip().upper()
151
+ if not name or not code or code.lower() == "nan":
152
+ continue
153
+ if market == "HK":
154
+ digits = re.sub(r"\D", "", code)
155
+ if digits:
156
+ code = f"{digits.zfill(4)}.HK"
157
+ out.append(MarketSymbol(name, code, market, source))
158
+ except Exception:
159
+ return []
160
+ return out
161
+
162
+
163
+ def fetch_market_universe() -> list[MarketSymbol]:
164
+ """Fetch A-share and HK symbol tables when akshare is available."""
165
+ symbols = _iter_static_symbols()
166
+ try:
167
+ import akshare as ak
168
+ try:
169
+ a_df = ak.stock_info_a_code_name()
170
+ symbols.extend(_symbols_from_frame(
171
+ a_df,
172
+ name_cols=("name", "证券简称", "股票简称", "名称"),
173
+ code_cols=("code", "证券代码", "股票代码", "代码"),
174
+ market="CN",
175
+ source="akshare:a_code_name",
176
+ ))
177
+ except Exception:
178
+ pass
179
+ try:
180
+ hk_df = ak.stock_hk_spot_em()
181
+ symbols.extend(_symbols_from_frame(
182
+ hk_df,
183
+ name_cols=("名称", "股票简称", "name"),
184
+ code_cols=("代码", "code", "股票代码"),
185
+ market="HK",
186
+ source="akshare:hk_spot",
187
+ ))
188
+ except Exception:
189
+ pass
190
+ except Exception:
191
+ pass
192
+
193
+ dedup: dict[tuple[str, str], MarketSymbol] = {}
194
+ for item in symbols:
195
+ dedup[(item.name.lower(), item.symbol.upper())] = item
196
+ return list(dedup.values())
197
+
198
+
199
+ def ensure_market_universe(*, force: bool = False) -> list[MarketSymbol]:
200
+ cached = [] if force else _load_cache()
201
+ if cached:
202
+ return _iter_static_symbols() + cached
203
+ fetched = fetch_market_universe()
204
+ _write_cache(fetched)
205
+ return fetched
206
+
207
+
208
+ def resolve_market_mentions(
209
+ text: str,
210
+ *,
211
+ limit: int = 6,
212
+ load_universe: Callable[[], list[MarketSymbol]] | None = None,
213
+ ) -> list[tuple[int, MarketSymbol]]:
214
+ """Resolve named market assets mentioned in text, preserving positions."""
215
+ if not text:
216
+ return []
217
+ low = text.lower()
218
+ hits: list[tuple[int, MarketSymbol]] = []
219
+ for alias, item in sorted(STATIC_MARKET_ALIASES.items(), key=lambda kv: -len(kv[0])):
220
+ idx = low.find(alias.lower())
221
+ if idx >= 0:
222
+ hits.append((idx, item))
223
+
224
+ def scan(items: Iterable[MarketSymbol]) -> None:
225
+ for item in sorted(items, key=lambda s: -len(s.name)):
226
+ if not item.name:
227
+ continue
228
+ idx = low.find(item.name.lower())
229
+ if idx >= 0:
230
+ hits.append((idx, item))
231
+
232
+ if load_universe is not None:
233
+ scan(load_universe())
234
+ else:
235
+ scan(_load_cache())
236
+ market_words = "走势|预测|股价|股票|行情|趋势|技术面|基本面|涨跌|价格|市值|k线|图表|财报"
237
+ if not hits and re.search(r"[\u4e00-\u9fff]", text) and re.search(market_words, text, re.I):
238
+ scan(ensure_market_universe(force=True))
239
+
240
+ ordered: list[tuple[int, MarketSymbol]] = []
241
+ seen_symbols: set[str] = set()
242
+ for idx, item in sorted(hits, key=lambda pair: (pair[0], -len(pair[1].name))):
243
+ sym = item.symbol.upper()
244
+ if sym in seen_symbols:
245
+ continue
246
+ ordered.append((idx, item))
247
+ seen_symbols.add(sym)
248
+ if len(ordered) >= limit:
249
+ break
250
+ return ordered
251
+
252
+
253
+ def resolve_market_symbol(text: str) -> str:
254
+ hits = resolve_market_mentions(text, limit=1)
255
+ return hits[0][1].symbol if hits else ""
256
+
257
+
258
+ def looks_like_unresolved_market_name(text: str) -> bool:
259
+ """Heuristic guard: a Chinese name before market words should not inherit history."""
260
+ if resolve_market_symbol(text):
261
+ return False
262
+ if not re.search(r"[\u4e00-\u9fff]{2,12}", text or ""):
263
+ return False
264
+ market_words = "走势|预测|股价|股票|行情|趋势|技术面|基本面|涨跌|价格|市值"
265
+ return bool(re.search(rf"[\u4e00-\u9fffA-Za-z0-9]{{2,16}}(?:的)?(?:{market_words})", text or ""))
@@ -0,0 +1,257 @@
1
+ """Message processing utilities extracted from aria_cli.py.
2
+
3
+ Functions here handle: parsing tool calls from text, stripping tool tags
4
+ from display output, and compacting conversation history.
5
+
6
+ Dependencies on aria_cli globals (LOCAL_TOOLS, get_model_cfg) are resolved
7
+ via lazy runtime imports to avoid circular import at load time.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ from typing import Optional
14
+
15
+
16
+ def _local_tools() -> dict:
17
+ import aria_cli
18
+ return aria_cli.LOCAL_TOOLS
19
+
20
+
21
+ def _fix_json(raw: str) -> str:
22
+ import aria_cli
23
+ return aria_cli._fix_json_string(raw)
24
+
25
+
26
+ def _get_model_cfg(key: str) -> dict:
27
+ import aria_cli
28
+ return aria_cli.get_model_cfg(key)
29
+
30
+
31
+ # ── Tool call parsing ─────────────────────────────────────────────────────────
32
+
33
+ def parse_text_tool_calls(text: str) -> list:
34
+ """Parse tool calls from AI response text.
35
+
36
+ Supports formats:
37
+ 1. <tool_call>{"name": "...", "arguments": {...}}</tool_call>
38
+ 2. ```json\\n{"name": "...", "arguments": {...}}\\n```
39
+ 3. Bare JSON: {"name": "...", "arguments": {...}}
40
+ """
41
+ local_tools = _local_tools()
42
+ calls: list = []
43
+
44
+ def _try_parse(raw: str) -> Optional[dict]:
45
+ try:
46
+ return json.loads(raw)
47
+ except json.JSONDecodeError:
48
+ pass
49
+ try:
50
+ return json.loads(_fix_json(raw))
51
+ except json.JSONDecodeError:
52
+ pass
53
+ return None
54
+
55
+ # Format 1: <tool_call>...</tool_call> tags
56
+ tag_pattern = re.compile(r'<tool_call>\s*([\s\S]*?)\s*</tool_call>', re.DOTALL)
57
+ for m in tag_pattern.finditer(text):
58
+ obj = _try_parse(m.group(1))
59
+ if obj:
60
+ name = obj.get("name", "")
61
+ args = obj.get("arguments", {})
62
+ if name and name in local_tools:
63
+ if isinstance(args, str):
64
+ try:
65
+ args = json.loads(args)
66
+ except json.JSONDecodeError:
67
+ args = json.loads(_fix_json(args))
68
+ calls.append({"tool": name, "params": args})
69
+
70
+ if calls:
71
+ return calls
72
+
73
+ # Format 2: code-fenced JSON
74
+ fence_pattern = re.compile(r'```(?:json)?\s*\n([\s\S]*?)\n\s*```', re.DOTALL)
75
+ for m in fence_pattern.finditer(text):
76
+ obj = _try_parse(m.group(1))
77
+ if obj:
78
+ name = obj.get("name", "")
79
+ args = obj.get("arguments", {})
80
+ if name and name in local_tools:
81
+ if isinstance(args, str):
82
+ args = _try_parse(args) or {}
83
+ calls.append({"tool": name, "params": args})
84
+
85
+ if calls:
86
+ return calls
87
+
88
+ # Format 3: bare JSON with balanced brace scan
89
+ brace_depth = 0
90
+ json_start = -1
91
+ for i, ch in enumerate(text):
92
+ if ch == '{':
93
+ if brace_depth == 0:
94
+ json_start = i
95
+ brace_depth += 1
96
+ elif ch == '}':
97
+ brace_depth -= 1
98
+ if brace_depth == 0 and json_start >= 0:
99
+ candidate = text[json_start:i + 1]
100
+ obj = _try_parse(candidate)
101
+ if obj:
102
+ name = obj.get("name", "")
103
+ args = obj.get("arguments", {})
104
+ if name and name in local_tools:
105
+ if isinstance(args, str):
106
+ args = _try_parse(args) or {}
107
+ calls.append({"tool": name, "params": args})
108
+ json_start = -1
109
+
110
+ return calls
111
+
112
+
113
+ def strip_tool_call_tags(text: str) -> str:
114
+ """Remove tool calls from display text (tags, fences, bare JSON, headers)."""
115
+ local_tools = _local_tools()
116
+
117
+ text = re.sub(r'<tool_call>[\s\S]*?</tool_call>', '', text, flags=re.DOTALL)
118
+
119
+ def _remove_fence(m: re.Match) -> str:
120
+ try:
121
+ obj = json.loads(m.group(1))
122
+ if obj.get("name") in local_tools and "arguments" in obj:
123
+ return ''
124
+ except (json.JSONDecodeError, TypeError):
125
+ pass
126
+ return m.group(0)
127
+
128
+ text = re.sub(r'```(?:json)?\s*\n([\s\S]*?)\n\s*```', _remove_fence, text, flags=re.DOTALL)
129
+
130
+ def _remove_bare(m: re.Match) -> str:
131
+ try:
132
+ obj = json.loads(m.group(0))
133
+ if obj.get("name") in local_tools and "arguments" in obj:
134
+ return ''
135
+ except (json.JSONDecodeError, TypeError):
136
+ pass
137
+ return m.group(0)
138
+
139
+ text = re.sub(
140
+ r'\{[^{}]*"name"\s*:\s*"[^"]*"[^{}]*"arguments"\s*:\s*\{[\s\S]*?\}\s*\}',
141
+ _remove_bare, text,
142
+ )
143
+ text = re.sub(r'###\s+Step\s+\d+.*\n?', '', text)
144
+ text = re.sub(r'###\s+.*工具调用.*\n?', '', text)
145
+ text = re.sub(r'\n{3,}', '\n\n', text)
146
+ return text.strip()
147
+
148
+
149
+ # ── History compaction ────────────────────────────────────────────────────────
150
+
151
+ def estimate_message_tokens(messages: list, extra_content: str = "") -> int:
152
+ """Return a rough token estimate matching the terminal context meter."""
153
+ from packages.aria_services.context import ContextService
154
+
155
+ return ContextService.estimate_message_tokens(messages, extra_content=extra_content)
156
+
157
+
158
+ def context_compaction_decision(
159
+ messages: list,
160
+ *,
161
+ model_key: str = "qwen7b",
162
+ extra_content: str = "",
163
+ threshold: float = 0.78,
164
+ min_messages: int = 8,
165
+ ) -> dict:
166
+ """Decide whether a conversation should be compacted before the next turn."""
167
+ try:
168
+ threshold = float(threshold)
169
+ except Exception:
170
+ threshold = 0.78
171
+ max_ctx = int(_get_model_cfg(model_key).get("num_ctx", 16384) or 16384)
172
+ from packages.aria_services.context import build_context_service
173
+
174
+ service = build_context_service(
175
+ max_tokens=max_ctx,
176
+ threshold=threshold,
177
+ min_messages=min_messages,
178
+ )
179
+ return service.compaction_decision(messages, extra_content=extra_content).to_dict()
180
+
181
+
182
+ def compact_messages(
183
+ messages: list,
184
+ max_chars: int = 0,
185
+ model_key: str = "qwen7b",
186
+ ) -> list:
187
+ """Smart synchronous compaction for the agentic tool loop.
188
+
189
+ Strategy (in order of priority):
190
+ 1. Always keep: system prompt + last 8 messages (recent context).
191
+ 2. For middle tool results: extract status line + error details (if any),
192
+ drop verbose success payloads.
193
+ 3. For middle assistant turns: keep first paragraph + last sentence.
194
+ 4. Error markers are never discarded.
195
+ """
196
+ ctx = int(_get_model_cfg(model_key).get("num_ctx", 16384) or 16384)
197
+ from packages.aria_services.context import build_context_service
198
+
199
+ service = build_context_service(max_tokens=ctx)
200
+ return service.compact_messages(messages, max_chars=max_chars)
201
+
202
+
203
+ # ── Broker context injection ──────────────────────────────────────────────────
204
+
205
+ def build_broker_context_block() -> str:
206
+ """Return a compact broker context block for injection into the system prompt.
207
+
208
+ Returns "" if no broker is connected or data fetch fails.
209
+ """
210
+ import aria_cli as _ac
211
+ if not getattr(_ac, "_HAS_BROKERS", False):
212
+ return ""
213
+ try:
214
+ reg = _ac._get_broker_registry()
215
+ if not reg:
216
+ return ""
217
+ broker = reg.active()
218
+ if not broker or not broker.is_connected:
219
+ return ""
220
+
221
+ parts = [f"## 券商账户实时快照 [{broker.label}]"]
222
+
223
+ try:
224
+ acct = broker.account_info()
225
+ parts.append(
226
+ f"- 账户: {acct.masked_account} 货币: {acct.currency}\n"
227
+ f"- 总资产: {acct.total_assets:,.2f} 可用现金: {acct.cash:,.2f}"
228
+ f" 持仓市值: {acct.market_value:,.2f}"
229
+ )
230
+ if acct.pnl_today:
231
+ parts.append(f"- 当日盈亏: {acct.pnl_today:+,.2f}")
232
+ except Exception:
233
+ pass
234
+
235
+ try:
236
+ positions = broker.positions()
237
+ if positions:
238
+ positions_sorted = sorted(positions, key=lambda p: -abs(p.market_value))[:10]
239
+ parts.append("\n持仓明细(市值降序,最多10条):")
240
+ for p in positions_sorted:
241
+ pnl_str = f" 盈亏 {p.pnl:+,.2f} ({p.pnl_pct:+.2f}%)" if p.pnl else ""
242
+ parts.append(
243
+ f" {p.symbol} {p.name[:8] if p.name else ''} "
244
+ f"持仓 {p.quantity:.0f} 成本 {p.cost_price:.3f} "
245
+ f"现价 {p.current_price:.3f} 市值 {p.market_value:,.2f}{pnl_str}"
246
+ )
247
+ except Exception:
248
+ pass
249
+
250
+ if len(parts) <= 1:
251
+ return ""
252
+
253
+ parts.append("\n(以上为实时账户数据,无需再调用 broker_query 获取基本账户/持仓信息,可直接引用。)")
254
+ return "\n".join(parts)
255
+
256
+ except Exception:
257
+ return ""
apps/cli/plan_mode.py ADDED
@@ -0,0 +1,139 @@
1
+ """Interactive plan mode — intercept tool calls for step-by-step approval.
2
+
3
+ When plan mode is active, the agent loop asks the user to approve each tool
4
+ call before execution. A plan-mode header shows which step is being proposed.
5
+
6
+ Usage (from aria_cli.py):
7
+ from apps.cli.plan_mode import PlanModeState
8
+
9
+ _PLAN = PlanModeState() # module-level singleton
10
+
11
+ # Enter plan mode:
12
+ _PLAN.enter()
13
+
14
+ # In _confirm_tool_execution_decision:
15
+ if _PLAN.active and tool_name not in _CONFIRM_TOOLS:
16
+ return _PLAN.confirm_step(tool_name, params, console, HAS_RICH)
17
+
18
+ # Exit:
19
+ _PLAN.exit()
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass, field
25
+ from typing import List, Optional
26
+
27
+
28
+ @dataclass
29
+ class PlanStep:
30
+ index: int
31
+ tool: str
32
+ params: dict
33
+ approved: Optional[bool] = None
34
+ skipped: bool = False
35
+
36
+
37
+ @dataclass
38
+ class PlanModeState:
39
+ active: bool = False
40
+ _steps: List[PlanStep] = field(default_factory=list)
41
+ _index: int = 0
42
+
43
+ # ── Lifecycle ─────────────────────────────────────────────────────────────
44
+
45
+ def enter(self) -> None:
46
+ self.active = True
47
+ self._steps = []
48
+ self._index = 0
49
+
50
+ def exit(self) -> None:
51
+ self.active = False
52
+ self._steps = []
53
+ self._index = 0
54
+
55
+ # ── Step tracking ─────────────────────────────────────────────────────────
56
+
57
+ def _next_index(self) -> int:
58
+ self._index += 1
59
+ return self._index
60
+
61
+ def record_step(self, tool: str, params: dict, *, approved: bool) -> PlanStep:
62
+ step = PlanStep(index=self._next_index(), tool=tool, params=params, approved=approved)
63
+ self._steps.append(step)
64
+ return step
65
+
66
+ def summary(self) -> dict:
67
+ total = len(self._steps)
68
+ approved = sum(1 for s in self._steps if s.approved is True)
69
+ rejected = sum(1 for s in self._steps if s.approved is False)
70
+ return {"total": total, "approved": approved, "rejected": rejected}
71
+
72
+ # ── Confirmation UI ───────────────────────────────────────────────────────
73
+
74
+ def confirm_step(
75
+ self,
76
+ tool_name: str,
77
+ params: dict,
78
+ *,
79
+ console,
80
+ has_rich: bool,
81
+ arrow_select_fn,
82
+ ):
83
+ """Show plan-mode confirmation prompt. Returns ApprovalDecision."""
84
+ from runtime.approval import ApprovalDecision
85
+
86
+ step_num = self._index + 1 # preview next number
87
+
88
+ if has_rich and console:
89
+ _param_preview = _format_params(params)
90
+ console.print(
91
+ f"\n [bold cyan]◆ Plan Mode[/bold cyan] "
92
+ f"[dim]Step {step_num} — tool:[/dim] [bold]{tool_name}[/bold]"
93
+ )
94
+ if _param_preview:
95
+ console.print(f" [dim]{_param_preview}[/dim]")
96
+ else:
97
+ print(f"\n [Plan Mode] Step {step_num}: {tool_name}")
98
+
99
+ options = [
100
+ ("Execute this step", "执行此工具调用"),
101
+ ("Skip this step", "跳过,继续下一步"),
102
+ ("Execute all remaining", "从此步开始自动执行所有后续工具"),
103
+ ("Abort plan", "取消整个任务"),
104
+ ]
105
+
106
+ choice = arrow_select_fn(options, selected=0, title="")
107
+
108
+ if choice == 0:
109
+ self.record_step(tool_name, params, approved=True)
110
+ return ApprovalDecision.allow()
111
+ elif choice == 1:
112
+ self.record_step(tool_name, params, approved=False)
113
+ return ApprovalDecision.deny("skipped in plan mode")
114
+ elif choice == 2:
115
+ self.record_step(tool_name, params, approved=True)
116
+ self.exit() # turn off plan mode so remaining tools auto-execute
117
+ return ApprovalDecision.allow(auto_approve_session=True)
118
+ else:
119
+ self.record_step(tool_name, params, approved=False)
120
+ self.exit()
121
+ return ApprovalDecision.deny("plan aborted by user")
122
+
123
+
124
+ def _format_params(params: dict) -> str:
125
+ """Return a short one-line preview of tool params."""
126
+ parts = []
127
+ for key in ("path", "command", "url", "query", "symbol", "action"):
128
+ if key in params:
129
+ val = str(params[key])
130
+ if len(val) > 80:
131
+ val = val[:77] + "…"
132
+ parts.append(f"{key}={val!r}")
133
+ if not parts and params:
134
+ first_key = next(iter(params))
135
+ val = str(params[first_key])
136
+ if len(val) > 80:
137
+ val = val[:77] + "…"
138
+ parts.append(f"{first_key}={val!r}")
139
+ return " ".join(parts)
@@ -0,0 +1,15 @@
1
+ """Helpers for generating offline-safe Plotly HTML fragments."""
2
+ from __future__ import annotations
3
+
4
+ from functools import lru_cache
5
+
6
+
7
+ @lru_cache(maxsize=1)
8
+ def plotly_script_tag() -> str:
9
+ """Return an inline Plotly <script> tag when the package is available."""
10
+ try:
11
+ from plotly.offline import get_plotlyjs
12
+
13
+ return f"<script>{get_plotlyjs()}</script>"
14
+ except Exception:
15
+ return '<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>'