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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Provider streaming helpers shared by SDK and CLI adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
from apps.cli.providers.base import (
|
|
9
|
+
LLMDone,
|
|
10
|
+
LLMProvider,
|
|
11
|
+
LLMStatus,
|
|
12
|
+
LLMThinking,
|
|
13
|
+
LLMToken,
|
|
14
|
+
LLMToolCall,
|
|
15
|
+
LLMToolResult,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def stream_provider_result(
|
|
20
|
+
provider: LLMProvider,
|
|
21
|
+
prompt: str,
|
|
22
|
+
history: list,
|
|
23
|
+
*,
|
|
24
|
+
tools: list | tuple | None = None,
|
|
25
|
+
cancel_event: asyncio.Event | None = None,
|
|
26
|
+
on_token: Callable[[str], None] | None = None,
|
|
27
|
+
on_thinking: Callable[[str], None] | None = None,
|
|
28
|
+
on_tool_call: Callable[[str, dict], None] | None = None,
|
|
29
|
+
on_tool_result: Callable[[str, str], None] | None = None,
|
|
30
|
+
on_status: Callable[[str, str], None] | None = None,
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""Stream one provider turn and return the standard Aria result dict."""
|
|
33
|
+
|
|
34
|
+
messages = list(history or []) + [{"role": "user", "content": prompt}]
|
|
35
|
+
response_parts: list[str] = []
|
|
36
|
+
tool_calls: list[dict] = []
|
|
37
|
+
final = LLMDone(response="", provider="unknown", success=True)
|
|
38
|
+
|
|
39
|
+
async for event in provider.stream(messages, list(tools or []), cancel_event=cancel_event):
|
|
40
|
+
if isinstance(event, LLMToken):
|
|
41
|
+
response_parts.append(event.text)
|
|
42
|
+
if on_token:
|
|
43
|
+
on_token(event.text)
|
|
44
|
+
elif isinstance(event, LLMThinking):
|
|
45
|
+
if on_thinking:
|
|
46
|
+
on_thinking(event.content)
|
|
47
|
+
elif isinstance(event, LLMToolCall):
|
|
48
|
+
call = {"tool": event.tool, "params": dict(event.params)}
|
|
49
|
+
tool_calls.append(call)
|
|
50
|
+
if on_tool_call:
|
|
51
|
+
on_tool_call(event.tool, dict(event.params))
|
|
52
|
+
elif isinstance(event, LLMToolResult):
|
|
53
|
+
if on_tool_result:
|
|
54
|
+
on_tool_result(event.tool, event.summary)
|
|
55
|
+
elif isinstance(event, LLMStatus):
|
|
56
|
+
if on_status:
|
|
57
|
+
on_status(event.state, event.message)
|
|
58
|
+
elif isinstance(event, LLMDone):
|
|
59
|
+
final = event
|
|
60
|
+
|
|
61
|
+
response = final.response or "".join(response_parts)
|
|
62
|
+
return {
|
|
63
|
+
"success": final.success,
|
|
64
|
+
"response": response,
|
|
65
|
+
"provider": final.provider,
|
|
66
|
+
"tool_calls_pending": tool_calls or list(final.tool_calls_pending),
|
|
67
|
+
"usage": dict(final.usage),
|
|
68
|
+
"cancelled": final.cancelled,
|
|
69
|
+
"error": final.error,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = ["stream_provider_result"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Typed public objects for the lightweight Aria Agent SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class AriaAgentOptions:
|
|
11
|
+
"""Options for an Aria SDK agent session.
|
|
12
|
+
|
|
13
|
+
The fields mirror the concerns exposed by modern agent SDKs: model/provider
|
|
14
|
+
selection, deterministic tool routing, permission intent, cwd, and metadata.
|
|
15
|
+
CLI rendering and terminal state stay outside this contract.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
model: str = "qwen2.5-coder:1.5b"
|
|
19
|
+
provider: str = "auto"
|
|
20
|
+
ollama_url: str = "http://localhost:11434"
|
|
21
|
+
api_url: str = "http://localhost:8000"
|
|
22
|
+
auth_token: str = ""
|
|
23
|
+
thinking_mode: str = "auto"
|
|
24
|
+
user_context: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
local_mode: bool = True
|
|
26
|
+
deterministic: bool = True
|
|
27
|
+
model_has_tools: bool = False
|
|
28
|
+
system_prompt: str = ""
|
|
29
|
+
permission_mode: str = "workspace-write"
|
|
30
|
+
allowed_tools: tuple[str, ...] = ()
|
|
31
|
+
disallowed_tools: tuple[str, ...] = ()
|
|
32
|
+
tool_schemas: tuple[dict[str, Any], ...] = ()
|
|
33
|
+
max_turns: int = 1
|
|
34
|
+
cwd: str = ""
|
|
35
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
36
|
+
has_brokers: bool = False
|
|
37
|
+
get_broker_registry: Callable[[], Any] | None = field(
|
|
38
|
+
default=None,
|
|
39
|
+
repr=False,
|
|
40
|
+
compare=False,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict[str, Any]:
|
|
44
|
+
data = asdict(self)
|
|
45
|
+
data["get_broker_registry"] = None
|
|
46
|
+
return data
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class AriaMessage:
|
|
51
|
+
"""Event/message yielded by the SDK query stream."""
|
|
52
|
+
|
|
53
|
+
kind: str
|
|
54
|
+
content: str = ""
|
|
55
|
+
role: str = ""
|
|
56
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> dict[str, Any]:
|
|
59
|
+
return {
|
|
60
|
+
"kind": self.kind,
|
|
61
|
+
"role": self.role,
|
|
62
|
+
"content": self.content,
|
|
63
|
+
"data": dict(self.data),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class AriaResult:
|
|
69
|
+
"""Final collected SDK result."""
|
|
70
|
+
|
|
71
|
+
success: bool
|
|
72
|
+
content: str = ""
|
|
73
|
+
provider: str = ""
|
|
74
|
+
session_id: str = ""
|
|
75
|
+
error: str = ""
|
|
76
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
77
|
+
|
|
78
|
+
def to_dict(self) -> dict[str, Any]:
|
|
79
|
+
return asdict(self)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
"AriaAgentOptions",
|
|
84
|
+
"AriaMessage",
|
|
85
|
+
"AriaResult",
|
|
86
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Service boundary manifests for Aria Code."""
|
|
2
|
+
|
|
3
|
+
from .context import (
|
|
4
|
+
ContextDecision,
|
|
5
|
+
ContextPolicy,
|
|
6
|
+
ContextService,
|
|
7
|
+
ContextSummaryEnvelope,
|
|
8
|
+
build_context_service,
|
|
9
|
+
)
|
|
10
|
+
from .provider_health import (
|
|
11
|
+
GLOBAL_PROVIDER_HEALTH,
|
|
12
|
+
ProviderHealthRegistry,
|
|
13
|
+
ProviderIssue,
|
|
14
|
+
ProviderState,
|
|
15
|
+
classify_provider_error,
|
|
16
|
+
summarize_provider_health,
|
|
17
|
+
)
|
|
18
|
+
from .registry import ServiceSpec, list_service_specs, required_service_names, service_map
|
|
19
|
+
from .usage import ServiceUsageSpec, list_service_usage_specs, service_usage_map
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ContextDecision",
|
|
23
|
+
"ContextPolicy",
|
|
24
|
+
"ContextService",
|
|
25
|
+
"ContextSummaryEnvelope",
|
|
26
|
+
"DataBundle",
|
|
27
|
+
"DataService",
|
|
28
|
+
"DataServiceResult",
|
|
29
|
+
"GLOBAL_PROVIDER_HEALTH",
|
|
30
|
+
"ProviderHealthRegistry",
|
|
31
|
+
"ProviderIssue",
|
|
32
|
+
"ProviderState",
|
|
33
|
+
"ServiceSpec",
|
|
34
|
+
"ServiceUsageSpec",
|
|
35
|
+
"build_context_service",
|
|
36
|
+
"classify_provider_error",
|
|
37
|
+
"list_service_specs",
|
|
38
|
+
"list_service_usage_specs",
|
|
39
|
+
"required_service_names",
|
|
40
|
+
"service_map",
|
|
41
|
+
"service_usage_map",
|
|
42
|
+
"summarize_provider_health",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def __getattr__(name: str):
|
|
47
|
+
if name in {"DataBundle", "DataService", "DataServiceResult"}:
|
|
48
|
+
from .data import DataBundle, DataService, DataServiceResult
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"DataBundle": DataBundle,
|
|
52
|
+
"DataService": DataService,
|
|
53
|
+
"DataServiceResult": DataServiceResult,
|
|
54
|
+
}[name]
|
|
55
|
+
raise AttributeError(name)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Context management service for Aria Code.
|
|
2
|
+
|
|
3
|
+
This module owns deterministic context pressure and local compaction behavior.
|
|
4
|
+
LLM-based summarisation remains an adapter concern, but the prompt/envelope
|
|
5
|
+
shape is defined here so CLI, daemon, and future channels can share it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import asdict, dataclass
|
|
11
|
+
from typing import Any, Dict, Iterable, List
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _int_or(value: Any, default: int) -> int:
|
|
15
|
+
try:
|
|
16
|
+
return int(value)
|
|
17
|
+
except Exception:
|
|
18
|
+
return default
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _float_or(value: Any, default: float) -> float:
|
|
22
|
+
try:
|
|
23
|
+
return float(value)
|
|
24
|
+
except Exception:
|
|
25
|
+
return default
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ContextPolicy:
|
|
30
|
+
max_tokens: int = 16384
|
|
31
|
+
threshold: float = 0.78
|
|
32
|
+
min_messages: int = 8
|
|
33
|
+
target_ratio: float = 0.55
|
|
34
|
+
compact_ratio: float = 0.70
|
|
35
|
+
tail_messages: int = 8
|
|
36
|
+
summary_tail_messages: int = 6
|
|
37
|
+
|
|
38
|
+
def normalized(self) -> "ContextPolicy":
|
|
39
|
+
max_tokens = max(1024, _int_or(self.max_tokens, 16384))
|
|
40
|
+
threshold = max(0.50, min(0.95, _float_or(self.threshold, 0.78)))
|
|
41
|
+
min_messages = max(1, _int_or(self.min_messages, 8))
|
|
42
|
+
target_ratio = max(0.20, min(0.85, _float_or(self.target_ratio, 0.55)))
|
|
43
|
+
compact_ratio = max(target_ratio, min(0.90, _float_or(self.compact_ratio, 0.70)))
|
|
44
|
+
tail_messages = max(2, _int_or(self.tail_messages, 8))
|
|
45
|
+
summary_tail_messages = max(2, _int_or(self.summary_tail_messages, 6))
|
|
46
|
+
return ContextPolicy(
|
|
47
|
+
max_tokens=max_tokens,
|
|
48
|
+
threshold=threshold,
|
|
49
|
+
min_messages=min_messages,
|
|
50
|
+
target_ratio=target_ratio,
|
|
51
|
+
compact_ratio=compact_ratio,
|
|
52
|
+
tail_messages=tail_messages,
|
|
53
|
+
summary_tail_messages=summary_tail_messages,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class ContextDecision:
|
|
59
|
+
should_compact: bool
|
|
60
|
+
estimated_tokens: int
|
|
61
|
+
max_tokens: int
|
|
62
|
+
fill_ratio: float
|
|
63
|
+
fill_pct: int
|
|
64
|
+
threshold: float
|
|
65
|
+
message_count: int
|
|
66
|
+
reason: str = ""
|
|
67
|
+
target_tokens: int = 0
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
70
|
+
return asdict(self)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class ContextSummaryEnvelope:
|
|
75
|
+
messages: List[Dict[str, str]]
|
|
76
|
+
old_message_count: int
|
|
77
|
+
new_message_count: int
|
|
78
|
+
tail_message_count: int
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
81
|
+
return asdict(self)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ContextService:
|
|
85
|
+
"""Pure context service with no terminal, Rich, or LLM dependency."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, policy: ContextPolicy | None = None):
|
|
88
|
+
self.policy = (policy or ContextPolicy()).normalized()
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def estimate_message_tokens(messages: Iterable[dict], extra_content: str = "") -> int:
|
|
92
|
+
total_chars = sum(len(str(message.get("content", ""))) for message in messages)
|
|
93
|
+
total_chars += len(str(extra_content or ""))
|
|
94
|
+
return total_chars // 3
|
|
95
|
+
|
|
96
|
+
def compaction_decision(self, messages: List[dict], extra_content: str = "") -> ContextDecision:
|
|
97
|
+
estimated = self.estimate_message_tokens(messages, extra_content=extra_content)
|
|
98
|
+
max_tokens = max(self.policy.max_tokens, 1)
|
|
99
|
+
fill_ratio = estimated / max_tokens
|
|
100
|
+
message_count = len(messages)
|
|
101
|
+
reason = ""
|
|
102
|
+
should_compact = False
|
|
103
|
+
if message_count < self.policy.min_messages:
|
|
104
|
+
reason = "message_count_below_minimum"
|
|
105
|
+
elif fill_ratio >= self.policy.threshold:
|
|
106
|
+
reason = "threshold_exceeded"
|
|
107
|
+
should_compact = True
|
|
108
|
+
else:
|
|
109
|
+
reason = "below_threshold"
|
|
110
|
+
return ContextDecision(
|
|
111
|
+
should_compact=should_compact,
|
|
112
|
+
estimated_tokens=estimated,
|
|
113
|
+
max_tokens=max_tokens,
|
|
114
|
+
fill_ratio=fill_ratio,
|
|
115
|
+
fill_pct=min(100, int(fill_ratio * 100)),
|
|
116
|
+
threshold=self.policy.threshold,
|
|
117
|
+
message_count=message_count,
|
|
118
|
+
reason=reason,
|
|
119
|
+
target_tokens=int(max_tokens * self.policy.target_ratio),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def compact_messages(self, messages: List[dict], max_chars: int = 0) -> List[dict]:
|
|
123
|
+
"""Compact history locally while preserving recent turns and errors."""
|
|
124
|
+
|
|
125
|
+
if max_chars <= 0:
|
|
126
|
+
max_chars = int(self.policy.max_tokens * 3 * self.policy.compact_ratio)
|
|
127
|
+
|
|
128
|
+
total = sum(len(str(message.get("content", ""))) for message in messages)
|
|
129
|
+
if total <= max_chars or len(messages) <= self.policy.tail_messages:
|
|
130
|
+
return messages
|
|
131
|
+
|
|
132
|
+
system = messages[0]
|
|
133
|
+
keep_tail = min(self.policy.tail_messages, max(2, len(messages) - 1))
|
|
134
|
+
middle = messages[1:-keep_tail]
|
|
135
|
+
tail = messages[-keep_tail:]
|
|
136
|
+
|
|
137
|
+
compacted: List[dict] = [system]
|
|
138
|
+
for message in middle:
|
|
139
|
+
compacted.append(self._compact_middle_message(message))
|
|
140
|
+
compacted.extend(tail)
|
|
141
|
+
return compacted
|
|
142
|
+
|
|
143
|
+
def _compact_middle_message(self, message: dict) -> dict:
|
|
144
|
+
content = str(message.get("content", ""))
|
|
145
|
+
role = str(message.get("role", ""))
|
|
146
|
+
|
|
147
|
+
if role == "tool" and len(content) > 200:
|
|
148
|
+
lines = content.splitlines()
|
|
149
|
+
kept: List[str] = []
|
|
150
|
+
has_error = False
|
|
151
|
+
for line in lines[:30]:
|
|
152
|
+
stripped = line.strip()
|
|
153
|
+
if not stripped:
|
|
154
|
+
continue
|
|
155
|
+
low = stripped.lower()
|
|
156
|
+
if any(keyword in low for keyword in ("error", "traceback", "exception", "failed", "failure")):
|
|
157
|
+
kept.append(stripped)
|
|
158
|
+
has_error = True
|
|
159
|
+
elif len(kept) < 4 and len(stripped) > 8:
|
|
160
|
+
kept.append(stripped)
|
|
161
|
+
summary = " | ".join(kept[:4]) if kept else content[:150]
|
|
162
|
+
flag = " [error preserved]" if has_error else " [compacted]"
|
|
163
|
+
return {"role": role, "content": f"{summary}{flag}"}
|
|
164
|
+
|
|
165
|
+
if role == "assistant" and len(content) > 500:
|
|
166
|
+
paras = [part.strip() for part in content.split("\n\n") if part.strip()]
|
|
167
|
+
if len(paras) >= 2:
|
|
168
|
+
head = paras[0][:280]
|
|
169
|
+
tail = paras[-1][-180:]
|
|
170
|
+
return {"role": role, "content": f"{head}\n...\n{tail} [compacted]"}
|
|
171
|
+
return {"role": role, "content": content[:350] + "... [compacted]"}
|
|
172
|
+
|
|
173
|
+
return message
|
|
174
|
+
|
|
175
|
+
def build_summary_transcript(self, messages: List[dict]) -> str:
|
|
176
|
+
parts: List[str] = []
|
|
177
|
+
for message in messages:
|
|
178
|
+
role = str(message.get("role", ""))
|
|
179
|
+
content = str(message.get("content", ""))
|
|
180
|
+
if role == "tool":
|
|
181
|
+
lines = [line.strip() for line in content.splitlines() if line.strip()]
|
|
182
|
+
has_error = any(
|
|
183
|
+
"error" in line.lower() or "traceback" in line.lower()
|
|
184
|
+
for line in lines[:10]
|
|
185
|
+
)
|
|
186
|
+
excerpt = " | ".join(lines[:3]) if lines else content[:200]
|
|
187
|
+
label = "Tool[error]" if has_error else "Tool"
|
|
188
|
+
parts.append(f"{label}: {excerpt[:300]}")
|
|
189
|
+
elif role == "user":
|
|
190
|
+
parts.append(f"User: {content[:800]}")
|
|
191
|
+
else:
|
|
192
|
+
parts.append(f"Aria: {content[:1200]}")
|
|
193
|
+
return "\n\n".join(parts)
|
|
194
|
+
|
|
195
|
+
def build_summary_prompt(self, messages: List[dict]) -> str:
|
|
196
|
+
transcript = self.build_summary_transcript(messages)
|
|
197
|
+
return (
|
|
198
|
+
"You are a context compressor for a quantitative finance AI assistant.\n"
|
|
199
|
+
"Given the conversation transcript, produce a DENSE SUMMARY (<=350 words).\n"
|
|
200
|
+
"You MUST preserve:\n"
|
|
201
|
+
" - All ticker symbols / asset names discussed\n"
|
|
202
|
+
" - Key numerical results (prices, rates, backtest metrics)\n"
|
|
203
|
+
" - Code files written or modified (file paths + purpose)\n"
|
|
204
|
+
" - Errors encountered and how they were resolved\n"
|
|
205
|
+
" - User preferences or decisions made\n"
|
|
206
|
+
" - The last task status (complete / in-progress / blocked)\n"
|
|
207
|
+
"Write in concise third-person present tense. "
|
|
208
|
+
"Start with: 'Session summary: ...'\n\n"
|
|
209
|
+
f"TRANSCRIPT:\n{transcript}\n\nSUMMARY:"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def build_summary_envelope(self, messages: List[dict], summary: str) -> ContextSummaryEnvelope:
|
|
213
|
+
tail_count = min(self.policy.summary_tail_messages, len(messages))
|
|
214
|
+
tail = messages[-tail_count:] if tail_count else []
|
|
215
|
+
envelope_messages = [
|
|
216
|
+
{
|
|
217
|
+
"role": "user",
|
|
218
|
+
"content": (
|
|
219
|
+
"[Session summary - earlier conversation compressed]\n\n"
|
|
220
|
+
f"{summary.strip()}\n\n"
|
|
221
|
+
"[Recent conversation follows]"
|
|
222
|
+
),
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"role": "assistant",
|
|
226
|
+
"content": "Summary loaded. Continuing with the current task.",
|
|
227
|
+
},
|
|
228
|
+
*tail,
|
|
229
|
+
]
|
|
230
|
+
return ContextSummaryEnvelope(
|
|
231
|
+
messages=envelope_messages,
|
|
232
|
+
old_message_count=len(messages),
|
|
233
|
+
new_message_count=len(envelope_messages),
|
|
234
|
+
tail_message_count=tail_count,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def build_context_service(
|
|
239
|
+
*,
|
|
240
|
+
max_tokens: int = 16384,
|
|
241
|
+
threshold: float = 0.78,
|
|
242
|
+
min_messages: int = 8,
|
|
243
|
+
target_ratio: float = 0.55,
|
|
244
|
+
compact_ratio: float = 0.70,
|
|
245
|
+
tail_messages: int = 8,
|
|
246
|
+
summary_tail_messages: int = 6,
|
|
247
|
+
) -> ContextService:
|
|
248
|
+
return ContextService(
|
|
249
|
+
ContextPolicy(
|
|
250
|
+
max_tokens=max_tokens,
|
|
251
|
+
threshold=threshold,
|
|
252
|
+
min_messages=min_messages,
|
|
253
|
+
target_ratio=target_ratio,
|
|
254
|
+
compact_ratio=compact_ratio,
|
|
255
|
+
tail_messages=tail_messages,
|
|
256
|
+
summary_tail_messages=summary_tail_messages,
|
|
257
|
+
)
|
|
258
|
+
)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Provider error classification and lightweight health state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ProviderIssue:
|
|
12
|
+
provider: str
|
|
13
|
+
category: str
|
|
14
|
+
message: str
|
|
15
|
+
retryable: bool = True
|
|
16
|
+
cooldown_seconds: int = 0
|
|
17
|
+
|
|
18
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
19
|
+
return asdict(self)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ProviderState:
|
|
24
|
+
provider: str
|
|
25
|
+
status: str = "ok"
|
|
26
|
+
last_error_category: str = ""
|
|
27
|
+
last_error: str = ""
|
|
28
|
+
failures: int = 0
|
|
29
|
+
cooldown_until: float = 0.0
|
|
30
|
+
last_seen_at: float = 0.0
|
|
31
|
+
last_success_at: float = 0.0
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
34
|
+
data = asdict(self)
|
|
35
|
+
data["cooldown_active"] = self.cooldown_until > time.time()
|
|
36
|
+
data["cooldown_remaining_seconds"] = max(0, int(self.cooldown_until - time.time()))
|
|
37
|
+
return data
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class ProviderHealthSummary:
|
|
42
|
+
schema: str
|
|
43
|
+
total: int
|
|
44
|
+
ok: int
|
|
45
|
+
warn: int
|
|
46
|
+
err: int
|
|
47
|
+
cooldown: int
|
|
48
|
+
auth_errors: int
|
|
49
|
+
providers: List[str]
|
|
50
|
+
status: str
|
|
51
|
+
detail: str
|
|
52
|
+
suggestion: str
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
55
|
+
return asdict(self)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def classify_provider_error(provider: str, error: Any) -> ProviderIssue:
|
|
59
|
+
text = str(error or "").strip()
|
|
60
|
+
low = text.lower()
|
|
61
|
+
if not text:
|
|
62
|
+
return ProviderIssue(provider, "unavailable", "provider returned no usable data", True, 30)
|
|
63
|
+
if any(token in low for token in ("429", "rate", "too many", "limit")):
|
|
64
|
+
return ProviderIssue(provider, "rate_limited", "provider rate limited the request", True, 60)
|
|
65
|
+
if any(token in low for token in ("timeout", "timed out", "curl: (28)", "read timed out")):
|
|
66
|
+
return ProviderIssue(provider, "timeout", "provider request timed out", True, 30)
|
|
67
|
+
if any(token in low for token in ("connection", "network", "refused", "remote", "dns", "name resolution")):
|
|
68
|
+
return ProviderIssue(provider, "network", "provider network connection failed", True, 30)
|
|
69
|
+
if any(token in low for token in ("empty", "no data", "not found", "none", "null")):
|
|
70
|
+
return ProviderIssue(provider, "no_data", "provider returned no market data", True, 15)
|
|
71
|
+
if any(token in low for token in ("unauthorized", "forbidden", "api key", "401", "403")):
|
|
72
|
+
return ProviderIssue(provider, "auth", "provider authentication failed", False, 0)
|
|
73
|
+
return ProviderIssue(provider, "error", text[:240], True, 30)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ProviderHealthRegistry:
|
|
77
|
+
"""In-process health state for data providers."""
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
self._states: Dict[str, ProviderState] = {}
|
|
81
|
+
|
|
82
|
+
def mark_success(self, provider: str) -> None:
|
|
83
|
+
if not provider:
|
|
84
|
+
return
|
|
85
|
+
state = self._states.setdefault(provider, ProviderState(provider=provider))
|
|
86
|
+
state.status = "ok"
|
|
87
|
+
state.last_error_category = ""
|
|
88
|
+
state.last_error = ""
|
|
89
|
+
state.failures = 0
|
|
90
|
+
state.cooldown_until = 0.0
|
|
91
|
+
now = time.time()
|
|
92
|
+
state.last_seen_at = now
|
|
93
|
+
state.last_success_at = now
|
|
94
|
+
|
|
95
|
+
def mark_issue(self, issue: ProviderIssue) -> None:
|
|
96
|
+
if not issue.provider:
|
|
97
|
+
return
|
|
98
|
+
state = self._states.setdefault(issue.provider, ProviderState(provider=issue.provider))
|
|
99
|
+
now = time.time()
|
|
100
|
+
state.status = issue.category
|
|
101
|
+
state.last_error_category = issue.category
|
|
102
|
+
state.last_error = issue.message
|
|
103
|
+
state.failures += 1
|
|
104
|
+
state.last_seen_at = now
|
|
105
|
+
if issue.cooldown_seconds:
|
|
106
|
+
state.cooldown_until = max(state.cooldown_until, now + issue.cooldown_seconds)
|
|
107
|
+
|
|
108
|
+
def provider_in_cooldown(self, provider: str) -> bool:
|
|
109
|
+
state = self._states.get(provider)
|
|
110
|
+
return bool(state and state.cooldown_until > time.time())
|
|
111
|
+
|
|
112
|
+
def snapshot(self) -> List[Dict[str, Any]]:
|
|
113
|
+
return [self._states[name].to_dict() for name in sorted(self._states)]
|
|
114
|
+
|
|
115
|
+
def summary(self) -> ProviderHealthSummary:
|
|
116
|
+
return summarize_provider_health(self.snapshot())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
GLOBAL_PROVIDER_HEALTH = ProviderHealthRegistry()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def summarize_provider_health(snapshot: List[Dict[str, Any]] | None = None) -> ProviderHealthSummary:
|
|
123
|
+
rows = list(snapshot or [])
|
|
124
|
+
if not rows:
|
|
125
|
+
return ProviderHealthSummary(
|
|
126
|
+
schema="aria.provider_health_summary.v1",
|
|
127
|
+
total=0,
|
|
128
|
+
ok=0,
|
|
129
|
+
warn=0,
|
|
130
|
+
err=0,
|
|
131
|
+
cooldown=0,
|
|
132
|
+
auth_errors=0,
|
|
133
|
+
providers=[],
|
|
134
|
+
status="warn",
|
|
135
|
+
detail="no provider calls recorded in this session",
|
|
136
|
+
suggestion="Run /quote, /ta, /analyze, or /report to populate provider health.",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
ok = warn = err = cooldown = auth_errors = 0
|
|
140
|
+
providers: list[str] = []
|
|
141
|
+
for row in rows:
|
|
142
|
+
provider = str(row.get("provider") or "provider")
|
|
143
|
+
providers.append(provider)
|
|
144
|
+
status = str(row.get("status") or "unknown")
|
|
145
|
+
error_category = str(row.get("last_error_category") or "")
|
|
146
|
+
if status == "ok":
|
|
147
|
+
ok += 1
|
|
148
|
+
elif error_category == "auth":
|
|
149
|
+
err += 1
|
|
150
|
+
auth_errors += 1
|
|
151
|
+
else:
|
|
152
|
+
warn += 1
|
|
153
|
+
if row.get("cooldown_active"):
|
|
154
|
+
cooldown += 1
|
|
155
|
+
|
|
156
|
+
if err:
|
|
157
|
+
status = "err"
|
|
158
|
+
elif warn or cooldown:
|
|
159
|
+
status = "warn"
|
|
160
|
+
else:
|
|
161
|
+
status = "ok"
|
|
162
|
+
|
|
163
|
+
parts = [f"{len(rows)} providers"]
|
|
164
|
+
if ok:
|
|
165
|
+
parts.append(f"{ok} ok")
|
|
166
|
+
if warn:
|
|
167
|
+
parts.append(f"{warn} warn")
|
|
168
|
+
if err:
|
|
169
|
+
parts.append(f"{err} err")
|
|
170
|
+
if cooldown:
|
|
171
|
+
parts.append(f"{cooldown} cooldown")
|
|
172
|
+
|
|
173
|
+
suggestion = "Run /doctor --network or inspect /apikey." if status != "ok" else "All providers healthy."
|
|
174
|
+
if auth_errors:
|
|
175
|
+
suggestion = "Fix API keys first, then retry /doctor or /cloud health."
|
|
176
|
+
|
|
177
|
+
return ProviderHealthSummary(
|
|
178
|
+
schema="aria.provider_health_summary.v1",
|
|
179
|
+
total=len(rows),
|
|
180
|
+
ok=ok,
|
|
181
|
+
warn=warn,
|
|
182
|
+
err=err,
|
|
183
|
+
cooldown=cooldown,
|
|
184
|
+
auth_errors=auth_errors,
|
|
185
|
+
providers=providers,
|
|
186
|
+
status=status,
|
|
187
|
+
detail=", ".join(parts),
|
|
188
|
+
suggestion=suggestion,
|
|
189
|
+
)
|