quantnodes 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (399) hide show
  1. QuantNodes/__init__.py +15 -0
  2. QuantNodes/__main__.py +14 -0
  3. QuantNodes/agent/__init__.py +158 -0
  4. QuantNodes/agent/agents/__init__.py +13 -0
  5. QuantNodes/agent/agents/definition.py +180 -0
  6. QuantNodes/agent/agents/manager.py +73 -0
  7. QuantNodes/agent/config/__init__.py +34 -0
  8. QuantNodes/agent/config/executor.py +958 -0
  9. QuantNodes/agent/config/loader.py +427 -0
  10. QuantNodes/agent/config/templates/bollinger_bands.yaml +84 -0
  11. QuantNodes/agent/config/templates/dual_ma.yaml +72 -0
  12. QuantNodes/agent/config/templates/empty.yaml +56 -0
  13. QuantNodes/agent/config/templates/mean_reversion.yaml +47 -0
  14. QuantNodes/agent/config/templates/mean_reversion_zscore.yaml +90 -0
  15. QuantNodes/agent/config/templates/momentum.yaml +81 -0
  16. QuantNodes/agent/config/templates/momentum_breakout.yaml +84 -0
  17. QuantNodes/agent/config/templates/rsi_strategy.yaml +72 -0
  18. QuantNodes/agent/config/templates/volume_price.yaml +86 -0
  19. QuantNodes/agent/config/types.py +156 -0
  20. QuantNodes/agent/config_mapper.py +293 -0
  21. QuantNodes/agent/core/__init__.py +19 -0
  22. QuantNodes/agent/core/dream.py +47 -0
  23. QuantNodes/agent/core/quant_dream.py +274 -0
  24. QuantNodes/agent/cron_jobs.py +314 -0
  25. QuantNodes/agent/nanobot_bridge.py +242 -0
  26. QuantNodes/agent/permission/__init__.py +30 -0
  27. QuantNodes/agent/permission/defaults.py +36 -0
  28. QuantNodes/agent/permission/evaluate.py +41 -0
  29. QuantNodes/agent/permission/models.py +59 -0
  30. QuantNodes/agent/permission/service.py +133 -0
  31. QuantNodes/agent/providers/__init__.py +11 -0
  32. QuantNodes/agent/providers/base.py +102 -0
  33. QuantNodes/agent/providers/quantnodes.py +610 -0
  34. QuantNodes/agent/providers/rate_limiter.py +326 -0
  35. QuantNodes/agent/providers/registry.py +163 -0
  36. QuantNodes/agent/skills/__init__.py +20 -0
  37. QuantNodes/agent/skills/base.py +118 -0
  38. QuantNodes/agent/skills/bridge.py +73 -0
  39. QuantNodes/agent/skills/factor/__init__.py +14 -0
  40. QuantNodes/agent/skills/factor/correlation.py +99 -0
  41. QuantNodes/agent/skills/factor/group_backtest.py +114 -0
  42. QuantNodes/agent/skills/factor/ic_analysis.py +106 -0
  43. QuantNodes/agent/skills/loader.py +107 -0
  44. QuantNodes/agent/skills/registry.py +105 -0
  45. QuantNodes/agent/skills/strategy/__init__.py +16 -0
  46. QuantNodes/agent/skills/strategy/bollinger.py +86 -0
  47. QuantNodes/agent/skills/strategy/dual_ma.py +82 -0
  48. QuantNodes/agent/skills/strategy/momentum.py +74 -0
  49. QuantNodes/agent/skills/strategy/rsi_reversal.py +99 -0
  50. QuantNodes/agent/skills_quant/__init__.py +14 -0
  51. QuantNodes/agent/skills_quant/backtest-analyze/SKILL.md +42 -0
  52. QuantNodes/agent/skills_quant/config-driven/SKILL.md +72 -0
  53. QuantNodes/agent/skills_quant/factor-research/SKILL.md +40 -0
  54. QuantNodes/agent/skills_quant/quant-dream/SKILL.md +55 -0
  55. QuantNodes/agent/skills_quant/risk-management/SKILL.md +45 -0
  56. QuantNodes/agent/skills_quant/strategy-design/SKILL.md +43 -0
  57. QuantNodes/agent/templates/__init__.py +4 -0
  58. QuantNodes/agent/tools/__init__.py +173 -0
  59. QuantNodes/agent/tools/_workspace.py +51 -0
  60. QuantNodes/agent/tools/alpha_backtest.py +328 -0
  61. QuantNodes/agent/tools/alpha_evaluate.py +493 -0
  62. QuantNodes/agent/tools/backtest.py +226 -0
  63. QuantNodes/agent/tools/base.py +133 -0
  64. QuantNodes/agent/tools/code_search.py +207 -0
  65. QuantNodes/agent/tools/config_backtest.py +401 -0
  66. QuantNodes/agent/tools/context.py +97 -0
  67. QuantNodes/agent/tools/dream_skill.py +77 -0
  68. QuantNodes/agent/tools/echo.py +38 -0
  69. QuantNodes/agent/tools/factor.py +231 -0
  70. QuantNodes/agent/tools/file_ops.py +201 -0
  71. QuantNodes/agent/tools/git_ops.py +190 -0
  72. QuantNodes/agent/tools/operator_lookup.py +218 -0
  73. QuantNodes/agent/tools/output_truncation.py +77 -0
  74. QuantNodes/agent/tools/path_check.py +43 -0
  75. QuantNodes/agent/tools/pipeline.py +62 -0
  76. QuantNodes/agent/tools/registry.py +150 -0
  77. QuantNodes/agent/tools/sandbox.py +62 -0
  78. QuantNodes/agent/tools/shell_safety.py +63 -0
  79. QuantNodes/agent/tools/strategy.py +106 -0
  80. QuantNodes/agent/tools/task.py +171 -0
  81. QuantNodes/agent/tools/web_fetch.py +142 -0
  82. QuantNodes/agent/tools/web_search.py +114 -0
  83. QuantNodes/agent/tools/wiki.py +370 -0
  84. QuantNodes/agent/utils/__init__.py +11 -0
  85. QuantNodes/agent/utils/helpers.py +43 -0
  86. QuantNodes/agent/utils/prompt_templates.py +30 -0
  87. QuantNodes/agent/workflows/__init__.py +20 -0
  88. QuantNodes/agent/workflows/implementations/__init__.py +8 -0
  89. QuantNodes/agent/workflows/implementations/alpha_gpt.py +508 -0
  90. QuantNodes/agent/workflows/implementations/mcts.py +442 -0
  91. QuantNodes/agent/workflows/parsers.py +44 -0
  92. QuantNodes/agent/workflows/registry.py +119 -0
  93. QuantNodes/agent/workflows/step_agent.py +219 -0
  94. QuantNodes/agent/workflows/tool.py +198 -0
  95. QuantNodes/ai/__init__.py +93 -0
  96. QuantNodes/ai/llm/__init__.py +75 -0
  97. QuantNodes/ai/llm/base.py +233 -0
  98. QuantNodes/ai/llm/decorators.py +281 -0
  99. QuantNodes/ai/llm/gateway.py +571 -0
  100. QuantNodes/ai/llm/null.py +76 -0
  101. QuantNodes/ai/llm/openai.py +435 -0
  102. QuantNodes/ai/optimizer.py +405 -0
  103. QuantNodes/ai/prompts/__init__.py +229 -0
  104. QuantNodes/ai/sandbox.py +371 -0
  105. QuantNodes/ai/sandbox_pandas_bridge.py +150 -0
  106. QuantNodes/ai/strategy_gen.py +396 -0
  107. QuantNodes/backtest/__init__.py +64 -0
  108. QuantNodes/backtest/backtest_node.py +188 -0
  109. QuantNodes/backtest/broker_node.py +378 -0
  110. QuantNodes/backtest/config_runner.py +397 -0
  111. QuantNodes/backtest/config_strategy.py +64 -0
  112. QuantNodes/backtest/risk_node.py +360 -0
  113. QuantNodes/backtest/strategy_node.py +268 -0
  114. QuantNodes/cache_node/__init__.py +19 -0
  115. QuantNodes/cache_node/base.py +244 -0
  116. QuantNodes/cache_node/cache_store.py +99 -0
  117. QuantNodes/cache_node/metadata.py +100 -0
  118. QuantNodes/cli/__init__.py +109 -0
  119. QuantNodes/cli/_helpers.py +511 -0
  120. QuantNodes/cli/command.py +110 -0
  121. QuantNodes/cli/commands/__init__.py +69 -0
  122. QuantNodes/cli/commands/agent.py +158 -0
  123. QuantNodes/cli/commands/alpha.py +951 -0
  124. QuantNodes/cli/commands/chat.py +38 -0
  125. QuantNodes/cli/commands/evolve.py +120 -0
  126. QuantNodes/cli/commands/factor.py +569 -0
  127. QuantNodes/cli/commands/init.py +190 -0
  128. QuantNodes/cli/commands/run.py +259 -0
  129. QuantNodes/cli/commands/serve.py +398 -0
  130. QuantNodes/cli/commands/version.py +120 -0
  131. QuantNodes/cli/enhanced.py +146 -0
  132. QuantNodes/conf_node/__init__.py +37 -0
  133. QuantNodes/conf_node/base.py +120 -0
  134. QuantNodes/conf_node/env_config.py +132 -0
  135. QuantNodes/conf_node/ini_config.py +70 -0
  136. QuantNodes/conf_node/json_config.py +69 -0
  137. QuantNodes/conf_node/yaml_config.py +78 -0
  138. QuantNodes/constants.py +17 -0
  139. QuantNodes/core/__init__.py +196 -0
  140. QuantNodes/core/_lookback_helpers.py +49 -0
  141. QuantNodes/core/ast_parser.py +198 -0
  142. QuantNodes/core/base.py +61 -0
  143. QuantNodes/core/cache_manager.py +344 -0
  144. QuantNodes/core/cache_utils.py +150 -0
  145. QuantNodes/core/cond_builder.py +53 -0
  146. QuantNodes/core/config.py +170 -0
  147. QuantNodes/core/constants.py +48 -0
  148. QuantNodes/core/control.py +412 -0
  149. QuantNodes/core/data_preprocessing.py +453 -0
  150. QuantNodes/core/data_source.py +46 -0
  151. QuantNodes/core/events.py +178 -0
  152. QuantNodes/core/evolution/__init__.py +22 -0
  153. QuantNodes/core/evolution/loop.py +583 -0
  154. QuantNodes/core/evolution/operators.py +289 -0
  155. QuantNodes/core/evolution/settings.py +44 -0
  156. QuantNodes/core/expression.py +841 -0
  157. QuantNodes/core/feedback/__init__.py +38 -0
  158. QuantNodes/core/feedback/channels.py +182 -0
  159. QuantNodes/core/feedback/collector.py +91 -0
  160. QuantNodes/core/feedback/dataclass.py +239 -0
  161. QuantNodes/core/feedback/llm_judge.py +138 -0
  162. QuantNodes/core/knowledge/__init__.py +69 -0
  163. QuantNodes/core/knowledge/knowledge_base.py +217 -0
  164. QuantNodes/core/knowledge/lineage_compress.py +196 -0
  165. QuantNodes/core/knowledge/lineage_expand.py +123 -0
  166. QuantNodes/core/knowledge/metrics/__init__.py +43 -0
  167. QuantNodes/core/knowledge/metrics/evaluator.py +176 -0
  168. QuantNodes/core/knowledge/metrics/metrics.py +220 -0
  169. QuantNodes/core/knowledge/rag_prompt.py +196 -0
  170. QuantNodes/core/knowledge/retriever.py +209 -0
  171. QuantNodes/core/lambda_node.py +81 -0
  172. QuantNodes/core/monitoring/__init__.py +22 -0
  173. QuantNodes/core/monitoring/collector.py +292 -0
  174. QuantNodes/core/monitoring/dashboard.py +365 -0
  175. QuantNodes/core/node.py +375 -0
  176. QuantNodes/core/pandas_utils.py +504 -0
  177. QuantNodes/core/parallel/__init__.py +15 -0
  178. QuantNodes/core/parallel/worker.py +140 -0
  179. QuantNodes/core/parallel/worker_process.py +265 -0
  180. QuantNodes/core/path_utils.py +73 -0
  181. QuantNodes/core/pipeline.py +328 -0
  182. QuantNodes/core/plugin.py +135 -0
  183. QuantNodes/core/quality_gate/__init__.py +32 -0
  184. QuantNodes/core/quality_gate/complexity.py +94 -0
  185. QuantNodes/core/quality_gate/consistency.py +26 -0
  186. QuantNodes/core/quality_gate/node.py +97 -0
  187. QuantNodes/core/quality_gate/redundancy.py +51 -0
  188. QuantNodes/core/quality_gate/settings.py +43 -0
  189. QuantNodes/core/quality_gate/zoo.py +98 -0
  190. QuantNodes/core/serializable.py +116 -0
  191. QuantNodes/core/serialization.py +673 -0
  192. QuantNodes/core/tools.py +333 -0
  193. QuantNodes/core/trajectory/__init__.py +25 -0
  194. QuantNodes/core/trajectory/entry.py +116 -0
  195. QuantNodes/core/trajectory/lineage.py +67 -0
  196. QuantNodes/core/trajectory/pool.py +211 -0
  197. QuantNodes/core/trajectory/selector.py +140 -0
  198. QuantNodes/core/visualization/__init__.py +33 -0
  199. QuantNodes/core/visualization/builder.py +233 -0
  200. QuantNodes/core/visualization/gate_breakdown.py +140 -0
  201. QuantNodes/core/visualization/lineage_dag.py +203 -0
  202. QuantNodes/core/visualization/metric_distribution.py +125 -0
  203. QuantNodes/core/visualization/report.py +68 -0
  204. QuantNodes/database_node/__init__.py +69 -0
  205. QuantNodes/database_node/base.py +135 -0
  206. QuantNodes/database_node/clickhouse_node.py +272 -0
  207. QuantNodes/database_node/csv_node.py +83 -0
  208. QuantNodes/database_node/duckdb_node.py +86 -0
  209. QuantNodes/database_node/factory.py +83 -0
  210. QuantNodes/database_node/mysql_node.py +100 -0
  211. QuantNodes/database_node/parquet_node.py +75 -0
  212. QuantNodes/database_node/sqlite_node.py +67 -0
  213. QuantNodes/factor_node/__init__.py +50 -0
  214. QuantNodes/factor_node/factor.py +563 -0
  215. QuantNodes/factor_node/factor_db.py +421 -0
  216. QuantNodes/factor_node/factor_functions/__init__.py +252 -0
  217. QuantNodes/factor_node/factor_functions/_helpers.py +358 -0
  218. QuantNodes/factor_node/factor_functions/_helpers_debug.py +317 -0
  219. QuantNodes/factor_node/factor_functions/composite_ops.py +136 -0
  220. QuantNodes/factor_node/factor_functions/math_ops.py +433 -0
  221. QuantNodes/factor_node/factor_functions/section_ops.py +290 -0
  222. QuantNodes/factor_node/factor_functions/talib_ops.py +1293 -0
  223. QuantNodes/factor_node/factor_functions/time_ops.py +535 -0
  224. QuantNodes/factor_node/factor_operation.py +1115 -0
  225. QuantNodes/factor_node/factor_table.py +1073 -0
  226. QuantNodes/factor_node/quant_nodes_object.py +60 -0
  227. QuantNodes/mcp_server/__init__.py +27 -0
  228. QuantNodes/mcp_server/__main__.py +4 -0
  229. QuantNodes/mcp_server/server.py +272 -0
  230. QuantNodes/methods/__init__.py +28 -0
  231. QuantNodes/methods/pipeline.py +100 -0
  232. QuantNodes/methods/sandbox.py +102 -0
  233. QuantNodes/monitor/__init__.py +27 -0
  234. QuantNodes/monitor/agent_tools/__init__.py +5 -0
  235. QuantNodes/monitor/agent_tools/monitor_tool.py +98 -0
  236. QuantNodes/monitor/agent_tools/schedule_tool.py +98 -0
  237. QuantNodes/monitor/agent_tools/version_tool.py +133 -0
  238. QuantNodes/monitor/monitor/__init__.py +6 -0
  239. QuantNodes/monitor/monitor/alerter.py +60 -0
  240. QuantNodes/monitor/monitor/collector.py +164 -0
  241. QuantNodes/monitor/monitor/dashboard.py +115 -0
  242. QuantNodes/monitor/monitor/drift.py +190 -0
  243. QuantNodes/monitor/scheduler/__init__.py +4 -0
  244. QuantNodes/monitor/scheduler/runner.py +133 -0
  245. QuantNodes/monitor/scheduler/scheduler.py +184 -0
  246. QuantNodes/monitor/storage/__init__.py +16 -0
  247. QuantNodes/monitor/storage/models.py +70 -0
  248. QuantNodes/monitor/storage/repository.py +407 -0
  249. QuantNodes/monitor/version/__init__.py +4 -0
  250. QuantNodes/monitor/version/diff.py +81 -0
  251. QuantNodes/monitor/version/version_manager.py +182 -0
  252. QuantNodes/operator_node/__init__.py +28 -0
  253. QuantNodes/operator_node/base.py +97 -0
  254. QuantNodes/operator_node/query_node.py +129 -0
  255. QuantNodes/operator_node/sql_builder.py +125 -0
  256. QuantNodes/operator_node/sql_utils.py +172 -0
  257. QuantNodes/operator_node/transform.py +130 -0
  258. QuantNodes/operators/__init__.py +90 -0
  259. QuantNodes/operators/_engine.py +108 -0
  260. QuantNodes/operators/composite.py +161 -0
  261. QuantNodes/operators/composite_dag.py +667 -0
  262. QuantNodes/operators/composite_dag_ops.py +343 -0
  263. QuantNodes/operators/composite_dag_pandas_ops.py +382 -0
  264. QuantNodes/operators/custom.py +408 -0
  265. QuantNodes/operators/facade.py +164 -0
  266. QuantNodes/operators/math.py +163 -0
  267. QuantNodes/operators/proxy.py +29 -0
  268. QuantNodes/operators/registry.py +144 -0
  269. QuantNodes/operators/section.py +99 -0
  270. QuantNodes/operators/talib.py +757 -0
  271. QuantNodes/operators/templates.py +95 -0
  272. QuantNodes/operators/time_series.py +136 -0
  273. QuantNodes/prompts/__init__.py +20 -0
  274. QuantNodes/prompts/backtest/__init__.py +12 -0
  275. QuantNodes/prompts/backtest/factor_based.py +86 -0
  276. QuantNodes/prompts/backtest/standard.py +73 -0
  277. QuantNodes/prompts/factor/__init__.py +14 -0
  278. QuantNodes/prompts/factor/correlation.py +77 -0
  279. QuantNodes/prompts/factor/group_backtest.py +86 -0
  280. QuantNodes/prompts/factor/ic_analysis.py +91 -0
  281. QuantNodes/prompts/strategy/__init__.py +18 -0
  282. QuantNodes/prompts/strategy/market_neutral.py +96 -0
  283. QuantNodes/prompts/strategy/mean_reversion.py +107 -0
  284. QuantNodes/prompts/strategy/momentum.py +160 -0
  285. QuantNodes/prompts/strategy/pairs_trading.py +107 -0
  286. QuantNodes/prompts/strategy/trend_following.py +96 -0
  287. QuantNodes/research/README.md +106 -0
  288. QuantNodes/research/__init__.py +154 -0
  289. QuantNodes/research/_legacy_3c/__init__.py +61 -0
  290. QuantNodes/research/_legacy_3c/auto_researcher.py +289 -0
  291. QuantNodes/research/_legacy_3c/factor_evaluator.py +560 -0
  292. QuantNodes/research/_legacy_3c/factor_miner.py +318 -0
  293. QuantNodes/research/_legacy_3c/mcts_search.py +324 -0
  294. QuantNodes/research/factor_test/__init__.py +25 -0
  295. QuantNodes/research/factor_test/config.py +184 -0
  296. QuantNodes/research/factor_test/config_builder.py +276 -0
  297. QuantNodes/research/factor_test/e2e/data_prep.py +163 -0
  298. QuantNodes/research/factor_test/e2e/run_evolution_e2e.py +309 -0
  299. QuantNodes/research/factor_test/evolution_adapter.py +231 -0
  300. QuantNodes/research/factor_test/feedback_wrapper.py +102 -0
  301. QuantNodes/research/factor_test/ifind_db/__init__.py +7 -0
  302. QuantNodes/research/factor_test/ifind_db/fetcher.py +224 -0
  303. QuantNodes/research/factor_test/ifind_db/ifind_database.py +689 -0
  304. QuantNodes/research/factor_test/nodes/__init__.py +1 -0
  305. QuantNodes/research/factor_test/nodes/_base.py +91 -0
  306. QuantNodes/research/factor_test/nodes/adjust_date_node.py +48 -0
  307. QuantNodes/research/factor_test/nodes/configs.py +240 -0
  308. QuantNodes/research/factor_test/nodes/factor_neutralize_node.py +87 -0
  309. QuantNodes/research/factor_test/nodes/factor_preprocess_node.py +222 -0
  310. QuantNodes/research/factor_test/nodes/factor_score_node.py +141 -0
  311. QuantNodes/research/factor_test/nodes/factor_test_report_node.py +153 -0
  312. QuantNodes/research/factor_test/nodes/group_analyzer_node.py +317 -0
  313. QuantNodes/research/factor_test/nodes/ic_analyzer_node.py +112 -0
  314. QuantNodes/research/factor_test/nodes/load_data_node.py +100 -0
  315. QuantNodes/research/factor_test/nodes/long_short_node.py +93 -0
  316. QuantNodes/research/factor_test/nodes/neutralizers.py +222 -0
  317. QuantNodes/research/factor_test/nodes/preprocess_strategies.py +277 -0
  318. QuantNodes/research/factor_test/nodes/risk_correlation_node.py +112 -0
  319. QuantNodes/research/factor_test/nodes/sample_pool_filter_node.py +110 -0
  320. QuantNodes/research/factor_test/nodes/tradability_filter_node.py +92 -0
  321. QuantNodes/research/factor_test/pipeline_runner.py +305 -0
  322. QuantNodes/research/factor_test/pipeline_spec.py +216 -0
  323. QuantNodes/research/factor_test/utils/__init__.py +26 -0
  324. QuantNodes/research/factor_test/utils/constants.py +86 -0
  325. QuantNodes/research/factor_test/utils/data_loader.py +141 -0
  326. QuantNodes/research/factor_test/utils/date_utils.py +232 -0
  327. QuantNodes/research/factor_test/utils/file_loaders.py +150 -0
  328. QuantNodes/research/factor_test/utils/labels.py +37 -0
  329. QuantNodes/research/factor_test/utils/metrics_extractor.py +55 -0
  330. QuantNodes/research/factor_test/utils/performance_metrics.py +175 -0
  331. QuantNodes/research/factor_test/utils/safe_load.py +106 -0
  332. QuantNodes/research/quant_alpha/CHANGELOG.md +80 -0
  333. QuantNodes/research/quant_alpha/README.md +142 -0
  334. QuantNodes/research/quant_alpha/__init__.py +45 -0
  335. QuantNodes/research/quant_alpha/adapters/__init__.py +99 -0
  336. QuantNodes/research/quant_alpha/adapters/calculator.py +503 -0
  337. QuantNodes/research/quant_alpha/adapters/expression.py +387 -0
  338. QuantNodes/research/quant_alpha/alpha101_design/__init__.py +50 -0
  339. QuantNodes/research/quant_alpha/alpha101_design/few_shot_examples.py +243 -0
  340. QuantNodes/research/quant_alpha/alpha101_design/philosophy.py +474 -0
  341. QuantNodes/research/quant_alpha/alpha158_design/__init__.py +63 -0
  342. QuantNodes/research/quant_alpha/alpha158_design/few_shot_examples.py +219 -0
  343. QuantNodes/research/quant_alpha/alpha158_design/philosophy.py +240 -0
  344. QuantNodes/research/quant_alpha/evaluation/__init__.py +47 -0
  345. QuantNodes/research/quant_alpha/evaluation/baselines/__init__.py +8 -0
  346. QuantNodes/research/quant_alpha/evaluation/baselines/g1_handcrafted.py +135 -0
  347. QuantNodes/research/quant_alpha/evaluation/baselines/g2_llm_only.py +269 -0
  348. QuantNodes/research/quant_alpha/evaluation/baselines/g3_alpha_gpt.py +152 -0
  349. QuantNodes/research/quant_alpha/evaluation/clickhouse_data_loader.py +227 -0
  350. QuantNodes/research/quant_alpha/evaluation/contracts.py +376 -0
  351. QuantNodes/research/quant_alpha/evaluation/evaluators/__init__.py +6 -0
  352. QuantNodes/research/quant_alpha/evaluation/evaluators/polars_evaluator.py +545 -0
  353. QuantNodes/research/quant_alpha/evaluation/mock_data_loader.py +226 -0
  354. QuantNodes/research/quant_alpha/evaluation/runner.py +243 -0
  355. QuantNodes/research/quant_alpha/llm/__init__.py +38 -0
  356. QuantNodes/research/quant_alpha/llm/parser.py +681 -0
  357. QuantNodes/research/quant_alpha/logic_driven_pipeline.py +411 -0
  358. QuantNodes/research/quant_alpha/logic_mining/__init__.py +74 -0
  359. QuantNodes/research/quant_alpha/logic_mining/compiler.py +457 -0
  360. QuantNodes/research/quant_alpha/logic_mining/generator.py +366 -0
  361. QuantNodes/research/quant_alpha/logic_mining/models.py +252 -0
  362. QuantNodes/research/quant_alpha/logic_mining/parser.py +287 -0
  363. QuantNodes/research/quant_alpha/logic_mining/pipelines.py +297 -0
  364. QuantNodes/research/quant_alpha/logic_mining/sources.py +149 -0
  365. QuantNodes/research/quant_alpha/mcts/__init__.py +66 -0
  366. QuantNodes/research/quant_alpha/mcts/cache.py +262 -0
  367. QuantNodes/research/quant_alpha/mcts/extension_ops.py +320 -0
  368. QuantNodes/research/quant_alpha/mcts/feedback.py +825 -0
  369. QuantNodes/research/quant_alpha/mcts/op_prior.py +180 -0
  370. QuantNodes/research/quant_alpha/mcts/search.py +540 -0
  371. QuantNodes/research/quant_alpha/mcts/tree.py +201 -0
  372. QuantNodes/research/quant_alpha/operator_vocab/__init__.py +50 -0
  373. QuantNodes/research/quant_alpha/operator_vocab/config.py +54 -0
  374. QuantNodes/research/quant_alpha/operator_vocab/metadata.py +263 -0
  375. QuantNodes/research/quant_alpha/operator_vocab/vocabulary.py +481 -0
  376. QuantNodes/research/quant_alpha/pipeline.py +1027 -0
  377. QuantNodes/research/quant_alpha/types/__init__.py +27 -0
  378. QuantNodes/research/quant_alpha/types/constants.py +28 -0
  379. QuantNodes/research/quant_alpha/types/state.py +205 -0
  380. QuantNodes/research/quant_alpha/workflow/__init__.py +32 -0
  381. QuantNodes/research/quant_alpha/workflow/alpha_gpt.py +911 -0
  382. QuantNodes/research/quant_alpha/workflow/alpha_logics.py +416 -0
  383. QuantNodes/research/quant_alpha/workflow/state.py +27 -0
  384. QuantNodes/research/report_reproducer.py +485 -0
  385. QuantNodes/research/wiki.py +1155 -0
  386. QuantNodes/symbolic/__init__.py +51 -0
  387. QuantNodes/symbolic/compiler.py +113 -0
  388. QuantNodes/symbolic/dialect.py +260 -0
  389. QuantNodes/symbolic/executor.py +147 -0
  390. QuantNodes/symbolic/expression.py +234 -0
  391. QuantNodes/symbolic/functions.py +433 -0
  392. QuantNodes/symbolic/optimizer.py +165 -0
  393. QuantNodes/ui_node/__init__.py +30 -0
  394. QuantNodes/ui_node/base.py +222 -0
  395. quantnodes-3.0.0.dist-info/METADATA +463 -0
  396. quantnodes-3.0.0.dist-info/RECORD +399 -0
  397. quantnodes-3.0.0.dist-info/WHEEL +5 -0
  398. quantnodes-3.0.0.dist-info/entry_points.txt +24 -0
  399. quantnodes-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,610 @@
1
+ # coding=utf-8
2
+ """
3
+ QuantNodes LLM Provider适配器
4
+
5
+ 适配现有LLMClientBase到Agent Provider接口。
6
+ 支持两种模式:
7
+ 1. LiteLLM SDK模式(默认):内置重试、连接池、速率限制
8
+ 2. 旧模式:使用LLMClientBase(向后兼容)
9
+
10
+ LiteLLM集成提供以下功能:
11
+ - 内置指数退避重试(区分429/500)
12
+ - httpx连接池(连接复用)
13
+ - 可配置的速率限制(Token Bucket)
14
+ - 多模型路由和Fallback支持
15
+ """
16
+
17
+ import logging
18
+ from typing import Any, Dict, List, Callable, Awaitable, Optional
19
+ import asyncio
20
+ import json
21
+ import re
22
+
23
+ from .base import LLMProvider, LLMResponse, ToolCallRequest
24
+ from .rate_limiter import AsyncTokenBucket
25
+ from QuantNodes.ai.llm.base import LLMClientBase, Message as QNMessage, MessageRole
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # 尝试导入 LiteLLM SDK
30
+ try:
31
+ from litellm import acompletion, RateLimitError, APIError
32
+ LITELLM_AVAILABLE = True
33
+ except ImportError:
34
+ LITELLM_AVAILABLE = False
35
+ logger.warning("LiteLLM SDK not installed. Falling back to legacy LLMClientBase.")
36
+
37
+
38
+ class QuantNodesLLMProvider(LLMProvider):
39
+ """适配QuantNodes现有LLM客户端的Provider
40
+
41
+ 支持两种初始化模式:
42
+ 1. LiteLLM模式(默认):QuantNodesLLMProvider(api_key, api_base, model)
43
+ - 内置重试、连接池、速率限制
44
+ 2. 旧模式:QuantNodesLLMProvider(client=client)
45
+ - 使用绑定的单个LLMClientBase(向后兼容)
46
+
47
+ 初始化参数:
48
+ api_key: API密钥(用于LiteLLM模式)
49
+ api_base: API基础URL(用于LiteLLM模式)
50
+ client: LLMClientBase实例(旧模式)
51
+ default_model: 默认模型名
52
+ default_max_tokens: 默认最大token数
53
+ registry: ProviderRegistry实例(多模型路由)
54
+ fallback_providers: fallback provider名称列表
55
+ use_litellm: 是否使用LiteLLM SDK(默认True)
56
+ rate_limit_rps: 每秒请求数(默认0.5,用于免费账号)
57
+ max_retries: LiteLLM最大重试次数(默认3)
58
+ timeout: 请求超时秒数(默认60)
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ api_key: str | None = None,
64
+ api_base: str | None = None,
65
+ client: LLMClientBase | None = None,
66
+ default_model: str | None = None,
67
+ default_max_tokens: int = 102400,
68
+ registry=None,
69
+ fallback_providers: list[str] | None = None,
70
+ use_litellm: bool = True,
71
+ rate_limit_rps: float = 0.5,
72
+ max_retries: int = 3,
73
+ timeout: float = 60.0,
74
+ ):
75
+ """
76
+ Args:
77
+ api_key: API密钥(LiteLLM模式)
78
+ api_base: API基础URL(LiteLLM模式)
79
+ client: LLMClientBase实例(旧模式)
80
+ default_model: 默认模型名
81
+ default_max_tokens: 默认最大token数
82
+ registry: ProviderRegistry实例
83
+ fallback_providers: fallback provider列表
84
+ use_litellm: 是否启用LiteLLM SDK(默认True)
85
+ rate_limit_rps: 每秒请求速率(免费账号建议0.5)
86
+ max_retries: 最大重试次数
87
+ timeout: 请求超时(秒)
88
+ """
89
+ super().__init__(api_key=api_key, api_base=api_base)
90
+ self.client = client
91
+ self.default_model = default_model
92
+ self.default_max_tokens = default_max_tokens
93
+ self.registry = registry
94
+ self.fallback_providers = fallback_providers or []
95
+ self.use_litellm = use_litellm and LITELLM_AVAILABLE
96
+ self.rate_limit_rps = rate_limit_rps
97
+ self.max_retries = max_retries
98
+ self.timeout = timeout
99
+
100
+ # 速率限制器(用于LiteLLM模式)
101
+ self._rate_limiter: Optional[AsyncTokenBucket] = None
102
+ if self.use_litellm:
103
+ self._rate_limiter = AsyncTokenBucket(
104
+ requests_per_second=rate_limit_rps,
105
+ burst=1,
106
+ )
107
+
108
+ def _get_client_for_model(self, model: str | None) -> tuple[LLMClientBase, str]:
109
+ """根据model找到对应client和实际model名
110
+
111
+ 旧模式(无registry):返回绑定的单个client
112
+ 新模式(有registry):按model动态路由
113
+ """
114
+ if self.registry is None:
115
+ return self.client, model or self.default_model
116
+
117
+ config = self.registry.resolve(model)
118
+ if config:
119
+ actual_model = model or self.default_model
120
+ return self.registry.get_client(config), actual_model
121
+
122
+ if self.client:
123
+ return self.client, model or self.default_model
124
+ default_client = self.registry.get_default_client()
125
+ return default_client, model or self.default_model
126
+
127
+ def _convert_messages(self, messages: List[Dict[str, Any]]) -> List[QNMessage]:
128
+ """将OpenAI格式消息转换为QuantNodes格式"""
129
+ result = []
130
+ for msg in messages:
131
+ role_str = msg.get("role", "user")
132
+ try:
133
+ role = MessageRole(role_str)
134
+ except ValueError:
135
+ role = MessageRole.USER
136
+ content = msg.get("content", "")
137
+ if content is None:
138
+ content = ""
139
+ result.append(QNMessage(role=role, content=content))
140
+ return result
141
+
142
+ def _parse_tool_calls(self, response_content: str | None) -> List[ToolCallRequest]:
143
+ """从响应中解析工具调用"""
144
+ tool_calls = []
145
+ if response_content is None:
146
+ return tool_calls
147
+ content = response_content.strip()
148
+
149
+ if "```tool_call" in content:
150
+ pattern = r"```tool_call\s*([\s\S]*?)\s*```"
151
+ matches = re.findall(pattern, content)
152
+ for match in matches:
153
+ try:
154
+ data = json.loads(match.strip())
155
+ tool_calls.append(ToolCallRequest(
156
+ id=data.get("id", "tc_0"),
157
+ name=data.get("name", ""),
158
+ arguments=data.get("arguments", {}),
159
+ ))
160
+ except (json.JSONDecodeError, ValueError):
161
+ continue
162
+
163
+ return tool_calls
164
+
165
+ def _convert_litellm_response(self, response: Any) -> LLMResponse:
166
+ """将LiteLLM响应转换为LLMResponse格式"""
167
+ content = ""
168
+ tool_calls = []
169
+ usage = {}
170
+
171
+ if hasattr(response, 'choices') and response.choices:
172
+ choice = response.choices[0]
173
+ if hasattr(choice, 'message'):
174
+ content = choice.message.content or ""
175
+ if hasattr(choice.message, 'tool_calls') and choice.message.tool_calls:
176
+ for tc in choice.message.tool_calls:
177
+ if hasattr(tc, 'id') and hasattr(tc, 'function'):
178
+ tool_calls.append(ToolCallRequest(
179
+ id=tc.id,
180
+ name=tc.function.name,
181
+ arguments=tc.function.arguments or {},
182
+ ))
183
+ if hasattr(choice.message, 'finish_reason'):
184
+ finish_reason = choice.message.finish_reason
185
+ else:
186
+ finish_reason = "tool_calls" if tool_calls else "stop"
187
+ else:
188
+ finish_reason = "stop"
189
+ else:
190
+ finish_reason = "stop"
191
+
192
+ if hasattr(response, 'usage') and response.usage:
193
+ usage = {
194
+ 'prompt_tokens': getattr(response.usage, 'prompt_tokens', 0),
195
+ 'completion_tokens': getattr(response.usage, 'completion_tokens', 0),
196
+ 'total_tokens': getattr(response.usage, 'total_tokens', 0),
197
+ }
198
+
199
+ return LLMResponse(
200
+ content=content,
201
+ tool_calls=tool_calls,
202
+ finish_reason=finish_reason if tool_calls else "stop",
203
+ usage=usage,
204
+ )
205
+
206
+ async def _call_litellm(
207
+ self,
208
+ messages: List[Dict[str, Any]],
209
+ tools: List[Dict[str, Any]] | None,
210
+ model: str | None,
211
+ max_tokens: int | None,
212
+ temperature: float,
213
+ tool_choice: str | Dict[str, Any] | None,
214
+ ) -> LLMResponse:
215
+ """使用LiteLLM SDK调用LLM"""
216
+ if self._rate_limiter:
217
+ await self._rate_limiter.acquire()
218
+
219
+ actual_model = model or self.default_model
220
+
221
+ if self.api_base and actual_model and "/" in actual_model:
222
+ parts = actual_model.split("/", 1)
223
+ known_prefixes = {
224
+ "openrouter", "anthropic", "huggingface", "bedrock", "vertex_ai",
225
+ "ollama", "deepseek", "groq", "fireworks_ai", "mistral",
226
+ "perplexity", "together_ai", "replicate",
227
+ }
228
+ if parts[0] in known_prefixes:
229
+ actual_model = parts[1]
230
+ logger.info(f"Stripped provider prefix for LiteLLM, model: {actual_model}")
231
+
232
+ try:
233
+ response = await acompletion(
234
+ model=actual_model,
235
+ messages=messages,
236
+ api_key=self.api_key,
237
+ base_url=self.api_base,
238
+ max_tokens=max_tokens or self.default_max_tokens,
239
+ temperature=temperature,
240
+ tools=tools,
241
+ tool_choice=tool_choice,
242
+ timeout=self.timeout,
243
+ max_retries=self.max_retries,
244
+ )
245
+ return self._convert_litellm_response(response)
246
+ except RateLimitError as e:
247
+ logger.warning(f"Rate limit hit, trying fallback: {e}")
248
+ raise
249
+ except APIError as e:
250
+ logger.error(f"LiteLLM API error: {e}")
251
+ if hasattr(e, 'response') and hasattr(e.response, 'url'):
252
+ logger.error(f"Request URL: {e.response.url}")
253
+ raise
254
+ except Exception as e:
255
+ logger.error(f"LiteLLM call failed: {e}")
256
+ logger.error(f"Model: {actual_model}, base_url: {self.api_base}")
257
+ raise
258
+
259
+ async def _fallback_to_legacy(
260
+ self,
261
+ messages: List[Dict[str, Any]],
262
+ tools: List[Dict[str, Any]] | None,
263
+ model: str | None,
264
+ max_tokens: int | None,
265
+ temperature: float,
266
+ ) -> LLMResponse:
267
+ """LiteLLM失败时降级到原有LLMClientBase"""
268
+ logger.info("Falling back to legacy LLMClientBase")
269
+
270
+ qn_messages = self._convert_messages(messages)
271
+
272
+ if tools:
273
+ tools_desc = "\n".join([
274
+ f"- {t['function']['name']}: {t['function']['description']}"
275
+ for t in tools
276
+ ])
277
+ system_msg = next((m for m in qn_messages if m.role == MessageRole.SYSTEM), None)
278
+ if system_msg:
279
+ system_msg.content += f"\n\n可用工具:\n{tools_desc}"
280
+ system_msg.content += (
281
+ "\n\n如果需要调用工具,请使用```tool_call```代码块输出JSON格式的工具调用。"
282
+ )
283
+
284
+ effective_max_tokens = max_tokens or self.default_max_tokens
285
+ client, actual_model = self._get_client_for_model(model)
286
+
287
+ # Strip litellm provider prefix for legacy client
288
+ # (e.g. openrouter/google/gemini -> google/gemini)
289
+ # LiteLLM needs the prefix to route correctly, but legacy OpenAI client doesn't use it
290
+ if actual_model and "/" in actual_model:
291
+ parts = actual_model.split("/", 1)
292
+ known_prefixes = {
293
+ "openrouter", "anthropic", "huggingface", "bedrock", "vertex_ai",
294
+ "ollama", "deepseek", "groq", "fireworks_ai", "mistral",
295
+ "perplexity", "together_ai", "replicate",
296
+ }
297
+ if parts[0] in known_prefixes:
298
+ actual_model = parts[1]
299
+ logger.info(f"Stripped provider prefix for legacy client, model: {actual_model}")
300
+
301
+ if client is None:
302
+ raise RuntimeError(
303
+ "No LLM client available for legacy fallback. "
304
+ "Configure api_key and api_base in settings.json."
305
+ )
306
+
307
+ def _call():
308
+ return client.chat(
309
+ messages=qn_messages,
310
+ model=actual_model,
311
+ temperature=temperature,
312
+ max_tokens=effective_max_tokens,
313
+ )
314
+
315
+ loop = asyncio.get_event_loop()
316
+ qn_response = await loop.run_in_executor(None, _call)
317
+
318
+ content = qn_response.content
319
+ tool_calls = self._parse_tool_calls(content)
320
+
321
+ if tool_calls:
322
+ content = content.split("```tool_call")[0].strip()
323
+
324
+ return LLMResponse(
325
+ content=content,
326
+ tool_calls=tool_calls,
327
+ finish_reason="tool_calls" if tool_calls else "stop",
328
+ usage=qn_response.usage or {},
329
+ )
330
+
331
+ async def chat(
332
+ self,
333
+ messages: List[Dict[str, Any]],
334
+ tools: List[Dict[str, Any]] | None = None,
335
+ model: str | None = None,
336
+ max_tokens: int | None = None,
337
+ temperature: float = 0.7,
338
+ tool_choice: str | Dict[str, Any] | None = None,
339
+ ) -> LLMResponse:
340
+ """调用LLM
341
+
342
+ 优先使用LiteLLM SDK,失败时降级到legacy client。
343
+ """
344
+ messages = self._enforce_role_alternation(messages)
345
+
346
+ # 如果禁用了LiteLLM或LiteLLM不可用,直接使用legacy
347
+ if not self.use_litellm:
348
+ return await self._fallback_to_legacy(
349
+ messages, tools, model, max_tokens, temperature
350
+ )
351
+
352
+ try:
353
+ return await self._call_litellm(
354
+ messages, tools, model, max_tokens, temperature, tool_choice
355
+ )
356
+ except Exception as e:
357
+ logger.warning(f"LiteLLM call failed, trying legacy: {e}")
358
+
359
+ # 检查是否有legacy client可用
360
+ if self.client is None and self.registry is None:
361
+ raise RuntimeError(
362
+ "LiteLLM failed and no legacy client available. "
363
+ f"Original error: {e}"
364
+ )
365
+
366
+ try:
367
+ return await self._fallback_to_legacy(
368
+ messages, tools, model, max_tokens, temperature
369
+ )
370
+ except Exception as fallback_error:
371
+ logger.error(f"Legacy fallback also failed: {fallback_error}")
372
+ raise fallback_error
373
+
374
+ async def chat_stream(
375
+ self,
376
+ messages: List[Dict[str, Any]],
377
+ tools: List[Dict[str, Any]] | None = None,
378
+ model: str | None = None,
379
+ max_tokens: int | None = None,
380
+ temperature: float = 0.7,
381
+ on_content_delta: Callable[[str], Awaitable[None]] | None = None,
382
+ ) -> LLMResponse:
383
+ """流式调用LLM
384
+
385
+ 优先使用LiteLLM SDK,失败时降级到legacy client。
386
+ 注意:LiteLLM的流式响应需要特殊处理。
387
+ """
388
+ messages = self._enforce_role_alternation(messages)
389
+
390
+ if not self.use_litellm:
391
+ return await self._stream_legacy(
392
+ messages, tools, model, max_tokens, temperature, on_content_delta
393
+ )
394
+
395
+ # 速率限制
396
+ if self._rate_limiter:
397
+ await self._rate_limiter.acquire()
398
+
399
+ actual_model = model or self.default_model
400
+
401
+ try:
402
+ full_content = ""
403
+ tool_call_buffer = ""
404
+ in_tool_call = False
405
+ streamed_content = ""
406
+ tool_calls = []
407
+
408
+ # LiteLLM 流式调用
409
+ async for chunk in await acompletion(
410
+ model=actual_model,
411
+ messages=messages,
412
+ api_key=self.api_key,
413
+ base_url=self.api_base,
414
+ max_tokens=max_tokens or self.default_max_tokens,
415
+ temperature=temperature,
416
+ tools=tools,
417
+ stream=True,
418
+ ):
419
+ # 解析chunk内容
420
+ delta = ""
421
+ if hasattr(chunk, 'choices') and chunk.choices:
422
+ choice = chunk.choices[0]
423
+ if hasattr(choice, 'delta') and choice.delta:
424
+ delta = choice.delta.content or ""
425
+ if hasattr(choice.delta, 'tool_calls') and choice.delta.tool_calls:
426
+ for tc in choice.delta.tool_calls:
427
+ if hasattr(tc, 'function'):
428
+ tool_call_buffer += tc.function.arguments or ""
429
+
430
+ if not delta and not tool_call_buffer:
431
+ continue
432
+
433
+ full_content += delta
434
+
435
+ if in_tool_call:
436
+ tool_call_buffer += delta
437
+ if "```" in tool_call_buffer:
438
+ in_tool_call = False
439
+ try:
440
+ data = json.loads(tool_call_buffer.replace("```", "").strip())
441
+ tool_calls.append(ToolCallRequest(
442
+ id=data.get("id", f"tc_{len(tool_calls)}"),
443
+ name=data.get("name", ""),
444
+ arguments=data.get("arguments", {}),
445
+ ))
446
+ except (json.JSONDecodeError, ValueError):
447
+ pass
448
+ tool_call_buffer = ""
449
+ continue
450
+
451
+ if "```tool_call" in full_content:
452
+ parts = full_content.split("```tool_call", 1)
453
+ before = parts[0]
454
+ if before[len(streamed_content):].strip():
455
+ new_text = before[len(streamed_content):]
456
+ streamed_content = before
457
+ if on_content_delta:
458
+ await on_content_delta(new_text)
459
+ in_tool_call = True
460
+ tool_call_buffer = delta
461
+ continue
462
+
463
+ new_text = full_content[len(streamed_content):]
464
+ if new_text:
465
+ streamed_content = full_content
466
+ if on_content_delta:
467
+ await on_content_delta(new_text)
468
+
469
+ # 解析最终的tool_calls
470
+ if not tool_calls and "```tool_call" in full_content:
471
+ parsed = self._parse_tool_calls(full_content)
472
+ tool_calls.extend(parsed)
473
+
474
+ content = (
475
+ full_content.split("```tool_call")[0].strip()
476
+ if tool_calls else full_content.strip()
477
+ )
478
+
479
+ return LLMResponse(
480
+ content=content,
481
+ tool_calls=tool_calls,
482
+ finish_reason="tool_calls" if tool_calls else "stop",
483
+ usage={},
484
+ )
485
+
486
+ except Exception as e:
487
+ logger.warning(f"LiteLLM stream failed, trying legacy: {e}")
488
+
489
+ if self.client is None and self.registry is None:
490
+ raise RuntimeError(
491
+ f"LiteLLM stream failed and no legacy client available. Original error: {e}"
492
+ )
493
+
494
+ try:
495
+ return await self._stream_legacy(
496
+ messages, tools, model, max_tokens, temperature, on_content_delta
497
+ )
498
+ except Exception as fallback_error:
499
+ logger.error(f"Legacy stream fallback also failed: {fallback_error}")
500
+ raise fallback_error
501
+
502
+ async def _stream_legacy(
503
+ self,
504
+ messages: List[Dict[str, Any]],
505
+ tools: List[Dict[str, Any]] | None,
506
+ model: str | None,
507
+ max_tokens: int | None,
508
+ temperature: float,
509
+ on_content_delta: Callable[[str], Awaitable[None]] | None,
510
+ ) -> LLMResponse:
511
+ """使用legacy LLMClientBase进行流式调用"""
512
+ qn_messages = self._convert_messages(messages)
513
+
514
+ if tools:
515
+ tools_desc = "\n".join([
516
+ f"- {t['function']['name']}: {t['function']['description']}"
517
+ for t in tools
518
+ ])
519
+ system_msg = next((m for m in qn_messages if m.role == MessageRole.SYSTEM), None)
520
+ if system_msg:
521
+ system_msg.content += f"\n\n可用工具:\n{tools_desc}"
522
+ system_msg.content += (
523
+ "\n\n如果需要调用工具,请使用```tool_call```代码块输出JSON格式的工具调用。"
524
+ )
525
+
526
+ effective_max_tokens = max_tokens or self.default_max_tokens
527
+ client, actual_model = self._get_client_for_model(model)
528
+
529
+ # Strip litellm provider prefix for legacy client
530
+ # (e.g. openrouter/google/gemini -> google/gemini)
531
+ # LiteLLM needs the prefix to route correctly, but legacy OpenAI client doesn't use it
532
+ if actual_model and "/" in actual_model:
533
+ parts = actual_model.split("/", 1)
534
+ known_prefixes = {
535
+ "openrouter", "anthropic", "huggingface", "bedrock", "vertex_ai",
536
+ "ollama", "deepseek", "groq", "fireworks_ai", "mistral",
537
+ "perplexity", "together_ai", "replicate",
538
+ }
539
+ if parts[0] in known_prefixes:
540
+ actual_model = parts[1]
541
+ logger.info(
542
+ f"Stripped provider prefix for legacy stream client, model: {actual_model}"
543
+ )
544
+
545
+ if client is None:
546
+ raise RuntimeError(
547
+ "No LLM client available for legacy stream fallback. "
548
+ "Configure api_key and api_base in settings.json."
549
+ )
550
+
551
+ full_content = ""
552
+ tool_call_buffer = ""
553
+ in_tool_call = False
554
+ streamed_content = ""
555
+
556
+ def _iter_chunks():
557
+ return client.chat_stream(
558
+ messages=qn_messages,
559
+ model=actual_model,
560
+ temperature=temperature,
561
+ max_tokens=effective_max_tokens,
562
+ )
563
+
564
+ loop = asyncio.get_event_loop()
565
+ chunks = await loop.run_in_executor(None, lambda: list(_iter_chunks()))
566
+
567
+ for chunk in chunks:
568
+ delta = chunk.content or ""
569
+ if not delta:
570
+ continue
571
+
572
+ full_content += delta
573
+
574
+ if in_tool_call:
575
+ tool_call_buffer += delta
576
+ if "```" in tool_call_buffer:
577
+ in_tool_call = False
578
+ tool_call_buffer = ""
579
+ continue
580
+
581
+ if "```tool_call" in full_content:
582
+ parts = full_content.split("```tool_call", 1)
583
+ before = parts[0]
584
+ if before[len(streamed_content):].strip():
585
+ new_text = before[len(streamed_content):]
586
+ streamed_content = before
587
+ if on_content_delta:
588
+ await on_content_delta(new_text)
589
+ in_tool_call = True
590
+ tool_call_buffer = delta
591
+ continue
592
+
593
+ new_text = full_content[len(streamed_content):]
594
+ if new_text:
595
+ streamed_content = full_content
596
+ if on_content_delta:
597
+ await on_content_delta(new_text)
598
+
599
+ tool_calls = self._parse_tool_calls(full_content)
600
+ content = (
601
+ full_content.split("```tool_call")[0].strip()
602
+ if tool_calls else full_content.strip()
603
+ )
604
+
605
+ return LLMResponse(
606
+ content=content,
607
+ tool_calls=tool_calls,
608
+ finish_reason="tool_calls" if tool_calls else "stop",
609
+ usage={},
610
+ )