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.
- QuantNodes/__init__.py +15 -0
- QuantNodes/__main__.py +14 -0
- QuantNodes/agent/__init__.py +158 -0
- QuantNodes/agent/agents/__init__.py +13 -0
- QuantNodes/agent/agents/definition.py +180 -0
- QuantNodes/agent/agents/manager.py +73 -0
- QuantNodes/agent/config/__init__.py +34 -0
- QuantNodes/agent/config/executor.py +958 -0
- QuantNodes/agent/config/loader.py +427 -0
- QuantNodes/agent/config/templates/bollinger_bands.yaml +84 -0
- QuantNodes/agent/config/templates/dual_ma.yaml +72 -0
- QuantNodes/agent/config/templates/empty.yaml +56 -0
- QuantNodes/agent/config/templates/mean_reversion.yaml +47 -0
- QuantNodes/agent/config/templates/mean_reversion_zscore.yaml +90 -0
- QuantNodes/agent/config/templates/momentum.yaml +81 -0
- QuantNodes/agent/config/templates/momentum_breakout.yaml +84 -0
- QuantNodes/agent/config/templates/rsi_strategy.yaml +72 -0
- QuantNodes/agent/config/templates/volume_price.yaml +86 -0
- QuantNodes/agent/config/types.py +156 -0
- QuantNodes/agent/config_mapper.py +293 -0
- QuantNodes/agent/core/__init__.py +19 -0
- QuantNodes/agent/core/dream.py +47 -0
- QuantNodes/agent/core/quant_dream.py +274 -0
- QuantNodes/agent/cron_jobs.py +314 -0
- QuantNodes/agent/nanobot_bridge.py +242 -0
- QuantNodes/agent/permission/__init__.py +30 -0
- QuantNodes/agent/permission/defaults.py +36 -0
- QuantNodes/agent/permission/evaluate.py +41 -0
- QuantNodes/agent/permission/models.py +59 -0
- QuantNodes/agent/permission/service.py +133 -0
- QuantNodes/agent/providers/__init__.py +11 -0
- QuantNodes/agent/providers/base.py +102 -0
- QuantNodes/agent/providers/quantnodes.py +610 -0
- QuantNodes/agent/providers/rate_limiter.py +326 -0
- QuantNodes/agent/providers/registry.py +163 -0
- QuantNodes/agent/skills/__init__.py +20 -0
- QuantNodes/agent/skills/base.py +118 -0
- QuantNodes/agent/skills/bridge.py +73 -0
- QuantNodes/agent/skills/factor/__init__.py +14 -0
- QuantNodes/agent/skills/factor/correlation.py +99 -0
- QuantNodes/agent/skills/factor/group_backtest.py +114 -0
- QuantNodes/agent/skills/factor/ic_analysis.py +106 -0
- QuantNodes/agent/skills/loader.py +107 -0
- QuantNodes/agent/skills/registry.py +105 -0
- QuantNodes/agent/skills/strategy/__init__.py +16 -0
- QuantNodes/agent/skills/strategy/bollinger.py +86 -0
- QuantNodes/agent/skills/strategy/dual_ma.py +82 -0
- QuantNodes/agent/skills/strategy/momentum.py +74 -0
- QuantNodes/agent/skills/strategy/rsi_reversal.py +99 -0
- QuantNodes/agent/skills_quant/__init__.py +14 -0
- QuantNodes/agent/skills_quant/backtest-analyze/SKILL.md +42 -0
- QuantNodes/agent/skills_quant/config-driven/SKILL.md +72 -0
- QuantNodes/agent/skills_quant/factor-research/SKILL.md +40 -0
- QuantNodes/agent/skills_quant/quant-dream/SKILL.md +55 -0
- QuantNodes/agent/skills_quant/risk-management/SKILL.md +45 -0
- QuantNodes/agent/skills_quant/strategy-design/SKILL.md +43 -0
- QuantNodes/agent/templates/__init__.py +4 -0
- QuantNodes/agent/tools/__init__.py +173 -0
- QuantNodes/agent/tools/_workspace.py +51 -0
- QuantNodes/agent/tools/alpha_backtest.py +328 -0
- QuantNodes/agent/tools/alpha_evaluate.py +493 -0
- QuantNodes/agent/tools/backtest.py +226 -0
- QuantNodes/agent/tools/base.py +133 -0
- QuantNodes/agent/tools/code_search.py +207 -0
- QuantNodes/agent/tools/config_backtest.py +401 -0
- QuantNodes/agent/tools/context.py +97 -0
- QuantNodes/agent/tools/dream_skill.py +77 -0
- QuantNodes/agent/tools/echo.py +38 -0
- QuantNodes/agent/tools/factor.py +231 -0
- QuantNodes/agent/tools/file_ops.py +201 -0
- QuantNodes/agent/tools/git_ops.py +190 -0
- QuantNodes/agent/tools/operator_lookup.py +218 -0
- QuantNodes/agent/tools/output_truncation.py +77 -0
- QuantNodes/agent/tools/path_check.py +43 -0
- QuantNodes/agent/tools/pipeline.py +62 -0
- QuantNodes/agent/tools/registry.py +150 -0
- QuantNodes/agent/tools/sandbox.py +62 -0
- QuantNodes/agent/tools/shell_safety.py +63 -0
- QuantNodes/agent/tools/strategy.py +106 -0
- QuantNodes/agent/tools/task.py +171 -0
- QuantNodes/agent/tools/web_fetch.py +142 -0
- QuantNodes/agent/tools/web_search.py +114 -0
- QuantNodes/agent/tools/wiki.py +370 -0
- QuantNodes/agent/utils/__init__.py +11 -0
- QuantNodes/agent/utils/helpers.py +43 -0
- QuantNodes/agent/utils/prompt_templates.py +30 -0
- QuantNodes/agent/workflows/__init__.py +20 -0
- QuantNodes/agent/workflows/implementations/__init__.py +8 -0
- QuantNodes/agent/workflows/implementations/alpha_gpt.py +508 -0
- QuantNodes/agent/workflows/implementations/mcts.py +442 -0
- QuantNodes/agent/workflows/parsers.py +44 -0
- QuantNodes/agent/workflows/registry.py +119 -0
- QuantNodes/agent/workflows/step_agent.py +219 -0
- QuantNodes/agent/workflows/tool.py +198 -0
- QuantNodes/ai/__init__.py +93 -0
- QuantNodes/ai/llm/__init__.py +75 -0
- QuantNodes/ai/llm/base.py +233 -0
- QuantNodes/ai/llm/decorators.py +281 -0
- QuantNodes/ai/llm/gateway.py +571 -0
- QuantNodes/ai/llm/null.py +76 -0
- QuantNodes/ai/llm/openai.py +435 -0
- QuantNodes/ai/optimizer.py +405 -0
- QuantNodes/ai/prompts/__init__.py +229 -0
- QuantNodes/ai/sandbox.py +371 -0
- QuantNodes/ai/sandbox_pandas_bridge.py +150 -0
- QuantNodes/ai/strategy_gen.py +396 -0
- QuantNodes/backtest/__init__.py +64 -0
- QuantNodes/backtest/backtest_node.py +188 -0
- QuantNodes/backtest/broker_node.py +378 -0
- QuantNodes/backtest/config_runner.py +397 -0
- QuantNodes/backtest/config_strategy.py +64 -0
- QuantNodes/backtest/risk_node.py +360 -0
- QuantNodes/backtest/strategy_node.py +268 -0
- QuantNodes/cache_node/__init__.py +19 -0
- QuantNodes/cache_node/base.py +244 -0
- QuantNodes/cache_node/cache_store.py +99 -0
- QuantNodes/cache_node/metadata.py +100 -0
- QuantNodes/cli/__init__.py +109 -0
- QuantNodes/cli/_helpers.py +511 -0
- QuantNodes/cli/command.py +110 -0
- QuantNodes/cli/commands/__init__.py +69 -0
- QuantNodes/cli/commands/agent.py +158 -0
- QuantNodes/cli/commands/alpha.py +951 -0
- QuantNodes/cli/commands/chat.py +38 -0
- QuantNodes/cli/commands/evolve.py +120 -0
- QuantNodes/cli/commands/factor.py +569 -0
- QuantNodes/cli/commands/init.py +190 -0
- QuantNodes/cli/commands/run.py +259 -0
- QuantNodes/cli/commands/serve.py +398 -0
- QuantNodes/cli/commands/version.py +120 -0
- QuantNodes/cli/enhanced.py +146 -0
- QuantNodes/conf_node/__init__.py +37 -0
- QuantNodes/conf_node/base.py +120 -0
- QuantNodes/conf_node/env_config.py +132 -0
- QuantNodes/conf_node/ini_config.py +70 -0
- QuantNodes/conf_node/json_config.py +69 -0
- QuantNodes/conf_node/yaml_config.py +78 -0
- QuantNodes/constants.py +17 -0
- QuantNodes/core/__init__.py +196 -0
- QuantNodes/core/_lookback_helpers.py +49 -0
- QuantNodes/core/ast_parser.py +198 -0
- QuantNodes/core/base.py +61 -0
- QuantNodes/core/cache_manager.py +344 -0
- QuantNodes/core/cache_utils.py +150 -0
- QuantNodes/core/cond_builder.py +53 -0
- QuantNodes/core/config.py +170 -0
- QuantNodes/core/constants.py +48 -0
- QuantNodes/core/control.py +412 -0
- QuantNodes/core/data_preprocessing.py +453 -0
- QuantNodes/core/data_source.py +46 -0
- QuantNodes/core/events.py +178 -0
- QuantNodes/core/evolution/__init__.py +22 -0
- QuantNodes/core/evolution/loop.py +583 -0
- QuantNodes/core/evolution/operators.py +289 -0
- QuantNodes/core/evolution/settings.py +44 -0
- QuantNodes/core/expression.py +841 -0
- QuantNodes/core/feedback/__init__.py +38 -0
- QuantNodes/core/feedback/channels.py +182 -0
- QuantNodes/core/feedback/collector.py +91 -0
- QuantNodes/core/feedback/dataclass.py +239 -0
- QuantNodes/core/feedback/llm_judge.py +138 -0
- QuantNodes/core/knowledge/__init__.py +69 -0
- QuantNodes/core/knowledge/knowledge_base.py +217 -0
- QuantNodes/core/knowledge/lineage_compress.py +196 -0
- QuantNodes/core/knowledge/lineage_expand.py +123 -0
- QuantNodes/core/knowledge/metrics/__init__.py +43 -0
- QuantNodes/core/knowledge/metrics/evaluator.py +176 -0
- QuantNodes/core/knowledge/metrics/metrics.py +220 -0
- QuantNodes/core/knowledge/rag_prompt.py +196 -0
- QuantNodes/core/knowledge/retriever.py +209 -0
- QuantNodes/core/lambda_node.py +81 -0
- QuantNodes/core/monitoring/__init__.py +22 -0
- QuantNodes/core/monitoring/collector.py +292 -0
- QuantNodes/core/monitoring/dashboard.py +365 -0
- QuantNodes/core/node.py +375 -0
- QuantNodes/core/pandas_utils.py +504 -0
- QuantNodes/core/parallel/__init__.py +15 -0
- QuantNodes/core/parallel/worker.py +140 -0
- QuantNodes/core/parallel/worker_process.py +265 -0
- QuantNodes/core/path_utils.py +73 -0
- QuantNodes/core/pipeline.py +328 -0
- QuantNodes/core/plugin.py +135 -0
- QuantNodes/core/quality_gate/__init__.py +32 -0
- QuantNodes/core/quality_gate/complexity.py +94 -0
- QuantNodes/core/quality_gate/consistency.py +26 -0
- QuantNodes/core/quality_gate/node.py +97 -0
- QuantNodes/core/quality_gate/redundancy.py +51 -0
- QuantNodes/core/quality_gate/settings.py +43 -0
- QuantNodes/core/quality_gate/zoo.py +98 -0
- QuantNodes/core/serializable.py +116 -0
- QuantNodes/core/serialization.py +673 -0
- QuantNodes/core/tools.py +333 -0
- QuantNodes/core/trajectory/__init__.py +25 -0
- QuantNodes/core/trajectory/entry.py +116 -0
- QuantNodes/core/trajectory/lineage.py +67 -0
- QuantNodes/core/trajectory/pool.py +211 -0
- QuantNodes/core/trajectory/selector.py +140 -0
- QuantNodes/core/visualization/__init__.py +33 -0
- QuantNodes/core/visualization/builder.py +233 -0
- QuantNodes/core/visualization/gate_breakdown.py +140 -0
- QuantNodes/core/visualization/lineage_dag.py +203 -0
- QuantNodes/core/visualization/metric_distribution.py +125 -0
- QuantNodes/core/visualization/report.py +68 -0
- QuantNodes/database_node/__init__.py +69 -0
- QuantNodes/database_node/base.py +135 -0
- QuantNodes/database_node/clickhouse_node.py +272 -0
- QuantNodes/database_node/csv_node.py +83 -0
- QuantNodes/database_node/duckdb_node.py +86 -0
- QuantNodes/database_node/factory.py +83 -0
- QuantNodes/database_node/mysql_node.py +100 -0
- QuantNodes/database_node/parquet_node.py +75 -0
- QuantNodes/database_node/sqlite_node.py +67 -0
- QuantNodes/factor_node/__init__.py +50 -0
- QuantNodes/factor_node/factor.py +563 -0
- QuantNodes/factor_node/factor_db.py +421 -0
- QuantNodes/factor_node/factor_functions/__init__.py +252 -0
- QuantNodes/factor_node/factor_functions/_helpers.py +358 -0
- QuantNodes/factor_node/factor_functions/_helpers_debug.py +317 -0
- QuantNodes/factor_node/factor_functions/composite_ops.py +136 -0
- QuantNodes/factor_node/factor_functions/math_ops.py +433 -0
- QuantNodes/factor_node/factor_functions/section_ops.py +290 -0
- QuantNodes/factor_node/factor_functions/talib_ops.py +1293 -0
- QuantNodes/factor_node/factor_functions/time_ops.py +535 -0
- QuantNodes/factor_node/factor_operation.py +1115 -0
- QuantNodes/factor_node/factor_table.py +1073 -0
- QuantNodes/factor_node/quant_nodes_object.py +60 -0
- QuantNodes/mcp_server/__init__.py +27 -0
- QuantNodes/mcp_server/__main__.py +4 -0
- QuantNodes/mcp_server/server.py +272 -0
- QuantNodes/methods/__init__.py +28 -0
- QuantNodes/methods/pipeline.py +100 -0
- QuantNodes/methods/sandbox.py +102 -0
- QuantNodes/monitor/__init__.py +27 -0
- QuantNodes/monitor/agent_tools/__init__.py +5 -0
- QuantNodes/monitor/agent_tools/monitor_tool.py +98 -0
- QuantNodes/monitor/agent_tools/schedule_tool.py +98 -0
- QuantNodes/monitor/agent_tools/version_tool.py +133 -0
- QuantNodes/monitor/monitor/__init__.py +6 -0
- QuantNodes/monitor/monitor/alerter.py +60 -0
- QuantNodes/monitor/monitor/collector.py +164 -0
- QuantNodes/monitor/monitor/dashboard.py +115 -0
- QuantNodes/monitor/monitor/drift.py +190 -0
- QuantNodes/monitor/scheduler/__init__.py +4 -0
- QuantNodes/monitor/scheduler/runner.py +133 -0
- QuantNodes/monitor/scheduler/scheduler.py +184 -0
- QuantNodes/monitor/storage/__init__.py +16 -0
- QuantNodes/monitor/storage/models.py +70 -0
- QuantNodes/monitor/storage/repository.py +407 -0
- QuantNodes/monitor/version/__init__.py +4 -0
- QuantNodes/monitor/version/diff.py +81 -0
- QuantNodes/monitor/version/version_manager.py +182 -0
- QuantNodes/operator_node/__init__.py +28 -0
- QuantNodes/operator_node/base.py +97 -0
- QuantNodes/operator_node/query_node.py +129 -0
- QuantNodes/operator_node/sql_builder.py +125 -0
- QuantNodes/operator_node/sql_utils.py +172 -0
- QuantNodes/operator_node/transform.py +130 -0
- QuantNodes/operators/__init__.py +90 -0
- QuantNodes/operators/_engine.py +108 -0
- QuantNodes/operators/composite.py +161 -0
- QuantNodes/operators/composite_dag.py +667 -0
- QuantNodes/operators/composite_dag_ops.py +343 -0
- QuantNodes/operators/composite_dag_pandas_ops.py +382 -0
- QuantNodes/operators/custom.py +408 -0
- QuantNodes/operators/facade.py +164 -0
- QuantNodes/operators/math.py +163 -0
- QuantNodes/operators/proxy.py +29 -0
- QuantNodes/operators/registry.py +144 -0
- QuantNodes/operators/section.py +99 -0
- QuantNodes/operators/talib.py +757 -0
- QuantNodes/operators/templates.py +95 -0
- QuantNodes/operators/time_series.py +136 -0
- QuantNodes/prompts/__init__.py +20 -0
- QuantNodes/prompts/backtest/__init__.py +12 -0
- QuantNodes/prompts/backtest/factor_based.py +86 -0
- QuantNodes/prompts/backtest/standard.py +73 -0
- QuantNodes/prompts/factor/__init__.py +14 -0
- QuantNodes/prompts/factor/correlation.py +77 -0
- QuantNodes/prompts/factor/group_backtest.py +86 -0
- QuantNodes/prompts/factor/ic_analysis.py +91 -0
- QuantNodes/prompts/strategy/__init__.py +18 -0
- QuantNodes/prompts/strategy/market_neutral.py +96 -0
- QuantNodes/prompts/strategy/mean_reversion.py +107 -0
- QuantNodes/prompts/strategy/momentum.py +160 -0
- QuantNodes/prompts/strategy/pairs_trading.py +107 -0
- QuantNodes/prompts/strategy/trend_following.py +96 -0
- QuantNodes/research/README.md +106 -0
- QuantNodes/research/__init__.py +154 -0
- QuantNodes/research/_legacy_3c/__init__.py +61 -0
- QuantNodes/research/_legacy_3c/auto_researcher.py +289 -0
- QuantNodes/research/_legacy_3c/factor_evaluator.py +560 -0
- QuantNodes/research/_legacy_3c/factor_miner.py +318 -0
- QuantNodes/research/_legacy_3c/mcts_search.py +324 -0
- QuantNodes/research/factor_test/__init__.py +25 -0
- QuantNodes/research/factor_test/config.py +184 -0
- QuantNodes/research/factor_test/config_builder.py +276 -0
- QuantNodes/research/factor_test/e2e/data_prep.py +163 -0
- QuantNodes/research/factor_test/e2e/run_evolution_e2e.py +309 -0
- QuantNodes/research/factor_test/evolution_adapter.py +231 -0
- QuantNodes/research/factor_test/feedback_wrapper.py +102 -0
- QuantNodes/research/factor_test/ifind_db/__init__.py +7 -0
- QuantNodes/research/factor_test/ifind_db/fetcher.py +224 -0
- QuantNodes/research/factor_test/ifind_db/ifind_database.py +689 -0
- QuantNodes/research/factor_test/nodes/__init__.py +1 -0
- QuantNodes/research/factor_test/nodes/_base.py +91 -0
- QuantNodes/research/factor_test/nodes/adjust_date_node.py +48 -0
- QuantNodes/research/factor_test/nodes/configs.py +240 -0
- QuantNodes/research/factor_test/nodes/factor_neutralize_node.py +87 -0
- QuantNodes/research/factor_test/nodes/factor_preprocess_node.py +222 -0
- QuantNodes/research/factor_test/nodes/factor_score_node.py +141 -0
- QuantNodes/research/factor_test/nodes/factor_test_report_node.py +153 -0
- QuantNodes/research/factor_test/nodes/group_analyzer_node.py +317 -0
- QuantNodes/research/factor_test/nodes/ic_analyzer_node.py +112 -0
- QuantNodes/research/factor_test/nodes/load_data_node.py +100 -0
- QuantNodes/research/factor_test/nodes/long_short_node.py +93 -0
- QuantNodes/research/factor_test/nodes/neutralizers.py +222 -0
- QuantNodes/research/factor_test/nodes/preprocess_strategies.py +277 -0
- QuantNodes/research/factor_test/nodes/risk_correlation_node.py +112 -0
- QuantNodes/research/factor_test/nodes/sample_pool_filter_node.py +110 -0
- QuantNodes/research/factor_test/nodes/tradability_filter_node.py +92 -0
- QuantNodes/research/factor_test/pipeline_runner.py +305 -0
- QuantNodes/research/factor_test/pipeline_spec.py +216 -0
- QuantNodes/research/factor_test/utils/__init__.py +26 -0
- QuantNodes/research/factor_test/utils/constants.py +86 -0
- QuantNodes/research/factor_test/utils/data_loader.py +141 -0
- QuantNodes/research/factor_test/utils/date_utils.py +232 -0
- QuantNodes/research/factor_test/utils/file_loaders.py +150 -0
- QuantNodes/research/factor_test/utils/labels.py +37 -0
- QuantNodes/research/factor_test/utils/metrics_extractor.py +55 -0
- QuantNodes/research/factor_test/utils/performance_metrics.py +175 -0
- QuantNodes/research/factor_test/utils/safe_load.py +106 -0
- QuantNodes/research/quant_alpha/CHANGELOG.md +80 -0
- QuantNodes/research/quant_alpha/README.md +142 -0
- QuantNodes/research/quant_alpha/__init__.py +45 -0
- QuantNodes/research/quant_alpha/adapters/__init__.py +99 -0
- QuantNodes/research/quant_alpha/adapters/calculator.py +503 -0
- QuantNodes/research/quant_alpha/adapters/expression.py +387 -0
- QuantNodes/research/quant_alpha/alpha101_design/__init__.py +50 -0
- QuantNodes/research/quant_alpha/alpha101_design/few_shot_examples.py +243 -0
- QuantNodes/research/quant_alpha/alpha101_design/philosophy.py +474 -0
- QuantNodes/research/quant_alpha/alpha158_design/__init__.py +63 -0
- QuantNodes/research/quant_alpha/alpha158_design/few_shot_examples.py +219 -0
- QuantNodes/research/quant_alpha/alpha158_design/philosophy.py +240 -0
- QuantNodes/research/quant_alpha/evaluation/__init__.py +47 -0
- QuantNodes/research/quant_alpha/evaluation/baselines/__init__.py +8 -0
- QuantNodes/research/quant_alpha/evaluation/baselines/g1_handcrafted.py +135 -0
- QuantNodes/research/quant_alpha/evaluation/baselines/g2_llm_only.py +269 -0
- QuantNodes/research/quant_alpha/evaluation/baselines/g3_alpha_gpt.py +152 -0
- QuantNodes/research/quant_alpha/evaluation/clickhouse_data_loader.py +227 -0
- QuantNodes/research/quant_alpha/evaluation/contracts.py +376 -0
- QuantNodes/research/quant_alpha/evaluation/evaluators/__init__.py +6 -0
- QuantNodes/research/quant_alpha/evaluation/evaluators/polars_evaluator.py +545 -0
- QuantNodes/research/quant_alpha/evaluation/mock_data_loader.py +226 -0
- QuantNodes/research/quant_alpha/evaluation/runner.py +243 -0
- QuantNodes/research/quant_alpha/llm/__init__.py +38 -0
- QuantNodes/research/quant_alpha/llm/parser.py +681 -0
- QuantNodes/research/quant_alpha/logic_driven_pipeline.py +411 -0
- QuantNodes/research/quant_alpha/logic_mining/__init__.py +74 -0
- QuantNodes/research/quant_alpha/logic_mining/compiler.py +457 -0
- QuantNodes/research/quant_alpha/logic_mining/generator.py +366 -0
- QuantNodes/research/quant_alpha/logic_mining/models.py +252 -0
- QuantNodes/research/quant_alpha/logic_mining/parser.py +287 -0
- QuantNodes/research/quant_alpha/logic_mining/pipelines.py +297 -0
- QuantNodes/research/quant_alpha/logic_mining/sources.py +149 -0
- QuantNodes/research/quant_alpha/mcts/__init__.py +66 -0
- QuantNodes/research/quant_alpha/mcts/cache.py +262 -0
- QuantNodes/research/quant_alpha/mcts/extension_ops.py +320 -0
- QuantNodes/research/quant_alpha/mcts/feedback.py +825 -0
- QuantNodes/research/quant_alpha/mcts/op_prior.py +180 -0
- QuantNodes/research/quant_alpha/mcts/search.py +540 -0
- QuantNodes/research/quant_alpha/mcts/tree.py +201 -0
- QuantNodes/research/quant_alpha/operator_vocab/__init__.py +50 -0
- QuantNodes/research/quant_alpha/operator_vocab/config.py +54 -0
- QuantNodes/research/quant_alpha/operator_vocab/metadata.py +263 -0
- QuantNodes/research/quant_alpha/operator_vocab/vocabulary.py +481 -0
- QuantNodes/research/quant_alpha/pipeline.py +1027 -0
- QuantNodes/research/quant_alpha/types/__init__.py +27 -0
- QuantNodes/research/quant_alpha/types/constants.py +28 -0
- QuantNodes/research/quant_alpha/types/state.py +205 -0
- QuantNodes/research/quant_alpha/workflow/__init__.py +32 -0
- QuantNodes/research/quant_alpha/workflow/alpha_gpt.py +911 -0
- QuantNodes/research/quant_alpha/workflow/alpha_logics.py +416 -0
- QuantNodes/research/quant_alpha/workflow/state.py +27 -0
- QuantNodes/research/report_reproducer.py +485 -0
- QuantNodes/research/wiki.py +1155 -0
- QuantNodes/symbolic/__init__.py +51 -0
- QuantNodes/symbolic/compiler.py +113 -0
- QuantNodes/symbolic/dialect.py +260 -0
- QuantNodes/symbolic/executor.py +147 -0
- QuantNodes/symbolic/expression.py +234 -0
- QuantNodes/symbolic/functions.py +433 -0
- QuantNodes/symbolic/optimizer.py +165 -0
- QuantNodes/ui_node/__init__.py +30 -0
- QuantNodes/ui_node/base.py +222 -0
- quantnodes-3.0.0.dist-info/METADATA +463 -0
- quantnodes-3.0.0.dist-info/RECORD +399 -0
- quantnodes-3.0.0.dist-info/WHEEL +5 -0
- quantnodes-3.0.0.dist-info/entry_points.txt +24 -0
- 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")
|