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,314 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""QuantNodes-specific cron jobs.
|
|
3
|
+
|
|
4
|
+
v3.0.0 Stage 5.5: Layer quant-domain periodic tasks on top of nanobot's
|
|
5
|
+
upstream ``CronService``. We register three system jobs that run on a
|
|
6
|
+
fixed schedule inside the in-process nanobot runtime:
|
|
7
|
+
|
|
8
|
+
1. **Daily 16:30** (Asia/Shanghai) — ``quant-daily-recap``
|
|
9
|
+
Factor IC recalc + backtest result archival + a summary message
|
|
10
|
+
delivered to the configured channel (e.g. Feishu group chat).
|
|
11
|
+
|
|
12
|
+
2. **Weekly Sunday 22:00** (Asia/Shanghai) — ``quant-weekly-review``
|
|
13
|
+
Factor performance report (IC / ICIR / decay) + risk attribution
|
|
14
|
+
summary across the live strategy pool.
|
|
15
|
+
|
|
16
|
+
3. **Monthly 1st 02:00** (Asia/Shanghai) — ``quant-monthly-strategy-pool``
|
|
17
|
+
Wiki incremental index + strategy-pool monthly review (best/worst,
|
|
18
|
+
capacity check, deprecation candidates).
|
|
19
|
+
|
|
20
|
+
All three are added via ``cron_service.add_job(...)`` so users can:
|
|
21
|
+
|
|
22
|
+
- Inspect them with ``GET /api/agent/cron`` (planned, see
|
|
23
|
+
``api/routers/agent.py``).
|
|
24
|
+
- Disable / re-enable / delete them like any other job
|
|
25
|
+
(``CronService.remove_job`` refuses to remove ``system_event`` jobs,
|
|
26
|
+
but ours are ``agent_turn`` jobs and are user-removable).
|
|
27
|
+
- Override the schedule / message via env vars (see
|
|
28
|
+
``build_quant_cron_jobs_from_env`` below).
|
|
29
|
+
|
|
30
|
+
We deliberately keep the **content** of each job a single concise message —
|
|
31
|
+
the agent has the quant tools (``factor``, ``wiki``, ``strategy``,
|
|
32
|
+
``backtest``) at its disposal and knows how to fan out into a proper
|
|
33
|
+
report. This matches the upstream design: cron payloads are LLM
|
|
34
|
+
prompts, not pre-baked Python scripts.
|
|
35
|
+
|
|
36
|
+
Reference:
|
|
37
|
+
|
|
38
|
+
- ``nanobot.cron.service.CronService`` — add_job / register_system_job
|
|
39
|
+
- ``nanobot.cron.types.CronSchedule`` — kind: at | every | cron
|
|
40
|
+
- ``nanobot.cron.types.CronPayload`` — kind: system_event | agent_turn
|
|
41
|
+
- ``QuantNodes/agent/core/quant_dream.py`` — Dream hook (separate
|
|
42
|
+
periodic memory consolidation; not part of cron jobs)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import logging
|
|
48
|
+
import os
|
|
49
|
+
from dataclasses import dataclass
|
|
50
|
+
from typing import Any, Callable, List, Optional
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Default cron expressions in Asia/Shanghai (the team's working TZ).
|
|
56
|
+
# Each entry pairs a job ``name`` with a default ``cron_expr`` and the
|
|
57
|
+
# prompt template that the agent runs on each tick. Users override any
|
|
58
|
+
# of these via env vars (see ``build_quant_cron_jobs_from_env``).
|
|
59
|
+
DEFAULT_TZ = "Asia/Shanghai"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class QuantCronJob:
|
|
64
|
+
"""A quant-domain periodic task definition.
|
|
65
|
+
|
|
66
|
+
Fields mirror what ``CronService.add_job`` consumes (we keep the
|
|
67
|
+
declaration local to avoid an import cycle through nanobot at module
|
|
68
|
+
load time — the optional dep).
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
name: str
|
|
72
|
+
cron_expr: str
|
|
73
|
+
message: str
|
|
74
|
+
enabled: bool = True
|
|
75
|
+
deliver: bool = True
|
|
76
|
+
channel: Optional[str] = None
|
|
77
|
+
session_key: Optional[str] = None
|
|
78
|
+
description: str = ""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ----------------------------------------------------------------------------
|
|
82
|
+
# Default job definitions
|
|
83
|
+
# ----------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
DAILY_RECAP_JOB = QuantCronJob(
|
|
86
|
+
name="quant-daily-recap",
|
|
87
|
+
cron_expr="30 16 * * 1-5", # 16:30 Mon–Fri (skip weekends)
|
|
88
|
+
message=(
|
|
89
|
+
"执行日终复盘任务:\n"
|
|
90
|
+
"1. 调 factor 工具对核心因子池做今日 IC / ICIR 重算\n"
|
|
91
|
+
"2. 调 backtest 工具对今日有变动的策略跑一次回测,确认无过拟合 / "
|
|
92
|
+
"未来函数 / 手续费侵蚀\n"
|
|
93
|
+
"3. 调 wiki 工具把今日新增/变更的因子和策略写入 Wiki\n"
|
|
94
|
+
"4. 输出一段 ≤200 字的日终摘要(含因子表现 / 风险归因 / 明日关注点)"
|
|
95
|
+
),
|
|
96
|
+
enabled=True,
|
|
97
|
+
deliver=True,
|
|
98
|
+
session_key="cron:quant-daily-recap",
|
|
99
|
+
description=(
|
|
100
|
+
"Daily 16:30 Mon-Fri — factor IC recalc + backtest archive + Wiki update."
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
WEEKLY_REVIEW_JOB = QuantCronJob(
|
|
106
|
+
name="quant-weekly-review",
|
|
107
|
+
cron_expr="0 22 * * 0", # 22:00 every Sunday
|
|
108
|
+
message=(
|
|
109
|
+
"执行周度复盘任务:\n"
|
|
110
|
+
"1. 调 factor 工具生成核心因子过去 5 个交易日的 IC / 衰减 / 换手率报告\n"
|
|
111
|
+
"2. 调 strategy 工具对策略池每个策略拉一次本周净值 / 风险归因\n"
|
|
112
|
+
"3. 调 wiki 工具把本周因子表现 / 策略表现 / 风险事件写入 Wiki 周报\n"
|
|
113
|
+
"4. 输出一段 ≤300 字的周度摘要(含因子池变化 / 策略池表现 / 风险事件)"
|
|
114
|
+
),
|
|
115
|
+
enabled=True,
|
|
116
|
+
deliver=True,
|
|
117
|
+
session_key="cron:quant-weekly-review",
|
|
118
|
+
description=(
|
|
119
|
+
"Weekly Sunday 22:00 — factor performance report + risk attribution + Wiki weekly."
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
MONTHLY_STRATEGY_POOL_JOB = QuantCronJob(
|
|
125
|
+
name="quant-monthly-strategy-pool",
|
|
126
|
+
cron_expr="0 2 1 * *", # 02:00 on the 1st of every month
|
|
127
|
+
message=(
|
|
128
|
+
"执行月度策略池评审任务:\n"
|
|
129
|
+
"1. 调 wiki 工具做 Wiki 增量索引(核对因子 / 策略 / 回测报告三类文档数量与覆盖率)\n"
|
|
130
|
+
"2. 调 strategy 工具对策略池每个策略做月度表现 / 容量 / 衰减评估\n"
|
|
131
|
+
"3. 识别本月的 top-3 / bottom-3 策略,列出待下线 / 待重构的策略\n"
|
|
132
|
+
"4. 调 wiki 工具把评审结论写入 Wiki 的 strategy_review.md\n"
|
|
133
|
+
"5. 输出一段 ≤400 字的月度摘要(含策略池变化 / 新增下线 / 容量评估)"
|
|
134
|
+
),
|
|
135
|
+
enabled=True,
|
|
136
|
+
deliver=True,
|
|
137
|
+
session_key="cron:quant-monthly-strategy-pool",
|
|
138
|
+
description=(
|
|
139
|
+
"Monthly 1st 02:00 — Wiki incremental + strategy-pool monthly review."
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
DEFAULT_QUANT_CRON_JOBS: List[QuantCronJob] = [
|
|
145
|
+
DAILY_RECAP_JOB,
|
|
146
|
+
WEEKLY_REVIEW_JOB,
|
|
147
|
+
MONTHLY_STRATEGY_POOL_JOB,
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ----------------------------------------------------------------------------
|
|
152
|
+
# Env-var overrides
|
|
153
|
+
# ----------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
# Each env var follows the pattern ``QUANTNODES__CRON__<NAME>__<FIELD>`` where
|
|
156
|
+
# ``<NAME>`` is uppercased job name and ``<FIELD>`` is one of:
|
|
157
|
+
# - ENABLED (bool: "1"/"true"/"false")
|
|
158
|
+
# - CRON_EXPR (cron expression override)
|
|
159
|
+
# - MESSAGE (full message override)
|
|
160
|
+
# - DELIVER (bool: "1"/"true"/"false")
|
|
161
|
+
# - CHANNEL (channel name override; e.g. "feishu")
|
|
162
|
+
#
|
|
163
|
+
# Example:
|
|
164
|
+
# QUANTNODES__CRON__QUANT_DAILY_RECAP__ENABLED=false
|
|
165
|
+
# QUANTNODES__CRON__QUANT_WEEKLY_REVIEW__CRON_EXPR="0 20 * * 0"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _env_flag(name: str, default: bool) -> bool:
|
|
169
|
+
raw = os.environ.get(name, "").strip().lower()
|
|
170
|
+
if not raw:
|
|
171
|
+
return default
|
|
172
|
+
return raw in ("1", "true", "yes", "on")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _apply_env_overrides(jobs: List[QuantCronJob]) -> List[QuantCronJob]:
|
|
176
|
+
"""Apply per-job env-var overrides to a copy of ``jobs``.
|
|
177
|
+
|
|
178
|
+
Non-destructive: returns new ``QuantCronJob`` instances. Unknown env
|
|
179
|
+
vars are ignored. To disable a job, set its ``ENABLED=false`` env var.
|
|
180
|
+
"""
|
|
181
|
+
overridden: List[QuantCronJob] = []
|
|
182
|
+
for job in jobs:
|
|
183
|
+
prefix = f"QUANTNODES__CRON__{job.name.replace('-', '_').upper()}__"
|
|
184
|
+
new_enabled = _env_flag(prefix + "ENABLED", job.enabled)
|
|
185
|
+
new_deliver = _env_flag(prefix + "DELIVER", job.deliver)
|
|
186
|
+
new_cron = os.environ.get(prefix + "CRON_EXPR", job.cron_expr).strip() or job.cron_expr
|
|
187
|
+
new_msg = os.environ.get(prefix + "MESSAGE", job.message) or job.message
|
|
188
|
+
new_chan = os.environ.get(prefix + "CHANNEL", job.channel or "") or job.channel
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
new_enabled == job.enabled
|
|
192
|
+
and new_deliver == job.deliver
|
|
193
|
+
and new_cron == job.cron_expr
|
|
194
|
+
and new_msg == job.message
|
|
195
|
+
and new_chan == job.channel
|
|
196
|
+
):
|
|
197
|
+
overridden.append(job)
|
|
198
|
+
else:
|
|
199
|
+
overridden.append(
|
|
200
|
+
QuantCronJob(
|
|
201
|
+
name=job.name,
|
|
202
|
+
cron_expr=new_cron,
|
|
203
|
+
message=new_msg,
|
|
204
|
+
enabled=new_enabled,
|
|
205
|
+
deliver=new_deliver,
|
|
206
|
+
channel=new_chan,
|
|
207
|
+
session_key=job.session_key,
|
|
208
|
+
description=job.description,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
return overridden
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def build_quant_cron_jobs_from_env(
|
|
215
|
+
base_jobs: Optional[List[QuantCronJob]] = None,
|
|
216
|
+
) -> List[QuantCronJob]:
|
|
217
|
+
"""Return the default list of quant cron jobs, with env-var overrides.
|
|
218
|
+
|
|
219
|
+
Always returns a fresh list (safe to mutate). Filters out jobs that
|
|
220
|
+
the user has explicitly disabled via env (``ENABLED=false``).
|
|
221
|
+
"""
|
|
222
|
+
base = list(base_jobs or DEFAULT_QUANT_CRON_JOBS)
|
|
223
|
+
merged = _apply_env_overrides(base)
|
|
224
|
+
enabled = [j for j in merged if j.enabled]
|
|
225
|
+
if len(enabled) != len(merged):
|
|
226
|
+
logger.info(
|
|
227
|
+
"build_quant_cron_jobs_from_env: disabled %d cron job(s) via env: %s",
|
|
228
|
+
len(merged) - len(enabled),
|
|
229
|
+
", ".join(j.name for j in merged if not j.enabled),
|
|
230
|
+
)
|
|
231
|
+
return enabled
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ----------------------------------------------------------------------------
|
|
235
|
+
# Registration helper
|
|
236
|
+
# ----------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
def register_quant_cron_jobs(cron_service: Any) -> List[str]:
|
|
239
|
+
"""Register all enabled quant cron jobs on a nanobot ``CronService``.
|
|
240
|
+
|
|
241
|
+
``cron_service`` is expected to be a ``nanobot.cron.service.CronService``
|
|
242
|
+
instance. We use ``register_system_job`` (idempotent on restart) so
|
|
243
|
+
that on every process restart the jobs are re-registered without
|
|
244
|
+
duplicating. ``system_event`` is the right payload kind for our
|
|
245
|
+
internal quant jobs — they cannot be removed via the public
|
|
246
|
+
``remove_job`` API and they survive config edits.
|
|
247
|
+
|
|
248
|
+
Returns the list of registered job IDs in the order they were added.
|
|
249
|
+
|
|
250
|
+
Lazy import of ``nanobot.cron.types`` to keep this module importable
|
|
251
|
+
even when ``nanobot-ai`` is not installed (the optional dep).
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
from nanobot.cron.types import (
|
|
255
|
+
CronJob,
|
|
256
|
+
CronJobState,
|
|
257
|
+
CronPayload,
|
|
258
|
+
CronSchedule,
|
|
259
|
+
)
|
|
260
|
+
except ImportError as e: # pragma: no cover - exercised in [agent] installs
|
|
261
|
+
raise NanobotNotInstalledForCron(str(e)) from e
|
|
262
|
+
|
|
263
|
+
jobs = build_quant_cron_jobs_from_env()
|
|
264
|
+
registered: List[str] = []
|
|
265
|
+
for job in jobs:
|
|
266
|
+
cron_job = CronJob(
|
|
267
|
+
id=f"quant-{job.name}", # deterministic id for idempotency
|
|
268
|
+
name=job.name,
|
|
269
|
+
enabled=job.enabled,
|
|
270
|
+
schedule=CronSchedule(
|
|
271
|
+
kind="cron",
|
|
272
|
+
expr=job.cron_expr,
|
|
273
|
+
tz=DEFAULT_TZ,
|
|
274
|
+
),
|
|
275
|
+
payload=CronPayload(
|
|
276
|
+
kind="system_event",
|
|
277
|
+
message=job.message,
|
|
278
|
+
deliver=job.deliver,
|
|
279
|
+
channel=job.channel,
|
|
280
|
+
session_key=job.session_key,
|
|
281
|
+
),
|
|
282
|
+
state=CronJobState(),
|
|
283
|
+
)
|
|
284
|
+
cron_service.register_system_job(cron_job)
|
|
285
|
+
registered.append(cron_job.id)
|
|
286
|
+
logger.info(
|
|
287
|
+
"Registered quant cron job '%s' (cron='%s', tz=%s)",
|
|
288
|
+
cron_job.name,
|
|
289
|
+
job.cron_expr,
|
|
290
|
+
DEFAULT_TZ,
|
|
291
|
+
)
|
|
292
|
+
return registered
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class NanobotNotInstalledForCron(ImportError):
|
|
296
|
+
"""Raised when ``register_quant_cron_jobs`` is called without nanobot.
|
|
297
|
+
|
|
298
|
+
Subclasses ``ImportError`` so callers (e.g. ``NanobotRuntime._build_components``)
|
|
299
|
+
can either catch it as ``ImportError`` for graceful degradation or as
|
|
300
|
+
the specific subclass for a clearer error message.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
__all__ = [
|
|
305
|
+
"DEFAULT_TZ",
|
|
306
|
+
"QuantCronJob",
|
|
307
|
+
"DAILY_RECAP_JOB",
|
|
308
|
+
"WEEKLY_REVIEW_JOB",
|
|
309
|
+
"MONTHLY_STRATEGY_POOL_JOB",
|
|
310
|
+
"DEFAULT_QUANT_CRON_JOBS",
|
|
311
|
+
"build_quant_cron_jobs_from_env",
|
|
312
|
+
"register_quant_cron_jobs",
|
|
313
|
+
"NanobotNotInstalledForCron",
|
|
314
|
+
]
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""Agent facade wrapping HKUDS nanobot 0.2.1 (Path A: direct upstream consumption).
|
|
3
|
+
|
|
4
|
+
Usage (backward-compatible v2.x signature):
|
|
5
|
+
|
|
6
|
+
from QuantNodes.agent import Agent
|
|
7
|
+
agent = Agent(workspace=".agent", config={...})
|
|
8
|
+
result = await agent.run("hello", session_id="default")
|
|
9
|
+
|
|
10
|
+
Under the hood, Agent constructs a ``Nanobot`` from
|
|
11
|
+
``.agent/nanobot_config.json`` (generated from ``.env`` by
|
|
12
|
+
``config_mapper.py``), injects all 15 quant tools into its ToolRegistry,
|
|
13
|
+
and exposes a thin compatibility layer for the streaming event protocol
|
|
14
|
+
that the API layer (api/services/agent_service.py) expects.
|
|
15
|
+
|
|
16
|
+
Reference: docs/13-Agent架构设计.md (v3.0.0) and docs/14-上游nanobot升级指南.md.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
# v3.0.0 Stage 5.3: nanobot-ai is now an optional dep. We delay the import
|
|
29
|
+
# until ``Agent.__init__`` is called so that ``from QuantNodes.agent import
|
|
30
|
+
# Agent`` works even when the [agent] extra is not installed (the import path
|
|
31
|
+
# returns a proxy that raises ``NanobotNotInstalled`` on actual use).
|
|
32
|
+
try:
|
|
33
|
+
from nanobot import Nanobot # type: ignore[import-not-found]
|
|
34
|
+
from nanobot.agent.tools.base import Tool # type: ignore[import-not-found]
|
|
35
|
+
from nanobot.agent.tools.registry import ToolRegistry # type: ignore[import-not-found]
|
|
36
|
+
_NANOBOT_OK = True
|
|
37
|
+
except ImportError: # pragma: no cover - exercised in nanobot-less envs
|
|
38
|
+
Nanobot = None # type: ignore[assignment,misc]
|
|
39
|
+
Tool = None # type: ignore[assignment,misc]
|
|
40
|
+
ToolRegistry = None # type: ignore[assignment,misc]
|
|
41
|
+
_NANOBOT_OK = False
|
|
42
|
+
|
|
43
|
+
from .config_mapper import build_nanobot_config, write_nanobot_config
|
|
44
|
+
from .tools import register_all_quant_tools
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Agent:
|
|
50
|
+
"""Thin wrapper around HKUDS nanobot's ``Nanobot`` programmatic facade.
|
|
51
|
+
|
|
52
|
+
See nanobot_bridge.py module docstring for the full design rationale.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
DEFAULT_WORKSPACE = ".agent"
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
workspace: str = DEFAULT_WORKSPACE,
|
|
60
|
+
config: Optional[Dict[str, Any]] = None,
|
|
61
|
+
):
|
|
62
|
+
if not _NANOBOT_OK:
|
|
63
|
+
from . import NanobotNotInstalled
|
|
64
|
+
raise NanobotNotInstalled("Agent")
|
|
65
|
+
config = config or {}
|
|
66
|
+
self.workspace = Path(workspace).expanduser().resolve()
|
|
67
|
+
self.workspace.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
settings = build_nanobot_config(self.workspace, config)
|
|
70
|
+
self.config_path = write_nanobot_config(self.workspace, settings)
|
|
71
|
+
|
|
72
|
+
self._bot: Nanobot = Nanobot.from_config(self.config_path, workspace=self.workspace)
|
|
73
|
+
self._loop = self._bot._loop
|
|
74
|
+
self._hook_lock = asyncio.Lock()
|
|
75
|
+
|
|
76
|
+
quant_count = register_all_quant_tools(
|
|
77
|
+
self._loop.tools,
|
|
78
|
+
workspace=self.workspace,
|
|
79
|
+
llm_client=getattr(self._loop, 'provider', None),
|
|
80
|
+
model=getattr(self._loop, 'model', None),
|
|
81
|
+
)
|
|
82
|
+
logger.info(
|
|
83
|
+
"QuantNodes Agent ready (workspace=%s, quant_tools=%d, "
|
|
84
|
+
"upstream_tools=%d)",
|
|
85
|
+
self.workspace,
|
|
86
|
+
quant_count,
|
|
87
|
+
len(self._loop.tools._tools) - quant_count,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def loop(self):
|
|
92
|
+
"""Backward-compatible access to underlying AgentLoop."""
|
|
93
|
+
return self._loop
|
|
94
|
+
|
|
95
|
+
async def run(self, prompt: str, session_id: str = "default") -> str:
|
|
96
|
+
"""Single-turn run returning the assistant's final text content."""
|
|
97
|
+
result = await self._bot.run(prompt, session_key=session_id)
|
|
98
|
+
return result.content or ""
|
|
99
|
+
|
|
100
|
+
async def chat(
|
|
101
|
+
self,
|
|
102
|
+
message: str,
|
|
103
|
+
session_id: str = "default",
|
|
104
|
+
model: Optional[str] = None,
|
|
105
|
+
max_tokens: Optional[int] = None,
|
|
106
|
+
mode: Optional[str] = None,
|
|
107
|
+
tools: Optional[List[str]] = None,
|
|
108
|
+
tool_choice: Optional[str] = None,
|
|
109
|
+
temperature: Optional[float] = None,
|
|
110
|
+
) -> AsyncGenerator[Dict[str, Any], None]:
|
|
111
|
+
"""Stream chat events using the v2.x event protocol.
|
|
112
|
+
|
|
113
|
+
Emits dicts with shape::
|
|
114
|
+
|
|
115
|
+
{"type": "token", "content": str}
|
|
116
|
+
{"type": "tool_call", "id": str, "name": str, "arguments": dict}
|
|
117
|
+
{"type": "tool_result", "id": str, "name": str, "content": Any, "success": bool}
|
|
118
|
+
{"type": "done", "content": str, "tools_used": list[str], "stop_reason": str}
|
|
119
|
+
{"type": "error", "content": str}
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
tools: 工具名列表 (None=全部, []=无工具)
|
|
123
|
+
tool_choice: "auto"/"none"/"required"
|
|
124
|
+
temperature: 采样温度(可选)
|
|
125
|
+
"""
|
|
126
|
+
from nanobot.agent.hook import SDKCaptureHook
|
|
127
|
+
|
|
128
|
+
capture = SDKCaptureHook()
|
|
129
|
+
async with self._hook_lock:
|
|
130
|
+
prev = self._loop._extra_hooks or []
|
|
131
|
+
self._loop._extra_hooks = [capture, *prev]
|
|
132
|
+
try:
|
|
133
|
+
async for event in self._stream_via_loop(
|
|
134
|
+
message, session_id, model, max_tokens, mode,
|
|
135
|
+
tools=tools, tool_choice=tool_choice, temperature=temperature,
|
|
136
|
+
):
|
|
137
|
+
yield event
|
|
138
|
+
finally:
|
|
139
|
+
async with self._hook_lock:
|
|
140
|
+
self._loop._extra_hooks = prev
|
|
141
|
+
|
|
142
|
+
def _filter_tools(self, tool_names: Optional[List[str]]) -> ToolRegistry:
|
|
143
|
+
"""过滤 nanobot 工具集。
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
tool_names: 工具名列表 (None=全部, []=无工具)
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
ToolRegistry (可能是子集)
|
|
150
|
+
"""
|
|
151
|
+
if tool_names is None:
|
|
152
|
+
return self._loop.tools
|
|
153
|
+
|
|
154
|
+
all_names = set(self._loop.tools._tools.keys())
|
|
155
|
+
valid = [n for n in tool_names if n in all_names]
|
|
156
|
+
invalid = [n for n in tool_names if n not in all_names]
|
|
157
|
+
if invalid:
|
|
158
|
+
logger.warning("LLMGateway: unknown tools filtered: %s", invalid)
|
|
159
|
+
|
|
160
|
+
sub = ToolRegistry()
|
|
161
|
+
for name in valid:
|
|
162
|
+
sub.register(self._loop.tools._tools[name])
|
|
163
|
+
return sub
|
|
164
|
+
|
|
165
|
+
async def _stream_via_loop(
|
|
166
|
+
self,
|
|
167
|
+
message: str,
|
|
168
|
+
session_id: str,
|
|
169
|
+
model: Optional[str],
|
|
170
|
+
max_tokens: Optional[int],
|
|
171
|
+
mode: Optional[str],
|
|
172
|
+
tools: Optional[List[str]] = None,
|
|
173
|
+
tool_choice: Optional[str] = None,
|
|
174
|
+
temperature: Optional[float] = None,
|
|
175
|
+
) -> AsyncGenerator[Dict[str, Any], None]:
|
|
176
|
+
"""Drive the upstream AgentLoop and re-emit events in v2.x protocol."""
|
|
177
|
+
from nanobot.agent.runner import AgentRunSpec
|
|
178
|
+
|
|
179
|
+
filtered = self._filter_tools(tools)
|
|
180
|
+
spec = AgentRunSpec(
|
|
181
|
+
initial_messages=[{"role": "user", "content": message}],
|
|
182
|
+
tools=filtered,
|
|
183
|
+
model=model or getattr(self._loop, "model", None),
|
|
184
|
+
max_tokens=max_tokens or 16384,
|
|
185
|
+
max_iterations=getattr(self._loop, "max_iterations", 5),
|
|
186
|
+
max_tool_result_chars=getattr(self._loop, "max_tool_result_chars", 50000),
|
|
187
|
+
temperature=temperature,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
tools_used: List[str] = []
|
|
191
|
+
final_content = ""
|
|
192
|
+
try:
|
|
193
|
+
async for event in self._loop.run_stream(spec):
|
|
194
|
+
etype = event.get("type")
|
|
195
|
+
if etype == "token":
|
|
196
|
+
yield {"type": "token", "content": event.get("content", "")}
|
|
197
|
+
elif etype == "tool_call":
|
|
198
|
+
name = event.get("name") or event.get("tool", "")
|
|
199
|
+
if name:
|
|
200
|
+
tools_used.append(name)
|
|
201
|
+
yield {
|
|
202
|
+
"type": "tool_call",
|
|
203
|
+
"id": event.get("id", ""),
|
|
204
|
+
"name": name,
|
|
205
|
+
"arguments": event.get("arguments", {}),
|
|
206
|
+
}
|
|
207
|
+
elif etype == "tool_result":
|
|
208
|
+
yield {
|
|
209
|
+
"type": "tool_result",
|
|
210
|
+
"id": event.get("id", ""),
|
|
211
|
+
"name": event.get("name", ""),
|
|
212
|
+
"content": event.get("content"),
|
|
213
|
+
"success": event.get("success", True),
|
|
214
|
+
}
|
|
215
|
+
elif etype == "done":
|
|
216
|
+
final_content = event.get("content", "") or final_content
|
|
217
|
+
yield {
|
|
218
|
+
"type": "done",
|
|
219
|
+
"content": final_content,
|
|
220
|
+
"tools_used": list(dict.fromkeys(tools_used)),
|
|
221
|
+
"stop_reason": event.get("stop_reason", "stop"),
|
|
222
|
+
}
|
|
223
|
+
elif etype == "error":
|
|
224
|
+
yield {"type": "error", "content": event.get("content", "")}
|
|
225
|
+
elif etype == "message":
|
|
226
|
+
delta = event.get("content")
|
|
227
|
+
if delta:
|
|
228
|
+
final_content += delta
|
|
229
|
+
except AttributeError:
|
|
230
|
+
result = await self._loop.process_direct(message, session_key=session_id)
|
|
231
|
+
yield {
|
|
232
|
+
"type": "done",
|
|
233
|
+
"content": getattr(result, "content", "") or "",
|
|
234
|
+
"tools_used": tools_used,
|
|
235
|
+
"stop_reason": getattr(result, "stop_reason", "stop") or "stop",
|
|
236
|
+
}
|
|
237
|
+
except Exception as exc:
|
|
238
|
+
logger.exception("Agent chat failed")
|
|
239
|
+
yield {"type": "error", "content": str(exc)}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
__all__ = ["Agent"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
权限系统模块
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .models import (
|
|
7
|
+
Action,
|
|
8
|
+
PermissionRule,
|
|
9
|
+
PermissionRequest,
|
|
10
|
+
PermissionReply,
|
|
11
|
+
Ruleset,
|
|
12
|
+
PermissionDeniedError,
|
|
13
|
+
PermissionRejectedError,
|
|
14
|
+
)
|
|
15
|
+
from .evaluate import evaluate
|
|
16
|
+
from .service import PermissionService
|
|
17
|
+
from .defaults import create_default_ruleset
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Action",
|
|
21
|
+
"PermissionRule",
|
|
22
|
+
"PermissionRequest",
|
|
23
|
+
"PermissionReply",
|
|
24
|
+
"Ruleset",
|
|
25
|
+
"PermissionDeniedError",
|
|
26
|
+
"PermissionRejectedError",
|
|
27
|
+
"evaluate",
|
|
28
|
+
"PermissionService",
|
|
29
|
+
"create_default_ruleset",
|
|
30
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
默认权限规则集
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from .models import PermissionRule, Action
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_default_ruleset(project_root: str | Path) -> list[PermissionRule]:
|
|
11
|
+
"""创建默认权限规则集
|
|
12
|
+
|
|
13
|
+
安全原则:
|
|
14
|
+
1. 默认询问(安全默认)
|
|
15
|
+
2. 项目内文件读取允许
|
|
16
|
+
3. 敏感文件(.env)需要审批
|
|
17
|
+
4. 外部目录访问需要审批
|
|
18
|
+
5. shell 命令需要审批
|
|
19
|
+
"""
|
|
20
|
+
project_root = Path(project_root)
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
PermissionRule("*", "*", Action.ASK),
|
|
24
|
+
PermissionRule("read", "*", Action.ALLOW),
|
|
25
|
+
PermissionRule("read", "*.env", Action.ASK),
|
|
26
|
+
PermissionRule("read", "*.env.*", Action.ASK),
|
|
27
|
+
PermissionRule("read", ".env", Action.ASK),
|
|
28
|
+
PermissionRule("read", ".env.*", Action.ASK),
|
|
29
|
+
PermissionRule("read", "*.env.example", Action.ALLOW),
|
|
30
|
+
PermissionRule("edit", "*", Action.ALLOW),
|
|
31
|
+
PermissionRule("bash", "*", Action.ASK),
|
|
32
|
+
PermissionRule("external_directory", "*", Action.ASK),
|
|
33
|
+
PermissionRule("external_directory", "/tmp/*", Action.ALLOW),
|
|
34
|
+
PermissionRule("webfetch", "*", Action.ALLOW),
|
|
35
|
+
PermissionRule("websearch", "*", Action.ALLOW),
|
|
36
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
权限规则评估引擎
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .models import PermissionRule, Action, Ruleset
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def evaluate(
|
|
10
|
+
permission: str,
|
|
11
|
+
target: str,
|
|
12
|
+
*rulesets: Ruleset,
|
|
13
|
+
) -> PermissionRule:
|
|
14
|
+
"""评估权限规则
|
|
15
|
+
|
|
16
|
+
规则评估逻辑:
|
|
17
|
+
1. 将所有 ruleset 扁平化
|
|
18
|
+
2. 从后向前查找第一个匹配的规则(后定义的规则优先)
|
|
19
|
+
3. 如果没有匹配规则,默认返回 ask(安全默认)
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
permission: 权限类别(如 "bash", "edit")
|
|
23
|
+
target: 目标模式(如 "git commit", "/path/to/file.py")
|
|
24
|
+
*rulesets: 规则集(按优先级从低到高排列)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
匹配的规则(包含 action)
|
|
28
|
+
"""
|
|
29
|
+
all_rules = []
|
|
30
|
+
for rs in rulesets:
|
|
31
|
+
all_rules.extend(rs)
|
|
32
|
+
|
|
33
|
+
for rule in reversed(all_rules):
|
|
34
|
+
if rule.matches(permission, target):
|
|
35
|
+
return rule
|
|
36
|
+
|
|
37
|
+
return PermissionRule(
|
|
38
|
+
permission=permission,
|
|
39
|
+
pattern="*",
|
|
40
|
+
action=Action.ASK,
|
|
41
|
+
)
|