aria-code 0.167.0__py3-none-win_amd64.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 (601) hide show
  1. aria_bots/__init__.py +7 -0
  2. aria_bots/channels/README.md +97 -0
  3. aria_bots/channels/__init__.py +1 -0
  4. aria_bots/channels/approvals.py +263 -0
  5. aria_bots/channels/conversation.py +280 -0
  6. aria_bots/channels/digest.py +186 -0
  7. aria_bots/channels/registry.py +95 -0
  8. aria_bots/engine.py +144 -0
  9. aria_bots/feishu_bot.py +1385 -0
  10. aria_bots/logistics/__init__.py +1 -0
  11. aria_bots/logistics/carriers.py +253 -0
  12. aria_bots/logistics/freight.py +154 -0
  13. aria_bots/logistics/inventory.py +242 -0
  14. aria_bots/logistics/mcp_server.py +156 -0
  15. aria_bots/logistics/records.py +119 -0
  16. aria_bots/logistics/tenancy.py +152 -0
  17. aria_bots/market.py +91 -0
  18. aria_bots/relay_client.py +326 -0
  19. aria_bots/telegram_bot.py +299 -0
  20. aria_code/__init__.py +111 -0
  21. aria_code/_bin/aria-native.exe +0 -0
  22. aria_code/_bin/aria-runtime.exe +0 -0
  23. aria_code/_version.py +20 -0
  24. aria_code/account_memory.py +296 -0
  25. aria_code/account_session.py +166 -0
  26. aria_code/adk_apps/__init__.py +1 -0
  27. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  28. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  29. aria_code/adk_apps/aria_research/__init__.py +1 -0
  30. aria_code/adk_apps/aria_research/agent.py +31 -0
  31. aria_code/agents/__init__.py +49 -0
  32. aria_code/agents/base.py +329 -0
  33. aria_code/agents/code_review.py +200 -0
  34. aria_code/agents/deep/__init__.py +37 -0
  35. aria_code/agents/deep/calibration_loop.py +144 -0
  36. aria_code/agents/deep/critic.py +125 -0
  37. aria_code/agents/deep/deepen.py +193 -0
  38. aria_code/agents/deep/models.py +149 -0
  39. aria_code/agents/deep/pipeline.py +164 -0
  40. aria_code/agents/deep/quant_fusion.py +192 -0
  41. aria_code/agents/deep/themes.py +95 -0
  42. aria_code/agents/deep/tiers.py +106 -0
  43. aria_code/agents/engineering/__init__.py +14 -0
  44. aria_code/agents/engineering/coder.py +222 -0
  45. aria_code/agents/engineering/tester.py +145 -0
  46. aria_code/agents/financial/__init__.py +21 -0
  47. aria_code/agents/financial/cashflow_burn.py +119 -0
  48. aria_code/agents/financial/catalyst.py +279 -0
  49. aria_code/agents/financial/corporate_finance.py +175 -0
  50. aria_code/agents/financial/debate.py +224 -0
  51. aria_code/agents/financial/earnings.py +315 -0
  52. aria_code/agents/financial/fundamental.py +178 -0
  53. aria_code/agents/financial/macro.py +111 -0
  54. aria_code/agents/financial/news.py +231 -0
  55. aria_code/agents/financial/risk.py +146 -0
  56. aria_code/agents/financial/sector.py +279 -0
  57. aria_code/agents/financial/strategist.py +121 -0
  58. aria_code/agents/financial/stripe_revenue.py +127 -0
  59. aria_code/agents/financial/synthesis.py +274 -0
  60. aria_code/agents/financial/technical.py +277 -0
  61. aria_code/agents/orchestrator.py +44 -0
  62. aria_code/agents/portfolio_agent.py +392 -0
  63. aria_code/agents/realty/__init__.py +62 -0
  64. aria_code/agents/realty/asset_diagnosis.py +152 -0
  65. aria_code/agents/realty/business_match.py +166 -0
  66. aria_code/agents/realty/cashflow_verify.py +209 -0
  67. aria_code/agents/realty/contract_rules.py +210 -0
  68. aria_code/agents/realty/energy_anomaly.py +189 -0
  69. aria_code/agents/realty/exit_settlement.py +208 -0
  70. aria_code/agents/realty/fulfillment_risk.py +206 -0
  71. aria_code/agents/realty/ops_optimize.py +160 -0
  72. aria_code/agents/realty/revenue_share.py +215 -0
  73. aria_code/agents/registry.py +168 -0
  74. aria_code/agents/signal_scheme.py +111 -0
  75. aria_code/agents/sports/__init__.py +0 -0
  76. aria_code/agents/sports/football_agent.py +197 -0
  77. aria_code/agents/supervisor.py +113 -0
  78. aria_code/agents/team.py +500 -0
  79. aria_code/agents/video/__init__.py +16 -0
  80. aria_code/agents/video/director.py +376 -0
  81. aria_code/agents/video/qa_agent.py +143 -0
  82. aria_code/agents/video/script_doctor.py +112 -0
  83. aria_code/agents/warehouse/__init__.py +25 -0
  84. aria_code/agents/warehouse/contracts.py +26 -0
  85. aria_code/agents/warehouse/dashboard.py +155 -0
  86. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  87. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  88. aria_code/agents/warehouse/inventory_health.py +24 -0
  89. aria_code/agents/warehouse/local_run.py +71 -0
  90. aria_code/agents/warehouse/local_snapshot.py +132 -0
  91. aria_code/agents/warehouse/logistics_cost.py +84 -0
  92. aria_code/agents/warehouse/logistics_sync.py +21 -0
  93. aria_code/agents/warehouse/navigation.schema.json +110 -0
  94. aria_code/agents/warehouse/task_agent.py +172 -0
  95. aria_code/agents/warehouse/workflow.py +31 -0
  96. aria_code/aliyun_data_client.py +4 -0
  97. aria_code/apps/README.md +12 -0
  98. aria_code/apps/__init__.py +2 -0
  99. aria_code/apps/channels/README.md +9 -0
  100. aria_code/apps/channels/__init__.py +17 -0
  101. aria_code/apps/channels/approvals.py +11 -0
  102. aria_code/apps/channels/conversation.py +11 -0
  103. aria_code/apps/channels/digest.py +11 -0
  104. aria_code/apps/channels/intake.py +92 -0
  105. aria_code/apps/channels/registry.py +11 -0
  106. aria_code/apps/channels/tradingview.py +94 -0
  107. aria_code/apps/cli/README.md +13 -0
  108. aria_code/apps/cli/__init__.py +2 -0
  109. aria_code/apps/cli/_optional.py +120 -0
  110. aria_code/apps/cli/app_server.py +656 -0
  111. aria_code/apps/cli/auto_update.py +125 -0
  112. aria_code/apps/cli/bootstrap.py +206 -0
  113. aria_code/apps/cli/broker_render.py +206 -0
  114. aria_code/apps/cli/chat_turn.py +1327 -0
  115. aria_code/apps/cli/cloud_models.py +231 -0
  116. aria_code/apps/cli/codegen_paths.py +29 -0
  117. aria_code/apps/cli/commands/__init__.py +34 -0
  118. aria_code/apps/cli/commands/_business_render.py +78 -0
  119. aria_code/apps/cli/commands/_ui.py +71 -0
  120. aria_code/apps/cli/commands/analysis_cmds.py +378 -0
  121. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  122. aria_code/apps/cli/commands/auth_cmds.py +215 -0
  123. aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
  124. aria_code/apps/cli/commands/broker_cmds.py +1236 -0
  125. aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
  126. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  127. aria_code/apps/cli/commands/catalog.py +189 -0
  128. aria_code/apps/cli/commands/core_cmds.py +2748 -0
  129. aria_code/apps/cli/commands/data_cmds.py +521 -0
  130. aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
  131. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  132. aria_code/apps/cli/commands/engineering_cmds.py +399 -0
  133. aria_code/apps/cli/commands/file_cmds.py +312 -0
  134. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  135. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  136. aria_code/apps/cli/commands/market.py +544 -0
  137. aria_code/apps/cli/commands/market_cmds.py +1419 -0
  138. aria_code/apps/cli/commands/market_context.py +432 -0
  139. aria_code/apps/cli/commands/market_render.py +7 -0
  140. aria_code/apps/cli/commands/model_cmds.py +2374 -0
  141. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  142. aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
  143. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  144. aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
  145. aria_code/apps/cli/commands/report.py +476 -0
  146. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  147. aria_code/apps/cli/commands/session_cmds.py +236 -0
  148. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  149. aria_code/apps/cli/commands/team.py +723 -0
  150. aria_code/apps/cli/commands/team_render.py +8 -0
  151. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  152. aria_code/apps/cli/commands/warehouse_cmds.py +297 -0
  153. aria_code/apps/cli/commands/workflow_cmds.py +629 -0
  154. aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
  155. aria_code/apps/cli/config_paths.py +73 -0
  156. aria_code/apps/cli/config_store.py +158 -0
  157. aria_code/apps/cli/constants.py +88 -0
  158. aria_code/apps/cli/context.py +34 -0
  159. aria_code/apps/cli/conversation_policy.py +53 -0
  160. aria_code/apps/cli/deterministic.py +200 -0
  161. aria_code/apps/cli/direct.py +48 -0
  162. aria_code/apps/cli/earnings_view.py +172 -0
  163. aria_code/apps/cli/endpoint_migration.py +64 -0
  164. aria_code/apps/cli/exec_events.py +255 -0
  165. aria_code/apps/cli/file_references.py +155 -0
  166. aria_code/apps/cli/finance_service_catalog.py +111 -0
  167. aria_code/apps/cli/football_reports.py +231 -0
  168. aria_code/apps/cli/github_app_auth.py +135 -0
  169. aria_code/apps/cli/google_login.py +239 -0
  170. aria_code/apps/cli/handlers/__init__.py +11 -0
  171. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  172. aria_code/apps/cli/handlers/chart_handlers.py +1314 -0
  173. aria_code/apps/cli/handlers/market_handlers.py +2570 -0
  174. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  175. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  176. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  177. aria_code/apps/cli/headless.py +352 -0
  178. aria_code/apps/cli/helpers.py +326 -0
  179. aria_code/apps/cli/hooks.py +180 -0
  180. aria_code/apps/cli/i18n.py +310 -0
  181. aria_code/apps/cli/intent.py +136 -0
  182. aria_code/apps/cli/intent_router.py +305 -0
  183. aria_code/apps/cli/intent_shadow.py +72 -0
  184. aria_code/apps/cli/intent_signals.py +347 -0
  185. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  186. aria_code/apps/cli/local_tool_schemas.py +550 -0
  187. aria_code/apps/cli/main.py +39 -0
  188. aria_code/apps/cli/market_hours.py +100 -0
  189. aria_code/apps/cli/market_metadata.py +135 -0
  190. aria_code/apps/cli/market_universe.py +487 -0
  191. aria_code/apps/cli/message_processing.py +310 -0
  192. aria_code/apps/cli/mixin_binding.py +32 -0
  193. aria_code/apps/cli/model_catalog.py +652 -0
  194. aria_code/apps/cli/model_probe.py +169 -0
  195. aria_code/apps/cli/model_retirement.py +65 -0
  196. aria_code/apps/cli/native_branding.py +12 -0
  197. aria_code/apps/cli/native_bridge.py +118 -0
  198. aria_code/apps/cli/native_entry.py +58 -0
  199. aria_code/apps/cli/pdf_report.py +977 -0
  200. aria_code/apps/cli/plan_mode.py +139 -0
  201. aria_code/apps/cli/plotly_html.py +15 -0
  202. aria_code/apps/cli/prediction_feedback.py +202 -0
  203. aria_code/apps/cli/preflight.py +502 -0
  204. aria_code/apps/cli/project_aria.py +60 -0
  205. aria_code/apps/cli/prompt_assembly.py +78 -0
  206. aria_code/apps/cli/prompts/__init__.py +0 -0
  207. aria_code/apps/cli/prompts/coding.py +71 -0
  208. aria_code/apps/cli/prompts/select.py +193 -0
  209. aria_code/apps/cli/prompts/system_prompts.py +562 -0
  210. aria_code/apps/cli/prompts/ui.py +436 -0
  211. aria_code/apps/cli/provider_endpoints.py +237 -0
  212. aria_code/apps/cli/provider_keys.py +73 -0
  213. aria_code/apps/cli/providers/__init__.py +1 -0
  214. aria_code/apps/cli/providers/base.py +775 -0
  215. aria_code/apps/cli/providers/chat_routing.py +139 -0
  216. aria_code/apps/cli/providers/collaboration.py +98 -0
  217. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  218. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  219. aria_code/apps/cli/providers/llm/ollama_stream.py +1574 -0
  220. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  221. aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
  222. aria_code/apps/cli/providers/runtime_bridge.py +586 -0
  223. aria_code/apps/cli/providers/vertexai_stream.py +592 -0
  224. aria_code/apps/cli/release_download.py +159 -0
  225. aria_code/apps/cli/review_runner.py +111 -0
  226. aria_code/apps/cli/runtime_consumer.py +769 -0
  227. aria_code/apps/cli/sandbox.py +32 -0
  228. aria_code/apps/cli/session_export.py +120 -0
  229. aria_code/apps/cli/session_jsonl.py +208 -0
  230. aria_code/apps/cli/session_refresh.py +123 -0
  231. aria_code/apps/cli/session_store.py +125 -0
  232. aria_code/apps/cli/skills_catalog.py +273 -0
  233. aria_code/apps/cli/task_isolation.py +211 -0
  234. aria_code/apps/cli/terminal_chrome.py +412 -0
  235. aria_code/apps/cli/todo_tracker.py +190 -0
  236. aria_code/apps/cli/tool_executor.py +932 -0
  237. aria_code/apps/cli/tool_registry.py +34 -0
  238. aria_code/apps/cli/tool_scope.py +113 -0
  239. aria_code/apps/cli/tools/__init__.py +40 -0
  240. aria_code/apps/cli/tools/context.py +46 -0
  241. aria_code/apps/cli/tools/file_tools.py +181 -0
  242. aria_code/apps/cli/tools/market_tools.py +626 -0
  243. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  244. aria_code/apps/cli/tools/system_tools.py +656 -0
  245. aria_code/apps/cli/tools/write_tools.py +961 -0
  246. aria_code/apps/cli/tradingview_bridge.py +529 -0
  247. aria_code/apps/cli/transactions.py +269 -0
  248. aria_code/apps/cli/turn_planning.py +137 -0
  249. aria_code/apps/cli/update_check.py +257 -0
  250. aria_code/apps/cli/updater.py +373 -0
  251. aria_code/apps/cli/utils/__init__.py +0 -0
  252. aria_code/apps/cli/utils/market_detect.py +1663 -0
  253. aria_code/apps/cli/web_login.py +136 -0
  254. aria_code/apps/cli/workflow_runner.py +127 -0
  255. aria_code/apps/cli/workspace_route.py +121 -0
  256. aria_code/apps/daemon/README.md +14 -0
  257. aria_code/apps/vscode/README.md +115 -0
  258. aria_code/apps/vscode/package.json +70 -0
  259. aria_code/aria_cli.py +5244 -0
  260. aria_code/aria_daemon.py +1372 -0
  261. aria_code/aria_feishu_bot.py +16 -0
  262. aria_code/aria_mcp_server.py +61 -0
  263. aria_code/aria_relay_client.py +4 -0
  264. aria_code/aria_telegram_bot.py +11 -0
  265. aria_code/ariarc.py +385 -0
  266. aria_code/artifacts.py +581 -0
  267. aria_code/backtest_engine.py +4 -0
  268. aria_code/backtest_report.py +4 -0
  269. aria_code/brokers/__init__.py +78 -0
  270. aria_code/brokers/automation.py +193 -0
  271. aria_code/brokers/base.py +231 -0
  272. aria_code/brokers/capabilities.py +264 -0
  273. aria_code/brokers/cn/__init__.py +10 -0
  274. aria_code/brokers/cn/easytrader_broker.py +193 -0
  275. aria_code/brokers/cn/futu_broker.py +194 -0
  276. aria_code/brokers/cn/longbridge_broker.py +190 -0
  277. aria_code/brokers/cn/tiger_broker.py +196 -0
  278. aria_code/brokers/cn/xtquant_broker.py +225 -0
  279. aria_code/brokers/config.py +414 -0
  280. aria_code/brokers/intl/__init__.py +5 -0
  281. aria_code/brokers/intl/alpaca_broker.py +183 -0
  282. aria_code/brokers/intl/ibkr_broker.py +215 -0
  283. aria_code/brokers/intl/trading212_broker.py +183 -0
  284. aria_code/brokers/intl/webull_broker.py +156 -0
  285. aria_code/brokers/paper_broker.py +266 -0
  286. aria_code/brokers/planning.py +296 -0
  287. aria_code/brokers/registry.py +213 -0
  288. aria_code/brokers/trading.py +267 -0
  289. aria_code/canva_client.py +4 -0
  290. aria_code/change_store.py +127 -0
  291. aria_code/clients/__init__.py +0 -0
  292. aria_code/clients/aliyun_data_client.py +660 -0
  293. aria_code/clients/aria_relay_client.py +16 -0
  294. aria_code/clients/canva_client.py +284 -0
  295. aria_code/clients/figma_client.py +100 -0
  296. aria_code/clients/football_data_client.py +1671 -0
  297. aria_code/clients/kling_video_client.py +182 -0
  298. aria_code/clients/market_data_client.py +1985 -0
  299. aria_code/clients/openai_image_client.py +242 -0
  300. aria_code/clients/runway_video_client.py +157 -0
  301. aria_code/clients/warehouse_erp_client.py +105 -0
  302. aria_code/cloud_memory.py +190 -0
  303. aria_code/command_safety.py +19 -0
  304. aria_code/computer_use_server.py +276 -0
  305. aria_code/computer_use_tools.py +4 -0
  306. aria_code/dashboard_generator.py +579 -0
  307. aria_code/data_analysis_tools.py +4 -0
  308. aria_code/data_cleaner.py +483 -0
  309. aria_code/data_service.py +513 -0
  310. aria_code/datasources/__init__.py +23 -0
  311. aria_code/datasources/base.py +166 -0
  312. aria_code/datasources/router.py +253 -0
  313. aria_code/datasources/sec_statements.py +217 -0
  314. aria_code/datasources/sources/__init__.py +15 -0
  315. aria_code/datasources/sources/akshare_source.py +269 -0
  316. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  317. aria_code/datasources/sources/edgar_source.py +219 -0
  318. aria_code/datasources/sources/finnhub_source.py +198 -0
  319. aria_code/datasources/sources/fred_source.py +223 -0
  320. aria_code/datasources/sources/tushare_source.py +143 -0
  321. aria_code/datasources/sources/web_scraper_source.py +278 -0
  322. aria_code/datasources/sources/world_bank_source.py +206 -0
  323. aria_code/datasources/sources/yfinance_source.py +152 -0
  324. aria_code/doctor.py +860 -0
  325. aria_code/domain/__init__.py +0 -0
  326. aria_code/domain/_finance_core/__init__.py +5 -0
  327. aria_code/domain/_finance_core/backtest_engine.py +325 -0
  328. aria_code/domain/_finance_core/performance_stats.py +101 -0
  329. aria_code/domain/backtest_engine.py +75 -0
  330. aria_code/domain/backtest_report.py +488 -0
  331. aria_code/domain/finance_formulas.py +389 -0
  332. aria_code/domain/performance_stats.py +6 -0
  333. aria_code/domain/portfolio_ledger.py +307 -0
  334. aria_code/domain/strategy_vault.py +421 -0
  335. aria_code/evals/__init__.py +37 -0
  336. aria_code/evals/behavior.py +113 -0
  337. aria_code/evals/gap.py +93 -0
  338. aria_code/evals/harness.py +949 -0
  339. aria_code/evals/review_probe.py +132 -0
  340. aria_code/evals/runner.py +425 -0
  341. aria_code/evals/scorecard.py +322 -0
  342. aria_code/evals/summary.py +125 -0
  343. aria_code/external_agent_runner.py +115 -0
  344. aria_code/figma_client.py +4 -0
  345. aria_code/file_analysis_tools.py +4 -0
  346. aria_code/finance_formulas.py +4 -0
  347. aria_code/football_data_client.py +4 -0
  348. aria_code/image_gen_tools.py +4 -0
  349. aria_code/intent_classifier.py +459 -0
  350. aria_code/kling_video_client.py +4 -0
  351. aria_code/licensing.py +151 -0
  352. aria_code/local_finance_tools.py +4 -0
  353. aria_code/local_image_provider.py +4 -0
  354. aria_code/local_llm_provider.py +4 -0
  355. aria_code/macro_tools.py +4 -0
  356. aria_code/markdown_pdf.py +289 -0
  357. aria_code/market_data_client.py +4 -0
  358. aria_code/mcp_client.py +744 -0
  359. aria_code/memory_manager.py +273 -0
  360. aria_code/model_capability.py +512 -0
  361. aria_code/notification_tools.py +4 -0
  362. aria_code/openai_image_client.py +4 -0
  363. aria_code/packages/__init__.py +26 -0
  364. aria_code/packages/adk_bridge/__init__.py +11 -0
  365. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  366. aria_code/packages/adk_bridge/market_tools.py +125 -0
  367. aria_code/packages/aria_agents/__init__.py +5 -0
  368. aria_code/packages/aria_agents/manifest.py +78 -0
  369. aria_code/packages/aria_core/__init__.py +34 -0
  370. aria_code/packages/aria_core/architecture.py +269 -0
  371. aria_code/packages/aria_core/export.py +147 -0
  372. aria_code/packages/aria_core/manifest.py +65 -0
  373. aria_code/packages/aria_core/paths.py +57 -0
  374. aria_code/packages/aria_core/secure_file.py +68 -0
  375. aria_code/packages/aria_infra/__init__.py +15 -0
  376. aria_code/packages/aria_infra/arthera.py +58 -0
  377. aria_code/packages/aria_infra/doctor.py +246 -0
  378. aria_code/packages/aria_infra/product.py +40 -0
  379. aria_code/packages/aria_mcp/__init__.py +25 -0
  380. aria_code/packages/aria_mcp/bridge.py +232 -0
  381. aria_code/packages/aria_mcp/circuit.py +82 -0
  382. aria_code/packages/aria_mcp/config.py +162 -0
  383. aria_code/packages/aria_mcp/installer.py +95 -0
  384. aria_code/packages/aria_mcp/server.py +1705 -0
  385. aria_code/packages/aria_mcp/tools.py +81 -0
  386. aria_code/packages/aria_sdk/__init__.py +19 -0
  387. aria_code/packages/aria_sdk/client.py +393 -0
  388. aria_code/packages/aria_sdk/providers.py +70 -0
  389. aria_code/packages/aria_sdk/streaming.py +75 -0
  390. aria_code/packages/aria_sdk/types.py +86 -0
  391. aria_code/packages/aria_services/__init__.py +92 -0
  392. aria_code/packages/aria_services/cache.py +60 -0
  393. aria_code/packages/aria_services/context.py +356 -0
  394. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  395. aria_code/packages/aria_services/data.py +11 -0
  396. aria_code/packages/aria_services/provider_health.py +304 -0
  397. aria_code/packages/aria_services/references.py +247 -0
  398. aria_code/packages/aria_services/registry.py +249 -0
  399. aria_code/packages/aria_services/research_protocol.py +137 -0
  400. aria_code/packages/aria_services/research_quality.py +186 -0
  401. aria_code/packages/aria_services/settings.py +160 -0
  402. aria_code/packages/aria_services/usage.py +151 -0
  403. aria_code/packages/aria_skills/__init__.py +46 -0
  404. aria_code/packages/aria_skills/catalog.py +149 -0
  405. aria_code/packages/aria_skills/loader.py +455 -0
  406. aria_code/packages/aria_skills/registry.py +84 -0
  407. aria_code/packages/aria_tools/__init__.py +5 -0
  408. aria_code/packages/aria_tools/financial/__init__.py +5 -0
  409. aria_code/packages/aria_tools/financial/_paths.py +92 -0
  410. aria_code/packages/aria_tools/registry.py +129 -0
  411. aria_code/packages/quant_engine/__init__.py +33 -0
  412. aria_code/packages/quant_engine/_namespace.py +24 -0
  413. aria_code/packages/quant_engine/backtest/__init__.py +10 -0
  414. aria_code/packages/quant_engine/backtest/core.py +53 -0
  415. aria_code/packages/quant_engine/backtest/engine.py +102 -0
  416. aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
  417. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  418. aria_code/packages/quant_engine/sports/__init__.py +76 -0
  419. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  420. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  421. aria_code/packages/quant_engine/sports/elo.py +300 -0
  422. aria_code/packages/quant_engine/sports/form.py +188 -0
  423. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  424. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  425. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  426. aria_code/packages/quant_engine/stochastic/__init__.py +31 -0
  427. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  428. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  429. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  430. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  431. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  432. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  433. aria_code/packs/__init__.py +85 -0
  434. aria_code/packs/base.py +168 -0
  435. aria_code/packs/finance/__init__.py +250 -0
  436. aria_code/packs/healthcare/__init__.py +40 -0
  437. aria_code/packs/logistics/__init__.py +171 -0
  438. aria_code/packs/payments/__init__.py +139 -0
  439. aria_code/packs/realty/__init__.py +190 -0
  440. aria_code/packs/registry.py +202 -0
  441. aria_code/packs/rules.py +90 -0
  442. aria_code/plan_utils.py +194 -0
  443. aria_code/plugin_loader.py +328 -0
  444. aria_code/portfolio_ledger.py +4 -0
  445. aria_code/preview_server.py +449 -0
  446. aria_code/privacy/__init__.py +5 -0
  447. aria_code/privacy/feedback.py +123 -0
  448. aria_code/project_review.py +278 -0
  449. aria_code/project_review_api.py +235 -0
  450. aria_code/project_review_server.py +18 -0
  451. aria_code/project_tools.py +4 -0
  452. aria_code/providers/__init__.py +30 -0
  453. aria_code/providers/google_video_provider.py +256 -0
  454. aria_code/providers/llm/__init__.py +19 -0
  455. aria_code/providers/llm/anthropic.py +184 -0
  456. aria_code/providers/llm/arthera_gateway.py +90 -0
  457. aria_code/providers/llm/autoconfig.py +213 -0
  458. aria_code/providers/llm/base.py +139 -0
  459. aria_code/providers/llm/custom.py +158 -0
  460. aria_code/providers/llm/ollama.py +137 -0
  461. aria_code/providers/llm/openai_compat.py +350 -0
  462. aria_code/providers/llm/registry.py +457 -0
  463. aria_code/providers/local_image_provider.py +202 -0
  464. aria_code/providers/local_llm_provider.py +549 -0
  465. aria_code/providers/ollama_models.py +79 -0
  466. aria_code/providers/openai_params.py +32 -0
  467. aria_code/providers/tool_messages.py +100 -0
  468. aria_code/realty_data_tools.py +4 -0
  469. aria_code/report_exporters.py +150 -0
  470. aria_code/report_generator.py +1764 -0
  471. aria_code/review_service.py +497 -0
  472. aria_code/runtime/__init__.py +176 -0
  473. aria_code/runtime/acceptance.py +632 -0
  474. aria_code/runtime/agent_loop.py +1851 -0
  475. aria_code/runtime/agent_profiles.py +75 -0
  476. aria_code/runtime/approval.py +71 -0
  477. aria_code/runtime/baseline.py +219 -0
  478. aria_code/runtime/budget.py +232 -0
  479. aria_code/runtime/checkpoints.py +481 -0
  480. aria_code/runtime/consult.py +65 -0
  481. aria_code/runtime/contract.py +282 -0
  482. aria_code/runtime/delivery.py +432 -0
  483. aria_code/runtime/events.py +113 -0
  484. aria_code/runtime/gateway.py +167 -0
  485. aria_code/runtime/lsp.py +484 -0
  486. aria_code/runtime/native_index.py +153 -0
  487. aria_code/runtime/orchestrator.py +55 -0
  488. aria_code/runtime/processes.py +343 -0
  489. aria_code/runtime/project_graph.py +614 -0
  490. aria_code/runtime/repo_map.py +712 -0
  491. aria_code/runtime/research_bundle.py +105 -0
  492. aria_code/runtime/review.py +269 -0
  493. aria_code/runtime/run_state.py +127 -0
  494. aria_code/runtime/run_store.py +522 -0
  495. aria_code/runtime/scoped_runs.py +113 -0
  496. aria_code/runtime/self_healing.py +422 -0
  497. aria_code/runtime/semantic_diff.py +171 -0
  498. aria_code/runtime/subagent.py +721 -0
  499. aria_code/runtime/symbol_edit.py +117 -0
  500. aria_code/runtime/task_brief.py +46 -0
  501. aria_code/runtime/task_context.py +145 -0
  502. aria_code/runtime/task_ledger.py +108 -0
  503. aria_code/runtime/task_orchestrator.py +90 -0
  504. aria_code/runtime/task_outcome.py +33 -0
  505. aria_code/runtime/task_verification.py +266 -0
  506. aria_code/runtime/task_worktree.py +448 -0
  507. aria_code/runtime/tool_executor.py +329 -0
  508. aria_code/runtime/tool_policy.py +107 -0
  509. aria_code/runtime/transactions.py +216 -0
  510. aria_code/runtime/windows_job.py +115 -0
  511. aria_code/runtime/workflows.py +222 -0
  512. aria_code/runtime/worktrees.py +215 -0
  513. aria_code/runway_video_client.py +4 -0
  514. aria_code/safety/__init__.py +27 -0
  515. aria_code/safety/client_isolation.py +178 -0
  516. aria_code/safety/permissions.py +339 -0
  517. aria_code/safety/risk.py +699 -0
  518. aria_code/safety/sandbox.py +148 -0
  519. aria_code/safety/service.py +102 -0
  520. aria_code/service_endpoints.py +14 -0
  521. aria_code/setup_wizard.py +684 -0
  522. aria_code/spreadsheet_tools.py +4 -0
  523. aria_code/strategy_vault.py +4 -0
  524. aria_code/tools/__init__.py +0 -0
  525. aria_code/tools/artifact_tools.py +252 -0
  526. aria_code/tools/broker_tools.py +92 -0
  527. aria_code/tools/build_quant_engine.py +97 -0
  528. aria_code/tools/code_audit_tools.py +192 -0
  529. aria_code/tools/computer_use_tools.py +509 -0
  530. aria_code/tools/data_analysis_tools.py +831 -0
  531. aria_code/tools/enterprise_finance_tools.py +202 -0
  532. aria_code/tools/extended_tools.py +240 -0
  533. aria_code/tools/file_analysis_tools.py +880 -0
  534. aria_code/tools/image_gen_tools.py +114 -0
  535. aria_code/tools/local_finance_tools.py +3488 -0
  536. aria_code/tools/logistics_carriers.py +11 -0
  537. aria_code/tools/logistics_inventory.py +11 -0
  538. aria_code/tools/logistics_io.py +11 -0
  539. aria_code/tools/logistics_tenancy.py +11 -0
  540. aria_code/tools/logistics_tools.py +11 -0
  541. aria_code/tools/macro_tools.py +412 -0
  542. aria_code/tools/notification_tools.py +249 -0
  543. aria_code/tools/project_tools.py +525 -0
  544. aria_code/tools/realty_data_tools.py +662 -0
  545. aria_code/tools/spreadsheet_tools.py +399 -0
  546. aria_code/tools/stripe_tools.py +115 -0
  547. aria_code/tools/video_tools.py +500 -0
  548. aria_code/training/__init__.py +1 -0
  549. aria_code/training/compare.py +117 -0
  550. aria_code/training/prelude_commands.py +111 -0
  551. aria_code/training/prelude_eval.py +390 -0
  552. aria_code/training/prelude_real.py +183 -0
  553. aria_code/training/prelude_train.py +248 -0
  554. aria_code/training/sft_dataset.py +230 -0
  555. aria_code/training/trajectory_dataset.py +263 -0
  556. aria_code/ui/__init__.py +105 -0
  557. aria_code/ui/banner.py +404 -0
  558. aria_code/ui/completer.py +571 -0
  559. aria_code/ui/console.py +285 -0
  560. aria_code/ui/input_box.py +505 -0
  561. aria_code/ui/mentions.py +206 -0
  562. aria_code/ui/picker.py +248 -0
  563. aria_code/ui/render/__init__.py +11 -0
  564. aria_code/ui/render/actions.py +297 -0
  565. aria_code/ui/render/finance.py +1732 -0
  566. aria_code/ui/render/market.py +253 -0
  567. aria_code/ui/render/output.py +954 -0
  568. aria_code/ui/render/responsive.py +75 -0
  569. aria_code/ui/render/team.py +367 -0
  570. aria_code/ui/robot.py +239 -0
  571. aria_code/ui/startup_dashboard.py +110 -0
  572. aria_code/video_analysis.py +147 -0
  573. aria_code/video_editor.py +518 -0
  574. aria_code/video_workflow/__init__.py +8 -0
  575. aria_code/video_workflow/__main__.py +61 -0
  576. aria_code/video_workflow/engines/__init__.py +18 -0
  577. aria_code/video_workflow/engines/base.py +69 -0
  578. aria_code/video_workflow/engines/custom_model.py +204 -0
  579. aria_code/video_workflow/engines/google_veo.py +94 -0
  580. aria_code/video_workflow/engines/local_engine.py +71 -0
  581. aria_code/video_workflow/engines/registry.py +98 -0
  582. aria_code/video_workflow/google_models.py +278 -0
  583. aria_code/video_workflow/music/__init__.py +5 -0
  584. aria_code/video_workflow/music/foley_service.py +132 -0
  585. aria_code/video_workflow/music/service.py +212 -0
  586. aria_code/video_workflow/pipeline.py +620 -0
  587. aria_code/video_workflow/plan.py +202 -0
  588. aria_code/video_workflow/session.py +297 -0
  589. aria_code/workspace/__init__.py +6 -0
  590. aria_code/workspace/files.py +219 -0
  591. aria_code/workspace/verify.py +167 -0
  592. aria_code/workspace_changes.py +199 -0
  593. aria_code/workspace_service.py +196 -0
  594. aria_code/workspace_tools.py +150 -0
  595. aria_code-0.167.0.dist-info/METADATA +330 -0
  596. aria_code-0.167.0.dist-info/RECORD +601 -0
  597. aria_code-0.167.0.dist-info/WHEEL +5 -0
  598. aria_code-0.167.0.dist-info/entry_points.txt +5 -0
  599. aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
  600. aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
  601. aria_code-0.167.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1574 @@
1
+ """stream_ollama — Ollama streaming function extracted from aria_cli.py.
2
+
3
+ Module globals are rebound to aria_cli's namespace by the function-rebind
4
+ shim in aria_cli.py after import, so all bare name references resolve correctly.
5
+ """
6
+ from __future__ import annotations
7
+ import asyncio
8
+ import re
9
+ from datetime import datetime
10
+
11
+ try:
12
+ from packages.aria_skills.loader import CORE_FILE_TOOLS as _CORE_FILE_TOOLS
13
+ except Exception:
14
+ _CORE_FILE_TOOLS: frozenset[str] = frozenset()
15
+
16
+ # Imported rather than borrowed from aria_cli's globals: this function is also
17
+ # reached without aria_cli loaded (SDK, daemon), where the rebind never runs.
18
+ from .response_cache import (
19
+ cache_get as _cache_get,
20
+ cache_key as _cache_key,
21
+ cache_set as _cache_set,
22
+ )
23
+
24
+ # ── names reclaimed from the borrow set ──────────────────────────────────────
25
+ # stream_ollama borrows 45 bare names from aria_cli's module globals. The
26
+ # rebind in aria_cli._rebind_stream_ollama MERGES rather than replaces —
27
+ # `dict(source.__globals__)` then `.update(globals())` — so anything defined
28
+ # here is the base of that merge and aria_cli still wins on conflicts. Adding a
29
+ # name here therefore cannot change the CLI path; it only gives the fallback
30
+ # path (SDK, daemon, any consumer that never imports aria_cli) a name it
31
+ # previously died on.
32
+ #
33
+ # Every name below was checked to be *the same object* aria_cli holds. That
34
+ # check is the whole point, not ceremony:
35
+ #
36
+ # · _CONFIRM_TOOLS looks importable from apps.cli.tool_executor, but that
37
+ # module's copy is a placeholder stub — an empty set() — while aria_cli's
38
+ # is the real {'edit_file', 'multi_edit', ...}. Importing the obvious
39
+ # source would have silently made every tool skip confirmation. Fail open.
40
+ # · _apply_tool_approval, _confirm_tool_execution_decision and
41
+ # _format_tool_summary resolve to different function objects in
42
+ # tool_executor than in aria_cli, and they sit on the approval path.
43
+ # · get_ariarc and build_tool_system_prompt differ only by import root
44
+ # (`ariarc` vs `aria_code.ariarc`) — importing either here would add a
45
+ # second module object rather than remove a borrow.
46
+ # · _try_prefetch_market_data exists in handlers.market_handlers, but
47
+ # aria_cli defines its own.
48
+ #
49
+ # tests/test_ollama_stream_imports.py enforces the identity rule, so this list
50
+ # cannot grow a diverging entry later.
51
+ import json
52
+ import logging
53
+ import os
54
+ import time
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+ # market_detect owns it; deterministic only re-exports. Same object either
59
+ # way, but going direct keeps this module from pulling in deterministic
60
+ # (and, now, the domain packs behind it).
61
+ from aria_code.apps.cli.utils.market_detect import _is_broker_intent
62
+ from aria_code.apps.cli.helpers import detect_ollama_models_rich
63
+ from aria_code.apps.cli.model_catalog import (
64
+ _HAS_MODEL_CAP,
65
+ get_model_cfg,
66
+ resolve_model_key,
67
+ )
68
+ from aria_code.apps.cli.prompts.coding import CODING_SYSTEM_PROMPT
69
+ from aria_code.apps.cli.tool_registry import LOCAL_TOOL_SCHEMAS
70
+ from aria_code.apps.cli.tools.market_tools import _HAS_MDC
71
+ from aria_code.ui.console import HAS_RICH, Panel, console
72
+
73
+ # Second batch. Same rule as above — each verified to be the object aria_cli
74
+ # holds, which is why _format_tool_summary is still absent: apps.cli.
75
+ # tool_executor's copy is a different function object from aria_cli's and sits
76
+ # on the tool-output path.
77
+ #
78
+ # The four intent predicates and the three message helpers were already
79
+ # extracted; aria_cli only aliases them, so importing the real modules here
80
+ # removes a borrow without moving any code. The three offline responders were
81
+ # genuinely still in aria_cli and moved to offline_responses.py — a provider's
82
+ # "backend unavailable" message belongs with the provider, and the fallback
83
+ # path could not previously build its own.
84
+ from aria_code.apps.cli.intent import (
85
+ is_analysis_request as _is_analysis_request,
86
+ is_coding_request as _is_coding_request,
87
+ is_general_knowledge as _is_general_knowledge,
88
+ is_sports_query as _is_sports_query,
89
+ )
90
+ from aria_code.apps.cli.message_processing import (
91
+ compact_messages as _compact_messages,
92
+ parse_text_tool_calls as _parse_text_tool_calls,
93
+ strip_tool_call_tags as _strip_tool_call_tags,
94
+ )
95
+ from .offline_responses import (
96
+ is_simple_greeting as _is_simple_greeting,
97
+ offline_greeting_response as _offline_greeting_response,
98
+ ollama_unavailable_result as _ollama_unavailable_result,
99
+ )
100
+
101
+ # Third batch. aria_cli held wrappers around all of these; converting those to
102
+ # aliases is what makes the import safe, because a wrapper is a different
103
+ # function object from the thing it wraps.
104
+ from aria_code.apps.cli.handlers.market_handlers import _try_prefetch_market_data
105
+ from aria_code.apps.cli.handlers.sports_handlers import (
106
+ try_prefetch_sports_data as _try_prefetch_sports_data,
107
+ )
108
+ from aria_code.apps.cli.message_processing import (
109
+ build_broker_context_block as _build_broker_context_block,
110
+ )
111
+ from aria_code.apps.cli.prompts.system_prompts import (
112
+ build_analysis_prompt_lite as _build_analysis_prompt_lite,
113
+ build_analysis_system_prompt as _build_analysis_system_prompt,
114
+ build_coding_prompt_lite as _build_coding_prompt_lite,
115
+ build_finance_prompt as _build_finance_prompt,
116
+ build_prefetched_analysis_prompt as _build_prefetched_analysis_prompt,
117
+ )
118
+
119
+ # Not an import: build_analysis_system_prompt() stamps today's date at call
120
+ # time, so this is a snapshot, and aria_cli holds its own taken when *it* was
121
+ # imported. The two are equal strings on the same day but never the same
122
+ # object, which is why the identity guard lists this name as a documented
123
+ # value-equality exception rather than skipping it. On the CLI path aria_cli's
124
+ # snapshot wins the rebind merge anyway; this one exists so the fallback path
125
+ # has one at all.
126
+ ANALYSIS_SYSTEM_PROMPT = _build_analysis_system_prompt()
127
+
128
+
129
+ def _try_inject_file_paths(_message: str) -> str:
130
+ """Legacy hook retained for the extracted stream implementation.
131
+
132
+ Context references are resolved by the typed ``@`` reference service before
133
+ provider dispatch. Silently reading path-like text here would bypass that
134
+ service's validation and duplicate context, so the legacy hook is now a
135
+ deliberate no-op.
136
+ """
137
+ return ""
138
+
139
+
140
+ def _recent_sports_quant_context(history: list, max_chars: int = 5000) -> str:
141
+ """Return the latest sports quant block from chat history for follow-ups."""
142
+ markers = (
143
+ "【泊松模型量化预测",
144
+ "【量化预测",
145
+ "最可能比分",
146
+ "可能比分",
147
+ )
148
+ for msg in reversed(history or []):
149
+ content = str(msg.get("content", "")) if isinstance(msg, dict) else ""
150
+ if not content:
151
+ continue
152
+ if "预期进球" in content and any(marker in content for marker in markers):
153
+ return content[-max_chars:]
154
+ return ""
155
+
156
+
157
+ _TOOL_NAME_ALIASES = {
158
+ "get_stock_data": "get_market_data",
159
+ "get_stock_price": "get_market_data",
160
+ "fetch_stock_data": "get_market_data",
161
+ "query_stock_data": "get_market_data",
162
+ "get_stock_history": "get_market_history",
163
+ "get_historical_data": "get_market_history",
164
+ "fetch_market_history": "get_market_history",
165
+ }
166
+
167
+
168
+ def _response_cache_eligible(message: str, history: list, enable_tools: bool) -> bool:
169
+ """Return whether a turn can safely reuse a prose-only model response."""
170
+ return not enable_tools and not history and len(message) < 300
171
+
172
+
173
+ def _routing_message_for_turn(message: str, history: list) -> str:
174
+ """Keep provider routing anchored to the user's original request.
175
+
176
+ Runtime tool rounds pass a generated ``## Tool Results`` prompt as the
177
+ current message. Classifying that synthetic prompt can switch a finance
178
+ turn into coding and expose unrelated file/shell tools.
179
+ """
180
+ current = str(message or "")
181
+ if not current.lstrip().startswith("## Tool Results"):
182
+ return current
183
+ for item in reversed(history or []):
184
+ if not isinstance(item, dict) or item.get("role") != "user":
185
+ continue
186
+ content = item.get("content", "")
187
+ if not isinstance(content, str):
188
+ continue
189
+ if content.strip() and not content.lstrip().startswith("## Tool Results"):
190
+ return content
191
+ return current
192
+
193
+
194
+ def _normalize_requested_tool_call(
195
+ tool_name: str,
196
+ params: dict,
197
+ available_names: set[str],
198
+ ) -> tuple[str, dict, str]:
199
+ """Map common model aliases and parameter names to registered tools."""
200
+ requested_name = str(tool_name or "").strip()
201
+ aliased_name = _TOOL_NAME_ALIASES.get(requested_name, requested_name)
202
+ resolved_name = aliased_name if aliased_name in available_names else requested_name
203
+ normalized_params = dict(params or {})
204
+
205
+ # Normalize symbol parameter across all market tools
206
+ if resolved_name in {"get_market_data", "get_market_history", "calculate_factors", "get_risk_metrics", "get_options_chain"}:
207
+ sym = normalized_params.get("symbol")
208
+ if not sym:
209
+ for key in ("symbols", "ticker", "tickers", "code", "stock", "asset"):
210
+ if normalized_params.get(key):
211
+ sym = normalized_params.pop(key)
212
+ break
213
+ if isinstance(sym, (list, tuple)):
214
+ sym = sym[0] if sym else ""
215
+ elif isinstance(sym, dict):
216
+ sym = sym.get("symbol") or sym.get("ticker") or sym.get("code") or ""
217
+ sym_str = str(sym or "").strip()
218
+ if (sym_str.startswith("[") and sym_str.endswith("]")) or (sym_str.startswith("(") and sym_str.endswith(")")):
219
+ sym_str = sym_str[1:-1].strip()
220
+ normalized_params["symbol"] = sym_str.strip("'\"` ").upper()
221
+
222
+ # Normalize query parameter for web_search
223
+ if resolved_name in {"web_search", "web_fetch"}:
224
+ q = (
225
+ normalized_params.get("query")
226
+ or normalized_params.get("q")
227
+ or normalized_params.get("search")
228
+ or normalized_params.get("keyword")
229
+ or normalized_params.get("keywords")
230
+ or normalized_params.get("text")
231
+ or normalized_params.get("symbol")
232
+ or ""
233
+ )
234
+ if isinstance(q, (list, tuple)):
235
+ q = " ".join(str(x) for x in q if x)
236
+ normalized_params["query"] = str(q).strip().strip("'\"`")
237
+
238
+ return resolved_name, normalized_params, requested_name
239
+
240
+
241
+ def _finance_tool_schema_allowed(
242
+ schema: dict,
243
+ allowed_names: set[str],
244
+ explicit_mcp: bool = False,
245
+ ) -> bool:
246
+ """Allow local finance tools and approved finance tools exposed over MCP."""
247
+ name = str(schema.get("function", {}).get("name", ""))
248
+ if name in allowed_names:
249
+ return True
250
+ if not name.startswith("mcp__"):
251
+ return False
252
+ short_name = name.rsplit("__", 1)[-1]
253
+ return explicit_mcp or short_name in allowed_names
254
+
255
+
256
+ def _skill_tool_schema_allowed(schema: dict, allowed_names: set[str]) -> bool:
257
+ """Match a Skill policy against local and namespaced MCP tool schemas.
258
+
259
+ Core file I/O tools (read_file, write_file, ...) are always allowed
260
+ regardless of the skill's policy — see CORE_FILE_TOOLS for why.
261
+ """
262
+ name = str(schema.get("function", {}).get("name", ""))
263
+ if name in _CORE_FILE_TOOLS or name in allowed_names:
264
+ return True
265
+ if name.startswith("mcp__"):
266
+ short_name = name.rsplit("__", 1)[-1]
267
+ return short_name in _CORE_FILE_TOOLS or short_name in allowed_names
268
+ return False
269
+
270
+
271
+ def _general_turn_prompt(message: str, history: list | None = None) -> str:
272
+ """System prompt for a knowledge or conversational turn on a local model.
273
+
274
+ This used to be one hard-coded Chinese prompt that opened by listing what
275
+ Aria can do — "金融量化分析、足球/体育赛事分析与预测(含泊松算法)、编程" —
276
+ and then gave a long section of football and Poisson rules, for every
277
+ general turn. A small local model told that will offer it back, so a bare
278
+ 你好 was answered with "是关于足球赛事还是其他方面的问题呢?".
279
+
280
+ The sports rules only mean anything when sports data is attached to the
281
+ message, so they are added for a sports query — the same test the sports
282
+ data path uses — or when the conversation already holds a prediction block
283
+ the model has to keep treating as fact ("那加拿大呢?" is not a sports query
284
+ by itself, but it is still about the match above it). Everything else gets
285
+ the neutral prompt every other general turn already uses.
286
+ """
287
+ from aria_code.apps.cli.prompts.select import _general_prompt
288
+
289
+ prompt = _general_prompt()
290
+ if _is_sports_query(message) or _recent_sports_quant_context(history or []):
291
+ prompt += (
292
+ "\n\n"
293
+ "## 体育量化分析规则(重要)\n"
294
+ "用户消息中可能包含两种特殊数据块:\n\n"
295
+ "### 【比赛信息】块\n"
296
+ "= football-data.org API 获取的真实赛程(比赛时间、状态、比分)。这是**事实**,不要质疑。\n\n"
297
+ "### 【泊松模型量化预测】块\n"
298
+ "= Aria 用 Dixon-Coles 泊松算法计算的量化结果,包含:\n"
299
+ " - 两队 FIFA 排名强度参数(进攻/防守)\n"
300
+ " - 预期进球数(λ值)\n"
301
+ " - 主胜/平局/客胜概率(%)\n"
302
+ " - 最可能比分及其概率\n"
303
+ " - 隐含赔率\n"
304
+ "当收到此块时,你应当:\n"
305
+ "1. **直接引用数字**(「算法显示加拿大胜率 42.4%」),不要重新计算或质疑\n"
306
+ "2. **解释概率背后的逻辑**:FIFA 排名差距、进攻强度对比说明了什么\n"
307
+ "3. **分析高频比分区间**:比如 1-0/1-1/2-1 集中说明比赛预计紧张胶着\n"
308
+ "4. **补充战术/球员层面的定性分析**(这是你的训练知识,算法没有的部分)\n"
309
+ "5. **绝对不要**说「我没有实时数据」「世界杯尚未开始」「不包含实时数据」「以上预测基于历史」——这些与上方数据矛盾\n"
310
+ "6. **绝对不要**在末尾添加「若需最新数据请使用/football命令」之类的建议——用户已经有数据了\n\n"
311
+ )
312
+ return prompt
313
+
314
+
315
+ async def stream_ollama(ollama_url: str, message: str, history: list,
316
+ model: str = "qwen2.5:7b",
317
+ on_token=None, on_thinking=None,
318
+ on_tool_call=None, on_tool_result=None,
319
+ cancel_event: asyncio.Event = None,
320
+ enable_tools: bool = True,
321
+ tool_schemas: list = None,
322
+ defer_tool_execution: bool = False,
323
+ system_override: str = None,
324
+ show_market_prefetch_status: bool = True,
325
+ include_ambient_context: bool = True) -> dict:
326
+ """Stream chat via local Ollama with tool calling support (native + text-based)."""
327
+ import aiohttp
328
+
329
+ # Check before response-cache lookup or task routing. Missing models must
330
+ # not produce a cached answer or silently use a different model.
331
+ from aria_code.providers.ollama_models import ollama_base_url, resolve_model_async
332
+ try:
333
+ ollama_url = ollama_base_url({"ollama_base_url": ollama_url})
334
+ model = await resolve_model_async(ollama_url, model)
335
+ except ValueError as exc:
336
+ return {"success": False, "response": "", "error": str(exc), "provider": "ollama", "fallback_allowed": False}
337
+
338
+ from model_capability import get_model_capability
339
+
340
+ _intent_message = _routing_message_for_turn(message, history)
341
+
342
+ # ── Response cache: skip Ollama for repeated stateless queries ───────────
343
+ # Only cache when there is no conversation history (stateless), the query
344
+ # is short (likely a simple quote/concept), and no tools are being called.
345
+ # A cached prose answer cannot stand in for a tool-capable turn: returning
346
+ # it here bypasses schema injection and the runtime tool loop entirely.
347
+ _should_cache = include_ambient_context and _response_cache_eligible(message, history, enable_tools)
348
+ if _should_cache:
349
+ _ck = _cache_key(f"{ollama_url}::{model}", message)
350
+ _cached = _cache_get(_ck)
351
+ if _cached:
352
+ if on_token:
353
+ on_token(_cached)
354
+ return {"success": True, "response": _cached,
355
+ "provider": "ollama_cache", "usage": {}}
356
+
357
+ _models_probe, _ollama_err = detect_ollama_models_rich(ollama_url)
358
+ if _ollama_err:
359
+ if _is_simple_greeting(_intent_message):
360
+ return _offline_greeting_response()
361
+ return _ollama_unavailable_result(ollama_url, _ollama_err)
362
+
363
+ # ── 五档路由:通过 Prelude 意图分类器(或关键词 fallback)决定 prompt ────
364
+ # Always rebuild finance prompt to get today's date
365
+ _finance_prompt = _build_finance_prompt(_intent_message)
366
+
367
+ try:
368
+ from intent_classifier import (
369
+ classify_intent_async,
370
+ INTENT_CODING, INTENT_ANALYSIS, INTENT_REALTIME,
371
+ INTENT_GENERAL, INTENT_FINANCE,
372
+ )
373
+ _intent = await classify_intent_async(_intent_message, ollama_url)
374
+ except Exception:
375
+ # Fallback to legacy keyword detection if intent_classifier unavailable
376
+ if _is_coding_request(_intent_message):
377
+ _intent = "coding"
378
+ elif _is_analysis_request(_intent_message):
379
+ _intent = "analysis"
380
+ elif _is_general_knowledge(_intent_message):
381
+ _intent = "general"
382
+ else:
383
+ _intent = "finance"
384
+
385
+ # The lightweight classifier can label stock-analysis prompts as generic
386
+ # finance. Preserve the stronger deterministic signal so tool-capable
387
+ # models receive the prompt that requires market data before analysis.
388
+ if _is_analysis_request(_intent_message) and _intent in {"general", "finance"}:
389
+ _intent = "analysis"
390
+
391
+ _is_general = (_intent == "general")
392
+ try:
393
+ from apps.cli.intent_router import build_intent_route
394
+ _route = build_intent_route(_intent_message)
395
+ except Exception:
396
+ _route = None
397
+
398
+ # Resolve portable workflow metadata before tool schemas are selected. This
399
+ # lets a verified Skill narrow the tools exposed to the model, rather than
400
+ # treating its declared policy as prompt-only guidance.
401
+ _is_tool_followup = message.lstrip().startswith("## Tool Results")
402
+ _skill_activation = None
403
+ if include_ambient_context:
404
+ try:
405
+ from packages.aria_skills import activate_external_skills as _activate_external_skills
406
+
407
+ _skill_activation = _activate_external_skills(
408
+ _intent_message,
409
+ record=not _is_tool_followup,
410
+ )
411
+ except Exception as _skill_exc:
412
+ logger.debug("skill activation failed: %s", _skill_exc)
413
+
414
+ # ── Context-aware tool schema filtering ───────────────────────────────────
415
+ # Intent drives tool exposure, but cross-intent requests (e.g. "分析AAPL然后
416
+ # 写一个回测策略") need BOTH market data tools AND coding tools.
417
+ # Detect overlap by checking if the message contains signals from both domains.
418
+ _CU_TOOL_NAMES = {"browser_navigate", "browser_screenshot",
419
+ "computer_screenshot", "computer_action"}
420
+ _CODE_TOOL_NAMES = {"read_file", "write_file", "edit_file", "list_files",
421
+ "search_code", "run_command", "process", "github",
422
+ "glob", "notebook_read", "notebook_edit"}
423
+ _FINANCE_TOOL_NAMES = {
424
+ "get_market_data", "get_market_history", "calculate_factors",
425
+ "get_risk_metrics", "analyze_news",
426
+ "get_market_insights", "get_predictions", "peer_comparison",
427
+ "web_search", "web_fetch",
428
+ "research_report_assess",
429
+ "research_run_create", "research_run_get", "research_run_transition",
430
+ "research_run_attach_artifact", "research_run_record_quality",
431
+ "research_run_events",
432
+ }
433
+
434
+ _available_tool_schemas = list(
435
+ LOCAL_TOOL_SCHEMAS if tool_schemas is None else tool_schemas
436
+ )
437
+ _msg_low = _intent_message.lower()
438
+ _explicit_tool_signal = any(k in _msg_low for k in (
439
+ "调用工具", "使用工具", "用工具", "调用 agent", "使用 agent",
440
+ "调用智能体", "使用智能体", "mcp", "tool call", "use tool",
441
+ "use agent", "spawn agent",
442
+ ))
443
+ _explicit_agent_signal = any(k in _msg_low for k in (
444
+ "agent", "智能体", "多代理", "多智能体", "研究团队", "subagent",
445
+ ))
446
+ _explicit_mcp_signal = "mcp" in _msg_low
447
+ _finance_tool_names = set(_FINANCE_TOOL_NAMES)
448
+ if any(k in _msg_low for k in ("加密", "比特币", "以太坊", "crypto", "btc", "eth")):
449
+ _finance_tool_names.add("get_crypto_data")
450
+ if any(k in _msg_low for k in ("外汇", "汇率", "forex", "fx", "usd/", "eur/")):
451
+ _finance_tool_names.add("get_forex_data")
452
+ if any(k in _msg_low for k in ("持仓", "账户", "下单", "broker", "portfolio", "position")):
453
+ _finance_tool_names.update({"broker_query", "broker_order", "get_risk_metrics"})
454
+ if any(k in _msg_low for k in ("期权", "option", "隐含波动率")):
455
+ _finance_tool_names.add("get_options_chain")
456
+ if _explicit_agent_signal:
457
+ _finance_tool_names.update({
458
+ "spawn_task", "task_status", "task_result", "task_list",
459
+ "task_cancel", "update_todos",
460
+ })
461
+ _explicit_code_signal = bool(getattr(_route, "explicit_code", False)) if _route else any(k in _msg_low for k in (
462
+ "代码", "脚本", "python", "程序", "实现", "开发", "修改文件",
463
+ "写代码", "编写代码", "策略代码", "保存为.py", ".py",
464
+ "script", "code", "program", "implement", "edit file", "write file",
465
+ ))
466
+ _is_visual_artifact_request = bool(getattr(_route, "visual_artifact", False)) if _route else any(k in _msg_low for k in (
467
+ "图表", "走势图", "k线图", "k线", "chart", "plot", "dashboard", "看板", "report", "报告",
468
+ ))
469
+ _has_coding_signal = _explicit_code_signal or (
470
+ not _is_visual_artifact_request
471
+ and any(k in _msg_low for k in ("写", "回测", "backtest", "save", "file"))
472
+ )
473
+ _has_finance_signal = any(k in _msg_low for k in (
474
+ "分析", "股票", "行情", "股价", "市场", "quantitative", "stock",
475
+ "price", "market", "analyze", "analysis", "ticker",
476
+ ))
477
+ _is_cross_intent = _has_coding_signal and _has_finance_signal
478
+
479
+ if _is_visual_artifact_request and not _explicit_code_signal:
480
+ _excluded = _CU_TOOL_NAMES | _CODE_TOOL_NAMES
481
+ _schemas_for_context = [
482
+ s for s in _available_tool_schemas
483
+ if s.get("function", {}).get("name") not in _excluded
484
+ ]
485
+ elif _intent in ("finance", "analysis", "realtime") and not _is_cross_intent:
486
+ # Keep finance turns focused. MCP tools remain available because they
487
+ # may provide institution-specific data, while file/code tools are
488
+ # excluded so the model does not search the repository for a quote API.
489
+ _schemas_for_context = [
490
+ s for s in _available_tool_schemas
491
+ if _finance_tool_schema_allowed(
492
+ s,
493
+ _finance_tool_names,
494
+ explicit_mcp=_explicit_mcp_signal,
495
+ )
496
+ ]
497
+ elif _is_cross_intent:
498
+ # Cross-intent: expose both finance AND coding tools (minus CU/browser).
499
+ # Intent hint injected into system prompt guides priority without hard exclusion.
500
+ _schemas_for_context = [
501
+ s for s in _available_tool_schemas
502
+ if s.get("function", {}).get("name") not in _CU_TOOL_NAMES
503
+ ]
504
+ else:
505
+ # Coding/general: all local tools except CU (browser/computer control)
506
+ _schemas_for_context = [
507
+ s for s in _available_tool_schemas
508
+ if s.get("function", {}).get("name") not in _CU_TOOL_NAMES
509
+ ]
510
+
511
+ _skill_allowed_tools = {
512
+ tool_name
513
+ for skill in (_skill_activation.skills if _skill_activation else ())
514
+ for tool_name in skill.policy.allowed_tools
515
+ }
516
+ if _skill_allowed_tools:
517
+ _schemas_for_context = [
518
+ schema for schema in _schemas_for_context
519
+ if _skill_tool_schema_allowed(schema, _skill_allowed_tools)
520
+ ]
521
+
522
+ # ── Select prompt size based on model capability ─────────────────────────
523
+ # Small / nano models (≤3B) cannot effectively use the full CODING_SYSTEM_PROMPT
524
+ # (6000+ tokens of examples they mostly ignore). Send a condensed version that
525
+ # keeps the essential rules and the single complete working template.
526
+ #
527
+ # Analysis uses the full tool-aware prompt for capable models and the lite
528
+ # prefetched-data prompt for small/text-only models.
529
+ try:
530
+ from model_capability import get_model_capability as _gmc
531
+ _prompt_cap = _gmc(model)
532
+ _model_size = _prompt_cap.size_class
533
+ _model_supports_native_tools = bool(
534
+ _prompt_cap.tool_calls and _prompt_cap.format == "ollama_native"
535
+ )
536
+ except Exception:
537
+ _model_size = "medium"
538
+ _model_supports_native_tools = False
539
+ _use_lite_prompt = _model_size in ("nano", "small")
540
+
541
+ if _intent == "coding":
542
+ _base_prompt = _build_coding_prompt_lite(_intent_message) if _use_lite_prompt else CODING_SYSTEM_PROMPT
543
+ elif _intent == "analysis":
544
+ # Native-tool models must receive the prompt that explicitly requires
545
+ # get_market_data. The lite prompt is the fallback for text-only/small
546
+ # models and tells them to rely on deterministic prefetched data.
547
+ _base_prompt = (
548
+ ANALYSIS_SYSTEM_PROMPT
549
+ if _model_supports_native_tools and not _use_lite_prompt
550
+ else _build_analysis_prompt_lite(_intent_message)
551
+ )
552
+ elif _intent == "general":
553
+ # 纯知识/概念问题:不注入工具 schema
554
+ _base_prompt = _general_turn_prompt(_intent_message, history)
555
+ else:
556
+ # realtime / finance: use full finance prompt with tool access
557
+ _base_prompt = _finance_prompt
558
+
559
+ # Project context injection: skip or condense for small/nano models.
560
+ # A 1.5B model with a 4000-token README injected into its context will
561
+ # either copy the README into its response or hallucinate beyond recovery.
562
+ _small_model = _model_size in ("nano", "small")
563
+ if include_ambient_context and not _is_general:
564
+ if not _small_model and _PROJECT_CONTEXT:
565
+ system_prompt = _base_prompt + _PROJECT_CONTEXT
566
+ else:
567
+ # For small models: skip the full README, only keep a 2-line summary
568
+ _ctx_brief = ""
569
+ if _PROJECT_CONTEXT:
570
+ _first_lines = [l for l in _PROJECT_CONTEXT.split("\n") if l.strip()][:3]
571
+ _ctx_brief = "\n# Context: " + " | ".join(_first_lines[:2]) + "\n"
572
+ system_prompt = _base_prompt + _ctx_brief
573
+ else:
574
+ system_prompt = _base_prompt
575
+
576
+ # Inject cross-intent hint so the model knows to use both tool sets in order
577
+ if _is_cross_intent:
578
+ _cross_hint = (
579
+ "\n\n## Task Hint\n"
580
+ "This request spans both **market analysis** and **code generation**. "
581
+ "Suggested order: (1) fetch market data first, (2) use the data to write/run code. "
582
+ "Use get_market_data for live prices, then write_file + run_command for scripts.\n"
583
+ )
584
+ system_prompt = system_prompt + _cross_hint
585
+
586
+ # Prepend global user memory (user profile, project history, preferences)
587
+ if include_ambient_context:
588
+ try:
589
+ from memory_manager import MemoryManager as _MM
590
+ _mem_block = _MM().load_context(max_chars=500)
591
+ if _mem_block:
592
+ system_prompt = _mem_block + "\n" + system_prompt
593
+ except Exception:
594
+ pass
595
+
596
+ # Append ariarc project context if available — small models skip this
597
+ if include_ambient_context and _HAS_ARIARC and not _is_general and not _small_model:
598
+ try:
599
+ _arc = get_ariarc()
600
+ _arc_block = _arc.build_system_prompt_block()
601
+ if _arc_block:
602
+ # Hard-cap ariarc block at 800 chars to prevent context overflow
603
+ _arc_short = _arc_block[:800] + ("…" if len(_arc_block) > 800 else "")
604
+ system_prompt = system_prompt + "\n\n" + _arc_short
605
+ except Exception:
606
+ pass
607
+
608
+ # Inject live broker context when user is asking about their own portfolio,
609
+ # or when a broker is connected and the message is finance-related.
610
+ if include_ambient_context and _is_broker_intent(_intent_message):
611
+ _broker_ctx = _build_broker_context_block()
612
+ if _broker_ctx:
613
+ system_prompt = system_prompt + "\n\n" + _broker_ctx
614
+
615
+ # Allow /file analyze and other commands to inject a specialist role override
616
+ if system_override:
617
+ system_prompt = system_override + "\n\n" + system_prompt
618
+
619
+ url = f"{ollama_url}/api/chat"
620
+ _mcfg = get_model_cfg(model)
621
+
622
+ if _HAS_MODEL_CAP:
623
+ _cap = get_model_capability(model)
624
+ _num_ctx = _cap.context_window
625
+ _temperature = _cap.temperature
626
+ else:
627
+ _num_ctx = _mcfg.get("num_ctx", 16384)
628
+ _temperature = _mcfg.get("temperature", 0.3)
629
+ _max_tokens = _mcfg.get("max_tokens", min(_mcfg.get("num_ctx", 8192) // 4, 8192))
630
+ _mkey = resolve_model_key(model)
631
+
632
+ # ── 上下文硬截断:保留 80% 上下文给历史,防止溢出 ────────────────────
633
+ # 用 1.5 chars/token(CJK 混合文本实际比率)而非英文假设的 4 chars/token
634
+ _chars_per_tok = 1.5
635
+ _ctx_chars_for_hist = int(_num_ctx * 0.80 * _chars_per_tok) - len(system_prompt) - len(message) - 512
636
+ _ctx_chars_limit = max(_ctx_chars_for_hist, 1000)
637
+ # 从最新历史往前选,确保总字符数不超限
638
+ _trimmed_history: list = []
639
+ _hist_chars = 0
640
+ for _hm in reversed(history):
641
+ _hm_len = len(_hm.get("content",""))
642
+ if _hist_chars + _hm_len > _ctx_chars_limit:
643
+ break
644
+ _trimmed_history.insert(0, _hm)
645
+ _hist_chars += _hm_len
646
+
647
+ messages = [{"role": "system", "content": system_prompt}]
648
+ for msg in _trimmed_history:
649
+ prepared_msg = {
650
+ "role": msg["role"],
651
+ "content": msg.get("content", ""),
652
+ }
653
+ for key in ("tool_calls", "tool_call_id", "name"):
654
+ if msg.get(key) is not None:
655
+ prepared_msg[key] = msg[key]
656
+ messages.append(prepared_msg)
657
+ # Runtime adapters pass history and the current prompt separately. Legacy
658
+ # callers may already have appended that exact prompt, so deduplicate by
659
+ # content rather than by role; a tool-result message is also role=user and
660
+ # must not cause the next agent instruction to be dropped.
661
+ _last_is_current = bool(
662
+ _trimmed_history
663
+ and _trimmed_history[-1].get("role") == "user"
664
+ and _trimmed_history[-1].get("content") == message
665
+ )
666
+ if not _last_is_current:
667
+ messages.append({"role": "user", "content": message})
668
+
669
+ # ── 工具注入:通识问答跳过,同时跳过无法可靠调用工具的小模型 ──────────
670
+ # 判断模型是否具备工具调用能力(text_only / format不支持的都跳过)
671
+ _model_can_use_tools = False
672
+ if (
673
+ _HAS_MODEL_CAP
674
+ and enable_tools
675
+ and _available_tool_schemas
676
+ and (not _is_general or _explicit_tool_signal)
677
+ ):
678
+ _tool_cap = get_model_capability(model)
679
+ # 只有明确支持工具且 context_window >= 8192 的模型才注入 tool schema
680
+ _model_can_use_tools = (
681
+ _tool_cap.format != "text_only"
682
+ and _tool_cap.context_window >= 8192
683
+ )
684
+ if _model_can_use_tools:
685
+ _tool_sys = build_tool_system_prompt(_schemas_for_context, model)
686
+ if _tool_sys and messages:
687
+ if messages[0].get("role") == "system":
688
+ messages[0]["content"] += _tool_sys
689
+ else:
690
+ messages.insert(0, {"role": "system", "content": _tool_sys.strip()})
691
+
692
+ # ── 实时数据预取:始终为分析/报价查询预取真实市场数据注入 prompt ──────────
693
+ # 无论模型是否支持工具调用,都注入真实数据,防止模型生成占位符($X.XX)
694
+ # 策略:
695
+ # 1. system prompt 替换为"数据已预取"专用 prompt
696
+ # 2. 数据同时注入到用户消息开头(本地模型对最近的 user message 最敏感)
697
+ _skip_market_prefetch = (
698
+ _is_general
699
+ or _is_visual_artifact_request
700
+ or _model_can_use_tools
701
+ )
702
+ if include_ambient_context and _HAS_MDC and not _skip_market_prefetch:
703
+ import time as _t_inj
704
+ _t_inj_start = _t_inj.time()
705
+ _market_inject = _try_prefetch_market_data(_intent_message, history)
706
+ _t_inj_ms = int((_t_inj.time() - _t_inj_start) * 1000)
707
+ if _market_inject:
708
+ # 过程可见化:⏺/✓ 格式,与工具调用步骤保持一致
709
+ import re as _re_inj
710
+ _inj_m = _re_inj.search(r'## 📊 (\S+) 实时行情(来源:(\S+))', _market_inject)
711
+ if HAS_RICH and show_market_prefetch_status:
712
+ _sym_label = _inj_m.group(1) if _inj_m else "market_data"
713
+ _src_label = _inj_m.group(2) if _inj_m else "local"
714
+ console.print(
715
+ f"\n [#C08050]⏺[/#C08050] [bold]market_data[/bold]"
716
+ f" [dim]{_sym_label} · {_src_label}[/dim]"
717
+ )
718
+ console.print(
719
+ f" [green]✓[/green] [dim]实时行情已注入[/dim]"
720
+ f" [dim]({_t_inj_ms}ms)[/dim]"
721
+ )
722
+ # Replace system prompt with data-first prompt.
723
+ # Use nano variant for 1-3B models (no template placeholders).
724
+ _is_nano_model = _use_lite_prompt or _model_size in ("nano", "small")
725
+ _prefetched_sys = _build_prefetched_analysis_prompt(nano=_is_nano_model, user_message=_intent_message)
726
+ if messages and messages[0].get("role") == "system":
727
+ messages[0]["content"] = _prefetched_sys
728
+ else:
729
+ messages.insert(0, {"role": "system", "content": _prefetched_sys})
730
+ # Prepend real data to the user message so the model sees it last
731
+ # (most recent = highest attention weight for local models).
732
+ _augmented_user = (
733
+ _market_inject
734
+ + "\n---\n"
735
+ "上面是真实实时数据。请只使用这些具体数字作答,不要引用训练记忆中的历史价格。\n\n"
736
+ + message
737
+ )
738
+ for _mi in reversed(messages):
739
+ if _mi.get("role") == "user":
740
+ _mi["content"] = _augmented_user
741
+ break
742
+
743
+ # Portable SKILL.md workflows are loaded after prompt/prefetch selection so
744
+ # they survive the data-first prompt replacement used by local models.
745
+ try:
746
+ _skill_block = _skill_activation.prompt_block
747
+ if _skill_block and messages:
748
+ if messages[0].get("role") == "system":
749
+ messages[0]["content"] += "\n\n" + _skill_block
750
+ else:
751
+ messages.insert(0, {"role": "system", "content": _skill_block})
752
+ if not _is_tool_followup:
753
+ for _active_skill in _skill_activation.skills:
754
+ if HAS_RICH:
755
+ console.print(
756
+ f" [#C08050]⏺[/#C08050] [bold]skill[/bold] "
757
+ f"[dim]{_active_skill.qualified_name} · "
758
+ f"{_active_skill.integrity}[/dim]"
759
+ )
760
+ else:
761
+ print(
762
+ f" skill {_active_skill.qualified_name} · "
763
+ f"{_active_skill.integrity}"
764
+ )
765
+ except (AttributeError, TypeError) as _skill_exc:
766
+ logger.debug("skill prompt injection failed: %s", _skill_exc)
767
+
768
+ # ── 体育赛事数据预取:sports query → inject live scores / WC data + Poisson ─
769
+ if _is_general and _is_sports_query(_intent_message):
770
+ _sports_ctx = _try_prefetch_sports_data(_intent_message) if include_ambient_context else ""
771
+ if not _sports_ctx:
772
+ _sports_ctx = _recent_sports_quant_context(history)
773
+ if _sports_ctx:
774
+ _has_quant = (
775
+ "泊松模型量化预测" in _sports_ctx
776
+ or ("预期进球" in _sports_ctx and "可能比分" in _sports_ctx)
777
+ )
778
+ if HAS_RICH:
779
+ _label = "[bold #50A0C0]sports_data+quant[/bold #50A0C0]" if _has_quant else "[bold #50A0C0]sports_data[/bold #50A0C0]"
780
+ console.print(f" {_label} [dim]赛事数据{'+ 泊松预测 ' if _has_quant else ''}已注入[/dim]")
781
+ # Print Poisson block directly so user sees it even if LLM ignores context
782
+ if _has_quant and not _ARIA_BOT_MODE:
783
+ console.print(Panel(
784
+ _sports_ctx,
785
+ title="[bold]⚽ 量化预测数据[/bold]",
786
+ border_style="cyan",
787
+ padding=(0, 1),
788
+ ))
789
+ for _mi in reversed(messages):
790
+ if _mi.get("role") == "user":
791
+ if _has_quant:
792
+ _injection_note = (
793
+ "\n---\n"
794
+ "## 数据说明\n"
795
+ "以上数据来自 football-data.org 实时 API + 泊松量化模型(Aria 本地计算)。\n"
796
+ f"【比赛信息】块 = API 真实赛程数据,获取日期为 {datetime.now().strftime('%Y-%m-%d')}。\n"
797
+ "【泊松模型量化预测】块 = Aria 使用 Dixon-Coles 泊松分布对此场比赛运行的算法结果。\n\n"
798
+ "## 你的任务\n"
799
+ "1. **直接引用预测数据中的概率数字**(如「加拿大胜率 42.4%」),不要说你没有数据\n"
800
+ "2. 解释为什么会有这样的概率分布(结合两队 FIFA 排名、进攻/防守强度)\n"
801
+ "3. 如果用户要“一个以上/多个/最准比分”,必须按 `top_scorelines` 概率降序列出候选比分和概率\n"
802
+ "4. 分析最可能的比分区间(高频比分说明比赛预计紧张/单方碾压)\n"
803
+ "5. 可以给出走势判断,但不要编造射正率、最近5场客场数据、历史交锋次数等输入数据之外的具体事实\n"
804
+ "6. 注意区分胜平负概率和准确比分概率:热门球队胜率最高,不代表最可能的单一比分一定是该队获胜\n"
805
+ "7. 不要否认上方已提供的赛程或模型数据;如需说明限制,具体指出缺失字段\n"
806
+ "8. **严禁**在回复末尾添加任何类似以下内容的免责声明:\n"
807
+ " - 「以上预测基于历史...并不包含实时数据」\n"
808
+ " - 「不包含实时数据或赛前最新信息」\n"
809
+ " - 「若需赛前最新数据,请使用 /football 命令」\n"
810
+ " 上方已有实时 API 数据 + 量化模型,这些免责声明与数据矛盾,请勿添加。\n\n"
811
+ )
812
+ else:
813
+ _injection_note = (
814
+ "\n---\n"
815
+ "以上是从 football-data.org 获取的赛事数据。\n"
816
+ "请基于这些数据给出分析,若数据不完整可结合训练知识合理推断。\n\n"
817
+ )
818
+ _mi["content"] = _sports_ctx + _injection_note + message
819
+ break
820
+
821
+ # ── 文件路径自动注入:若用户消息引用了本地文件,预读并注入内容 ────────────
822
+ # 无论意图是什么,只要消息里有可读的文件路径就注入(coding / analysis 均有效)
823
+ _file_inject = _try_inject_file_paths(_intent_message) if include_ambient_context else ""
824
+ if _file_inject:
825
+ for _mi in reversed(messages):
826
+ if _mi.get("role") == "user":
827
+ _mi["content"] = _file_inject + _mi["content"]
828
+ break
829
+
830
+ # ── Token budget 分级策略 ────────────────────────────────────────────────
831
+ # 小模型(<8K ctx)防止无限延伸;通识问答分两档:
832
+ # · 纯问候/一句话问题 → 200 tokens(快速)
833
+ # · 知识解释问题("什么是X", "如何…") → 1500 tokens(保证完整性)
834
+ # · 正常问题 → 模型 max_tokens 配置值
835
+ _is_small_model = _HAS_MODEL_CAP and get_model_capability(model).context_window < 8192
836
+ _is_greeting = _is_simple_greeting(_intent_message)
837
+ _wants_complete_output = any(k in _intent_message.lower() for k in (
838
+ "完整", "完整输出", "完整给出", "全面", "详细", "不要中断", "不要截断",
839
+ "complete", "full output", "comprehensive", "do not stop", "don't stop",
840
+ "do not truncate", "end-to-end",
841
+ ))
842
+
843
+ if _is_greeting:
844
+ # Reasoning-capable Ollama Cloud models can spend more than 200 tokens
845
+ # in ``message.thinking`` before emitting the short visible greeting.
846
+ _effective_max_tokens = 512 if (
847
+ _HAS_MODEL_CAP and get_model_capability(model).thinking
848
+ ) else 200
849
+ elif _wants_complete_output:
850
+ _complete_cap = max(2048, min(8192, int(_num_ctx * 0.45)))
851
+ _effective_max_tokens = max(_max_tokens, _complete_cap)
852
+ elif _is_general:
853
+ _effective_max_tokens = max(1500, min(_max_tokens, 4096)) # 足够完整回答概念解释,不截断
854
+ elif _use_lite_prompt:
855
+ # Small/nano model: coding tasks need more room for complete scripts;
856
+ # analysis/finance keep a tighter cap to prevent runaway echo generation.
857
+ if _intent == "coding":
858
+ _effective_max_tokens = 2000
859
+ else:
860
+ _effective_max_tokens = 512
861
+ elif _is_small_model:
862
+ _effective_max_tokens = min(_max_tokens, 2048)
863
+ else:
864
+ _effective_max_tokens = _max_tokens
865
+
866
+ # 停止词:覆盖常见 hallucination 模式
867
+ # 包含:英文求助模板、工具执行幻觉、"任务就绪"尾部幻觉(中文小模型常见)
868
+ _stop_seqs = [
869
+ # ── 英文求助/拒绝模板 ─────────────────────────────────────────────
870
+ "I'm sorry, as an AI",
871
+ "I'm sorry for any confusion",
872
+ "I cannot perform",
873
+ "I can't perform",
874
+ "Do You Need Help",
875
+ "Are There Specific Areas",
876
+ "Let us brainstorm together",
877
+ "AWAITING FEEDBACK",
878
+ "Would love more context",
879
+ "Please provide more details",
880
+ "Could you please provide",
881
+ "Without knowing those specifics",
882
+ "os.system('pip install",
883
+ "git clone https://github.com",
884
+ "Let's download these libraries",
885
+ # ── 中文"任务就绪"尾部幻觉(小模型在回答结束后常产生) ────────────
886
+ "好的,我将开始执行任务",
887
+ "好的,我已经准备好了要做的工作",
888
+ "请告诉我您希望我在接下来做什么",
889
+ "请问有什么我可以帮助您的吗",
890
+ "请告诉我你需要什么帮助",
891
+ "我会尽快为您完成这项任务",
892
+ "如果您有任何其他问题,请随时告诉我",
893
+ "如果你有其他问题,请随时提问",
894
+ # ── 英文任务就绪幻觉 ─────────────────────────────────────────────
895
+ "I'm ready to help with your next",
896
+ "Let me know if you need anything else",
897
+ "Is there anything else you'd like me to",
898
+ "Feel free to ask if you have more questions",
899
+ # ── 工具调用幻觉(声称已调用但实际没有 tool_call 事件)────────────
900
+ "I have already called `get_market_data`",
901
+ "I have already called `get_stock_price`",
902
+ "I have already called get_market_data",
903
+ "I have already fetched",
904
+ "I have already retrieved",
905
+ "我已经调用了",
906
+ "我已调用工具",
907
+ "我已经获取了最新数据",
908
+ # ── 模板占位符输出(模型把 system prompt 模板当内容输出)────────────
909
+ "${real_price_from_data",
910
+ "${data['day_range']}",
911
+ "{actual date today}",
912
+ "{real price from data}",
913
+ "List real recent headlines",
914
+ ]
915
+
916
+ payload = {
917
+ "model": model, "messages": messages, "stream": True,
918
+ "options": {
919
+ "num_ctx": _num_ctx,
920
+ "temperature": _temperature,
921
+ "top_p": 0.9,
922
+ "repeat_penalty": 1.4,
923
+ "repeat_last_n": 256,
924
+ "num_predict": _effective_max_tokens,
925
+ },
926
+ "stop": _stop_seqs,
927
+ }
928
+ # Only inject native Ollama tools field for capable models.
929
+ # Small (≤4K ctx) or text_only models must never receive tool schemas —
930
+ # they produce malformed partial JSON that leaks into the output stream.
931
+ if _model_can_use_tools:
932
+ _cap2 = get_model_capability(model) if _HAS_MODEL_CAP else None
933
+ if _cap2 and _cap2.tool_calls and _cap2.format == "ollama_native":
934
+ payload["tools"] = _schemas_for_context
935
+
936
+ full_response = ""
937
+ response_segments = []
938
+ tools_used = []
939
+ tool_calls_pending = []
940
+ _tool_call_counts = {}
941
+ _tool_name_counts = {}
942
+ _available_tool_names = {
943
+ s.get("function", {}).get("name", "")
944
+ for s in _schemas_for_context
945
+ if s.get("function", {}).get("name")
946
+ }
947
+ max_tool_rounds = 25 if _wants_complete_output or _intent == "coding" else 12
948
+ _server_retry_budget = 2
949
+ _continuation_count = 0
950
+ usage = {"prompt_tokens": 0, "completion_tokens": 0, "thinking_tokens": 0}
951
+ _last_tool_had_error = False # Track if previous tool failed
952
+ _in_error_recovery = False # Stays True until run_command succeeds (not reset by read_file)
953
+ _nudge_count = 0 # Limit error recovery nudges
954
+ _consecutive_reads = 0 # Track repeated read_file without fixing
955
+ _last_failed_cmd = "" # Track last failed run_command to detect repeats
956
+ _consecutive_cmd_failures = 0 # Count consecutive failures of same command
957
+ # Repetition loop detection — check every 80 chars (was 200, too slow for 200-token responses)
958
+ _rep_check_interval = 80
959
+ _rep_token_count = [0] # mutable for closure
960
+ _rep_cancelled = [False] # signals loop to stop
961
+
962
+ def _tool_signature(tool_name: str, params: dict) -> tuple[str, str]:
963
+ try:
964
+ payload = json.dumps(params or {}, sort_keys=True, ensure_ascii=False, default=str)
965
+ except Exception:
966
+ payload = str(params or {})
967
+ return tool_name, payload
968
+
969
+ def _register_tool_call(tool_name: str, params: dict) -> dict:
970
+ tool_name, normalized_params, requested_name = _normalize_requested_tool_call(
971
+ tool_name,
972
+ params,
973
+ _available_tool_names,
974
+ )
975
+ call = {"tool": tool_name, "params": normalized_params}
976
+ if requested_name != tool_name:
977
+ call["_aria_requested_tool"] = requested_name
978
+ sig = _tool_signature(tool_name, params)
979
+ seen = _tool_call_counts.get(sig, 0)
980
+ _tool_call_counts[sig] = seen + 1
981
+ _tool_name_counts[tool_name] = _tool_name_counts.get(tool_name, 0) + 1
982
+ # Market data calls are expensive and usually deterministic for a turn.
983
+ # One successful/attempted call is enough; repeated identical calls are
984
+ # almost always model looping.
985
+ limit = 1 if tool_name in {
986
+ "get_market_data", "get_crypto_data", "get_forex_data",
987
+ "get_technical_indicators",
988
+ } else 2
989
+ total_limit = {
990
+ "web_search": 3,
991
+ "web_fetch": 4,
992
+ "search_news": 3,
993
+ }.get(tool_name)
994
+ if seen >= limit:
995
+ call["_aria_duplicate"] = True
996
+ call["_aria_limit_reason"] = "duplicate parameters"
997
+ elif total_limit is not None and _tool_name_counts[tool_name] > total_limit:
998
+ call["_aria_duplicate"] = True
999
+ call["_aria_limit_reason"] = f"turn budget exceeded ({total_limit})"
1000
+ return call
1001
+
1002
+ def _check_repetition(text: str) -> bool:
1003
+ """Return True if the response is looping.
1004
+
1005
+ Covers three patterns:
1006
+ A. Paragraph-level loop: same long block (50-400 chars) reappears
1007
+ B. Sentence-level tail loop: short sentence (15-50 chars) appears
1008
+ 2+ times at the END — catches "好的,我已经准备好了" × 2 style tails
1009
+ C. Beginning-restart loop: model generates the full response, then
1010
+ starts again from the very beginning. Detects when the opening
1011
+ 80 chars of the accumulated response reappear after the midpoint.
1012
+ This is the most common 1.5B model failure mode.
1013
+ """
1014
+ if len(text) < 100:
1015
+ return False
1016
+
1017
+ # Pattern C: restart-from-beginning (fast path, checked first)
1018
+ if len(text) > 300:
1019
+ _opening = text[:80].strip()
1020
+ if _opening and len(_opening) >= 20:
1021
+ _after_half = text[len(text) // 2:]
1022
+ if _opening in _after_half:
1023
+ return True
1024
+
1025
+ tail = text[-4000:]
1026
+
1027
+ # Pattern A: medium-to-large probe in trailing window
1028
+ for sub_len in (400, 250, 150, 80, 50):
1029
+ if len(tail) < sub_len * 2:
1030
+ continue
1031
+ probe = tail[-sub_len:].strip()
1032
+ if len(probe) < 20:
1033
+ continue
1034
+ if tail[:-sub_len].count(probe) >= 1:
1035
+ return True
1036
+
1037
+ # Pattern B: short sentence repetition at tail (boilerplate hallucination)
1038
+ # Split by Chinese sentence-ending punctuation + newlines
1039
+ import re as _re2
1040
+ # Common non-looping phrases that legitimately repeat (disclaimers, transitions)
1041
+ _B_IGNORE = {
1042
+ "本内容不构成投资建议", "不构成投资建议", "请注意风险",
1043
+ "以上仅供参考", "仅供参考", "请以官方数据为准",
1044
+ "如有问题请咨询专业人士", "投资有风险,入市需谨慎",
1045
+ "好的", "当然", "当然可以", "明白了", "好的,我来",
1046
+ }
1047
+ sentences = [s.strip() for s in _re2.split(r'[。!?\n]+', tail) if s.strip()]
1048
+ if len(sentences) >= 4:
1049
+ # Check if any sentence in the last 3 also appears before it in the tail
1050
+ for sent in sentences[-3:]:
1051
+ if len(sent) < 15: # raised from 10 → less hair-trigger
1052
+ continue
1053
+ if sent in _B_IGNORE:
1054
+ continue
1055
+ # Require 3+ occurrences (raised from 2) to reduce false positives
1056
+ if tail.count(sent) >= 3:
1057
+ return True
1058
+
1059
+ return False
1060
+
1061
+ for tool_round in range(max_tool_rounds):
1062
+ # Context compaction: compress older messages if context too large
1063
+ if tool_round > 0:
1064
+ payload["messages"] = _compact_messages(payload["messages"], model_key=_mkey)
1065
+
1066
+ full_response = ""
1067
+ tool_calls_this_round = []
1068
+ _done_reason = ""
1069
+ # Do not show provisional finance prose before this round is known to
1070
+ # be prose-only. Some local models append malformed function JSON after
1071
+ # a sentence, and streaming it first makes the protocol visible.
1072
+ _buffer_finance_prose = enable_tools and _intent in {"finance", "analysis", "realtime"}
1073
+ _buffered_tokens: list[str] = []
1074
+
1075
+ try:
1076
+ async with aiohttp.ClientSession(trust_env=True) as session:
1077
+ async with session.post(url, json=payload,
1078
+ timeout=aiohttp.ClientTimeout(total=300)) as resp:
1079
+ if resp.status != 200:
1080
+ try:
1081
+ _body = await resp.text()
1082
+ _json = json.loads(_body) if _body.strip().startswith("{") else {}
1083
+ _ollama_err = _json.get("error") or _body[:200]
1084
+ except Exception:
1085
+ _ollama_err = f"HTTP {resp.status}"
1086
+ # Invalidate model cache so next call re-probes
1087
+ try:
1088
+ from local_llm_provider import _model_cache
1089
+ _model_cache.clear()
1090
+ except Exception:
1091
+ pass
1092
+ if resp.status >= 500 and _server_retry_budget > 0:
1093
+ _server_retry_budget -= 1
1094
+ if HAS_RICH:
1095
+ console.print(" [yellow]模型服务暂时错误,已压缩上下文并重试…[/yellow]")
1096
+ payload["messages"] = _compact_messages(payload["messages"], model_key=_mkey)
1097
+ payload["messages"].append({
1098
+ "role": "user",
1099
+ "content": (
1100
+ "SYSTEM: The model server returned a transient 5xx error. "
1101
+ "Continue the current task from the available tool results. "
1102
+ "Do not restart or repeat completed work."
1103
+ ),
1104
+ })
1105
+ await asyncio.sleep(1.0)
1106
+ continue
1107
+ return {"success": False, "error": f"Ollama {resp.status}: {_ollama_err}"}
1108
+ async for line in resp.content:
1109
+ if cancel_event and cancel_event.is_set():
1110
+ return {"success": True, "response": full_response,
1111
+ "cancelled": True, "provider": "ollama", "usage": usage}
1112
+ text = line.decode("utf-8", errors="ignore").strip()
1113
+ if not text:
1114
+ continue
1115
+ try:
1116
+ data = json.loads(text)
1117
+ except json.JSONDecodeError:
1118
+ continue
1119
+
1120
+ # Check for native tool calls from Ollama
1121
+ msg = data.get("message", {})
1122
+ reasoning = msg.get("thinking", "")
1123
+ if reasoning:
1124
+ if on_thinking:
1125
+ on_thinking(reasoning)
1126
+ if msg.get("tool_calls"):
1127
+ for tc in msg["tool_calls"]:
1128
+ fn = tc.get("function", {})
1129
+ tool_name = fn.get("name", "")
1130
+ tool_args = fn.get("arguments", {})
1131
+ if isinstance(tool_args, str):
1132
+ try:
1133
+ tool_args = json.loads(tool_args)
1134
+ except json.JSONDecodeError:
1135
+ tool_args = {}
1136
+ call = _register_tool_call(tool_name, tool_args)
1137
+ tool_calls_this_round.append(call)
1138
+ if not call.get("_aria_duplicate"):
1139
+ tools_used.append(tool_name)
1140
+ if on_tool_call and not call.get("_aria_duplicate"):
1141
+ on_tool_call(tool_name, tool_args)
1142
+
1143
+ if data.get("done"):
1144
+ # Capture Ollama usage stats from final message
1145
+ usage["prompt_tokens"] += data.get("prompt_eval_count", 0)
1146
+ usage["completion_tokens"] += data.get("eval_count", 0)
1147
+ _done_reason = str(data.get("done_reason") or data.get("stop_reason") or "")
1148
+ break
1149
+
1150
+ token = msg.get("content", "")
1151
+ if token:
1152
+ full_response += token
1153
+ # 重复检测:先检测再流出,避免重复内容流到用户终端
1154
+ _rep_token_count[0] += len(token)
1155
+ if _rep_token_count[0] >= _rep_check_interval:
1156
+ _rep_token_count[0] = 0
1157
+ if _check_repetition(full_response):
1158
+ # 定位重复起始点:找到最长不重复前缀
1159
+ _fr = full_response
1160
+ _cut = len(_fr) // 2
1161
+ # 尝试精确裁切:找重复开始的位置
1162
+ for _probe_len in (300, 200, 150, 100):
1163
+ if len(_fr) < _probe_len * 2:
1164
+ continue
1165
+ _probe = _fr[-_probe_len:]
1166
+ _pos = _fr[:-_probe_len].find(_probe)
1167
+ if _pos > 0:
1168
+ _cut = _pos
1169
+ break
1170
+ full_response = _fr[:_cut].rstrip()
1171
+ _rep_cancelled[0] = True
1172
+ if cancel_event:
1173
+ cancel_event.set()
1174
+ break
1175
+ # 流出 token — 过滤条件:
1176
+ # 1. 以 <tool_call 开头的 XML 工具调用(内部处理,不显示)
1177
+ # 2. 以 { 开头的裸 JSON 工具调用(小模型幻觉,直接屏蔽)
1178
+ _fr_lstrip = full_response.lstrip()
1179
+ _looks_like_tool_json = (
1180
+ _fr_lstrip.startswith("{")
1181
+ and ('"name"' in full_response or '"function"' in full_response)
1182
+ and '"arguments"' in full_response
1183
+ )
1184
+ # 3. 孤立的 ``` 围栏(未配对的代码块标记,过滤掉)
1185
+ _stripped_tok = token.strip()
1186
+ _is_orphan_fence = (
1187
+ _stripped_tok.startswith("```")
1188
+ and len(_stripped_tok) <= 6 # just ``` or ```py etc
1189
+ and full_response.count("```") % 2 == 1 # unpaired
1190
+ )
1191
+ _has_internal_protocol = bool(
1192
+ re.search(r"(?<![A-Za-z0-9_])\??brtc\b", full_response, re.IGNORECASE)
1193
+ )
1194
+ if not _fr_lstrip.startswith("<tool_call") \
1195
+ and not _looks_like_tool_json \
1196
+ and not _is_orphan_fence \
1197
+ and not _has_internal_protocol:
1198
+ if _buffer_finance_prose:
1199
+ _buffered_tokens.append(token)
1200
+ elif on_token:
1201
+ on_token(token)
1202
+ except Exception as e:
1203
+ err_msg = str(e) or type(e).__name__
1204
+ if any(x in err_msg.lower() for x in ("cannot connect", "connect call failed", "connection refused", "errno 61")):
1205
+ return _ollama_unavailable_result(ollama_url, err_msg)
1206
+ return {"success": False, "error": f"Ollama: {err_msg}"}
1207
+
1208
+ # Fallback: parse text-based tool calls if no native ones found
1209
+ if not tool_calls_this_round and full_response.strip():
1210
+ text_calls = _parse_text_tool_calls(full_response)
1211
+ if text_calls:
1212
+ tool_calls_this_round = [
1213
+ _register_tool_call(tc["tool"], tc["params"])
1214
+ for tc in text_calls
1215
+ ]
1216
+ for tc in tool_calls_this_round:
1217
+ if not tc.get("_aria_duplicate"):
1218
+ tools_used.append(tc["tool"])
1219
+ if on_tool_call and not tc.get("_aria_duplicate"):
1220
+ on_tool_call(tc["tool"], tc["params"])
1221
+
1222
+ # If repetition was detected, truncate and return cleanly
1223
+ if _rep_cancelled[0]:
1224
+ # Remove the repeated tail — keep only the first clean portion
1225
+ lines = full_response.strip().splitlines()
1226
+ # Find where repetition started: keep up to the point where unique content ends
1227
+ seen_paragraphs = set()
1228
+ clean_lines = []
1229
+ for line in lines:
1230
+ key = line.strip()
1231
+ if key and len(key) > 20:
1232
+ if key in seen_paragraphs:
1233
+ break # Hit a repeated paragraph — stop here
1234
+ seen_paragraphs.add(key)
1235
+ clean_lines.append(line)
1236
+ full_response = "\n".join(clean_lines).rstrip()
1237
+ if on_token:
1238
+ # The repetition note is appended as a final token
1239
+ on_token("\n\n*[model stopped — repetition detected]*")
1240
+ full_response += "\n\n*[model stopped — repetition detected]*"
1241
+ return {
1242
+ "success": True, "response": full_response,
1243
+ "tools_used": tools_used, "sources": [],
1244
+ "tool_calls_pending": [], "usage": usage, "provider": "ollama",
1245
+ }
1246
+
1247
+ # If no tool calls this round
1248
+ if not tool_calls_this_round:
1249
+ full_response = _strip_tool_call_tags(full_response)
1250
+ if _buffer_finance_prose and on_token and full_response:
1251
+ on_token(full_response)
1252
+ clean_text = full_response.strip().lower()
1253
+
1254
+ if _done_reason in {"length", "num_predict"} and _continuation_count < 3:
1255
+ _continuation_count += 1
1256
+ response_segments.append(full_response.rstrip())
1257
+ payload["messages"].append({"role": "assistant", "content": full_response})
1258
+ payload["messages"].append({
1259
+ "role": "user",
1260
+ "content": (
1261
+ "继续完成上一条回答,从刚才中断处接着写。"
1262
+ "不要重写已经输出的内容,不要总结,直到任务完整结束。"
1263
+ ),
1264
+ })
1265
+ if HAS_RICH:
1266
+ from aria_code.apps.cli.tools.write_tools import _ui_zh
1267
+ console.print(f"\n [dim]{'继续输出未完成内容…' if _ui_zh() else 'Continuing where the answer was cut off…'}[/dim]\n")
1268
+ continue
1269
+
1270
+ # Detect "intent without action" — model says it will do something
1271
+ # but didn't output a tool call
1272
+ _intent_words = [
1273
+ "let me", "i will", "i'll", "let's", "让我", "我会", "我将",
1274
+ "让我们", "我来", "接下来", "下面", "我们来", "我需要",
1275
+ "再次", "重新", "检查", "修复", "fix", "retry", "check",
1276
+ ]
1277
+ has_intent = any(w in clean_text for w in _intent_words)
1278
+ should_nudge = (_in_error_recovery or _last_tool_had_error or has_intent) and _nudge_count < 5
1279
+
1280
+ if should_nudge and tool_round < max_tool_rounds - 1:
1281
+ _nudge_count += 1
1282
+ if _in_error_recovery:
1283
+ nudge = (
1284
+ "SYSTEM: You are in error recovery mode. The script FAILED and is NOT yet fixed. "
1285
+ "You MUST call a tool NOW to fix it:\n"
1286
+ "- If you already read the file: call edit_file to fix the specific error, or write_file to rewrite.\n"
1287
+ "- If you haven't read it: call read_file first.\n"
1288
+ "- After fixing: call run_command to retry.\n"
1289
+ "Do NOT output text. Output ONLY a <tool_call>."
1290
+ )
1291
+ elif _last_tool_had_error:
1292
+ nudge = (
1293
+ "SYSTEM: The previous step FAILED. Fix it NOW by calling a tool:\n"
1294
+ "1. read_file to see the code.\n"
1295
+ "2. edit_file or write_file to fix.\n"
1296
+ "3. run_command to retry.\n"
1297
+ "Output a <tool_call> NOW."
1298
+ )
1299
+ else:
1300
+ nudge = (
1301
+ "SYSTEM: You said you would do something but did not call a tool. "
1302
+ "Do NOT describe what you will do — just DO it. "
1303
+ "Output a <tool_call> NOW to take the next action."
1304
+ )
1305
+ payload["messages"].append({"role": "assistant", "content": full_response})
1306
+ payload["messages"].append({"role": "user", "content": nudge})
1307
+ continue
1308
+
1309
+ # Truly done. Tokens were already streamed above; do not print the
1310
+ # accumulated response again or the terminal shows duplicate blocks.
1311
+ break
1312
+
1313
+ # Tool calls present — suppress model text (tool UI provides feedback)
1314
+ # Large models may emit multiple write_file calls in one round (project scaffolding).
1315
+ # Destructive / interactive tools (run_command, edit_file) remain sequential.
1316
+ _MUST_SERIALIZE = {"run_command", "edit_file"}
1317
+ if len(tool_calls_this_round) > 1:
1318
+ _all_safe = all(tc["tool"] not in _MUST_SERIALIZE for tc in tool_calls_this_round)
1319
+ _is_large_mdl = _model_size in ("large",)
1320
+ if _is_large_mdl and _all_safe:
1321
+ tool_calls_this_round = tool_calls_this_round[:5] # max 5 parallel writes
1322
+ else:
1323
+ tool_calls_this_round = tool_calls_this_round[:1]
1324
+
1325
+ if defer_tool_execution:
1326
+ clean_text = _strip_tool_call_tags(full_response)
1327
+ return {
1328
+ "success": True,
1329
+ "response": clean_text,
1330
+ "tools_used": [],
1331
+ "sources": [],
1332
+ "tool_calls_pending": [
1333
+ {"tool": tc["tool"], "params": tc["params"]}
1334
+ for tc in tool_calls_this_round
1335
+ if not tc.get("_aria_duplicate")
1336
+ ],
1337
+ "usage": usage,
1338
+ "provider": "ollama",
1339
+ }
1340
+
1341
+ # Execute tool calls locally and feed results back
1342
+ clean_text = _strip_tool_call_tags(full_response)
1343
+ payload["messages"].append({"role": "assistant", "content": clean_text,
1344
+ "tool_calls": [{"function": {"name": tc["tool"], "arguments": tc["params"]}}
1345
+ for tc in tool_calls_this_round]})
1346
+
1347
+ ollama_cancelled = False
1348
+ for tc in tool_calls_this_round:
1349
+ # Check cancel between tools
1350
+ if cancel_event and cancel_event.is_set():
1351
+ ollama_cancelled = True
1352
+ break
1353
+
1354
+ tool_name = tc["tool"]
1355
+ # Note: _print_tool_call already called by on_tool_call during streaming
1356
+
1357
+ if tc.get("_aria_duplicate"):
1358
+ reason = tc.get("_aria_limit_reason") or "duplicate tool call"
1359
+ summary = (
1360
+ f"SYSTEM: Tool call skipped ({reason}): {tool_name}. "
1361
+ "Use the existing tool result already available in this turn. "
1362
+ "Do not call this tool again in this turn; finish the answer from available evidence."
1363
+ )
1364
+ payload["messages"].append({
1365
+ "role": "tool",
1366
+ "content": summary,
1367
+ })
1368
+ continue
1369
+
1370
+ # Ask user confirmation for destructive tools
1371
+ if tool_name in _CONFIRM_TOOLS:
1372
+ try:
1373
+ approval = _confirm_tool_execution_decision(
1374
+ tool_name,
1375
+ tc["params"],
1376
+ config_policy=_ACTIVE_COMMAND_POLICY[0],
1377
+ )
1378
+ _apply_tool_approval(tc["params"], approval)
1379
+ if not approval.approved:
1380
+ ollama_cancelled = True
1381
+ if HAS_RICH:
1382
+ console.print("\n [dim]Cancelled[/dim]")
1383
+ break
1384
+ # Persist "Allow & set balanced" choice
1385
+ if approval.upgrade_policy:
1386
+ tc["params"].pop("_upgrade_policy", None)
1387
+ _ACTIVE_COMMAND_POLICY[0] = "balanced"
1388
+ if HAS_RICH:
1389
+ console.print(" [dim]策略已升级为 balanced(本会话)[/dim]")
1390
+ except KeyboardInterrupt:
1391
+ ollama_cancelled = True
1392
+ break
1393
+
1394
+ try:
1395
+ tool_t0 = time.time()
1396
+ # Inject current policy for run_command so post-approval execution
1397
+ # isn't re-blocked by the default "safe" policy
1398
+ if tool_name == "run_command" and "policy" not in tc["params"]:
1399
+ tc["params"]["policy"] = _ACTIVE_COMMAND_POLICY[0]
1400
+ result = execute_local_tool(tool_name, tc["params"])
1401
+ tool_dt = time.time() - tool_t0
1402
+ except KeyboardInterrupt:
1403
+ ollama_cancelled = True
1404
+ break
1405
+ _print_tool_result(tool_name, result, tool_dt)
1406
+
1407
+ summary = _format_tool_summary(tool_name, result)
1408
+
1409
+ # Track if this tool had an error (for nudge logic)
1410
+ _last_tool_had_error = not result.get("success", False)
1411
+ if result.get("success") and tool_name == "run_command":
1412
+ exit_code = result.get("data", {}).get("exit_code", 0)
1413
+ _last_tool_had_error = (exit_code != 0)
1414
+
1415
+ # Error recovery state machine
1416
+ if _last_tool_had_error:
1417
+ _in_error_recovery = True
1418
+ _consecutive_reads = 0
1419
+ # Detect repeated failed run_command (same command failing 2+ times)
1420
+ if tool_name == "run_command":
1421
+ cmd_str = tc["params"].get("command", "")
1422
+ if cmd_str == _last_failed_cmd:
1423
+ _consecutive_cmd_failures += 1
1424
+ else:
1425
+ _last_failed_cmd = cmd_str
1426
+ _consecutive_cmd_failures = 1
1427
+ if _consecutive_cmd_failures >= 2:
1428
+ summary += ("\n\nSYSTEM: You have run the SAME command and it FAILED again with the same error. "
1429
+ "STOP re-running it. You MUST fix the code first:\n"
1430
+ "1. read_file to see the script content\n"
1431
+ "2. edit_file to fix the specific error (or write_file to rewrite entirely)\n"
1432
+ "3. THEN run_command to retry.\n"
1433
+ "Do NOT run the same command again until you have fixed the code.")
1434
+ elif tool_name in ("read_file", "list_files", "search_code"):
1435
+ # Diagnostic tools do NOT exit error recovery
1436
+ _consecutive_reads += 1
1437
+ # If model read the file 2+ times without fixing, inject directive
1438
+ if _in_error_recovery and _consecutive_reads >= 2:
1439
+ summary += ("\n\nSYSTEM: You have read this file multiple times without fixing it. "
1440
+ "STOP reading. Use edit_file to fix the specific error, "
1441
+ "or use write_file to rewrite the entire script. Then run_command to retry.")
1442
+ elif tool_name in ("edit_file", "write_file"):
1443
+ # Fix was applied — stay in recovery until run_command succeeds
1444
+ _consecutive_reads = 0
1445
+ _consecutive_cmd_failures = 0 # Reset — code was changed
1446
+ _last_failed_cmd = ""
1447
+ elif tool_name == "run_command" and not _last_tool_had_error:
1448
+ # run_command succeeded — exit error recovery
1449
+ _in_error_recovery = False
1450
+ _consecutive_reads = 0
1451
+ _consecutive_cmd_failures = 0
1452
+ _last_failed_cmd = ""
1453
+ _nudge_count = 0
1454
+
1455
+ if on_tool_result:
1456
+ on_tool_result(tool_name, summary)
1457
+
1458
+ # Feed tool result back to Ollama for next round
1459
+ payload["messages"].append({
1460
+ "role": "tool",
1461
+ "content": summary,
1462
+ })
1463
+
1464
+ if ollama_cancelled:
1465
+ return {"success": True, "response": full_response,
1466
+ "cancelled": True, "tools_used": tools_used,
1467
+ "sources": [], "thinking": "", "provider": "ollama", "usage": usage}
1468
+
1469
+ # Continue streaming with tool results in context
1470
+ if HAS_RICH:
1471
+ console.print() # newline before next AI response
1472
+
1473
+ # Write successful stateless response to cache for future reuse
1474
+ if response_segments:
1475
+ full_response = "\n".join(part for part in response_segments + [full_response] if part)
1476
+
1477
+ if _should_cache and full_response and not tools_used:
1478
+ _cache_set(_ck, full_response)
1479
+
1480
+ # ── Code-block executor fallback ─────────────────────────────────────────
1481
+ # Small models often ignore the <tool_call> instruction and write plain code
1482
+ # blocks instead. When the intent is "coding" and the model produced a
1483
+ # Python block but zero tool calls, auto-extract the code and queue
1484
+ # write_file + run_command so the outer agentic loop executes it.
1485
+ _auto_tool_calls: list = []
1486
+ _allow_code_block_autorun = (
1487
+ _intent == "coding"
1488
+ and _has_coding_signal
1489
+ and (_explicit_code_signal or not _is_visual_artifact_request)
1490
+ )
1491
+ if _allow_code_block_autorun and not tools_used and full_response:
1492
+ import re as _re
1493
+ # Accept both complete (``` closed) and truncated (unclosed) code blocks
1494
+ _py_blocks = _re.findall(r"```python\n(.*?)```", full_response, _re.DOTALL)
1495
+ if not _py_blocks:
1496
+ # Fallback: grab everything after the opening fence (handles truncation)
1497
+ _m = _re.search(r"```python\n(.*)", full_response, _re.DOTALL)
1498
+ if _m:
1499
+ _py_blocks = [_m.group(1)]
1500
+ if _py_blocks:
1501
+ _code = _py_blocks[-1].strip()
1502
+ # Basic sanitisation: strip leading spaces from ticker assignments
1503
+ _code = _re.sub(
1504
+ r"""(ticker\s*=\s*['"])(\s+)([A-Z]{1,10})(['"])""",
1505
+ r"\1\3\4", _code
1506
+ )
1507
+ # Auto-add missing `import mplfinance as mpf` when mpf is used
1508
+ if "mpf." in _code and "import mplfinance" not in _code:
1509
+ _code = "import mplfinance as mpf\n" + _code
1510
+ # Auto-add `import matplotlib.pyplot as plt` when plt is used
1511
+ if "plt." in _code and "import matplotlib.pyplot as plt" not in _code:
1512
+ _code = (
1513
+ "import matplotlib; matplotlib.use('Agg')\n"
1514
+ "import matplotlib.pyplot as plt\n" + _code
1515
+ )
1516
+ # Try to extract user-specified filename from the original message
1517
+ _fname_match = _re.search(
1518
+ r'保存(?:到|为|成)?\s*([^\s,,。]+\.py)'
1519
+ r'|save\s+(?:to\s+|as\s+)?([^\s,]+\.py)'
1520
+ r'|(?:named?|called?|filename?)\s+([^\s,]+\.py)',
1521
+ _intent_message, _re.IGNORECASE
1522
+ )
1523
+ if _fname_match:
1524
+ _fname = next(g for g in _fname_match.groups() if g)
1525
+ # Strip any path prefix from the extracted name
1526
+ _fname = os.path.basename(_fname)
1527
+ else:
1528
+ _fname = f"aria_generated_{int(time.time())}.py"
1529
+ _fpath = f"~/Documents/Aria Code/generated/{_fname}"
1530
+
1531
+ # Validate Python syntax before writing — prepend warning comment if broken
1532
+ import py_compile as _pyc
1533
+ import tempfile as _tf2
1534
+ try:
1535
+ with _tf2.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as _stmp:
1536
+ _stmp.write(_code)
1537
+ _stmp_path = _stmp.name
1538
+ _pyc.compile(_stmp_path, doraise=True)
1539
+ os.unlink(_stmp_path)
1540
+ except _pyc.PyCompileError as _pce:
1541
+ # Surface the error prominently; file is still saved so user can fix it
1542
+ _err_line = str(_pce).replace(str(_stmp_path), _fname)
1543
+ _code = f"# ⚠️ SYNTAX ERROR (fix before running):\n# {_err_line}\n\n" + _code
1544
+ try:
1545
+ os.unlink(_stmp_path)
1546
+ except Exception:
1547
+ pass
1548
+ except Exception:
1549
+ pass
1550
+
1551
+ _auto_tool_calls = [
1552
+ {"tool": "write_file", "params": {"path": _fpath, "content": _code}},
1553
+ ]
1554
+ import shlex as _shlex_auto
1555
+ _auto_tool_calls.append({
1556
+ "tool": "run_command",
1557
+ "params": {
1558
+ "command": f"python3 {_shlex_auto.quote(os.path.expanduser(_fpath))}",
1559
+ "timeout": 120,
1560
+ },
1561
+ })
1562
+ if on_tool_call:
1563
+ on_tool_call("write_file", _auto_tool_calls[0]["params"])
1564
+ on_tool_call("run_command", _auto_tool_calls[1]["params"])
1565
+
1566
+ if _auto_tool_calls:
1567
+ return {"success": True, "response": full_response,
1568
+ "tool_calls_pending": _auto_tool_calls,
1569
+ "tools_used": tools_used, "sources": [], "thinking": "",
1570
+ "provider": "ollama", "usage": usage}
1571
+
1572
+ return {"success": True, "response": full_response,
1573
+ "tools_used": tools_used, "sources": [], "thinking": "", "provider": "ollama",
1574
+ "usage": usage}