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,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
|
+
)
|