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.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- 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)
|