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
aliyun_data_client.py
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
"""
|
|
2
|
+
aliyun_data_client.py — Arthera Alibaba Cloud data services client.
|
|
3
|
+
|
|
4
|
+
Architecture
|
|
5
|
+
------------
|
|
6
|
+
The Arthera quant backend runs two HTTP services on Alibaba Cloud:
|
|
7
|
+
|
|
8
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ cloud_api_server.py (FastAPI, default port 8000) │
|
|
10
|
+
│ /api/v1/quant/factors/{symbol} → calculate_factors │
|
|
11
|
+
│ /api/v1/quant/ai/signal → AI trading signal │
|
|
12
|
+
│ /api/v1/quant/backtest → run backtest │
|
|
13
|
+
│ /api/v1/quant/predict → ML predictions │
|
|
14
|
+
│ /api/v1/ai/market-insights → market insights (AI) │
|
|
15
|
+
│ /api/v1/ai/portfolio-analysis → portfolio analysis │
|
|
16
|
+
│ /api/v1/ai/investment-decision → investment decision │
|
|
17
|
+
│ /api/v1/market/quote/{symbol} → real-time quote │
|
|
18
|
+
│ /api/v1/market/search → stock search │
|
|
19
|
+
│ /api/v1/market/popular → popular stocks list │
|
|
20
|
+
│ /health → health check │
|
|
21
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
22
|
+
|
|
23
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
24
|
+
│ akshare_data_server.py (aiohttp, default port 8002) │
|
|
25
|
+
│ /stock/{symbol} → A股历史 OHLCV │
|
|
26
|
+
│ /stocks → multi-symbol batch │
|
|
27
|
+
│ /cn/indices → 上证/深成/沪深300 indices │
|
|
28
|
+
│ /hk/realtime → Hong Kong real-time │
|
|
29
|
+
│ /health → health check │
|
|
30
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
31
|
+
|
|
32
|
+
Configuration (env vars or ~/.arthera/config.json)
|
|
33
|
+
---------------------------------------------------
|
|
34
|
+
ARTHERA_CLOUD_URL base URL of cloud_api_server (default: http://127.0.0.1:8000)
|
|
35
|
+
ARTHERA_DATA_URL base URL of akshare_data_server (default: http://127.0.0.1:8002)
|
|
36
|
+
ARTHERA_API_TOKEN JWT Bearer token for authenticated endpoints
|
|
37
|
+
|
|
38
|
+
Circuit-breaker fallback
|
|
39
|
+
-------------------------
|
|
40
|
+
If cloud services are unreachable, all methods silently fall back to local
|
|
41
|
+
yfinance / akshare calls so the CLI never hard-errors on connectivity.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import asyncio
|
|
47
|
+
import json
|
|
48
|
+
import logging
|
|
49
|
+
import os
|
|
50
|
+
import time
|
|
51
|
+
from dataclasses import dataclass, field
|
|
52
|
+
from typing import Any, Dict, List, Optional
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class CloudHealthSummary:
|
|
59
|
+
schema: str
|
|
60
|
+
total: int
|
|
61
|
+
ok: int
|
|
62
|
+
warn: int
|
|
63
|
+
err: int
|
|
64
|
+
breaker_open: int
|
|
65
|
+
token_set: bool
|
|
66
|
+
status: str
|
|
67
|
+
detail: str
|
|
68
|
+
suggestion: str
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
71
|
+
return {
|
|
72
|
+
"schema": self.schema,
|
|
73
|
+
"total": self.total,
|
|
74
|
+
"ok": self.ok,
|
|
75
|
+
"warn": self.warn,
|
|
76
|
+
"err": self.err,
|
|
77
|
+
"breaker_open": self.breaker_open,
|
|
78
|
+
"token_set": self.token_set,
|
|
79
|
+
"status": self.status,
|
|
80
|
+
"detail": self.detail,
|
|
81
|
+
"suggestion": self.suggestion,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def summarize_cloud_health(
|
|
86
|
+
cloud_health: Optional[Dict[str, Any]] = None,
|
|
87
|
+
data_health: Optional[Dict[str, Any]] = None,
|
|
88
|
+
status: Optional[Dict[str, Any]] = None,
|
|
89
|
+
) -> CloudHealthSummary:
|
|
90
|
+
cloud_health = dict(cloud_health or {})
|
|
91
|
+
data_health = dict(data_health or {})
|
|
92
|
+
status = dict(status or {})
|
|
93
|
+
|
|
94
|
+
checks = [
|
|
95
|
+
("cloud_api_server", cloud_health),
|
|
96
|
+
("akshare_data_server", data_health),
|
|
97
|
+
]
|
|
98
|
+
ok = warn = err = breaker_open = 0
|
|
99
|
+
detail_bits: list[str] = []
|
|
100
|
+
for name, health in checks:
|
|
101
|
+
svc_status = str(health.get("status") or "unknown")
|
|
102
|
+
breaker_value = status.get("cloud_cb") if name == "cloud_api_server" else status.get("data_cb")
|
|
103
|
+
breaker = str(breaker_value or "closed")
|
|
104
|
+
breaker_is_open = breaker == "open"
|
|
105
|
+
if breaker_is_open:
|
|
106
|
+
breaker_open += 1
|
|
107
|
+
if svc_status in ("healthy", "ok", "ready", "online"):
|
|
108
|
+
ok += 1
|
|
109
|
+
elif svc_status == "unreachable" or breaker_is_open:
|
|
110
|
+
err += 1
|
|
111
|
+
else:
|
|
112
|
+
warn += 1
|
|
113
|
+
detail_bits.append(f"{name}={svc_status}")
|
|
114
|
+
|
|
115
|
+
if err:
|
|
116
|
+
overall = "err"
|
|
117
|
+
elif warn or breaker_open:
|
|
118
|
+
overall = "warn"
|
|
119
|
+
else:
|
|
120
|
+
overall = "ok"
|
|
121
|
+
|
|
122
|
+
token_set = bool(status.get("has_token"))
|
|
123
|
+
if overall == "ok":
|
|
124
|
+
suggestion = "All cloud services healthy."
|
|
125
|
+
elif token_set:
|
|
126
|
+
suggestion = "Retry /cloud health or /doctor --network after cooldown."
|
|
127
|
+
else:
|
|
128
|
+
suggestion = "Check /cloud set, /cloud data, and /cloud token."
|
|
129
|
+
|
|
130
|
+
return CloudHealthSummary(
|
|
131
|
+
schema="aria.cloud_health_summary.v1",
|
|
132
|
+
total=2,
|
|
133
|
+
ok=ok,
|
|
134
|
+
warn=warn,
|
|
135
|
+
err=err,
|
|
136
|
+
breaker_open=breaker_open,
|
|
137
|
+
token_set=token_set,
|
|
138
|
+
status=overall,
|
|
139
|
+
detail=", ".join(detail_bits),
|
|
140
|
+
suggestion=suggestion,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# Configuration helpers
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def _cfg_path() -> str:
|
|
148
|
+
return os.path.join(os.path.expanduser("~"), ".arthera", "config.json")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _load_cloud_config() -> Dict[str, str]:
|
|
152
|
+
"""Load cloud config from ~/.arthera/config.json, override with env vars."""
|
|
153
|
+
cfg: Dict[str, str] = {
|
|
154
|
+
"cloud_url": "http://127.0.0.1:8000",
|
|
155
|
+
"data_url": "http://127.0.0.1:8002",
|
|
156
|
+
"api_token": "",
|
|
157
|
+
}
|
|
158
|
+
path = _cfg_path()
|
|
159
|
+
if os.path.exists(path):
|
|
160
|
+
try:
|
|
161
|
+
with open(path) as f:
|
|
162
|
+
saved = json.load(f)
|
|
163
|
+
cfg["cloud_url"] = saved.get("cloud_url", cfg["cloud_url"])
|
|
164
|
+
cfg["data_url"] = saved.get("data_url", cfg["data_url"])
|
|
165
|
+
cfg["api_token"] = saved.get("api_token", cfg["api_token"])
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
# Env-var overrides (highest priority)
|
|
169
|
+
if os.environ.get("ARTHERA_CLOUD_URL"):
|
|
170
|
+
cfg["cloud_url"] = os.environ["ARTHERA_CLOUD_URL"].rstrip("/")
|
|
171
|
+
if os.environ.get("ARTHERA_DATA_URL"):
|
|
172
|
+
cfg["data_url"] = os.environ["ARTHERA_DATA_URL"].rstrip("/")
|
|
173
|
+
if os.environ.get("ARTHERA_API_TOKEN"):
|
|
174
|
+
cfg["api_token"] = os.environ["ARTHERA_API_TOKEN"]
|
|
175
|
+
return cfg
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def save_cloud_config(cloud_url: str = "", data_url: str = "",
|
|
179
|
+
api_token: str = "") -> None:
|
|
180
|
+
"""Persist cloud configuration to ~/.arthera/config.json."""
|
|
181
|
+
import pathlib
|
|
182
|
+
p = pathlib.Path(_cfg_path())
|
|
183
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
existing: dict = {}
|
|
185
|
+
if p.exists():
|
|
186
|
+
try:
|
|
187
|
+
existing = json.loads(p.read_text())
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
if cloud_url:
|
|
191
|
+
existing["cloud_url"] = cloud_url.rstrip("/")
|
|
192
|
+
if data_url:
|
|
193
|
+
existing["data_url"] = data_url.rstrip("/")
|
|
194
|
+
if api_token:
|
|
195
|
+
existing["api_token"] = api_token
|
|
196
|
+
p.write_text(json.dumps(existing, indent=2, ensure_ascii=False))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Circuit breaker (lightweight, no external deps)
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
@dataclass
|
|
204
|
+
class _CircuitBreaker:
|
|
205
|
+
"""Simple state-machine circuit breaker."""
|
|
206
|
+
failure_threshold: int = 4
|
|
207
|
+
recovery_timeout: float = 120.0 # seconds before trying again
|
|
208
|
+
_failures: int = field(default=0, repr=False)
|
|
209
|
+
_last_failure: float = field(default=0.0, repr=False)
|
|
210
|
+
_open: bool = field(default=False, repr=False)
|
|
211
|
+
|
|
212
|
+
def allow(self) -> bool:
|
|
213
|
+
if not self._open:
|
|
214
|
+
return True
|
|
215
|
+
if time.monotonic() - self._last_failure > self.recovery_timeout:
|
|
216
|
+
self._open = False # enter half-open
|
|
217
|
+
return True
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
def record_success(self) -> None:
|
|
221
|
+
self._failures = 0
|
|
222
|
+
self._open = False
|
|
223
|
+
|
|
224
|
+
def record_failure(self) -> None:
|
|
225
|
+
self._failures += 1
|
|
226
|
+
self._last_failure = time.monotonic()
|
|
227
|
+
if self._failures >= self.failure_threshold:
|
|
228
|
+
self._open = True
|
|
229
|
+
logger.debug("AliyunDataClient: circuit breaker OPEN for this endpoint")
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def is_open(self) -> bool:
|
|
233
|
+
return self._open
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# Main client
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
class AliyunDataClient:
|
|
241
|
+
"""
|
|
242
|
+
Async HTTP client for Arthera's Alibaba Cloud quant services.
|
|
243
|
+
|
|
244
|
+
Usage (inside an async context)::
|
|
245
|
+
|
|
246
|
+
client = AliyunDataClient()
|
|
247
|
+
result = await client.get_quote("600519") # A股
|
|
248
|
+
result = await client.get_quote("AAPL") # US
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
_instance: Optional["AliyunDataClient"] = None
|
|
252
|
+
|
|
253
|
+
def __init__(self):
|
|
254
|
+
cfg = _load_cloud_config()
|
|
255
|
+
self.cloud_url = cfg["cloud_url"] # cloud_api_server
|
|
256
|
+
self.data_url = cfg["data_url"] # akshare_data_server
|
|
257
|
+
self.api_token = cfg["api_token"]
|
|
258
|
+
|
|
259
|
+
self._cb_cloud = _CircuitBreaker()
|
|
260
|
+
self._cb_data = _CircuitBreaker()
|
|
261
|
+
|
|
262
|
+
# Cached aiohttp session — created lazily
|
|
263
|
+
self._session: Any = None
|
|
264
|
+
|
|
265
|
+
# ── singleton ──────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
@classmethod
|
|
268
|
+
def get(cls) -> "AliyunDataClient":
|
|
269
|
+
if cls._instance is None:
|
|
270
|
+
cls._instance = cls()
|
|
271
|
+
return cls._instance
|
|
272
|
+
|
|
273
|
+
@classmethod
|
|
274
|
+
def reset(cls) -> None:
|
|
275
|
+
"""Force re-read config (useful after /cloud config)."""
|
|
276
|
+
cls._instance = None
|
|
277
|
+
|
|
278
|
+
# ── HTTP helpers ───────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
async def _get_session(self):
|
|
281
|
+
try:
|
|
282
|
+
import aiohttp
|
|
283
|
+
except ImportError:
|
|
284
|
+
return None
|
|
285
|
+
if self._session is None or self._session.closed:
|
|
286
|
+
timeout = aiohttp.ClientTimeout(total=15, connect=5)
|
|
287
|
+
self._session = aiohttp.ClientSession(timeout=timeout)
|
|
288
|
+
return self._session
|
|
289
|
+
|
|
290
|
+
def _headers(self, auth: bool = False) -> Dict[str, str]:
|
|
291
|
+
h = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
292
|
+
if auth and self.api_token:
|
|
293
|
+
h["Authorization"] = f"Bearer {self.api_token}"
|
|
294
|
+
return h
|
|
295
|
+
|
|
296
|
+
async def _get(self, base: str, path: str,
|
|
297
|
+
params: Optional[Dict] = None,
|
|
298
|
+
auth: bool = False,
|
|
299
|
+
cb: Optional[_CircuitBreaker] = None) -> Optional[Dict]:
|
|
300
|
+
if cb and not cb.allow():
|
|
301
|
+
logger.debug("Circuit breaker open — skipping %s%s", base, path)
|
|
302
|
+
return None
|
|
303
|
+
session = await self._get_session()
|
|
304
|
+
if session is None:
|
|
305
|
+
return None
|
|
306
|
+
url = f"{base}{path}"
|
|
307
|
+
try:
|
|
308
|
+
async with session.get(url, params=params, headers=self._headers(auth)) as r:
|
|
309
|
+
if r.status == 200:
|
|
310
|
+
data = await r.json(content_type=None)
|
|
311
|
+
if cb:
|
|
312
|
+
cb.record_success()
|
|
313
|
+
return data
|
|
314
|
+
else:
|
|
315
|
+
logger.debug("GET %s → HTTP %d", url, r.status)
|
|
316
|
+
if cb:
|
|
317
|
+
cb.record_failure()
|
|
318
|
+
return None
|
|
319
|
+
except Exception as exc:
|
|
320
|
+
logger.debug("GET %s failed: %s", url, exc)
|
|
321
|
+
if cb:
|
|
322
|
+
cb.record_failure()
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
async def _post(self, base: str, path: str,
|
|
326
|
+
body: Dict,
|
|
327
|
+
auth: bool = True,
|
|
328
|
+
cb: Optional[_CircuitBreaker] = None) -> Optional[Dict]:
|
|
329
|
+
if cb and not cb.allow():
|
|
330
|
+
logger.debug("Circuit breaker open — skipping %s%s", base, path)
|
|
331
|
+
return None
|
|
332
|
+
session = await self._get_session()
|
|
333
|
+
if session is None:
|
|
334
|
+
return None
|
|
335
|
+
url = f"{base}{path}"
|
|
336
|
+
try:
|
|
337
|
+
async with session.post(url, json=body, headers=self._headers(auth)) as r:
|
|
338
|
+
if r.status == 200:
|
|
339
|
+
data = await r.json(content_type=None)
|
|
340
|
+
if cb:
|
|
341
|
+
cb.record_success()
|
|
342
|
+
return data
|
|
343
|
+
else:
|
|
344
|
+
text = await r.text()
|
|
345
|
+
logger.debug("POST %s → HTTP %d: %s", url, r.status, text[:200])
|
|
346
|
+
if cb:
|
|
347
|
+
cb.record_failure()
|
|
348
|
+
return None
|
|
349
|
+
except Exception as exc:
|
|
350
|
+
logger.debug("POST %s failed: %s", url, exc)
|
|
351
|
+
if cb:
|
|
352
|
+
cb.record_failure()
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
# ── Public API ─────────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
async def health_cloud(self) -> Dict[str, Any]:
|
|
358
|
+
"""Check cloud_api_server health."""
|
|
359
|
+
data = await self._get(self.cloud_url, "/health", cb=self._cb_cloud)
|
|
360
|
+
return data or {"status": "unreachable", "cloud_url": self.cloud_url}
|
|
361
|
+
|
|
362
|
+
async def health_data(self) -> Dict[str, Any]:
|
|
363
|
+
"""Check akshare_data_server health."""
|
|
364
|
+
data = await self._get(self.data_url, "/health", cb=self._cb_data)
|
|
365
|
+
return data or {"status": "unreachable", "data_url": self.data_url}
|
|
366
|
+
|
|
367
|
+
# ── Market data ────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
async def get_quote(self, symbol: str) -> Optional[Dict[str, Any]]:
|
|
370
|
+
"""
|
|
371
|
+
Fetch real-time quote from cloud_api_server.
|
|
372
|
+
|
|
373
|
+
Returns: { symbol, name, price, change, change_percent, volume,
|
|
374
|
+
high, low, open, prev_close, market, timestamp }
|
|
375
|
+
"""
|
|
376
|
+
return await self._get(
|
|
377
|
+
self.cloud_url,
|
|
378
|
+
f"/api/v1/market/quote/{symbol}",
|
|
379
|
+
cb=self._cb_cloud,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
async def get_stock_history(self, symbol: str,
|
|
383
|
+
start: str = "", end: str = "",
|
|
384
|
+
period: str = "daily") -> Optional[Dict[str, Any]]:
|
|
385
|
+
"""
|
|
386
|
+
Fetch OHLCV history from akshare_data_server.
|
|
387
|
+
|
|
388
|
+
Returns: { symbol, data: [{date, open, high, low, close, volume},...] }
|
|
389
|
+
"""
|
|
390
|
+
params: Dict[str, str] = {}
|
|
391
|
+
if start:
|
|
392
|
+
params["start_date"] = start
|
|
393
|
+
if end:
|
|
394
|
+
params["end_date"] = end
|
|
395
|
+
if period:
|
|
396
|
+
params["period"] = period
|
|
397
|
+
# Normalise to bare 6-digit code that akshare server expects
|
|
398
|
+
sym = symbol.lower()
|
|
399
|
+
for prefix in ("sh", "sz", ".ss", ".sz"):
|
|
400
|
+
sym = sym.replace(prefix, "")
|
|
401
|
+
return await self._get(
|
|
402
|
+
self.data_url,
|
|
403
|
+
f"/stock/{sym}",
|
|
404
|
+
params=params or None,
|
|
405
|
+
cb=self._cb_data,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
async def get_multiple_stocks(self, symbols: List[str],
|
|
409
|
+
start: str = "", end: str = "") -> Optional[Dict[str, Any]]:
|
|
410
|
+
"""Batch-fetch history for multiple symbols."""
|
|
411
|
+
syms = ",".join(
|
|
412
|
+
s.lower().replace("sh", "").replace("sz", "").replace(".ss", "").replace(".sz", "")
|
|
413
|
+
for s in symbols
|
|
414
|
+
)
|
|
415
|
+
params: Dict[str, str] = {"symbols": syms}
|
|
416
|
+
if start:
|
|
417
|
+
params["start_date"] = start
|
|
418
|
+
if end:
|
|
419
|
+
params["end_date"] = end
|
|
420
|
+
return await self._get(self.data_url, "/stocks", params=params, cb=self._cb_data)
|
|
421
|
+
|
|
422
|
+
async def get_cn_indices(self) -> Optional[Dict[str, Any]]:
|
|
423
|
+
"""Fetch 上证/深成/沪深300/创业板 index quotes."""
|
|
424
|
+
return await self._get(self.data_url, "/cn/indices", cb=self._cb_data)
|
|
425
|
+
|
|
426
|
+
async def get_popular_stocks(self, limit: int = 20) -> Optional[List[Dict]]:
|
|
427
|
+
"""沪深300 热门成分股列表。"""
|
|
428
|
+
data = await self._get(
|
|
429
|
+
self.cloud_url,
|
|
430
|
+
"/api/v1/market/popular",
|
|
431
|
+
params={"limit": str(limit)},
|
|
432
|
+
cb=self._cb_cloud,
|
|
433
|
+
)
|
|
434
|
+
if data and "stocks" in data:
|
|
435
|
+
return data["stocks"]
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
async def search_stocks(self, q: str, limit: int = 10) -> Optional[List[Dict]]:
|
|
439
|
+
"""搜索股票(按代码或名称)。"""
|
|
440
|
+
data = await self._get(
|
|
441
|
+
self.cloud_url,
|
|
442
|
+
"/api/v1/market/search",
|
|
443
|
+
params={"q": q, "limit": str(limit)},
|
|
444
|
+
cb=self._cb_cloud,
|
|
445
|
+
)
|
|
446
|
+
if data:
|
|
447
|
+
return data.get("results", [])
|
|
448
|
+
return None
|
|
449
|
+
|
|
450
|
+
# ── Factor / signal analysis ───────────────────────────────────────────
|
|
451
|
+
|
|
452
|
+
async def get_factors(self, symbol: str) -> Optional[Dict[str, Any]]:
|
|
453
|
+
"""
|
|
454
|
+
Call /api/v1/quant/factors/{symbol} — returns enhanced factor snapshot.
|
|
455
|
+
Falls back to local if cloud unavailable.
|
|
456
|
+
"""
|
|
457
|
+
return await self._get(
|
|
458
|
+
self.cloud_url,
|
|
459
|
+
f"/api/v1/quant/factors/{symbol}",
|
|
460
|
+
auth=bool(self.api_token),
|
|
461
|
+
cb=self._cb_cloud,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
async def get_ai_signal(self, symbol: str,
|
|
465
|
+
market: str = "CN") -> Optional[Dict[str, Any]]:
|
|
466
|
+
"""
|
|
467
|
+
POST /api/v1/quant/ai/signal — DeepSeek-powered signal generation.
|
|
468
|
+
|
|
469
|
+
Returns: { symbol, action, confidence, reasoning, stop_loss, take_profit }
|
|
470
|
+
"""
|
|
471
|
+
return await self._post(
|
|
472
|
+
self.cloud_url,
|
|
473
|
+
"/api/v1/quant/ai/signal",
|
|
474
|
+
body={"symbol": symbol, "market": market},
|
|
475
|
+
auth=bool(self.api_token),
|
|
476
|
+
cb=self._cb_cloud,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
async def get_predictions(self, symbols: List[str],
|
|
480
|
+
prediction_days: int = 5,
|
|
481
|
+
market: str = "CN") -> Optional[Dict[str, Any]]:
|
|
482
|
+
"""
|
|
483
|
+
POST /api/v1/quant/predict — ML model predictions.
|
|
484
|
+
|
|
485
|
+
Returns: { predictions: [{symbol, predicted_return, confidence, factors},...] }
|
|
486
|
+
"""
|
|
487
|
+
return await self._post(
|
|
488
|
+
self.cloud_url,
|
|
489
|
+
"/api/v1/quant/predict",
|
|
490
|
+
body={
|
|
491
|
+
"symbols": symbols,
|
|
492
|
+
"prediction_days": prediction_days,
|
|
493
|
+
"market": market,
|
|
494
|
+
},
|
|
495
|
+
auth=bool(self.api_token),
|
|
496
|
+
cb=self._cb_cloud,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
async def run_backtest(self, symbols: List[str],
|
|
500
|
+
strategy_config: Dict[str, Any],
|
|
501
|
+
start_date: str = "",
|
|
502
|
+
end_date: str = "",
|
|
503
|
+
market: str = "CN") -> Optional[Dict[str, Any]]:
|
|
504
|
+
"""
|
|
505
|
+
POST /api/v1/quant/backtest — run full ML-powered backtest.
|
|
506
|
+
|
|
507
|
+
Returns: { backtest_id, status, result: { performance, equity_curve, trades } }
|
|
508
|
+
"""
|
|
509
|
+
body: Dict[str, Any] = {
|
|
510
|
+
"symbols": symbols,
|
|
511
|
+
"strategy_config": strategy_config,
|
|
512
|
+
"market": market,
|
|
513
|
+
}
|
|
514
|
+
if start_date:
|
|
515
|
+
body["start_date"] = start_date
|
|
516
|
+
if end_date:
|
|
517
|
+
body["end_date"] = end_date
|
|
518
|
+
return await self._post(
|
|
519
|
+
self.cloud_url,
|
|
520
|
+
"/api/v1/quant/backtest",
|
|
521
|
+
body=body,
|
|
522
|
+
auth=bool(self.api_token),
|
|
523
|
+
cb=self._cb_cloud,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# ── AI analysis ────────────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
async def get_market_insights(self, symbols: List[str],
|
|
529
|
+
market: str = "CN") -> Optional[Dict[str, Any]]:
|
|
530
|
+
"""
|
|
531
|
+
POST /api/v1/ai/market-insights — AI narrative market analysis.
|
|
532
|
+
|
|
533
|
+
Returns: { insights, sentiment, key_risks, opportunities }
|
|
534
|
+
"""
|
|
535
|
+
return await self._post(
|
|
536
|
+
self.cloud_url,
|
|
537
|
+
"/api/v1/ai/market-insights",
|
|
538
|
+
body={"symbols": symbols, "market": market},
|
|
539
|
+
auth=bool(self.api_token),
|
|
540
|
+
cb=self._cb_cloud,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
async def get_portfolio_analysis(self, portfolio: List[Dict[str, Any]],
|
|
544
|
+
market: str = "CN") -> Optional[Dict[str, Any]]:
|
|
545
|
+
"""
|
|
546
|
+
POST /api/v1/ai/portfolio-analysis.
|
|
547
|
+
|
|
548
|
+
portfolio: [{ symbol, weight }]
|
|
549
|
+
Returns: { risk_metrics, diversification_score, recommendations }
|
|
550
|
+
"""
|
|
551
|
+
return await self._post(
|
|
552
|
+
self.cloud_url,
|
|
553
|
+
"/api/v1/ai/portfolio-analysis",
|
|
554
|
+
body={"portfolio": portfolio, "market": market},
|
|
555
|
+
auth=bool(self.api_token),
|
|
556
|
+
cb=self._cb_cloud,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
async def get_investment_decision(self, symbol: str,
|
|
560
|
+
context: str = "",
|
|
561
|
+
market: str = "CN") -> Optional[Dict[str, Any]]:
|
|
562
|
+
"""
|
|
563
|
+
POST /api/v1/ai/investment-decision — full AI investment analysis.
|
|
564
|
+
"""
|
|
565
|
+
return await self._post(
|
|
566
|
+
self.cloud_url,
|
|
567
|
+
"/api/v1/ai/investment-decision",
|
|
568
|
+
body={"symbol": symbol, "context": context, "market": market},
|
|
569
|
+
auth=bool(self.api_token),
|
|
570
|
+
cb=self._cb_cloud,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# ── Utility ────────────────────────────────────────────────────────────
|
|
574
|
+
|
|
575
|
+
async def close(self) -> None:
|
|
576
|
+
"""Close the underlying aiohttp session."""
|
|
577
|
+
if self._session and not self._session.closed:
|
|
578
|
+
await self._session.close()
|
|
579
|
+
self._session = None
|
|
580
|
+
|
|
581
|
+
def status(self) -> Dict[str, Any]:
|
|
582
|
+
"""Return current circuit breaker status for /cloud status."""
|
|
583
|
+
payload = {
|
|
584
|
+
"cloud_url": self.cloud_url,
|
|
585
|
+
"data_url": self.data_url,
|
|
586
|
+
"has_token": bool(self.api_token),
|
|
587
|
+
"cloud_cb": "open" if self._cb_cloud.is_open else "closed",
|
|
588
|
+
"data_cb": "open" if self._cb_data.is_open else "closed",
|
|
589
|
+
}
|
|
590
|
+
try:
|
|
591
|
+
payload["health_summary"] = summarize_cloud_health(status=payload).to_dict()
|
|
592
|
+
except Exception:
|
|
593
|
+
pass
|
|
594
|
+
return payload
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
# ---------------------------------------------------------------------------
|
|
598
|
+
# Sync helper for use in non-async contexts (e.g. local_finance_tools.py)
|
|
599
|
+
# ---------------------------------------------------------------------------
|
|
600
|
+
|
|
601
|
+
def run_async(coro) -> Any:
|
|
602
|
+
"""
|
|
603
|
+
Run an async coroutine from sync code (e.g. inside tool handlers).
|
|
604
|
+
|
|
605
|
+
Uses the running event loop's run_in_executor pattern so we never
|
|
606
|
+
accidentally create nested event loops.
|
|
607
|
+
"""
|
|
608
|
+
if not hasattr(coro, "__await__"):
|
|
609
|
+
close = getattr(coro, "close", None)
|
|
610
|
+
if callable(close):
|
|
611
|
+
close()
|
|
612
|
+
return None
|
|
613
|
+
|
|
614
|
+
async def _run_and_close():
|
|
615
|
+
try:
|
|
616
|
+
return await coro
|
|
617
|
+
finally:
|
|
618
|
+
try:
|
|
619
|
+
await AliyunDataClient.get().close()
|
|
620
|
+
except Exception:
|
|
621
|
+
pass
|
|
622
|
+
|
|
623
|
+
try:
|
|
624
|
+
try:
|
|
625
|
+
asyncio.get_running_loop()
|
|
626
|
+
except RuntimeError:
|
|
627
|
+
return asyncio.run(_run_and_close())
|
|
628
|
+
|
|
629
|
+
# We're already inside an async context — run the coroutine on a fresh
|
|
630
|
+
# event loop in a worker thread to avoid nested-loop errors.
|
|
631
|
+
import concurrent.futures
|
|
632
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
633
|
+
future = pool.submit(asyncio.run, _run_and_close())
|
|
634
|
+
return future.result(timeout=15)
|
|
635
|
+
except Exception as exc:
|
|
636
|
+
logger.debug("run_async failed: %s", exc)
|
|
637
|
+
close = getattr(coro, "close", None)
|
|
638
|
+
if callable(close):
|
|
639
|
+
close()
|
|
640
|
+
return None
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def cloud_get_quote_sync(symbol: str) -> Optional[Dict[str, Any]]:
|
|
644
|
+
"""Blocking wrapper for AliyunDataClient.get_quote — safe to call from sync code."""
|
|
645
|
+
return run_async(AliyunDataClient.get().get_quote(symbol))
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def cloud_get_history_sync(symbol: str, start: str = "", end: str = "") -> Optional[Dict[str, Any]]:
|
|
649
|
+
"""Blocking wrapper for AliyunDataClient.get_stock_history."""
|
|
650
|
+
return run_async(AliyunDataClient.get().get_stock_history(symbol, start=start, end=end))
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def cloud_get_factors_sync(symbol: str) -> Optional[Dict[str, Any]]:
|
|
654
|
+
"""Blocking wrapper for AliyunDataClient.get_factors."""
|
|
655
|
+
return run_async(AliyunDataClient.get().get_factors(symbol))
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def cloud_get_ai_signal_sync(symbol: str, market: str = "CN") -> Optional[Dict[str, Any]]:
|
|
659
|
+
"""Blocking wrapper for AliyunDataClient.get_ai_signal."""
|
|
660
|
+
return run_async(AliyunDataClient.get().get_ai_signal(symbol, market=market))
|
apps/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Aria Code Apps
|
|
2
|
+
|
|
3
|
+
`apps/` contains product entrypoints. Keep business logic in `packages/` and
|
|
4
|
+
make each app a thin adapter over service contracts.
|
|
5
|
+
|
|
6
|
+
- `cli/`: interactive terminal client.
|
|
7
|
+
- `daemon/`: local-first background gateway.
|
|
8
|
+
- `channels/`: external channel adapters such as relay, Feishu, Telegram, and webhooks.
|
|
9
|
+
|
|
10
|
+
Current implementation still lives mostly in root modules such as `aria_cli.py`.
|
|
11
|
+
New work should add service code behind `packages/` first, then migrate app
|
|
12
|
+
entrypoints here in small steps.
|
apps/__init__.py
ADDED
apps/channels/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Channel Apps
|
|
2
|
+
|
|
3
|
+
Target role: adapters for non-terminal entrypoints.
|
|
4
|
+
|
|
5
|
+
Examples:
|
|
6
|
+
|
|
7
|
+
- relay server/client;
|
|
8
|
+
- Feishu bot;
|
|
9
|
+
- Telegram bot;
|
|
10
|
+
- webhooks;
|
|
11
|
+
- future desktop or browser UI.
|
|
12
|
+
|
|
13
|
+
Each channel should translate inbound messages into gateway requests and render
|
|
14
|
+
gateway responses back to the channel. It should not bypass safety, runtime, or
|
|
15
|
+
artifact policies.
|