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
aria_telegram_bot.py ADDED
@@ -0,0 +1,202 @@
1
+ """
2
+ aria_telegram_bot.py — Lightweight Telegram Bot client for Aria Daemon.
3
+
4
+ Uses the Telegram Bot API directly via httpx (no heavy python-telegram-bot dep).
5
+ Supports long-polling updates and sending messages/documents back to users.
6
+
7
+ Commands handled:
8
+ /price SYMBOL — quick quote
9
+ /report SYMBOL — full analysis (async, returns text summary)
10
+ /brief — morning brief
11
+ /alerts — list active alerts
12
+ /alert SYMBOL cond v — add alert (e.g. /alert 600362 price_below 39.5)
13
+ /screen — hot A-share screener
14
+ /help — command list
15
+
16
+ Usage:
17
+ from aria_telegram_bot import TelegramBot
18
+ bot = TelegramBot(token="...", allowed_ids={123456})
19
+ await bot.start(command_handler) # command_handler(cmd, args, chat_id) -> str
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import asyncio
24
+ import logging
25
+ from typing import Any, Callable, Coroutine, Optional, Set
26
+
27
+ import httpx
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ _API = "https://api.telegram.org/bot{token}/{method}"
32
+
33
+
34
+ class TelegramBot:
35
+ def __init__(
36
+ self,
37
+ token: str,
38
+ allowed_chat_ids: Optional[Set[int]] = None,
39
+ poll_timeout: int = 30,
40
+ ):
41
+ self.token = token
42
+ self.allowed_chat_ids = allowed_chat_ids or set()
43
+ self.poll_timeout = poll_timeout
44
+ self._offset = 0
45
+ self._running = False
46
+ self._client: Optional[httpx.AsyncClient] = None
47
+
48
+ # ── Low-level API ─────────────────────────────────────────────────────────
49
+
50
+ def _url(self, method: str) -> str:
51
+ return _API.format(token=self.token, method=method)
52
+
53
+ async def _call(self, method: str, **kwargs: Any) -> Optional[dict]:
54
+ try:
55
+ if self._client is None:
56
+ self._client = httpx.AsyncClient(timeout=self.poll_timeout + 5)
57
+ resp = await self._client.post(self._url(method), json=kwargs)
58
+ data = resp.json()
59
+ if not data.get("ok"):
60
+ logger.warning("Telegram %s error: %s", method, data.get("description"))
61
+ return None
62
+ return data.get("result")
63
+ except Exception as exc:
64
+ logger.error("Telegram API call %s failed: %s", method, exc)
65
+ return None
66
+
67
+ # ── Sending ───────────────────────────────────────────────────────────────
68
+
69
+ async def send_message(
70
+ self, chat_id: int, text: str, parse_mode: str = "Markdown"
71
+ ) -> bool:
72
+ # Telegram has a 4096-char message limit
73
+ if len(text) > 4096:
74
+ text = text[:4090] + "\n…"
75
+ result = await self._call(
76
+ "sendMessage",
77
+ chat_id=chat_id,
78
+ text=text,
79
+ parse_mode=parse_mode,
80
+ disable_web_page_preview=True,
81
+ )
82
+ return result is not None
83
+
84
+ async def send_long_message(self, chat_id: int, text: str) -> None:
85
+ """Split and send messages that exceed Telegram's 4096-char limit."""
86
+ chunks = [text[i : i + 4000] for i in range(0, len(text), 4000)]
87
+ for chunk in chunks:
88
+ await self.send_message(chat_id, chunk)
89
+
90
+ async def send_typing(self, chat_id: int) -> None:
91
+ await self._call("sendChatAction", chat_id=chat_id, action="typing")
92
+
93
+ async def send_document(
94
+ self, chat_id: int, file_path: str, caption: str = ""
95
+ ) -> bool:
96
+ """Send a file as a document attachment."""
97
+ try:
98
+ import aiofiles
99
+ url = self._url("sendDocument")
100
+ async with aiofiles.open(file_path, "rb") as f:
101
+ content = await f.read()
102
+ import os
103
+ filename = os.path.basename(file_path)
104
+ if self._client is None:
105
+ self._client = httpx.AsyncClient(timeout=60)
106
+ resp = await self._client.post(
107
+ url,
108
+ data={"chat_id": str(chat_id), "caption": caption},
109
+ files={"document": (filename, content)},
110
+ )
111
+ return resp.json().get("ok", False)
112
+ except ImportError:
113
+ # Fallback: just send caption as text
114
+ await self.send_message(chat_id, caption or "File ready (aiofiles not installed)")
115
+ return False
116
+ except Exception as exc:
117
+ logger.error("send_document failed: %s", exc)
118
+ return False
119
+
120
+ # ── Polling loop ─────────────────────────────────────────────────────────
121
+
122
+ async def get_updates(self) -> list[dict]:
123
+ result = await self._call(
124
+ "getUpdates",
125
+ offset=self._offset,
126
+ timeout=self.poll_timeout,
127
+ allowed_updates=["message"],
128
+ )
129
+ return result or []
130
+
131
+ async def start(
132
+ self,
133
+ command_handler: Callable[[str, str, int], Coroutine[Any, Any, str]],
134
+ ) -> None:
135
+ """
136
+ Start long-polling. For each received message, parse the command and
137
+ call command_handler(command, args, chat_id) → reply text.
138
+ command_handler should be an async coroutine.
139
+ """
140
+ self._running = True
141
+ logger.info("Telegram bot polling started")
142
+ while self._running:
143
+ try:
144
+ updates = await self.get_updates()
145
+ except asyncio.CancelledError:
146
+ break
147
+ except Exception as exc:
148
+ logger.error("Polling error: %s", exc)
149
+ await asyncio.sleep(5)
150
+ continue
151
+
152
+ for update in updates:
153
+ self._offset = max(self._offset, update["update_id"] + 1)
154
+ msg = update.get("message", {})
155
+ text = (msg.get("text") or "").strip()
156
+ chat_id = msg.get("chat", {}).get("id")
157
+ if not text or not chat_id:
158
+ continue
159
+
160
+ # ACL check
161
+ if self.allowed_chat_ids and chat_id not in self.allowed_chat_ids:
162
+ await self.send_message(
163
+ chat_id,
164
+ "⛔ 未授权。请将你的 Chat ID 添加到 `TELEGRAM_ALLOWED_IDS`。\n你的 ID: `" + str(chat_id) + "`",
165
+ )
166
+ continue
167
+
168
+ asyncio.create_task(self._handle(text, chat_id, command_handler))
169
+
170
+ async def _handle(
171
+ self,
172
+ text: str,
173
+ chat_id: int,
174
+ command_handler: Callable[[str, str, int], Coroutine[Any, Any, str]],
175
+ ) -> None:
176
+ # Parse "/command args" or plain text
177
+ if text.startswith("/"):
178
+ parts = text[1:].split(None, 1)
179
+ cmd = parts[0].lower().split("@")[0] # strip @botname suffix
180
+ args = parts[1] if len(parts) > 1 else ""
181
+ else:
182
+ cmd = "chat"
183
+ args = text
184
+
185
+ await self.send_typing(chat_id)
186
+ try:
187
+ reply = await command_handler(cmd, args, chat_id)
188
+ except Exception as exc:
189
+ reply = f"⚠️ 执行出错: {exc}"
190
+ logger.exception("command_handler error cmd=%s", cmd)
191
+
192
+ if reply:
193
+ await self.send_long_message(chat_id, reply)
194
+
195
+ async def stop(self) -> None:
196
+ self._running = False
197
+ if self._client:
198
+ await self._client.aclose()
199
+ self._client = None
200
+
201
+ async def get_me(self) -> Optional[dict]:
202
+ return await self._call("getMe")
ariarc.py ADDED
@@ -0,0 +1,328 @@
1
+ """
2
+ ariarc.py — Project-level configuration loader for Aria Code.
3
+
4
+ Searches for ``.ariarc`` or ``.ariarc.json`` in the current directory and all
5
+ parent directories (walks up to filesystem root, stops at $HOME).
6
+
7
+ .ariarc format (JSON or JSONC)::
8
+
9
+ {
10
+ // Project identity
11
+ "project": "Arthera Quant Engine",
12
+ "description": "Quantitative trading system for A-share and US markets",
13
+
14
+ // Extra system prompt injected before every conversation
15
+ "system_prompt": "You are helping with an A-share quant strategy codebase...",
16
+
17
+ // Files whose contents are prepended as context
18
+ "context_files": ["README.md", "docs/architecture.md"],
19
+
20
+ // Tool allow/deny lists (applied on top of global policy)
21
+ "tools_whitelist": ["read_file", "search_code", "calculate_factors"],
22
+ "tools_blacklist": ["run_command"],
23
+
24
+ // Default symbols for watchlist / quick commands
25
+ "default_symbols": ["sh600519", "sh601318", "sz000858"],
26
+ "market": "cn", // cn | us | global
27
+
28
+ // A-share specific settings
29
+ "ashare": {
30
+ "broker": "东方财富",
31
+ "account_type": "普通账户",
32
+ "risk_level": "moderate" // conservative | moderate | aggressive
33
+ },
34
+
35
+ // Slash commands defined in-project
36
+ "commands": {
37
+ "/morning-cn": "生成A股早盘简报,重点关注 {default_symbols}",
38
+ "/factor-check": "计算 {symbol} 的技术因子并分析当前趋势"
39
+ },
40
+
41
+ // Files to auto-read at session start (feeds AI context)
42
+ "auto_context": [
43
+ "packages/quant_engine/strategies/quant_strategy_base.py",
44
+ "packages/quant_engine/analysis/signal_pipeline.py"
45
+ ],
46
+
47
+ // Disable AI from proposing certain file patterns (safety)
48
+ "write_deny_patterns": ["*.env", "config/secrets.*", "**/credentials*"]
49
+ }
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ import json
55
+ import os
56
+ import pathlib
57
+ import re
58
+ from typing import Any, Dict, List, Optional
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # JSONC parser (JSON with // and /* */ comments)
62
+ # ---------------------------------------------------------------------------
63
+
64
+ def _strip_comments(text: str) -> str:
65
+ """Remove // line comments and /* */ block comments from JSON text."""
66
+ # Block comments
67
+ text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
68
+ # Line comments (not inside strings — good-enough heuristic)
69
+ text = re.sub(r'(?<!:)(?<!https)//[^\n]*', "", text)
70
+ return text
71
+
72
+
73
+ def _parse_jsonc(text: str) -> Any:
74
+ return json.loads(_strip_comments(text))
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Default / empty ariarc
79
+ # ---------------------------------------------------------------------------
80
+
81
+ ARIARC_DEFAULTS: Dict[str, Any] = {
82
+ "project": None,
83
+ "description": None,
84
+ "system_prompt": "",
85
+ "context_files": [],
86
+ "tools_whitelist": [],
87
+ "tools_blacklist": [],
88
+ "default_symbols": [],
89
+ "market": "global",
90
+ "ashare": {},
91
+ "commands": {},
92
+ "auto_context": [],
93
+ "write_deny_patterns": ["*.env", "**/.env*", "**/secrets.*", "**/credentials*"],
94
+ }
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # Finder
99
+ # ---------------------------------------------------------------------------
100
+
101
+ def find_ariarc(start_dir: Optional[str] = None) -> Optional[pathlib.Path]:
102
+ """
103
+ Walk up from *start_dir* (default: cwd) looking for .ariarc or .ariarc.json.
104
+ Stops at $HOME or filesystem root.
105
+ """
106
+ home = pathlib.Path.home()
107
+ cwd = pathlib.Path(start_dir or os.getcwd()).resolve()
108
+ names = [".ariarc", ".ariarc.json", ".ariarc.jsonc"]
109
+
110
+ current = cwd
111
+ while True:
112
+ for name in names:
113
+ candidate = current / name
114
+ if candidate.exists() and candidate.is_file():
115
+ return candidate
116
+ if current == home or current.parent == current:
117
+ break
118
+ current = current.parent
119
+ return None
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # Loader
124
+ # ---------------------------------------------------------------------------
125
+
126
+ class AriaRC:
127
+ """
128
+ Parsed project configuration from .ariarc.
129
+
130
+ Usage::
131
+
132
+ rc = AriaRC.load() # searches cwd upward
133
+ rc = AriaRC.load("/path") # explicit start dir
134
+
135
+ rc.project # "Arthera Quant Engine"
136
+ rc.system_prompt # extra text injected into system prompt
137
+ rc.get_context_text() # concatenated content of context_files
138
+ rc.is_tool_allowed("run_command")
139
+ """
140
+
141
+ def __init__(self, data: Dict[str, Any], source_path: Optional[pathlib.Path] = None):
142
+ cfg = {**ARIARC_DEFAULTS, **data}
143
+ self.source_path: Optional[pathlib.Path] = source_path
144
+ self.project: Optional[str] = cfg.get("project")
145
+ self.description: Optional[str] = cfg.get("description")
146
+ self.system_prompt: str = cfg.get("system_prompt", "")
147
+ self.context_files: List[str] = list(cfg.get("context_files", []))
148
+ self.tools_whitelist: List[str] = list(cfg.get("tools_whitelist", []))
149
+ self.tools_blacklist: List[str] = list(cfg.get("tools_blacklist", []))
150
+ self.default_symbols: List[str] = list(cfg.get("default_symbols", []))
151
+ self.market: str = cfg.get("market", "global")
152
+ self.ashare: Dict[str, Any] = cfg.get("ashare", {})
153
+ self.commands: Dict[str, str] = cfg.get("commands", {})
154
+ self.auto_context: List[str] = list(cfg.get("auto_context", []))
155
+ self.write_deny_patterns: List[str] = list(cfg.get("write_deny_patterns", []))
156
+
157
+ # ── class methods ──────────────────────────────────────────────────────
158
+
159
+ @classmethod
160
+ def load(cls, start_dir: Optional[str] = None) -> "AriaRC":
161
+ path = find_ariarc(start_dir)
162
+ if path is None:
163
+ return cls({})
164
+ try:
165
+ text = path.read_text(encoding="utf-8")
166
+ data = _parse_jsonc(text)
167
+ if not isinstance(data, dict):
168
+ data = {}
169
+ return cls(data, source_path=path)
170
+ except Exception:
171
+ return cls({}, source_path=path)
172
+
173
+ @classmethod
174
+ def empty(cls) -> "AriaRC":
175
+ return cls({})
176
+
177
+ # ── helpers ────────────────────────────────────────────────────────────
178
+
179
+ @property
180
+ def found(self) -> bool:
181
+ return self.source_path is not None
182
+
183
+ def is_tool_allowed(self, tool_name: str) -> bool:
184
+ """Return True if tool is allowed under whitelist/blacklist rules."""
185
+ if self.tools_blacklist and tool_name in self.tools_blacklist:
186
+ return False
187
+ if self.tools_whitelist:
188
+ return tool_name in self.tools_whitelist
189
+ return True
190
+
191
+ def get_context_text(self, base_dir: Optional[str] = None) -> str:
192
+ """
193
+ Read all context_files and return their concatenated content.
194
+ Paths are relative to the .ariarc location (or cwd if not found).
195
+ """
196
+ base = pathlib.Path(base_dir or (self.source_path.parent if self.source_path else os.getcwd()))
197
+ parts: List[str] = []
198
+ for rel_path in self.context_files:
199
+ p = base / rel_path
200
+ if p.exists() and p.is_file():
201
+ try:
202
+ content = p.read_text(encoding="utf-8", errors="replace")
203
+ parts.append(f"## {rel_path}\n\n```\n{content[:4000]}\n```")
204
+ except Exception:
205
+ pass
206
+ return "\n\n".join(parts)
207
+
208
+ def get_auto_context_text(self, base_dir: Optional[str] = None) -> str:
209
+ """Same as get_context_text but for auto_context files."""
210
+ base = pathlib.Path(base_dir or (self.source_path.parent if self.source_path else os.getcwd()))
211
+ parts: List[str] = []
212
+ for rel_path in self.auto_context:
213
+ p = base / rel_path
214
+ if p.exists() and p.is_file():
215
+ try:
216
+ content = p.read_text(encoding="utf-8", errors="replace")
217
+ # Show only first 80 lines to avoid blowing up context
218
+ lines = content.splitlines()[:80]
219
+ snippet = "\n".join(lines)
220
+ parts.append(f"## {rel_path} (first {len(lines)} lines)\n\n```python\n{snippet}\n```")
221
+ except Exception:
222
+ pass
223
+ return "\n\n".join(parts)
224
+
225
+ def build_system_prompt_block(self, base_dir: Optional[str] = None) -> str:
226
+ """
227
+ Build the full system-prompt injection block from this ariarc.
228
+ Returns empty string if nothing to inject.
229
+ """
230
+ lines: List[str] = []
231
+
232
+ if self.project:
233
+ lines.append(f"**Project:** {self.project}")
234
+ if self.description:
235
+ lines.append(f"**Description:** {self.description}")
236
+ if self.market != "global":
237
+ mkt = "A股 (Chinese equities)" if self.market == "cn" else self.market.upper()
238
+ lines.append(f"**Primary market:** {mkt}")
239
+ if self.default_symbols:
240
+ lines.append(f"**Default symbols:** {', '.join(self.default_symbols)}")
241
+ if self.ashare:
242
+ a = self.ashare
243
+ if a.get("risk_level"):
244
+ lines.append(f"**Risk preference:** {a['risk_level']}")
245
+
246
+ header = "\n".join(lines)
247
+ extra = self.system_prompt.strip()
248
+ ctx = self.get_context_text(base_dir)
249
+ auto = self.get_auto_context_text(base_dir)
250
+
251
+ parts: List[str] = []
252
+ if header:
253
+ parts.append(header)
254
+ if extra:
255
+ parts.append(extra)
256
+ if ctx:
257
+ parts.append("### Project Context Files\n\n" + ctx)
258
+ if auto:
259
+ parts.append("### Auto-loaded Code Context\n\n" + auto)
260
+
261
+ if not parts:
262
+ return ""
263
+
264
+ return "\n\n---\n\n# Project Context (.ariarc)\n\n" + "\n\n".join(parts)
265
+
266
+ def resolve_command(self, command: str, symbol: str = "", **kwargs) -> Optional[str]:
267
+ """
268
+ Resolve a custom command defined in .ariarc ``commands`` dict.
269
+
270
+ Example:
271
+ .ariarc: { "commands": { "/morning-cn": "生成A股早盘简报 {symbols}" } }
272
+ rc.resolve_command("/morning-cn")
273
+ → "生成A股早盘简报 sh600519, sh601318"
274
+ """
275
+ template = self.commands.get(command)
276
+ if template is None:
277
+ return None
278
+ syms = symbol or ", ".join(self.default_symbols)
279
+ return template.format(
280
+ symbol=symbol,
281
+ symbols=syms,
282
+ default_symbols=syms,
283
+ market=self.market,
284
+ **kwargs,
285
+ )
286
+
287
+ def is_write_denied(self, file_path: str) -> bool:
288
+ """Return True if writing to file_path is blocked by write_deny_patterns."""
289
+ import fnmatch
290
+ p = file_path.replace("\\", "/")
291
+ for pattern in self.write_deny_patterns:
292
+ if fnmatch.fnmatch(p, pattern) or fnmatch.fnmatch(pathlib.Path(p).name, pattern):
293
+ return True
294
+ return False
295
+
296
+ def to_dict(self) -> Dict[str, Any]:
297
+ return {
298
+ "source_path": str(self.source_path) if self.source_path else None,
299
+ "project": self.project,
300
+ "description": self.description,
301
+ "market": self.market,
302
+ "default_symbols": self.default_symbols,
303
+ "tools_whitelist": self.tools_whitelist,
304
+ "tools_blacklist": self.tools_blacklist,
305
+ "commands": list(self.commands.keys()),
306
+ "context_files": self.context_files,
307
+ "auto_context": self.auto_context,
308
+ "write_deny_patterns": self.write_deny_patterns,
309
+ }
310
+
311
+
312
+ # ---------------------------------------------------------------------------
313
+ # Module-level singleton
314
+ # ---------------------------------------------------------------------------
315
+
316
+ _current_rc: Optional[AriaRC] = None
317
+
318
+
319
+ def get_ariarc(reload: bool = False) -> AriaRC:
320
+ """Return the current session's AriaRC (lazy-loaded from cwd)."""
321
+ global _current_rc
322
+ if _current_rc is None or reload:
323
+ _current_rc = AriaRC.load()
324
+ return _current_rc
325
+
326
+
327
+ def reload_ariarc() -> AriaRC:
328
+ return get_ariarc(reload=True)