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,271 @@
1
+ """LLM provider abstraction layer.
2
+
3
+ Defines the minimal Protocol that every provider must satisfy so the
4
+ agent loop can call any backend (Ollama, AriaSSE, DeepSeek, etc.)
5
+ without importing provider-specific code.
6
+
7
+ Usage
8
+ -----
9
+ Implement the protocol on any class or pass a coroutine that matches
10
+ ``stream()``'s signature as a bare ``provider_fn`` callable.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ from dataclasses import dataclass, field
16
+ from typing import AsyncGenerator, Optional, Protocol, runtime_checkable
17
+
18
+
19
+ # ── Event types emitted by LLMProvider.stream() ──────────────────────────────
20
+
21
+ @dataclass(frozen=True)
22
+ class LLMToken:
23
+ """A single text token from the model."""
24
+ text: str
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class LLMThinking:
29
+ """One thinking/reasoning token (extended-thinking models)."""
30
+ content: str
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class LLMToolCall:
35
+ """Model requested a tool call."""
36
+ tool: str
37
+ params: dict
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class LLMToolResult:
42
+ """Provider reported a tool execution result summary."""
43
+ tool: str
44
+ summary: str
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class LLMStatus:
49
+ """Provider emitted a streaming status update."""
50
+ state: str
51
+ message: str
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class LLMDone:
56
+ """Stream finished. Carries the aggregated result."""
57
+ response: str
58
+ tool_calls_pending: list = field(default_factory=list)
59
+ usage: dict = field(default_factory=dict)
60
+ provider: str = "unknown"
61
+ success: bool = True
62
+ cancelled: bool = False
63
+ error: str = ""
64
+
65
+
66
+ # Union type for type-checkers
67
+ LLMEvent = LLMToken | LLMThinking | LLMToolCall | LLMToolResult | LLMStatus | LLMDone
68
+
69
+
70
+ def _resolve_ollama_stream():
71
+ """Prefer the aria_cli rebound stream_ollama when available."""
72
+ import sys
73
+
74
+ aria_cli = sys.modules.get("aria_cli")
75
+ rebound = getattr(aria_cli, "stream_ollama", None) if aria_cli else None
76
+ if callable(rebound):
77
+ return rebound
78
+ from apps.cli.providers.llm.ollama_stream import stream_ollama
79
+
80
+ return stream_ollama
81
+
82
+
83
+ async def _stream_callback_provider(invoke, *, done_provider: str) -> AsyncGenerator[LLMEvent, None]:
84
+ """Convert a callback-based provider coroutine into a real async event stream."""
85
+
86
+ queue: asyncio.Queue[LLMEvent] = asyncio.Queue()
87
+
88
+ def _on_token(tok: str) -> None:
89
+ queue.put_nowait(LLMToken(text=tok))
90
+
91
+ def _on_thinking(content: str) -> None:
92
+ queue.put_nowait(LLMThinking(content=content))
93
+
94
+ def _on_tool_call(tool: str, params: dict) -> None:
95
+ queue.put_nowait(LLMToolCall(tool=tool, params=params))
96
+
97
+ def _on_tool_result(tool: str, summary: str) -> None:
98
+ queue.put_nowait(LLMToolResult(tool=tool, summary=summary))
99
+
100
+ def _on_status(state: str, message: str) -> None:
101
+ queue.put_nowait(LLMStatus(state=state, message=message))
102
+
103
+ task = asyncio.create_task(
104
+ invoke(_on_token, _on_thinking, _on_tool_call, _on_tool_result, _on_status)
105
+ )
106
+ while not task.done() or not queue.empty():
107
+ try:
108
+ yield await asyncio.wait_for(queue.get(), timeout=0.05)
109
+ except asyncio.TimeoutError:
110
+ continue
111
+
112
+ try:
113
+ result = await task
114
+ except Exception as exc:
115
+ yield LLMDone(response="", provider=done_provider, success=False, error=str(exc))
116
+ return
117
+
118
+ yield LLMDone(
119
+ response=result.get("response", ""),
120
+ tool_calls_pending=result.get("tool_calls_pending", []),
121
+ usage=result.get("usage", {}),
122
+ provider=result.get("provider", done_provider),
123
+ success=result.get("success", False),
124
+ cancelled=result.get("cancelled", False),
125
+ error=result.get("error", ""),
126
+ )
127
+
128
+
129
+ # ── Protocol ─────────────────────────────────────────────────────────────────
130
+
131
+ @runtime_checkable
132
+ class LLMProvider(Protocol):
133
+ """Minimal interface every LLM backend must implement.
134
+
135
+ ``stream()`` is an async generator that yields ``LLMEvent`` objects.
136
+ The final event is always ``LLMDone``; callers may break early on
137
+ ``LLMDone`` or consume the full stream.
138
+
139
+ Parameters
140
+ ----------
141
+ messages:
142
+ Full conversation history (list of {"role": …, "content": …} dicts).
143
+ tools:
144
+ OpenAI-format function schema list; empty list disables tool calls.
145
+ cancel_event:
146
+ asyncio.Event that, when set, signals the provider to stop.
147
+ """
148
+
149
+ async def stream(
150
+ self,
151
+ messages: list,
152
+ tools: list,
153
+ *,
154
+ cancel_event: Optional[asyncio.Event] = None,
155
+ ) -> AsyncGenerator[LLMEvent, None]:
156
+ ...
157
+
158
+
159
+ # ── Thin adapters (wrap existing callables as LLMProvider) ───────────────────
160
+
161
+ class OllamaProvider:
162
+ """Wraps ``stream_ollama`` as an ``LLMProvider``.
163
+
164
+ Import lazily to avoid circular dependencies — ``stream_ollama`` lives in
165
+ the same providers package and rebinds globals from aria_cli at startup.
166
+ """
167
+
168
+ def __init__(
169
+ self,
170
+ ollama_url: str,
171
+ model: str,
172
+ *,
173
+ system_override: Optional[str] = None,
174
+ show_market_prefetch_status: bool = True,
175
+ ) -> None:
176
+ self.ollama_url = ollama_url
177
+ self.model = model
178
+ self.system_override = system_override
179
+ self.show_market_prefetch_status = show_market_prefetch_status
180
+
181
+ async def stream(
182
+ self,
183
+ messages: list,
184
+ tools: list,
185
+ *,
186
+ cancel_event: Optional[asyncio.Event] = None,
187
+ ) -> AsyncGenerator[LLMEvent, None]:
188
+ stream_ollama = _resolve_ollama_stream()
189
+
190
+ # Extract last user message as the prompt; the rest is history
191
+ history = [m for m in messages if not (m.get("role") == "user" and m is messages[-1])]
192
+ prompt = messages[-1].get("content", "") if messages else ""
193
+
194
+ async def _invoke(on_token, on_thinking, on_tool_call, on_tool_result, _on_status):
195
+ return await stream_ollama(
196
+ self.ollama_url,
197
+ prompt,
198
+ history,
199
+ model=self.model,
200
+ on_token=on_token,
201
+ on_thinking=on_thinking,
202
+ on_tool_call=on_tool_call,
203
+ on_tool_result=on_tool_result,
204
+ cancel_event=cancel_event,
205
+ enable_tools=bool(tools),
206
+ system_override=self.system_override,
207
+ show_market_prefetch_status=self.show_market_prefetch_status,
208
+ )
209
+
210
+ async for event in _stream_callback_provider(_invoke, done_provider="ollama"):
211
+ yield event
212
+
213
+
214
+ class AriaSSEProvider:
215
+ """Wraps ``stream_chat`` (Aria cloud SSE) as an ``LLMProvider``."""
216
+
217
+ def __init__(
218
+ self,
219
+ api_url: str,
220
+ model: str,
221
+ *,
222
+ auth_token: Optional[str] = None,
223
+ thinking_mode: str = "auto",
224
+ user_context: Optional[dict] = None,
225
+ system_override: Optional[str] = None,
226
+ project_context: str = "",
227
+ ) -> None:
228
+ self.api_url = api_url
229
+ self.model = model
230
+ self.auth_token = auth_token
231
+ self.thinking_mode = thinking_mode
232
+ self.user_context = user_context or {}
233
+ self.system_override = system_override
234
+ self.project_context = project_context
235
+
236
+ async def stream(
237
+ self,
238
+ messages: list,
239
+ tools: list,
240
+ *,
241
+ cancel_event: Optional[asyncio.Event] = None,
242
+ ) -> AsyncGenerator[LLMEvent, None]:
243
+ from apps.cli.providers.llm.sse_stream import stream_chat
244
+
245
+ history = [m for m in messages if not (m.get("role") == "user" and m is messages[-1])]
246
+ prompt = messages[-1].get("content", "") if messages else ""
247
+
248
+ uctx = dict(self.user_context)
249
+ if self.system_override:
250
+ uctx["system_role_override"] = self.system_override
251
+
252
+ async def _invoke(on_token, on_thinking, on_tool_call, on_tool_result, on_status):
253
+ return await stream_chat(
254
+ self.api_url,
255
+ prompt,
256
+ history,
257
+ model=self.model,
258
+ thinking_mode=self.thinking_mode,
259
+ user_context=uctx or None,
260
+ auth_token=self.auth_token,
261
+ on_token=on_token,
262
+ on_thinking=on_thinking,
263
+ on_tool_call=on_tool_call,
264
+ on_tool_result=on_tool_result,
265
+ on_status=on_status,
266
+ cancel_event=cancel_event,
267
+ project_context=self.project_context,
268
+ )
269
+
270
+ async for event in _stream_callback_provider(_invoke, done_provider="aria_sse"):
271
+ yield event
@@ -0,0 +1,80 @@
1
+ """Provider routing + fallback DECISIONS for the chat loop (pure, testable).
2
+
3
+ Extracted from ``aria_cli.send_message`` as the keystone for the documented
4
+ runtime next step ("route the whole CLI tool loop through run_agent"). The
5
+ *decision* of which provider a round uses, and whether to fall back, is pure
6
+ logic; pulling it out of the streaming machinery lets it be unit-tested and
7
+ reused as a ``provider_fn`` selector without touching the live REPL path.
8
+
9
+ Routing rules (mirrors send_message):
10
+ • local_mode → always local Ollama
11
+ • cloud-named model ("provider/x") → cloud backend (AriaSSE)
12
+ • ollama-named model ("x:y") → cloud backend ONLY if backend_chat forces
13
+ it; otherwise skip the backend stub and go
14
+ straight to the local/cloud fallback chain
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Callable, Optional
20
+
21
+
22
+ def is_cloud_model(model: str) -> bool:
23
+ """Cloud models are provider-prefixed, e.g. ``openai/gpt-4.5``, ``anthropic/…``."""
24
+ return "/" in (model or "")
25
+
26
+
27
+ def is_ollama_model(model: str) -> bool:
28
+ """Ollama models have no ``/`` (``gpt-oss:120b-cloud``, ``deepseek-r1:14b``)."""
29
+ return "/" not in (model or "")
30
+
31
+
32
+ def force_backend(config: dict, api_url: Optional[str]) -> bool:
33
+ """backend_chat=True routes ALL chat through the self-hosted backend (which
34
+ proxies to its own Ollama + collects training data), requiring an api_url."""
35
+ return bool(config.get("backend_chat")) and bool(api_url)
36
+
37
+
38
+ def first_round_route(model: str, config: dict, api_url: Optional[str]) -> str:
39
+ """Return where the first round's generation goes: ``ollama`` | ``cloud`` | ``skip``.
40
+
41
+ ``skip`` means the backend would only return a stub for this (ollama-named)
42
+ model, so the round is skipped and the fallback chain runs directly.
43
+ """
44
+ if config.get("local_mode", False):
45
+ return "ollama"
46
+ if is_cloud_model(model) or force_backend(config, api_url):
47
+ return "cloud"
48
+ return "skip"
49
+
50
+
51
+ def is_placeholder_response(
52
+ response: str,
53
+ token_count: int,
54
+ stub_detector: Optional[Callable[[str], bool]] = None,
55
+ ) -> bool:
56
+ """A 'successful' result that is actually empty / canned / a backend stub."""
57
+ resp = response or ""
58
+ if len(resp) < 20:
59
+ return True
60
+ if stub_detector is not None and stub_detector(resp):
61
+ return True
62
+ # Long "response" with ~no streamed tokens ⇒ canned backend reply, not a generation.
63
+ if token_count <= 2 and len(resp) > 80:
64
+ return True
65
+ return False
66
+
67
+
68
+ def should_fallback(route: str, result: dict, *, is_placeholder: bool) -> bool:
69
+ """Whether to run the local/cloud fallback chain after the primary round.
70
+
71
+ Keyed on the *route* (not the model name), so a forced-backend round that
72
+ genuinely succeeded does NOT fall back — which is the bug-free version of the
73
+ old ``_should_fallback`` that keyed on ``is_ollama_model`` and could discard a
74
+ good backend answer (causing a re-run / hang).
75
+ """
76
+ if route == "skip":
77
+ return True
78
+ if not result.get("success") and not result.get("cancelled"):
79
+ return True
80
+ return is_placeholder
@@ -0,0 +1 @@
1
+ """LLM streaming providers."""