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
plan_utils.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
plan_utils.py — Plan parsing helpers for Aria Code CLI workflow commands.
|
|
3
|
+
|
|
4
|
+
Supports several natural input styles:
|
|
5
|
+
|
|
6
|
+
Numbered steps (most common):
|
|
7
|
+
1. Fetch AAPL quote
|
|
8
|
+
2. Generate 6-month chart
|
|
9
|
+
3. Output analysis report
|
|
10
|
+
|
|
11
|
+
Bullet list:
|
|
12
|
+
- Fetch quote
|
|
13
|
+
- Generate chart
|
|
14
|
+
- Output report
|
|
15
|
+
|
|
16
|
+
Inline arrow / semicolon chain:
|
|
17
|
+
fetch quote -> generate chart -> output report
|
|
18
|
+
fetch quote; generate chart; output report
|
|
19
|
+
|
|
20
|
+
Mixed (numbered + description):
|
|
21
|
+
Step 1: Fetch AAPL quote
|
|
22
|
+
Step 2: Generate chart with SMA20
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from typing import List, Optional
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── Data model ────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class PlanStep:
|
|
36
|
+
"""A single executable step in a plan."""
|
|
37
|
+
index: int # 1-based position
|
|
38
|
+
description: str # human-readable description
|
|
39
|
+
name: Optional[str] = None # optional short name / label
|
|
40
|
+
deps: List[int] = field(default_factory=list) # dependency indices
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
dep_str = f" [deps: {','.join(str(d) for d in self.deps)}]" if self.deps else ""
|
|
44
|
+
return f"{self.index}. {self.description}{dep_str}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ── Patterns ──────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
# "1. text", "1) text", "Step 1: text", "Step 1 — text"
|
|
50
|
+
_RE_NUMBERED = re.compile(
|
|
51
|
+
r"^(?:step\s*)?(\d+)[.):\-–—]\s*(.+)$",
|
|
52
|
+
re.IGNORECASE,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# "- text", "• text", "* text", "· text"
|
|
56
|
+
_RE_BULLET = re.compile(r"^[-•*·]\s+(.+)$")
|
|
57
|
+
|
|
58
|
+
# Metadata tags like [name: Build] or [deps: 1,3]
|
|
59
|
+
_RE_META_NAME = re.compile(r"\[name:\s*([^\]]+)\]", re.IGNORECASE)
|
|
60
|
+
_RE_META_DEPS = re.compile(r"\[deps:\s*([^\]]+)\]", re.IGNORECASE)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _strip_meta(text: str) -> tuple[str, Optional[str], List[int]]:
|
|
64
|
+
"""Extract and remove [name:...] and [deps:...] metadata from text."""
|
|
65
|
+
name: Optional[str] = None
|
|
66
|
+
deps: List[int] = []
|
|
67
|
+
|
|
68
|
+
m = _RE_META_NAME.search(text)
|
|
69
|
+
if m:
|
|
70
|
+
name = m.group(1).strip()
|
|
71
|
+
text = text[:m.start()] + text[m.end():]
|
|
72
|
+
|
|
73
|
+
m = _RE_META_DEPS.search(text)
|
|
74
|
+
if m:
|
|
75
|
+
raw = m.group(1)
|
|
76
|
+
deps = [int(x.strip()) for x in re.split(r"[,;\s]+", raw) if x.strip().isdigit()]
|
|
77
|
+
text = text[:m.start()] + text[m.end():]
|
|
78
|
+
|
|
79
|
+
return text.strip(), name, deps
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── Public API ────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
def parse_plan_steps(raw: str) -> List[str]:
|
|
85
|
+
"""
|
|
86
|
+
Parse '/plan' argument string into a list of plain step description strings.
|
|
87
|
+
|
|
88
|
+
This is the backwards-compatible API used by aria_cli.py.
|
|
89
|
+
|
|
90
|
+
Returns a list of non-empty step strings, in order.
|
|
91
|
+
"""
|
|
92
|
+
return [s.description for s in parse_plan(raw)]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse_plan(raw: str) -> List[PlanStep]:
|
|
96
|
+
"""
|
|
97
|
+
Full parser — returns a list of PlanStep objects with index, description,
|
|
98
|
+
optional name, and dependency list.
|
|
99
|
+
|
|
100
|
+
Handles mixed input styles (numbered, bulleted, arrow/semicolon chained).
|
|
101
|
+
"""
|
|
102
|
+
if not raw or not raw.strip():
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
text = raw.strip()
|
|
106
|
+
|
|
107
|
+
# ── Strategy 1: multiline numbered or bulleted steps ─────────────────────
|
|
108
|
+
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
|
|
109
|
+
if len(lines) >= 2:
|
|
110
|
+
steps = _parse_lines(lines)
|
|
111
|
+
if steps:
|
|
112
|
+
return steps
|
|
113
|
+
|
|
114
|
+
# ── Strategy 2: inline arrow chain fetch quote -> chart -> report ───────
|
|
115
|
+
# Also handles semicolons within arrow-separated parts:
|
|
116
|
+
# "git status -> rg TODO . ; pytest -q" → 3 steps
|
|
117
|
+
if "->" in text:
|
|
118
|
+
arrow_parts = [p.strip() for p in text.replace("→", "->").split("->") if p.strip()]
|
|
119
|
+
parts: List[str] = []
|
|
120
|
+
for ap in arrow_parts:
|
|
121
|
+
if ";" in ap:
|
|
122
|
+
parts.extend(p.strip() for p in ap.split(";") if p.strip())
|
|
123
|
+
else:
|
|
124
|
+
parts.append(ap)
|
|
125
|
+
return [PlanStep(index=i + 1, description=p) for i, p in enumerate(parts)]
|
|
126
|
+
|
|
127
|
+
# ── Strategy 3: semicolon-separated steps ────────────────────────────────
|
|
128
|
+
if ";" in text:
|
|
129
|
+
parts = [p.strip() for p in text.split(";") if p.strip()]
|
|
130
|
+
return [PlanStep(index=i + 1, description=p) for i, p in enumerate(parts)]
|
|
131
|
+
|
|
132
|
+
# ── Strategy 4: single step ───────────────────────────────────────────────
|
|
133
|
+
desc, name, deps = _strip_meta(text)
|
|
134
|
+
return [PlanStep(index=1, description=desc, name=name, deps=deps)] if desc else []
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _parse_lines(lines: List[str]) -> List[PlanStep]:
|
|
138
|
+
"""Try to extract ordered steps from a list of text lines."""
|
|
139
|
+
steps: List[PlanStep] = []
|
|
140
|
+
expected_idx = 1
|
|
141
|
+
|
|
142
|
+
for line in lines:
|
|
143
|
+
# Try numbered pattern
|
|
144
|
+
m = _RE_NUMBERED.match(line)
|
|
145
|
+
if m:
|
|
146
|
+
idx = int(m.group(1))
|
|
147
|
+
desc_raw = m.group(2).strip()
|
|
148
|
+
desc, name, deps = _strip_meta(desc_raw)
|
|
149
|
+
if desc:
|
|
150
|
+
steps.append(PlanStep(index=idx, description=desc, name=name, deps=deps))
|
|
151
|
+
expected_idx = idx + 1
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
# Try bullet pattern
|
|
155
|
+
m = _RE_BULLET.match(line)
|
|
156
|
+
if m:
|
|
157
|
+
desc_raw = m.group(1).strip()
|
|
158
|
+
desc, name, deps = _strip_meta(desc_raw)
|
|
159
|
+
if desc:
|
|
160
|
+
steps.append(PlanStep(index=expected_idx, description=desc, name=name, deps=deps))
|
|
161
|
+
expected_idx += 1
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
return steps
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── Formatting helpers ────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
def format_plan(steps: List[PlanStep], title: str = "Plan") -> str:
|
|
170
|
+
"""Format a plan for terminal display."""
|
|
171
|
+
if not steps:
|
|
172
|
+
return f"[{title}] (empty)"
|
|
173
|
+
lines = [f"── {title} ({len(steps)} steps) ──"]
|
|
174
|
+
for s in steps:
|
|
175
|
+
dep_str = f" (after {', '.join(str(d) for d in s.deps)})" if s.deps else ""
|
|
176
|
+
label = f" [{s.name}]" if s.name else ""
|
|
177
|
+
lines.append(f" {s.index}.{label} {s.description}{dep_str}")
|
|
178
|
+
return "\n".join(lines)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def steps_to_prompt(steps: List[PlanStep], context: str = "") -> str:
|
|
182
|
+
"""
|
|
183
|
+
Convert a list of PlanStep objects to a structured prompt string
|
|
184
|
+
that the AI can execute step-by-step.
|
|
185
|
+
"""
|
|
186
|
+
intro = f"{context}\n\n" if context else ""
|
|
187
|
+
numbered = "\n".join(f"{s.index}. {s.description}" for s in steps)
|
|
188
|
+
return (
|
|
189
|
+
f"{intro}"
|
|
190
|
+
f"Execute the following plan steps in order:\n\n"
|
|
191
|
+
f"{numbered}\n\n"
|
|
192
|
+
"Complete each step fully before moving to the next. "
|
|
193
|
+
"After all steps, provide a brief summary of what was accomplished."
|
|
194
|
+
)
|
plugin_loader.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
plugin_loader.py — Auto-discovery of custom tool plugins for Aria Code.
|
|
3
|
+
|
|
4
|
+
Scans the current directory and its parents for ``aria_tools.py`` files and
|
|
5
|
+
loads any tools they export. This lets project-specific tools appear
|
|
6
|
+
automatically in the model's tool loop without modifying aria_cli.py.
|
|
7
|
+
|
|
8
|
+
Plugin contract (aria_tools.py)::
|
|
9
|
+
|
|
10
|
+
# Minimal example
|
|
11
|
+
def get_my_tools():
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
"name": "my_custom_tool",
|
|
15
|
+
"description": "Does something useful for this project",
|
|
16
|
+
"parameters": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"input": {"type": "string", "description": "Input data"},
|
|
20
|
+
},
|
|
21
|
+
"required": ["input"],
|
|
22
|
+
},
|
|
23
|
+
"handler": lambda params: {"result": params["input"].upper()},
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# Extended example with finance tools
|
|
28
|
+
import akshare as ak
|
|
29
|
+
|
|
30
|
+
def get_my_tools():
|
|
31
|
+
def fetch_my_positions(params):
|
|
32
|
+
# read from broker API / local file
|
|
33
|
+
return {"positions": [...]}
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
"name": "get_my_positions",
|
|
38
|
+
"description": "Return current portfolio positions from local CSV",
|
|
39
|
+
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
40
|
+
"handler": fetch_my_positions,
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
Discovery order
|
|
45
|
+
---------------
|
|
46
|
+
1. ``./aria_tools.py`` (current working directory)
|
|
47
|
+
2. ``../aria_tools.py`` (one level up)
|
|
48
|
+
3. …up to $HOME
|
|
49
|
+
|
|
50
|
+
The first file found is used. Set ``ARIA_TOOLS_PATH`` env var to override.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import importlib.util
|
|
56
|
+
import logging
|
|
57
|
+
import os
|
|
58
|
+
import pathlib
|
|
59
|
+
import sys
|
|
60
|
+
import traceback
|
|
61
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
62
|
+
|
|
63
|
+
logger = logging.getLogger(__name__)
|
|
64
|
+
|
|
65
|
+
# Env override
|
|
66
|
+
ARIA_TOOLS_ENV = "ARIA_TOOLS_PATH"
|
|
67
|
+
PLUGIN_FILENAME = "aria_tools.py"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Discovery
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def find_plugin_file(start_dir: Optional[str] = None) -> Optional[pathlib.Path]:
|
|
75
|
+
"""
|
|
76
|
+
Walk upward from *start_dir* looking for aria_tools.py.
|
|
77
|
+
Returns the first match, or None.
|
|
78
|
+
"""
|
|
79
|
+
# Env var override takes priority
|
|
80
|
+
env_path = os.getenv(ARIA_TOOLS_ENV)
|
|
81
|
+
if env_path:
|
|
82
|
+
p = pathlib.Path(env_path).expanduser().resolve()
|
|
83
|
+
if p.exists():
|
|
84
|
+
return p
|
|
85
|
+
|
|
86
|
+
home = pathlib.Path.home()
|
|
87
|
+
current = pathlib.Path(start_dir or os.getcwd()).resolve()
|
|
88
|
+
|
|
89
|
+
while True:
|
|
90
|
+
candidate = current / PLUGIN_FILENAME
|
|
91
|
+
if candidate.exists() and candidate.is_file():
|
|
92
|
+
return candidate
|
|
93
|
+
if current == home or current.parent == current:
|
|
94
|
+
break
|
|
95
|
+
current = current.parent
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Loader
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def load_plugin(plugin_path: pathlib.Path) -> List[Dict[str, Any]]:
|
|
104
|
+
"""
|
|
105
|
+
Import *plugin_path* as a module and call its ``get_my_tools()`` function.
|
|
106
|
+
|
|
107
|
+
Returns list of tool dicts with keys:
|
|
108
|
+
name, description, parameters (JSON Schema), handler (callable)
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
spec = importlib.util.spec_from_file_location("aria_tools_plugin", plugin_path)
|
|
112
|
+
module = importlib.util.module_from_spec(spec)
|
|
113
|
+
# Add plugin directory to sys.path so it can import local modules
|
|
114
|
+
plugin_dir = str(plugin_path.parent)
|
|
115
|
+
if plugin_dir not in sys.path:
|
|
116
|
+
sys.path.insert(0, plugin_dir)
|
|
117
|
+
spec.loader.exec_module(module)
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
logger.warning("Plugin load error (%s): %s", plugin_path, exc)
|
|
120
|
+
logger.debug(traceback.format_exc())
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
# Try standard function name first, then fallback names
|
|
124
|
+
for fn_name in ("get_my_tools", "get_tools", "register_tools", "tools"):
|
|
125
|
+
fn = getattr(module, fn_name, None)
|
|
126
|
+
if callable(fn):
|
|
127
|
+
try:
|
|
128
|
+
tools = fn()
|
|
129
|
+
if isinstance(tools, list):
|
|
130
|
+
logger.info("Plugin %s: loaded %d tools via %s()", plugin_path.name, len(tools), fn_name)
|
|
131
|
+
return _validate_tools(tools, plugin_path)
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
logger.warning("Plugin %s: %s() raised: %s", plugin_path.name, fn_name, exc)
|
|
134
|
+
continue
|
|
135
|
+
# Also support module-level TOOLS list
|
|
136
|
+
if fn_name == "tools" and isinstance(fn, list):
|
|
137
|
+
return _validate_tools(fn, plugin_path)
|
|
138
|
+
|
|
139
|
+
logger.warning("Plugin %s: no get_my_tools() / get_tools() function found", plugin_path.name)
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _validate_tools(raw: List[Any], plugin_path: pathlib.Path) -> List[Dict[str, Any]]:
|
|
144
|
+
"""Validate and normalise plugin tool definitions."""
|
|
145
|
+
valid = []
|
|
146
|
+
for item in raw:
|
|
147
|
+
if not isinstance(item, dict):
|
|
148
|
+
continue
|
|
149
|
+
name = item.get("name", "").strip()
|
|
150
|
+
desc = item.get("description", "")
|
|
151
|
+
handler = item.get("handler") or item.get("fn") or item.get("function")
|
|
152
|
+
params = item.get("parameters") or item.get("params") or {
|
|
153
|
+
"type": "object", "properties": {}, "required": []
|
|
154
|
+
}
|
|
155
|
+
if not name:
|
|
156
|
+
logger.debug("Plugin tool missing 'name', skipping")
|
|
157
|
+
continue
|
|
158
|
+
if not callable(handler):
|
|
159
|
+
logger.debug("Plugin tool %r has no callable handler, skipping", name)
|
|
160
|
+
continue
|
|
161
|
+
valid.append({
|
|
162
|
+
"name": name,
|
|
163
|
+
"description": desc,
|
|
164
|
+
"parameters": params,
|
|
165
|
+
"handler": handler,
|
|
166
|
+
"source": str(plugin_path),
|
|
167
|
+
})
|
|
168
|
+
return valid
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# Registration
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def register_plugin_tools(
|
|
176
|
+
tool_registry: Dict,
|
|
177
|
+
schema_registry: List,
|
|
178
|
+
start_dir: Optional[str] = None,
|
|
179
|
+
overwrite: bool = False,
|
|
180
|
+
) -> Tuple[int, Optional[pathlib.Path]]:
|
|
181
|
+
"""
|
|
182
|
+
Auto-discover and register plugin tools.
|
|
183
|
+
|
|
184
|
+
Returns (count_registered, plugin_path_or_None).
|
|
185
|
+
"""
|
|
186
|
+
plugin_path = find_plugin_file(start_dir)
|
|
187
|
+
if plugin_path is None:
|
|
188
|
+
return 0, None
|
|
189
|
+
|
|
190
|
+
tools = load_plugin(plugin_path)
|
|
191
|
+
if not tools:
|
|
192
|
+
return 0, plugin_path
|
|
193
|
+
|
|
194
|
+
added = 0
|
|
195
|
+
existing_names = set(tool_registry.keys())
|
|
196
|
+
existing_schema_names = {
|
|
197
|
+
s.get("function", {}).get("name") for s in schema_registry
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for tool in tools:
|
|
201
|
+
name = tool["name"]
|
|
202
|
+
handler = tool["handler"]
|
|
203
|
+
desc = tool["description"]
|
|
204
|
+
params = tool["parameters"]
|
|
205
|
+
|
|
206
|
+
if name in existing_names and not overwrite:
|
|
207
|
+
logger.debug("Plugin tool %r skipped (already registered)", name)
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
# Wrap handler with error guard
|
|
211
|
+
def _safe_handler(p: dict, h: Callable = handler) -> dict:
|
|
212
|
+
try:
|
|
213
|
+
result = h(p)
|
|
214
|
+
if not isinstance(result, dict):
|
|
215
|
+
result = {"result": result}
|
|
216
|
+
return result
|
|
217
|
+
except Exception as exc:
|
|
218
|
+
return {"success": False, "error": str(exc), "source": "plugin"}
|
|
219
|
+
|
|
220
|
+
tool_registry[name] = (_safe_handler, desc)
|
|
221
|
+
added += 1
|
|
222
|
+
|
|
223
|
+
if name not in existing_schema_names:
|
|
224
|
+
schema_registry.append({
|
|
225
|
+
"type": "function",
|
|
226
|
+
"function": {
|
|
227
|
+
"name": name,
|
|
228
|
+
"description": desc,
|
|
229
|
+
"parameters": params,
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
if added:
|
|
234
|
+
logger.info("Plugin %s: registered %d tools", plugin_path.name, added)
|
|
235
|
+
|
|
236
|
+
return added, plugin_path
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
# Hot-reload support
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
class PluginWatcher:
|
|
244
|
+
"""
|
|
245
|
+
Watch the plugin file for changes and reload tools automatically.
|
|
246
|
+
Uses simple mtime polling (no watchdog dependency needed).
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def __init__(
|
|
250
|
+
self,
|
|
251
|
+
tool_registry: Dict,
|
|
252
|
+
schema_registry: List,
|
|
253
|
+
start_dir: Optional[str] = None,
|
|
254
|
+
poll_interval: float = 3.0,
|
|
255
|
+
):
|
|
256
|
+
self._tool_registry = tool_registry
|
|
257
|
+
self._schema_registry = schema_registry
|
|
258
|
+
self._start_dir = start_dir
|
|
259
|
+
self._poll_interval = poll_interval
|
|
260
|
+
self._plugin_path: Optional[pathlib.Path] = None
|
|
261
|
+
self._last_mtime: float = 0.0
|
|
262
|
+
self._task: Optional[Any] = None # asyncio.Task
|
|
263
|
+
self._plugin_tool_names: List[str] = []
|
|
264
|
+
|
|
265
|
+
async def start(self):
|
|
266
|
+
"""Start the background polling task."""
|
|
267
|
+
import asyncio
|
|
268
|
+
# Initial load
|
|
269
|
+
n, path = register_plugin_tools(
|
|
270
|
+
self._tool_registry, self._schema_registry, self._start_dir
|
|
271
|
+
)
|
|
272
|
+
if path:
|
|
273
|
+
self._plugin_path = path
|
|
274
|
+
self._last_mtime = path.stat().st_mtime
|
|
275
|
+
self._plugin_tool_names = [
|
|
276
|
+
t["name"] for t in load_plugin(path)
|
|
277
|
+
]
|
|
278
|
+
if n:
|
|
279
|
+
logger.info("PluginWatcher: initial load %d tools from %s", n, path.name)
|
|
280
|
+
|
|
281
|
+
self._task = asyncio.create_task(self._watch_loop())
|
|
282
|
+
|
|
283
|
+
async def _watch_loop(self):
|
|
284
|
+
import asyncio
|
|
285
|
+
while True:
|
|
286
|
+
await asyncio.sleep(self._poll_interval)
|
|
287
|
+
if self._plugin_path is None:
|
|
288
|
+
# Try to find newly created plugin
|
|
289
|
+
found = find_plugin_file(self._start_dir)
|
|
290
|
+
if found:
|
|
291
|
+
self._plugin_path = found
|
|
292
|
+
self._last_mtime = 0.0
|
|
293
|
+
if self._plugin_path and self._plugin_path.exists():
|
|
294
|
+
mtime = self._plugin_path.stat().st_mtime
|
|
295
|
+
if mtime != self._last_mtime:
|
|
296
|
+
self._last_mtime = mtime
|
|
297
|
+
await self._reload()
|
|
298
|
+
|
|
299
|
+
async def _reload(self):
|
|
300
|
+
if not self._plugin_path:
|
|
301
|
+
return
|
|
302
|
+
# Remove previously registered plugin tools
|
|
303
|
+
for name in self._plugin_tool_names:
|
|
304
|
+
self._tool_registry.pop(name, None)
|
|
305
|
+
# Remove from schema registry
|
|
306
|
+
self._schema_registry[:] = [
|
|
307
|
+
s for s in self._schema_registry
|
|
308
|
+
if s.get("function", {}).get("name") not in self._plugin_tool_names
|
|
309
|
+
]
|
|
310
|
+
# Re-register
|
|
311
|
+
n, _ = register_plugin_tools(
|
|
312
|
+
self._tool_registry, self._schema_registry,
|
|
313
|
+
str(self._plugin_path.parent), overwrite=True
|
|
314
|
+
)
|
|
315
|
+
self._plugin_tool_names = [
|
|
316
|
+
t["name"] for t in load_plugin(self._plugin_path)
|
|
317
|
+
]
|
|
318
|
+
logger.info("PluginWatcher: hot-reloaded %d tools from %s", n, self._plugin_path.name)
|
|
319
|
+
|
|
320
|
+
async def stop(self):
|
|
321
|
+
if self._task:
|
|
322
|
+
self._task.cancel()
|
|
323
|
+
try:
|
|
324
|
+
import asyncio
|
|
325
|
+
await asyncio.wait_for(self._task, timeout=1.0)
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
self._task = None
|