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,571 @@
1
+ # coding=utf-8
2
+ """LLMGateway — 统一 LLM 调用入口。
3
+
4
+ 所有模块通过 LLMGateway 调用 LLM,内部委托 nanobot Agent。
5
+
6
+ 支持 4 种调用接口:
7
+ - chat(messages) → ChatCompletion # LLMClientBase 兼容
8
+ - complete(agent_id, prompt) → str # alpha_gpt 兼容
9
+ - __call__(prompt) → str # callable 兼容
10
+ - await run(prompt, session_id) → str # nanobot 原生
11
+
12
+ 支持工具调用:
13
+ - tools: 工具名列表 (None=全部, []=无工具)
14
+ - tool_choice: "auto"/"none"/"required"
15
+ - with_tool_events: 异步接口返回 ToolCallResponse
16
+
17
+ 支持重试机制和超时控制:
18
+ - max_retries: 最大重试次数 (默认 3)
19
+ - retry_delay: 重试间隔秒数 (默认 1.0)
20
+ - timeout: 超时秒数 (默认 120)
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import logging
26
+ import time
27
+ from dataclasses import dataclass, field
28
+ from typing import Any, Dict, List, Optional, Union
29
+
30
+ from QuantNodes.ai.llm.base import (
31
+ ChatCompletion,
32
+ LLMClientBase,
33
+ Message,
34
+ MessageRole,
35
+ )
36
+
37
+ logger = logging.getLogger("llm.gateway")
38
+
39
+
40
+ @dataclass
41
+ class LLMConfig:
42
+ """LLM 配置(重试机制和超时控制)"""
43
+ max_retries: int = 3 # 最大重试次数
44
+ retry_delay: float = 1.0 # 重试间隔秒数
45
+ timeout: float = 300.0 # 超时秒数(MiniMax M3 复杂 JSON 需要较长时间)
46
+
47
+
48
+ @dataclass
49
+ class ToolCallResponse:
50
+ """工具调用响应 (with_tool_events=True 时返回)。"""
51
+ content: str
52
+ tools_used: List[str] = field(default_factory=list)
53
+ stop_reason: str = "stop"
54
+ events: List[Dict[str, Any]] = field(default_factory=list)
55
+
56
+ @property
57
+ def has_tool_calls(self) -> bool:
58
+ return len(self.tools_used) > 0
59
+
60
+
61
+ class LLMGateway(LLMClientBase):
62
+ """统一 LLM 调用门面 — 所有 LLM 调用的唯一入口。
63
+
64
+ 实现 LLMClientBase 接口 (chat / chat_stream),同时提供:
65
+ - complete(): alpha_gpt 兼容
66
+ - __call__(): callable injection 兼容
67
+ - run(): nanobot 原生 async
68
+
69
+ 支持工具调用:
70
+ - tools: 工具名列表 (None=全部, []=无工具)
71
+ - tool_choice: "auto"/"none"/"required"
72
+ - with_tool_events: 异步接口返回 ToolCallResponse
73
+
74
+ 支持重试机制和超时控制:
75
+ - max_retries: 最大重试次数 (默认 3)
76
+ - retry_delay: 重试间隔秒数 (默认 1.0)
77
+ - timeout: 超时秒数 (默认 120)
78
+
79
+ Examples:
80
+ >>> gateway = LLMGateway()
81
+ >>> response = gateway.chat([Message(role="user", content="Hello")])
82
+ >>> print(response.content)
83
+ >>> response = gateway.chat(messages, tools=["backtest", "factor"])
84
+ >>> print(response.tool_calls)
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ agent: Any = None,
90
+ workspace: str = ".agent",
91
+ llm_config: Optional[LLMConfig] = None,
92
+ agent_factory: Optional[Callable] = None,
93
+ **kwargs,
94
+ ):
95
+ """初始化 LLMGateway。
96
+
97
+ Args:
98
+ agent: nanobot Agent 实例 (可选, 不传则自动创建)
99
+ workspace: nanobot workspace 路径
100
+ llm_config: LLM 配置(重试机制和超时控制)
101
+ agent_factory: Agent 工厂函数 (可选, 用于延迟创建 Agent)
102
+ """
103
+ super().__init__(**kwargs)
104
+ self._agent = agent
105
+ self._workspace = workspace
106
+ self._agent_resolved = False
107
+ self._llm_config = llm_config or LLMConfig()
108
+ self._agent_factory = agent_factory
109
+
110
+ def _ensure_agent(self) -> Any:
111
+ """懒加载 nanobot Agent。"""
112
+ if self._agent is not None:
113
+ return self._agent
114
+
115
+ if not self._agent_resolved:
116
+ self._agent_resolved = True
117
+ try:
118
+ if self._agent_factory is not None:
119
+ self._agent = self._agent_factory()
120
+ else:
121
+ raise RuntimeError(
122
+ "No agent or agent_factory configured. "
123
+ "Pass agent=Agent(...) or agent_factory=lambda: Agent(...)"
124
+ )
125
+ # 覆盖 provider.generation.max_tokens(nanobot 默认 8192 太小)
126
+ from dataclasses import replace
127
+ loop = self._agent._loop
128
+ gen = getattr(loop.provider, "generation", None)
129
+ if gen is not None and gen.max_tokens < 16384:
130
+ loop.provider.generation = replace(gen, max_tokens=16384)
131
+ logger.info("LLMGateway: provider.generation.max_tokens → 16384")
132
+ logger.info("LLMGateway: nanobot Agent 已初始化")
133
+ except Exception as e:
134
+ logger.warning("LLMGateway: nanobot 不可用, 降级到 NullLLMClient: %s", e)
135
+ from QuantNodes.ai.llm.null import NullLLMClient
136
+ self._agent = None
137
+ self._fallback = NullLLMClient()
138
+
139
+ return self._agent
140
+
141
+ # ─── 接口 A: LLMClientBase.chat() 兼容 ───
142
+
143
+ def _call_api(
144
+ self,
145
+ messages: List[Message],
146
+ model: Optional[str] = None,
147
+ tools: Optional[List[str]] = None,
148
+ tool_choice: Optional[str] = None,
149
+ **kwargs,
150
+ ) -> ChatCompletion:
151
+ """LLMClientBase 抽象方法实现。
152
+
153
+ Args:
154
+ messages: 对话消息列表
155
+ model: 模型名称 (未使用, 由 nanobot 配置决定)
156
+ tools: 工具名列表 (None=全部, []=无工具)
157
+ tool_choice: "auto"/"none"/"required"
158
+
159
+ Returns:
160
+ ChatCompletion, 含:
161
+ - content: 最终文本
162
+ - tool_calls: [{id, name, arguments}, ...] (若 LLM 调了工具)
163
+ - finish_reason: "stop" / "tool_calls"
164
+ """
165
+ prompt = self._messages_to_prompt(messages)
166
+ agent = self._ensure_agent()
167
+
168
+ if agent is None:
169
+ return self._fallback._call_api(messages, model, **kwargs)
170
+
171
+ result = self._run_sync(
172
+ self._async_chat_collect(
173
+ agent, prompt,
174
+ tools=tools, tool_choice=tool_choice,
175
+ )
176
+ )
177
+ return ChatCompletion(
178
+ content=result["content"],
179
+ role=MessageRole.ASSISTANT,
180
+ finish_reason=result["stop_reason"],
181
+ tool_calls=result["tool_calls"],
182
+ )
183
+
184
+ # ─── 接口 B: complete 兼容 ───
185
+
186
+ def complete(
187
+ self,
188
+ agent_id: str = "default",
189
+ prompt: str = "",
190
+ tools: Optional[List[str]] = None,
191
+ tool_choice: Optional[str] = None,
192
+ temperature: Optional[float] = None,
193
+ ) -> str:
194
+ """同步调用 LLM, 返回字符串结果。
195
+
196
+ 对于 alpha-gpt 系列 agent_id,直接走 OpenAI 兼容 API(无 nanobot
197
+ agent 开销),避免 SOUL.md + tool 定义 + session history 导致
198
+ prompt 膨胀到 30K+ tokens。
199
+
200
+ 其他 agent_id 走 nanobot agent.chat() 路径。
201
+ """
202
+ # alpha-gpt 系列直接走轻量 API
203
+ if agent_id.startswith("alpha-gpt-") or agent_id == "default":
204
+ return self._complete_direct(prompt, temperature)
205
+
206
+ agent = self._ensure_agent()
207
+ if agent is None:
208
+ return self._fallback._call_api(
209
+ [Message(role=MessageRole.USER, content=prompt)]
210
+ ).content
211
+
212
+ # 重试机制
213
+ last_error = None
214
+ for attempt in range(self._llm_config.max_retries + 1):
215
+ try:
216
+ start_time = time.time()
217
+
218
+ result = self._run_sync(
219
+ self._async_chat_collect(
220
+ agent, prompt,
221
+ session_id=agent_id,
222
+ tools=tools, tool_choice=tool_choice,
223
+ temperature=temperature,
224
+ )
225
+ )
226
+
227
+ # 检查超时
228
+ elapsed = time.time() - start_time
229
+ if elapsed > self._llm_config.timeout:
230
+ raise TimeoutError(f"LLM call timed out after {elapsed:.1f}s")
231
+
232
+ return result["content"] or ""
233
+
234
+ except (TimeoutError, Exception) as e:
235
+ last_error = e
236
+ if attempt < self._llm_config.max_retries:
237
+ delay = self._llm_config.retry_delay * (2 ** attempt) # 指数退避
238
+ logger.warning(
239
+ "LLM call failed (attempt %d/%d), retrying in %.1fs: %s",
240
+ attempt + 1, self._llm_config.max_retries, delay, str(e)[:100]
241
+ )
242
+ time.sleep(delay)
243
+ else:
244
+ logger.error(
245
+ "LLM call failed after %d attempts: %s",
246
+ self._llm_config.max_retries + 1, str(e)[:200]
247
+ )
248
+
249
+ # 所有重试都失败
250
+ raise last_error
251
+
252
+ def complete_with_thinking(
253
+ self,
254
+ agent_id: str = "default",
255
+ prompt: str = "",
256
+ temperature: Optional[float] = None,
257
+ persist_thinking_dir: Optional[str] = None,
258
+ ) -> Tuple[str, str]:
259
+ """调用 LLM 同时返回 content 和 thinking(用于 Tier 1+2 思维链利用)。
260
+
261
+ 行为与 ``complete()`` 类似,但额外返回 ``thinking`` 块(如果存在)
262
+ 并可选择持久化到 ``persist_thinking_dir`` 目录。
263
+
264
+ Args:
265
+ agent_id: agent 标识(影响 dispatch 路径 + 持久化文件名)
266
+ prompt: 用户 prompt
267
+ temperature: 采样温度
268
+ persist_thinking_dir: 持久化 thinking 的目录(None=不持久化)
269
+
270
+ Returns:
271
+ (content, thinking) tuple
272
+ """
273
+ if agent_id.startswith("alpha-gpt-") or agent_id == "default":
274
+ return self._complete_direct(
275
+ prompt,
276
+ temperature,
277
+ return_thinking=True,
278
+ persist_thinking_dir=persist_thinking_dir,
279
+ agent_id=agent_id,
280
+ )
281
+
282
+ # 其他 agent 走 nanobot 路径(thinking 通常为空)
283
+ content = self.complete(agent_id=agent_id, prompt=prompt, temperature=temperature)
284
+ return content, ""
285
+
286
+ # ─── 轻量直接调用(alpha-gpt 专用) ───
287
+
288
+ def _complete_direct(
289
+ self,
290
+ prompt: str,
291
+ temperature: Optional[float] = None,
292
+ *,
293
+ return_thinking: bool = False,
294
+ persist_thinking_dir: Optional[str] = None,
295
+ agent_id: Optional[str] = None,
296
+ ) -> Union[str, Tuple[str, str]]:
297
+ """直接调 OpenAI 兼容 API,不经过 nanobot agent。
298
+
299
+ 避免 SOUL.md + tool 定义 + session history 导致 prompt 膨胀。
300
+ 同时清理 MiniMax M3 的 <think> 标签。
301
+
302
+ Args:
303
+ prompt: 输入 prompt
304
+ temperature: 采样温度
305
+ return_thinking: 是否返回 (content, thinking) tuple
306
+ persist_thinking_dir: 持久化 thinking 的目录(None=不持久化)
307
+ agent_id: agent 标识(用于持久化文件名)
308
+
309
+ Returns:
310
+ str (default) 或 (content, thinking) tuple(return_thinking=True)
311
+ """
312
+ import os
313
+ import re as _re
314
+ try:
315
+ from openai import OpenAI
316
+ except ImportError:
317
+ from QuantNodes.ai.llm.null import NullLLMClient
318
+ if return_thinking:
319
+ return NullLLMClient().canned_response, ""
320
+ return NullLLMClient().canned_response
321
+
322
+ agent = self._ensure_agent()
323
+ provider = getattr(getattr(agent, "_loop", None), "provider", None) if agent else None
324
+ api_key = getattr(provider, "api_key", None) or os.environ.get("QUANTNODES__LLM__API_KEY")
325
+ api_base = getattr(provider, "api_base", None) or os.environ.get("QUANTNODES__LLM__BASE_URL")
326
+ model = getattr(provider, "default_model", None) or os.environ.get("QUANTNODES__LLM__MODEL", "minimax-M3")
327
+
328
+ if not api_key:
329
+ if return_thinking:
330
+ return "[_complete_direct] no API key configured", ""
331
+ return "[_complete_direct] no API key configured"
332
+
333
+ client = OpenAI(api_key=api_key, base_url=api_base, timeout=self._llm_config.timeout)
334
+ temp = temperature if temperature is not None else 0.7
335
+
336
+ for attempt in range(self._llm_config.max_retries + 1):
337
+ try:
338
+ resp = client.chat.completions.create(
339
+ model=model,
340
+ messages=[{"role": "user", "content": prompt}],
341
+ temperature=temp,
342
+ max_tokens=16384,
343
+ )
344
+ raw_content = resp.choices[0].message.content or ""
345
+
346
+ # 提取 thinking 块(保留)
347
+ thinking = ""
348
+ think_match = _re.search(r"<think>([\s\S]*?)</think>", raw_content)
349
+ if think_match:
350
+ thinking = think_match.group(1).strip()
351
+
352
+ # 清理 content 中的 <think> 标签
353
+ content = _re.sub(r"<think>[\s\S]*?</think>", "", raw_content).strip()
354
+ content = _re.sub(r"<think>[^<]*(?:<(?!/thinking)[^<]*)*</think>", "", content).strip()
355
+
356
+ # 持久化 thinking(如需要)
357
+ if persist_thinking_dir and thinking and agent_id:
358
+ from pathlib import Path
359
+ import time as _time
360
+ persist_path = Path(persist_thinking_dir)
361
+ persist_path.mkdir(parents=True, exist_ok=True)
362
+ ts = int(_time.time() * 1000)
363
+ fname = f"{agent_id}_thinking_{ts}.txt"
364
+ (persist_path / fname).write_text(thinking, encoding="utf-8")
365
+
366
+ if return_thinking:
367
+ return content, thinking
368
+ return content
369
+ except Exception as e:
370
+ if attempt < self._llm_config.max_retries:
371
+ import time as _time
372
+ _time.sleep(self._llm_config.retry_delay * (2 ** attempt))
373
+ continue
374
+ raise
375
+
376
+ return ""
377
+
378
+ # ─── 接口 C: callable 兼容 ───
379
+
380
+ def __call__(
381
+ self,
382
+ prompt: str,
383
+ tools: Optional[List[str]] = None,
384
+ tool_choice: Optional[str] = None,
385
+ ) -> str:
386
+ """callable 兼容: llm_judge / lineage_compress / operators 使用。"""
387
+ agent = self._ensure_agent()
388
+
389
+ if agent is None:
390
+ return self._fallback._call_api(
391
+ [Message(role=MessageRole.USER, content=prompt)]
392
+ ).content
393
+
394
+ result = self._run_sync(
395
+ self._async_chat_collect(
396
+ agent, prompt,
397
+ tools=tools, tool_choice=tool_choice,
398
+ )
399
+ )
400
+ return result["content"] or ""
401
+
402
+ # ─── 接口 D: nanobot 原生 async ───
403
+
404
+ async def run(
405
+ self,
406
+ prompt: str,
407
+ session_id: str = "default",
408
+ tools: Optional[List[str]] = None,
409
+ tool_choice: Optional[str] = None,
410
+ with_tool_events: bool = False,
411
+ ) -> Union[str, ToolCallResponse]:
412
+ """异步调用 nanobot Agent。
413
+
414
+ Args:
415
+ prompt: 用户消息
416
+ session_id: 会话 ID
417
+ tools: 工具名列表 (None=全部, []=无工具)
418
+ tool_choice: "auto"/"none"/"required"
419
+ with_tool_events: True → 返回 ToolCallResponse (含工具调用详情)
420
+
421
+ Returns:
422
+ str: 默认行为, 仅返回最终文本
423
+ ToolCallResponse: with_tool_events=True, 包含 tools_used/stop_reason/events
424
+ """
425
+ agent = self._ensure_agent()
426
+
427
+ if agent is None:
428
+ result = self._fallback._call_api(
429
+ [Message(role=MessageRole.USER, content=prompt)]
430
+ )
431
+ if with_tool_events:
432
+ return ToolCallResponse(
433
+ content=result.content,
434
+ stop_reason="stop",
435
+ )
436
+ return result.content
437
+
438
+ if with_tool_events:
439
+ return await self._async_chat_collect(
440
+ agent, prompt,
441
+ session_id=session_id,
442
+ tools=tools, tool_choice=tool_choice,
443
+ collect_events=True,
444
+ )
445
+
446
+ result = await self._async_chat_collect(
447
+ agent, prompt,
448
+ session_id=session_id,
449
+ tools=tools, tool_choice=tool_choice,
450
+ )
451
+ return result["content"] or ""
452
+
453
+ # ─── 内部工具 ───
454
+
455
+ async def _async_chat_collect(
456
+ self,
457
+ agent,
458
+ prompt: str,
459
+ session_id: str = "default",
460
+ tools: Optional[List[str]] = None,
461
+ tool_choice: Optional[str] = None,
462
+ collect_events: bool = False,
463
+ temperature: Optional[float] = None,
464
+ ) -> Union[Dict[str, Any], ToolCallResponse]:
465
+ """异步消费 agent.chat() 流, 收集最终结果 + 工具调用。"""
466
+ final_content = ""
467
+ tool_calls = []
468
+ stop_reason = "stop"
469
+ events = []
470
+
471
+ async for event in agent.chat(
472
+ prompt,
473
+ session_id=session_id,
474
+ tools=tools,
475
+ tool_choice=tool_choice,
476
+ temperature=temperature,
477
+ ):
478
+ etype = event.get("type")
479
+
480
+ if collect_events:
481
+ events.append(event)
482
+
483
+ if etype == "tool_call":
484
+ tool_calls.append({
485
+ "id": event.get("id", ""),
486
+ "name": event.get("name", ""),
487
+ "arguments": event.get("arguments", {}),
488
+ })
489
+ elif etype == "done":
490
+ final_content = event.get("content", final_content)
491
+ stop_reason = event.get("stop_reason", "stop")
492
+ if tool_calls:
493
+ stop_reason = "tool_calls"
494
+ elif etype == "error":
495
+ final_content = event.get("content", final_content)
496
+ stop_reason = "error"
497
+
498
+ if collect_events:
499
+ return ToolCallResponse(
500
+ content=final_content,
501
+ tools_used=[tc["name"] for tc in tool_calls],
502
+ stop_reason=stop_reason,
503
+ events=events,
504
+ )
505
+
506
+ return {
507
+ "content": final_content,
508
+ "tool_calls": tool_calls if tool_calls else None,
509
+ "stop_reason": stop_reason,
510
+ }
511
+
512
+ def _messages_to_prompt(self, messages: List[Message]) -> str:
513
+ """将 Message 列表转为单一 prompt 字符串。"""
514
+ parts = []
515
+ for msg in messages:
516
+ if msg.role == MessageRole.SYSTEM:
517
+ parts.append(f"[System] {msg.content}")
518
+ elif msg.role == MessageRole.USER:
519
+ parts.append(msg.content)
520
+ elif msg.role == MessageRole.ASSISTANT:
521
+ parts.append(f"[Assistant] {msg.content}")
522
+ else:
523
+ parts.append(msg.content)
524
+ return "\n\n".join(parts)
525
+
526
+ def _run_sync(self, coro) -> Any:
527
+ """在同步上下文中运行 async 协程。"""
528
+ try:
529
+ loop = asyncio.get_event_loop()
530
+ if loop.is_running():
531
+ import concurrent.futures
532
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex:
533
+ return ex.submit(asyncio.run, coro).result()
534
+ return loop.run_until_complete(coro)
535
+ except RuntimeError:
536
+ return asyncio.run(coro)
537
+
538
+
539
+ # ─── 全局单例 ───
540
+
541
+ _global_gateway: Optional[LLMGateway] = None
542
+
543
+
544
+ def _default_agent_factory(workspace: str = ".agent"):
545
+ """Default agent factory — imports nanobot Agent lazily."""
546
+ from QuantNodes.agent.nanobot_bridge import Agent
547
+ return Agent(workspace=workspace)
548
+
549
+
550
+ def get_llm_gateway(workspace: str = ".agent") -> LLMGateway:
551
+ """获取全局 LLMGateway 单例。"""
552
+ global _global_gateway
553
+ if _global_gateway is None:
554
+ _global_gateway = LLMGateway(
555
+ workspace=workspace,
556
+ agent_factory=lambda: _default_agent_factory(workspace),
557
+ )
558
+ return _global_gateway
559
+
560
+
561
+ def create_llm_gateway(
562
+ agent: Any = None, workspace: str = ".agent"
563
+ ) -> LLMGateway:
564
+ """创建新的 LLMGateway 实例(非单例)。"""
565
+ return LLMGateway(agent=agent, workspace=workspace)
566
+
567
+
568
+ def reset_llm_gateway() -> None:
569
+ """重置全局 LLMGateway 单例(测试用)。"""
570
+ global _global_gateway
571
+ _global_gateway = None
@@ -0,0 +1,76 @@
1
+ # coding=utf-8
2
+ """NullLLMClient (Null Object pattern, Phase 1.1).
3
+
4
+ 替代 None 检查, 当无 LLM 客户端可用时返回确定性 canned response + warning 日志。
5
+ 避免调用方散落的 `if self._llm_client is None` 模式 (见 agent/tools/strategy.py)。
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, List, Optional
10
+
11
+ from QuantNodes.ai.llm.base import (
12
+ ChatCompletion,
13
+ LLMClientBase,
14
+ Message,
15
+ MessageRole,
16
+ )
17
+
18
+
19
+ class NullLLMClient(LLMClientBase):
20
+ """LLM 客户端的空对象 (Null Object pattern)。
21
+
22
+ 行为:
23
+ - chat() 返回固定 canned response, 内容含标识 `[NullLLMClient]`
24
+ - 每次调用 logger.warning 一次 (避免刷屏, 用 _warned 标记)
25
+ - 不抛异常, 不访问网络
26
+
27
+ 使用场景:
28
+ - 单元测试中无需真实 LLM
29
+ - 配置缺失或 API key 未设置时的 fallback
30
+ - Agent 框架中的 "无 LLM 模式" 占位
31
+
32
+ Args:
33
+ canned_response: 固定返回内容, 默认带 [NullLLMClient] 前缀
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ canned_response: Optional[str] = None,
39
+ **kwargs: Any,
40
+ ) -> None:
41
+ # 避免父类要求 api_key, base_url 等真实字段
42
+ super().__init__(api_key=None, base_url=None, timeout=0, max_retries=0, **kwargs)
43
+ self.canned_response = canned_response or (
44
+ "[NullLLMClient] no real LLM configured; "
45
+ "this is a deterministic placeholder response."
46
+ )
47
+ self._warned = False
48
+ self.call_count = 0
49
+
50
+ def _call_api(
51
+ self,
52
+ messages: List[Message],
53
+ model: Optional[str] = None,
54
+ **kwargs: Any,
55
+ ) -> ChatCompletion:
56
+ if not self._warned:
57
+ self.logger.warning(
58
+ "NullLLMClient is in use; downstream code will receive "
59
+ "deterministic placeholder responses."
60
+ )
61
+ self._warned = True
62
+ self.call_count += 1
63
+ return ChatCompletion(
64
+ content=self.canned_response,
65
+ role=MessageRole.ASSISTANT,
66
+ finish_reason="null",
67
+ usage={"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
68
+ )
69
+
70
+ def _call_api_stream(self, messages: List[Message], model: Optional[str] = None, **kwargs: Any):
71
+ if not self._warned:
72
+ self.logger.warning("NullLLMClient is in use (stream).")
73
+ self._warned = True
74
+ self.call_count += 1
75
+ from QuantNodes.ai.llm.base import ChatCompletionChunk
76
+ yield ChatCompletionChunk(content=self.canned_response, finish_reason="null")