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.
Files changed (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. 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,11 @@
1
+ """Data service facade for package-level imports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from data_service import DataBundle, DataService, DataServiceResult
6
+
7
+ __all__ = [
8
+ "DataBundle",
9
+ "DataService",
10
+ "DataServiceResult",
11
+ ]
@@ -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
+ )