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
ui/render/team.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Rendering helpers for /team output — table, verdict banner, adaptive widths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
# ── Signal → Rich style ──────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
SIGNAL_COLORS: dict[str, str] = {
|
|
15
|
+
"BUY": "green",
|
|
16
|
+
"STRONG_BUY": "bold green",
|
|
17
|
+
"SELL": "red",
|
|
18
|
+
"STRONG_SELL": "bold red",
|
|
19
|
+
"HOLD": "yellow",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# ── Verdict banner ────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
VERDICT_STYLE: dict[str, tuple[str, str]] = {
|
|
25
|
+
"HEALTHY": ("green", "✅"),
|
|
26
|
+
"NEEDS_ATTENTION": ("yellow", "⚠️ "),
|
|
27
|
+
"HIGH_RISK": ("red", "🔴"),
|
|
28
|
+
"STRONG_BUY": ("bold green", "▲▲"),
|
|
29
|
+
"BUY": ("green", "▲ "),
|
|
30
|
+
"HOLD": ("dim", "─ "),
|
|
31
|
+
"SELL": ("red", "▼ "),
|
|
32
|
+
"STRONG_SELL": ("bold red", "▼▼"),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── Streaming agent tree (Claude Code-style nested ⏺ rendering) ────────────────
|
|
37
|
+
|
|
38
|
+
AGENT_LABELS: dict[str, str] = {
|
|
39
|
+
"fundamental": "基本面", "technical": "技术面", "macro": "宏观",
|
|
40
|
+
"risk": "风险", "news": "新闻", "catalyst": "催化剂",
|
|
41
|
+
"sector": "行业", "sentiment": "情绪", "debate": "分歧调解",
|
|
42
|
+
"valuation": "估值", "quant": "量化", "synthesis": "综合",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def agent_label(name: str) -> str:
|
|
47
|
+
return AGENT_LABELS.get((name or "").lower(), name or "?")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def render_agent_tree_root(console, sym: str, n_agents: int, lang: str = "zh") -> None:
|
|
51
|
+
"""Print the root of the agent tree: ⏺ 多代理分析 SYM N 个分析师并行"""
|
|
52
|
+
head = "多代理分析" if lang == "zh" else "Multi-agent analysis"
|
|
53
|
+
sub = (f"{n_agents} 个分析师并行" if lang == "zh"
|
|
54
|
+
else f"{n_agents} analysts in parallel")
|
|
55
|
+
console.print(f"\n [#C08050]⏺[/#C08050] [bold]{head} {sym}[/bold] [dim]{sub}[/dim]")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def render_agent_node(console, name: str, signal: str | None,
|
|
59
|
+
key_point: str | None, success: bool = True,
|
|
60
|
+
error: str | None = None) -> None:
|
|
61
|
+
"""Print one completed-agent leaf: ⎿ ⏺ 基本面 BUY ROE 24%·PE 32 偏高"""
|
|
62
|
+
label = agent_label(name)
|
|
63
|
+
if not success or error:
|
|
64
|
+
_err_label = {
|
|
65
|
+
"timeout": "超时", "rate_limited": "数据源限流",
|
|
66
|
+
"no_data": "无数据", "": "失败",
|
|
67
|
+
}.get((error or "").lower(), error or "失败")
|
|
68
|
+
console.print(f" [dim]⎿ ⏺ {label} {_err_label}[/dim]")
|
|
69
|
+
return
|
|
70
|
+
sig = (signal or "").upper()
|
|
71
|
+
color = SIGNAL_COLORS.get(sig, "dim")
|
|
72
|
+
sig_disp = f"[{color}]{sig}[/{color}] " if sig else ""
|
|
73
|
+
kp = (key_point or "").strip().replace("\n", " ")
|
|
74
|
+
if len(kp) > 52:
|
|
75
|
+
kp = kp[:52] + "…"
|
|
76
|
+
console.print(
|
|
77
|
+
f" [dim]⎿[/dim] [#C08050]⏺[/#C08050] [bold]{label}[/bold] "
|
|
78
|
+
f"{sig_disp}[dim]{kp}[/dim]"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def render_agent_synthesis_leaf(console, signal: str | None,
|
|
83
|
+
confidence: float | None, elapsed: float | None,
|
|
84
|
+
lang: str = "zh") -> None:
|
|
85
|
+
"""Print the synthesis leaf: ⎿ 综合: ▲ BUY (置信 68%) 耗时 4.2s"""
|
|
86
|
+
sig = (signal or "").upper()
|
|
87
|
+
color, icon = VERDICT_STYLE.get(sig, ("dim", "●"))
|
|
88
|
+
conf = (f" [dim]置信 {confidence:.0%}[/dim]" if confidence else "")
|
|
89
|
+
el = (f" [dim]耗时 {elapsed:.1f}s[/dim]" if elapsed else "")
|
|
90
|
+
lab = "综合" if lang == "zh" else "Synthesis"
|
|
91
|
+
console.print(
|
|
92
|
+
f" [dim]⎿[/dim] [bold]{lab}[/bold] [{color}]{icon} {sig}[/{color}]{conf}{el}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def build_verdict_body(
|
|
97
|
+
verdict: str,
|
|
98
|
+
subtitle: str = "",
|
|
99
|
+
confidence: float | None = None,
|
|
100
|
+
) -> str:
|
|
101
|
+
"""Return a Rich markup string suitable for a Panel body."""
|
|
102
|
+
verdict_upper = verdict.upper()
|
|
103
|
+
style, icon = VERDICT_STYLE.get(verdict_upper, ("dim", "●"))
|
|
104
|
+
conf_str = f" [dim]置信度 {confidence:.0%}[/dim]" if confidence else ""
|
|
105
|
+
body = f"[{style}]{icon} {verdict_upper}[/{style}]{conf_str}"
|
|
106
|
+
if subtitle:
|
|
107
|
+
body += f"\n[dim]{subtitle}[/dim]"
|
|
108
|
+
return body
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def render_verdict_banner(
|
|
112
|
+
verdict: str,
|
|
113
|
+
subtitle: str = "",
|
|
114
|
+
confidence: float | None = None,
|
|
115
|
+
*,
|
|
116
|
+
console: "Console | None" = None,
|
|
117
|
+
has_rich: bool = True,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Print a visually prominent verdict/signal result.
|
|
120
|
+
|
|
121
|
+
Falls back to a plain print when Rich is unavailable or *console* is None.
|
|
122
|
+
"""
|
|
123
|
+
if not verdict:
|
|
124
|
+
return
|
|
125
|
+
verdict_upper = verdict.upper()
|
|
126
|
+
|
|
127
|
+
if not has_rich or console is None:
|
|
128
|
+
conf_str = f" ({confidence:.0%})" if confidence else ""
|
|
129
|
+
sub_str = f" {subtitle}" if subtitle else ""
|
|
130
|
+
print(f"\n {verdict_upper}{conf_str}{sub_str}\n")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
from rich import box as _rbox
|
|
135
|
+
from rich.panel import Panel
|
|
136
|
+
body = build_verdict_body(verdict_upper, subtitle, confidence)
|
|
137
|
+
console.print(Panel(body, box=_rbox.SIMPLE, padding=(0, 2)))
|
|
138
|
+
except Exception:
|
|
139
|
+
conf_str = f" ({confidence:.0%})" if confidence else ""
|
|
140
|
+
console.print(f"\n {verdict_upper}{conf_str}\n")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ── Team table ────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class TeamTableRow:
|
|
147
|
+
agent: str
|
|
148
|
+
signal: str
|
|
149
|
+
confidence: str
|
|
150
|
+
key_point: str
|
|
151
|
+
success: bool
|
|
152
|
+
signal_color: str = "dim"
|
|
153
|
+
is_debate: bool = False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Minimum usable terminal width before we drop the key_point column entirely.
|
|
157
|
+
_KEY_POINT_MIN_TERMINAL = 72
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def calc_column_widths(terminal_width: int) -> tuple[int, int, int, int]:
|
|
161
|
+
"""Return ``(agent_w, signal_w, conf_w, key_w)`` adaptive to *terminal_width*.
|
|
162
|
+
|
|
163
|
+
Column budget (chars used by content only, excluding Rich padding/borders):
|
|
164
|
+
|
|
165
|
+
+-----------+---------+-----------+--------------------+
|
|
166
|
+
| Agent | Signal | Conf | Key point |
|
|
167
|
+
+-----------+---------+-----------+--------------------+
|
|
168
|
+
Fixed overhead per row: ~7 chars (borders + padding × 4 cols).
|
|
169
|
+
|
|
170
|
+
Width tiers
|
|
171
|
+
-----------
|
|
172
|
+
≥ 120 cols full layout 14 | 10 | 7 | 38
|
|
173
|
+
≥ 100 cols medium 12 | 9 | 6 | 30
|
|
174
|
+
≥ 80 cols compact 10 | 8 | 6 | 20
|
|
175
|
+
≥ 72 cols tight 9 | 8 | 6 | 12
|
|
176
|
+
< 72 cols no key col 9 | 8 | 6 | 0 (column omitted)
|
|
177
|
+
"""
|
|
178
|
+
if terminal_width >= 120:
|
|
179
|
+
return (14, 10, 7, 38)
|
|
180
|
+
if terminal_width >= 100:
|
|
181
|
+
return (12, 9, 6, 30)
|
|
182
|
+
if terminal_width >= 80:
|
|
183
|
+
return (10, 8, 6, 20)
|
|
184
|
+
if terminal_width >= _KEY_POINT_MIN_TERMINAL:
|
|
185
|
+
return ( 9, 8, 6, 12)
|
|
186
|
+
return (9, 8, 6, 0)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def truncate_cell(value: Any, width: int = 36) -> str:
|
|
190
|
+
text = str(value or "").strip().replace("\n", " ")
|
|
191
|
+
if width <= 0:
|
|
192
|
+
return ""
|
|
193
|
+
if width <= 1:
|
|
194
|
+
return text[:width]
|
|
195
|
+
if len(text) <= width:
|
|
196
|
+
return text
|
|
197
|
+
return text[: width - 1].rstrip() + "…"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def team_mode_label(results: list[Any], use_full: bool = False) -> str:
|
|
201
|
+
non_debate = [r for r in results if getattr(r, "agent", None) != "debate"]
|
|
202
|
+
return f"{len(non_debate)}-agent {'完整分析' if use_full else '分析'}"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
_AGENT_DISPLAY = {
|
|
206
|
+
"fundamental": "fundmntl",
|
|
207
|
+
"synthesis": "synthsis",
|
|
208
|
+
"catalyst": "catalyst",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def build_team_table_rows(results: list[Any], key_width: int = 36) -> list[TeamTableRow]:
|
|
213
|
+
rows: list[TeamTableRow] = []
|
|
214
|
+
debate_rows: list[TeamTableRow] = []
|
|
215
|
+
|
|
216
|
+
for result in results:
|
|
217
|
+
agent = str(getattr(result, "agent", "") or "")
|
|
218
|
+
agent = _AGENT_DISPLAY.get(agent, agent)
|
|
219
|
+
success = bool(getattr(result, "success", False))
|
|
220
|
+
raw_signal = str(getattr(result, "signal", "") or "")
|
|
221
|
+
signal = raw_signal or "N/A"
|
|
222
|
+
conf_val = float(getattr(result, "confidence", 0.0) or 0.0)
|
|
223
|
+
confidence = f"{conf_val:.0%}" if success else "-"
|
|
224
|
+
color = SIGNAL_COLORS.get(raw_signal.upper(), "dim")
|
|
225
|
+
|
|
226
|
+
if agent == "debate":
|
|
227
|
+
debate_rows.append(TeamTableRow(
|
|
228
|
+
agent="debate",
|
|
229
|
+
signal=raw_signal or "ADJ",
|
|
230
|
+
confidence=confidence,
|
|
231
|
+
key_point="信号分歧调解",
|
|
232
|
+
success=success,
|
|
233
|
+
signal_color="orange1",
|
|
234
|
+
is_debate=True,
|
|
235
|
+
))
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
if success:
|
|
239
|
+
kpts = getattr(result, "key_points", []) or []
|
|
240
|
+
key_point = truncate_cell(kpts[0] if kpts else "", key_width)
|
|
241
|
+
else:
|
|
242
|
+
_raw_err = getattr(result, "error", None) or "failed"
|
|
243
|
+
_err_display = {
|
|
244
|
+
"stale_or_conflicting_price": "数据冲突 — 价格与行情不符",
|
|
245
|
+
"timeout": "分析超时",
|
|
246
|
+
"no_data": "数据不可用",
|
|
247
|
+
"agent_failed": "Agent 执行失败",
|
|
248
|
+
}.get(_raw_err, _raw_err)
|
|
249
|
+
key_point = truncate_cell(_err_display, key_width)
|
|
250
|
+
|
|
251
|
+
rows.append(TeamTableRow(
|
|
252
|
+
agent=agent,
|
|
253
|
+
signal=signal,
|
|
254
|
+
confidence=confidence,
|
|
255
|
+
key_point=key_point,
|
|
256
|
+
success=success,
|
|
257
|
+
signal_color=color,
|
|
258
|
+
))
|
|
259
|
+
|
|
260
|
+
return rows + debate_rows
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def render_team_rows_plain(rows: list[TeamTableRow]) -> list[str]:
|
|
264
|
+
"""Plain-text fallback (no Rich)."""
|
|
265
|
+
lines: list[str] = []
|
|
266
|
+
for row in rows:
|
|
267
|
+
icon = "OK" if row.success else "WARN"
|
|
268
|
+
lines.append(
|
|
269
|
+
f" {icon} [{row.agent}] {row.signal} ({row.confidence}) {row.key_point}".rstrip()
|
|
270
|
+
)
|
|
271
|
+
return lines
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def render_team_table(
|
|
275
|
+
sym: str,
|
|
276
|
+
rows: list[TeamTableRow],
|
|
277
|
+
use_full: bool = False,
|
|
278
|
+
*,
|
|
279
|
+
console: "Console | None" = None,
|
|
280
|
+
terminal_width: int | None = None,
|
|
281
|
+
has_rich: bool = True,
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Print the /team results table, adapting column widths to *terminal_width*.
|
|
284
|
+
|
|
285
|
+
Falls back to :func:`render_team_rows_plain` when Rich is unavailable.
|
|
286
|
+
"""
|
|
287
|
+
if not has_rich or console is None:
|
|
288
|
+
for line in render_team_rows_plain(rows):
|
|
289
|
+
print(line)
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
from rich import box as _rbox
|
|
294
|
+
from rich.table import Table as _Table
|
|
295
|
+
except ImportError:
|
|
296
|
+
for line in render_team_rows_plain(rows):
|
|
297
|
+
console.print(line)
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
# Determine terminal width from console if not supplied
|
|
301
|
+
if terminal_width is None:
|
|
302
|
+
terminal_width = getattr(console, "width", None) or shutil.get_terminal_size().columns
|
|
303
|
+
|
|
304
|
+
agent_w, signal_w, conf_w, key_w = calc_column_widths(terminal_width)
|
|
305
|
+
mode_label = team_mode_label(rows, use_full) # rows already built; agent count approximate
|
|
306
|
+
|
|
307
|
+
tbl = _Table(
|
|
308
|
+
title=f"[bold]/team {sym}[/bold] · [dim]{mode_label}[/dim]",
|
|
309
|
+
box=_rbox.ROUNDED,
|
|
310
|
+
border_style="dim",
|
|
311
|
+
show_header=True,
|
|
312
|
+
header_style="bold dim",
|
|
313
|
+
padding=(0, 1),
|
|
314
|
+
)
|
|
315
|
+
tbl.add_column("Agent", width=agent_w)
|
|
316
|
+
tbl.add_column("信号", width=signal_w)
|
|
317
|
+
tbl.add_column("置信度", justify="right", width=conf_w)
|
|
318
|
+
if key_w > 0:
|
|
319
|
+
tbl.add_column("关键点", width=key_w)
|
|
320
|
+
|
|
321
|
+
for row in rows:
|
|
322
|
+
# Re-truncate key_point to the current adaptive width
|
|
323
|
+
kp = truncate_cell(row.key_point, key_w) if key_w > 0 else None
|
|
324
|
+
|
|
325
|
+
agent_cell = (
|
|
326
|
+
f"[orange1]{row.agent}[/orange1]"
|
|
327
|
+
if row.is_debate
|
|
328
|
+
else f"[dim]{row.agent}[/dim]"
|
|
329
|
+
)
|
|
330
|
+
key_cell = (
|
|
331
|
+
"[dim][orange1]信号分歧调解[/orange1][/dim]"
|
|
332
|
+
if row.is_debate
|
|
333
|
+
else f"[dim]{kp}[/dim]"
|
|
334
|
+
) if key_w > 0 else None
|
|
335
|
+
|
|
336
|
+
cells = [
|
|
337
|
+
agent_cell,
|
|
338
|
+
f"[{row.signal_color}]{row.signal}[/{row.signal_color}]",
|
|
339
|
+
f"[dim]{row.confidence}[/dim]" if row.success else "[dim]—[/dim]",
|
|
340
|
+
]
|
|
341
|
+
if key_cell is not None:
|
|
342
|
+
cells.append(key_cell)
|
|
343
|
+
tbl.add_row(*cells)
|
|
344
|
+
|
|
345
|
+
console.print()
|
|
346
|
+
console.print(tbl)
|
ui/robot.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Aria robot mascot — animated terminal character.
|
|
2
|
+
|
|
3
|
+
States
|
|
4
|
+
------
|
|
5
|
+
The mascot stays visually stable at startup. Runtime state is shown by the
|
|
6
|
+
compact status dot so the banner keeps the same low-noise feel as Claude Code.
|
|
7
|
+
|
|
8
|
+
Robot shape (7 rows, 13 columns, hand-placed). It is intentionally drawn as a
|
|
9
|
+
terminal icon, not a raster-image conversion: light shell, dark screen, one
|
|
10
|
+
white eye, one copper eye, a copper status strip, and four small legs.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from enum import Enum
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RobotState(Enum):
|
|
23
|
+
IDLE = "idle"
|
|
24
|
+
THINKING = "thinking"
|
|
25
|
+
STREAMING = "streaming"
|
|
26
|
+
ERROR = "error"
|
|
27
|
+
DONE = "done"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── Shared mutable state (written by aria_cli, read by input_box) ─────────────
|
|
31
|
+
_state = RobotState.IDLE
|
|
32
|
+
_state_lock = threading.Lock()
|
|
33
|
+
_done_at: float | None = None # timestamp when DONE state was set
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def set_robot_state(state: RobotState) -> None:
|
|
37
|
+
global _state, _done_at
|
|
38
|
+
with _state_lock:
|
|
39
|
+
_state = state
|
|
40
|
+
_done_at = time.monotonic() if state is RobotState.DONE else None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_robot_state() -> RobotState:
|
|
44
|
+
with _state_lock:
|
|
45
|
+
# Auto-revert DONE → IDLE after 1.5 s
|
|
46
|
+
if _state is RobotState.DONE and _done_at is not None:
|
|
47
|
+
if time.monotonic() - _done_at > 1.5:
|
|
48
|
+
return RobotState.IDLE
|
|
49
|
+
return _state
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Eye symbols per state. IDLE mirrors the mascot art: one light square eye and
|
|
53
|
+
# one copper dash eye.
|
|
54
|
+
_EYES = {
|
|
55
|
+
RobotState.IDLE: ("■", "▬"),
|
|
56
|
+
RobotState.THINKING: ("◐", "◑"),
|
|
57
|
+
RobotState.STREAMING: ("▸", "▸"),
|
|
58
|
+
RobotState.ERROR: ("×", "×"),
|
|
59
|
+
RobotState.DONE: ("✓", "✓"),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Accent colour per state
|
|
63
|
+
_COLOUR = {
|
|
64
|
+
RobotState.IDLE: "#C08050",
|
|
65
|
+
RobotState.THINKING: "#d29922",
|
|
66
|
+
RobotState.STREAMING: "#3fb950",
|
|
67
|
+
RobotState.ERROR: "#f85149",
|
|
68
|
+
RobotState.DONE: "#3fb950",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Status text per state (used by status bar dot)
|
|
72
|
+
_STATUS = {
|
|
73
|
+
RobotState.IDLE: "",
|
|
74
|
+
RobotState.THINKING: "thinking…",
|
|
75
|
+
RobotState.STREAMING: "generating…",
|
|
76
|
+
RobotState.ERROR: "error",
|
|
77
|
+
RobotState.DONE: "done",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# ── Theme-aware palette ───────────────────────────────────────────────────────
|
|
81
|
+
# Every region is a BACKGROUND fill — a space painted with an `on <colour>` style.
|
|
82
|
+
# The terminal paints a cell background across the whole line height (including the
|
|
83
|
+
# inter-row gap), so fills join into a solid shape with no horizontal striping.
|
|
84
|
+
# Two palettes (light/dark) are swapped from the OS/terminal theme so the robot
|
|
85
|
+
# inverts (white-on-dark ↔ dark-on-light) and never disappears into the background.
|
|
86
|
+
# The copper accent reads on both, so it is shared (just darkened a touch on light).
|
|
87
|
+
_PALETTES = {
|
|
88
|
+
"dark": {
|
|
89
|
+
"shelltop": "#e8e2d4", # thin top cap (▄): fg only → transparent above
|
|
90
|
+
"shell": "on #e8e2d4", # light shell body
|
|
91
|
+
"screen": "on #0d1117", # dark screen
|
|
92
|
+
"eye": "#f6f2ea on #0d1117", # light square eye (▀)
|
|
93
|
+
"dash": "#C08050 on #0d1117", # copper dash (▬)
|
|
94
|
+
"ear": "#C08050 on #9d9488", # copper dot on a gray ear nub (▪)
|
|
95
|
+
"strip": "#C08050 on #e8e2d4", # copper strip on the body bottom (▬)
|
|
96
|
+
"leg": "#8a8176", # gray legs (▀)
|
|
97
|
+
},
|
|
98
|
+
"light": {
|
|
99
|
+
"shelltop": "#E7E1D3", # warm cap, matching the light shell
|
|
100
|
+
"shell": "on #E7E1D3", # warm shell, distinct from white terminal bg
|
|
101
|
+
"screen": "on #0D1117", # same dark screen as dark mode
|
|
102
|
+
"eye": "#F6F2EA on #0D1117", # light eye on dark screen
|
|
103
|
+
"dash": "#9A6700 on #0D1117", # dark copper on dark screen
|
|
104
|
+
"ear": "#9A6700 on #6E7781", # copper dot on gray side nub
|
|
105
|
+
"strip": "#9A6700 on #E7E1D3", # copper strip on warm shell
|
|
106
|
+
"leg": "#6E7781", # medium gray legs
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_theme_cache: str | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _resolve_theme() -> str:
|
|
114
|
+
pref = os.environ.get("ARIA_THEME", "").strip().lower()
|
|
115
|
+
if pref in ("light", "dark"):
|
|
116
|
+
return pref
|
|
117
|
+
if sys.platform == "darwin":
|
|
118
|
+
try:
|
|
119
|
+
import subprocess
|
|
120
|
+
r = subprocess.run(
|
|
121
|
+
["defaults", "read", "-g", "AppleInterfaceStyle"],
|
|
122
|
+
capture_output=True, text=True, timeout=1,
|
|
123
|
+
)
|
|
124
|
+
# `defaults` prints "Dark" in dark mode and errors (non-zero) in light.
|
|
125
|
+
return "dark" if (r.returncode == 0 and "dark" in r.stdout.lower()) else "light"
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
# xterm-style COLORFGBG ("fg;bg"): bg 0-6/8 → dark, 7/9-15 → light.
|
|
129
|
+
cfb = os.environ.get("COLORFGBG", "")
|
|
130
|
+
if ";" in cfb:
|
|
131
|
+
try:
|
|
132
|
+
bg = int(cfb.split(";")[-1])
|
|
133
|
+
return "light" if (bg == 7 or bg >= 9) else "dark"
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
return "dark"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def detect_theme() -> str:
|
|
140
|
+
"""Return 'light' or 'dark' for the mascot, auto-detected once and cached.
|
|
141
|
+
|
|
142
|
+
Order: ``ARIA_THEME`` env override → macOS system appearance → ``COLORFGBG``
|
|
143
|
+
→ default dark.
|
|
144
|
+
"""
|
|
145
|
+
global _theme_cache
|
|
146
|
+
if _theme_cache is None:
|
|
147
|
+
_theme_cache = _resolve_theme()
|
|
148
|
+
return _theme_cache
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# 11 cols × 5 rows — a SOLID body with a "monitor" screen cut into it, modelled on
|
|
152
|
+
# the minimal robot icon and kept short/flat to sit beside the three-line banner
|
|
153
|
+
# text. Each cell is ``(palette-role, text)``; get_robot_row() resolves the role
|
|
154
|
+
# to a themed style. Layout: ▄ top cap · screen · eye(▀) + dash(▬) + ear dots(▪)
|
|
155
|
+
# · copper strip(▬) · legs(▀).
|
|
156
|
+
_MASCOT_TEMPLATE = [
|
|
157
|
+
[("", " "), ("shelltop", "▄▄▄▄▄▄▄▄▄"), ("", " ")],
|
|
158
|
+
[("", " "), ("shell", " "), ("screen", " "), ("shell", " "), ("", " ")],
|
|
159
|
+
[
|
|
160
|
+
("ear", "▪"), ("shell", " "),
|
|
161
|
+
("screen", " "), ("eye", "▀"), ("screen", " "),
|
|
162
|
+
("dash", "▬"), ("screen", " "),
|
|
163
|
+
("shell", " "), ("ear", "▪"),
|
|
164
|
+
],
|
|
165
|
+
[("", " "), ("strip", "▬▬▬▬▬▬▬▬▬"), ("", " ")],
|
|
166
|
+
[
|
|
167
|
+
("", " "), ("leg", "▀"), ("", " "), ("leg", "▀"), ("", " "),
|
|
168
|
+
("leg", "▀"), ("", " "), ("leg", "▀"), ("", " "),
|
|
169
|
+
],
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
ROBOT_ROW_COUNT = len(_MASCOT_TEMPLATE)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _resolve_eyes(state: RobotState, tick: int) -> tuple[str, str]:
|
|
176
|
+
el, er = _EYES[state]
|
|
177
|
+
if state is RobotState.IDLE:
|
|
178
|
+
if tick % 24 in (0, 1):
|
|
179
|
+
el, er = "·", "·"
|
|
180
|
+
elif state is RobotState.THINKING:
|
|
181
|
+
frames = (("◐", "◑"), ("◓", "◒"), ("◑", "◐"), ("◒", "◓"))
|
|
182
|
+
el, er = frames[tick % len(frames)]
|
|
183
|
+
elif state is RobotState.STREAMING:
|
|
184
|
+
el = er = "▸" if (tick % 4) < 2 else "▹"
|
|
185
|
+
return el, er
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_robot_row(tick: int, row: int) -> list:
|
|
189
|
+
"""Return FormattedText fragments for a single robot row, themed.
|
|
190
|
+
|
|
191
|
+
Rows: 0 shell top · 1 screen · 2 ear dots + eyes (square · dash) · 3 copper
|
|
192
|
+
strip · 4 legs. Each role is resolved to a colour for the active light/dark
|
|
193
|
+
theme (see detect_theme()).
|
|
194
|
+
"""
|
|
195
|
+
del tick
|
|
196
|
+
pal = _PALETTES[detect_theme()]
|
|
197
|
+
return [(pal[key] if key else "", text) for key, text in _MASCOT_TEMPLATE[row]]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_robot_frame(tick: int) -> list:
|
|
201
|
+
"""Legacy single-fragment list (not split by row). Use get_robot_row() instead."""
|
|
202
|
+
out = []
|
|
203
|
+
for r in range(ROBOT_ROW_COUNT):
|
|
204
|
+
out += get_robot_row(tick, r)
|
|
205
|
+
return out
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_status_dot(tick: int) -> list:
|
|
209
|
+
"""Compact inline indicator for the status bar — one animated glyph + state label.
|
|
210
|
+
|
|
211
|
+
IDLE: • (copper, slow blink)
|
|
212
|
+
THINKING: ◐ thinking… (yellow spinner)
|
|
213
|
+
STREAMING: ▶ generating… (green pulse)
|
|
214
|
+
ERROR: ✕ error (red)
|
|
215
|
+
DONE: ✓ done (green, brief)
|
|
216
|
+
"""
|
|
217
|
+
state = get_robot_state()
|
|
218
|
+
col = _COLOUR[state]
|
|
219
|
+
if detect_theme() == "light":
|
|
220
|
+
col = {
|
|
221
|
+
RobotState.IDLE: "#9A6700",
|
|
222
|
+
RobotState.THINKING: "#9A6700",
|
|
223
|
+
RobotState.STREAMING: "#1A7F37",
|
|
224
|
+
RobotState.ERROR: "#CF222E",
|
|
225
|
+
RobotState.DONE: "#1A7F37",
|
|
226
|
+
}[state]
|
|
227
|
+
el, _ = _resolve_eyes(state, tick)
|
|
228
|
+
if state is RobotState.IDLE:
|
|
229
|
+
el = "•"
|
|
230
|
+
label = _STATUS[state]
|
|
231
|
+
|
|
232
|
+
frags: list = [(f"bold {col}", el)]
|
|
233
|
+
if label:
|
|
234
|
+
frags.append((col, f" {label}"))
|
|
235
|
+
return frags
|
workspace/__init__.py
ADDED