devsper 2.1.6__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.
- devsper/__init__.py +14 -0
- devsper/agents/a2a/__init__.py +27 -0
- devsper/agents/a2a/client.py +126 -0
- devsper/agents/a2a/discovery.py +24 -0
- devsper/agents/a2a/server.py +128 -0
- devsper/agents/a2a/tool_adapter.py +68 -0
- devsper/agents/a2a/types.py +49 -0
- devsper/agents/agent.py +602 -0
- devsper/agents/critic.py +80 -0
- devsper/agents/message_bus.py +124 -0
- devsper/agents/roles.py +181 -0
- devsper/agents/run_agent.py +78 -0
- devsper/analytics/__init__.py +5 -0
- devsper/analytics/tool_analytics.py +78 -0
- devsper/audit/__init__.py +5 -0
- devsper/audit/logger.py +214 -0
- devsper/bus/__init__.py +29 -0
- devsper/bus/backends/__init__.py +5 -0
- devsper/bus/backends/base.py +38 -0
- devsper/bus/backends/memory.py +55 -0
- devsper/bus/backends/redis.py +146 -0
- devsper/bus/message.py +56 -0
- devsper/bus/schema_version.py +3 -0
- devsper/bus/topics.py +19 -0
- devsper/cache/__init__.py +6 -0
- devsper/cache/embedding_index.py +98 -0
- devsper/cache/hashing.py +24 -0
- devsper/cache/store.py +153 -0
- devsper/cache/task_cache.py +191 -0
- devsper/cli/__init__.py +6 -0
- devsper/cli/commands/reg.py +733 -0
- devsper/cli/github_oauth.py +157 -0
- devsper/cli/init.py +637 -0
- devsper/cli/main.py +2956 -0
- devsper/cli/run_progress.py +103 -0
- devsper/cli/ui/__init__.py +65 -0
- devsper/cli/ui/components.py +94 -0
- devsper/cli/ui/errors.py +104 -0
- devsper/cli/ui/logging.py +120 -0
- devsper/cli/ui/onboarding.py +102 -0
- devsper/cli/ui/progress.py +43 -0
- devsper/cli/ui/run_view.py +308 -0
- devsper/cli/ui/theme.py +40 -0
- devsper/cluster/__init__.py +29 -0
- devsper/cluster/election.py +84 -0
- devsper/cluster/local.py +97 -0
- devsper/cluster/node_info.py +77 -0
- devsper/cluster/registry.py +71 -0
- devsper/cluster/router.py +117 -0
- devsper/cluster/state_backend.py +105 -0
- devsper/compliance/__init__.py +5 -0
- devsper/compliance/pii.py +147 -0
- devsper/config/__init__.py +52 -0
- devsper/config/config_loader.py +121 -0
- devsper/config/defaults.py +77 -0
- devsper/config/resolver.py +342 -0
- devsper/config/schema.py +237 -0
- devsper/credentials/__init__.py +19 -0
- devsper/credentials/cli.py +197 -0
- devsper/credentials/migration.py +124 -0
- devsper/credentials/store.py +142 -0
- devsper/dashboard/__init__.py +9 -0
- devsper/dashboard/dashboard.py +87 -0
- devsper/dev/__init__.py +25 -0
- devsper/dev/builder.py +195 -0
- devsper/dev/debugger.py +95 -0
- devsper/dev/repo_index.py +138 -0
- devsper/dev/sandbox.py +203 -0
- devsper/dev/scaffold.py +122 -0
- devsper/embeddings/__init__.py +5 -0
- devsper/embeddings/service.py +36 -0
- devsper/explainability/__init__.py +14 -0
- devsper/explainability/decision_tree.py +104 -0
- devsper/explainability/rationale.py +38 -0
- devsper/explainability/simulation.py +56 -0
- devsper/hitl/__init__.py +13 -0
- devsper/hitl/approval.py +160 -0
- devsper/hitl/escalation.py +95 -0
- devsper/intelligence/__init__.py +9 -0
- devsper/intelligence/adaptation.py +88 -0
- devsper/intelligence/analysis/__init__.py +19 -0
- devsper/intelligence/analysis/analyzer.py +71 -0
- devsper/intelligence/analysis/cost_estimator.py +66 -0
- devsper/intelligence/analysis/formatter.py +103 -0
- devsper/intelligence/analysis/run_report.py +402 -0
- devsper/intelligence/learning_engine.py +92 -0
- devsper/intelligence/strategies/__init__.py +23 -0
- devsper/intelligence/strategies/base.py +14 -0
- devsper/intelligence/strategies/code_analysis_strategy.py +33 -0
- devsper/intelligence/strategies/data_science_strategy.py +33 -0
- devsper/intelligence/strategies/document_pipeline_strategy.py +33 -0
- devsper/intelligence/strategies/experiment_strategy.py +33 -0
- devsper/intelligence/strategies/research_strategy.py +34 -0
- devsper/intelligence/strategy_selector.py +84 -0
- devsper/intelligence/synthesis.py +132 -0
- devsper/intelligence/task_optimizer.py +92 -0
- devsper/knowledge/__init__.py +5 -0
- devsper/knowledge/extractor.py +204 -0
- devsper/knowledge/knowledge_graph.py +184 -0
- devsper/knowledge/query.py +285 -0
- devsper/memory/__init__.py +35 -0
- devsper/memory/consolidation.py +138 -0
- devsper/memory/embeddings.py +60 -0
- devsper/memory/memory_index.py +97 -0
- devsper/memory/memory_router.py +62 -0
- devsper/memory/memory_store.py +221 -0
- devsper/memory/memory_types.py +54 -0
- devsper/memory/namespaces.py +45 -0
- devsper/memory/scoring.py +77 -0
- devsper/memory/summarizer.py +52 -0
- devsper/nodes/__init__.py +5 -0
- devsper/nodes/controller.py +449 -0
- devsper/nodes/rpc.py +127 -0
- devsper/nodes/single.py +161 -0
- devsper/nodes/worker.py +506 -0
- devsper/orchestration/__init__.py +19 -0
- devsper/orchestration/meta_planner.py +239 -0
- devsper/orchestration/priority_queue.py +61 -0
- devsper/plugins/__init__.py +19 -0
- devsper/plugins/marketplace/__init__.py +0 -0
- devsper/plugins/plugin_loader.py +70 -0
- devsper/plugins/plugin_registry.py +34 -0
- devsper/plugins/registry.py +83 -0
- devsper/protocols/__init__.py +6 -0
- devsper/providers/__init__.py +17 -0
- devsper/providers/anthropic.py +84 -0
- devsper/providers/base.py +75 -0
- devsper/providers/complexity_router.py +94 -0
- devsper/providers/gemini.py +36 -0
- devsper/providers/github.py +180 -0
- devsper/providers/model_router.py +40 -0
- devsper/providers/openai.py +105 -0
- devsper/providers/router/__init__.py +21 -0
- devsper/providers/router/backends/__init__.py +19 -0
- devsper/providers/router/backends/anthropic_backend.py +111 -0
- devsper/providers/router/backends/custom_backend.py +138 -0
- devsper/providers/router/backends/gemini_backend.py +89 -0
- devsper/providers/router/backends/github_backend.py +165 -0
- devsper/providers/router/backends/ollama_backend.py +104 -0
- devsper/providers/router/backends/openai_backend.py +142 -0
- devsper/providers/router/backends/vllm_backend.py +35 -0
- devsper/providers/router/base.py +60 -0
- devsper/providers/router/factory.py +92 -0
- devsper/providers/router/legacy.py +101 -0
- devsper/providers/router/router.py +135 -0
- devsper/reasoning/__init__.py +12 -0
- devsper/reasoning/graph.py +59 -0
- devsper/reasoning/nodes.py +20 -0
- devsper/reasoning/store.py +67 -0
- devsper/runtime/__init__.py +12 -0
- devsper/runtime/health.py +88 -0
- devsper/runtime/replay.py +53 -0
- devsper/runtime/replay_engine.py +142 -0
- devsper/runtime/run_history.py +204 -0
- devsper/runtime/telemetry.py +116 -0
- devsper/runtime/visualize.py +58 -0
- devsper/sandbox/__init__.py +13 -0
- devsper/sandbox/sandbox.py +161 -0
- devsper/swarm/checkpointer.py +65 -0
- devsper/swarm/executor.py +558 -0
- devsper/swarm/map_reduce.py +44 -0
- devsper/swarm/planner.py +197 -0
- devsper/swarm/prefetcher.py +91 -0
- devsper/swarm/scheduler.py +153 -0
- devsper/swarm/speculation.py +47 -0
- devsper/swarm/swarm.py +562 -0
- devsper/tools/__init__.py +33 -0
- devsper/tools/base.py +29 -0
- devsper/tools/code_intelligence/__init__.py +13 -0
- devsper/tools/code_intelligence/api_surface_extractor.py +73 -0
- devsper/tools/code_intelligence/architecture_analyzer.py +65 -0
- devsper/tools/code_intelligence/codebase_indexer.py +71 -0
- devsper/tools/code_intelligence/dependency_graph_builder.py +67 -0
- devsper/tools/code_intelligence/design_pattern_detector.py +62 -0
- devsper/tools/code_intelligence/large_function_detector.py +68 -0
- devsper/tools/code_intelligence/module_responsibility_mapper.py +56 -0
- devsper/tools/code_intelligence/parallel_codebase_analysis.py +44 -0
- devsper/tools/code_intelligence/refactor_candidate_detector.py +81 -0
- devsper/tools/code_intelligence/repository_semantic_index.py +61 -0
- devsper/tools/code_intelligence/test_coverage_estimator.py +62 -0
- devsper/tools/coding/__init__.py +12 -0
- devsper/tools/coding/analyze_code_complexity.py +48 -0
- devsper/tools/coding/dependency_analyzer.py +42 -0
- devsper/tools/coding/extract_functions.py +38 -0
- devsper/tools/coding/format_python.py +50 -0
- devsper/tools/coding/generate_docstrings.py +40 -0
- devsper/tools/coding/generate_unit_tests.py +42 -0
- devsper/tools/coding/lint_python.py +51 -0
- devsper/tools/coding/refactor_function.py +41 -0
- devsper/tools/coding/repo_structure_map.py +54 -0
- devsper/tools/coding/run_python.py +53 -0
- devsper/tools/data/__init__.py +12 -0
- devsper/tools/data/column_type_detection.py +64 -0
- devsper/tools/data/csv_summary.py +52 -0
- devsper/tools/data/dataframe_filter.py +51 -0
- devsper/tools/data/dataframe_groupby.py +47 -0
- devsper/tools/data/dataframe_stats.py +38 -0
- devsper/tools/data/dataset_sampling.py +55 -0
- devsper/tools/data/dataset_schema.py +45 -0
- devsper/tools/data/json_pretty_print.py +37 -0
- devsper/tools/data/json_query.py +46 -0
- devsper/tools/data/missing_value_report.py +47 -0
- devsper/tools/data_science/__init__.py +13 -0
- devsper/tools/data_science/correlation_heatmap.py +72 -0
- devsper/tools/data_science/dataset_bias_detector.py +49 -0
- devsper/tools/data_science/dataset_distribution_report.py +64 -0
- devsper/tools/data_science/dataset_drift_detector.py +64 -0
- devsper/tools/data_science/dataset_outlier_detector.py +65 -0
- devsper/tools/data_science/dataset_profile.py +76 -0
- devsper/tools/data_science/distributed_dataset_processor.py +54 -0
- devsper/tools/data_science/feature_engineering_suggestions.py +69 -0
- devsper/tools/data_science/feature_importance_estimator.py +82 -0
- devsper/tools/data_science/model_input_validator.py +59 -0
- devsper/tools/data_science/time_series_analyzer.py +57 -0
- devsper/tools/documents/__init__.py +11 -0
- devsper/tools/documents/_docproc.py +56 -0
- devsper/tools/documents/document_to_markdown.py +29 -0
- devsper/tools/documents/extract_document_images.py +39 -0
- devsper/tools/documents/extract_document_text.py +29 -0
- devsper/tools/documents/extract_equations.py +36 -0
- devsper/tools/documents/extract_tables.py +47 -0
- devsper/tools/documents/summarize_document.py +42 -0
- devsper/tools/documents/write_latex_document.py +133 -0
- devsper/tools/documents/write_markdown_document.py +89 -0
- devsper/tools/documents/write_word_document.py +149 -0
- devsper/tools/experiments/__init__.py +13 -0
- devsper/tools/experiments/bootstrap_estimator.py +54 -0
- devsper/tools/experiments/experiment_report_generator.py +50 -0
- devsper/tools/experiments/experiment_tracker.py +36 -0
- devsper/tools/experiments/grid_search_runner.py +50 -0
- devsper/tools/experiments/model_benchmark_runner.py +45 -0
- devsper/tools/experiments/monte_carlo_experiment.py +38 -0
- devsper/tools/experiments/parameter_sweep_runner.py +51 -0
- devsper/tools/experiments/result_comparator.py +58 -0
- devsper/tools/experiments/simulation_runner.py +43 -0
- devsper/tools/experiments/statistical_significance_test.py +56 -0
- devsper/tools/experiments/swarm_map_reduce.py +42 -0
- devsper/tools/filesystem/__init__.py +12 -0
- devsper/tools/filesystem/append_file.py +42 -0
- devsper/tools/filesystem/file_hash.py +40 -0
- devsper/tools/filesystem/file_line_count.py +36 -0
- devsper/tools/filesystem/file_metadata.py +38 -0
- devsper/tools/filesystem/file_preview.py +55 -0
- devsper/tools/filesystem/find_large_files.py +50 -0
- devsper/tools/filesystem/list_directory.py +39 -0
- devsper/tools/filesystem/read_file.py +35 -0
- devsper/tools/filesystem/search_files.py +60 -0
- devsper/tools/filesystem/write_file.py +41 -0
- devsper/tools/flagship/__init__.py +15 -0
- devsper/tools/flagship/distributed_document_analysis.py +77 -0
- devsper/tools/flagship/docproc_corpus_pipeline.py +91 -0
- devsper/tools/flagship/repository_semantic_map.py +99 -0
- devsper/tools/flagship/research_graph_builder.py +111 -0
- devsper/tools/flagship/swarm_experiment_runner.py +86 -0
- devsper/tools/knowledge/__init__.py +10 -0
- devsper/tools/knowledge/citation_graph_builder.py +69 -0
- devsper/tools/knowledge/concept_frequency_analyzer.py +74 -0
- devsper/tools/knowledge/corpus_builder.py +66 -0
- devsper/tools/knowledge/cross_document_entity_linker.py +71 -0
- devsper/tools/knowledge/document_corpus_summary.py +68 -0
- devsper/tools/knowledge/document_topic_extractor.py +58 -0
- devsper/tools/knowledge/knowledge_graph_extractor.py +58 -0
- devsper/tools/knowledge/timeline_extractor.py +59 -0
- devsper/tools/math/__init__.py +12 -0
- devsper/tools/math/calculate_expression.py +52 -0
- devsper/tools/math/correlation.py +44 -0
- devsper/tools/math/distribution_summary.py +39 -0
- devsper/tools/math/histogram.py +53 -0
- devsper/tools/math/linear_regression.py +47 -0
- devsper/tools/math/matrix_multiply.py +38 -0
- devsper/tools/math/mean_std.py +35 -0
- devsper/tools/math/monte_carlo_simulation.py +43 -0
- devsper/tools/math/polynomial_fit.py +40 -0
- devsper/tools/math/random_sample.py +36 -0
- devsper/tools/mcp/__init__.py +23 -0
- devsper/tools/mcp/adapter.py +53 -0
- devsper/tools/mcp/client.py +235 -0
- devsper/tools/mcp/discovery.py +53 -0
- devsper/tools/memory/__init__.py +16 -0
- devsper/tools/memory/delete_memory.py +25 -0
- devsper/tools/memory/list_memory.py +34 -0
- devsper/tools/memory/search_memory.py +36 -0
- devsper/tools/memory/store_memory.py +47 -0
- devsper/tools/memory/summarize_memory.py +41 -0
- devsper/tools/memory/tag_memory.py +47 -0
- devsper/tools/pipelines.py +92 -0
- devsper/tools/registry.py +39 -0
- devsper/tools/research/__init__.py +12 -0
- devsper/tools/research/arxiv_download.py +55 -0
- devsper/tools/research/arxiv_search.py +58 -0
- devsper/tools/research/citation_extractor.py +35 -0
- devsper/tools/research/duckduckgo_search.py +42 -0
- devsper/tools/research/paper_metadata_extractor.py +45 -0
- devsper/tools/research/paper_summarizer.py +41 -0
- devsper/tools/research/research_question_generator.py +39 -0
- devsper/tools/research/topic_cluster.py +46 -0
- devsper/tools/research/web_search.py +47 -0
- devsper/tools/research/wikipedia_lookup.py +50 -0
- devsper/tools/research_advanced/__init__.py +14 -0
- devsper/tools/research_advanced/citation_context_extractor.py +60 -0
- devsper/tools/research_advanced/literature_review_generator.py +79 -0
- devsper/tools/research_advanced/methodology_extractor.py +58 -0
- devsper/tools/research_advanced/paper_contribution_extractor.py +50 -0
- devsper/tools/research_advanced/paper_dataset_identifier.py +49 -0
- devsper/tools/research_advanced/paper_method_comparator.py +62 -0
- devsper/tools/research_advanced/paper_similarity_search.py +69 -0
- devsper/tools/research_advanced/paper_trend_analyzer.py +69 -0
- devsper/tools/research_advanced/parallel_document_analyzer.py +56 -0
- devsper/tools/research_advanced/research_gap_finder.py +71 -0
- devsper/tools/research_advanced/research_topic_mapper.py +69 -0
- devsper/tools/research_advanced/swarm_literature_review.py +58 -0
- devsper/tools/scoring/__init__.py +52 -0
- devsper/tools/scoring/report.py +44 -0
- devsper/tools/scoring/scorer.py +39 -0
- devsper/tools/scoring/selector.py +61 -0
- devsper/tools/scoring/store.py +267 -0
- devsper/tools/selector.py +130 -0
- devsper/tools/system/__init__.py +12 -0
- devsper/tools/system/cpu_usage.py +22 -0
- devsper/tools/system/disk_usage.py +35 -0
- devsper/tools/system/environment_variables.py +29 -0
- devsper/tools/system/memory_usage.py +23 -0
- devsper/tools/system/pip_install.py +44 -0
- devsper/tools/system/pip_search.py +29 -0
- devsper/tools/system/process_list.py +34 -0
- devsper/tools/system/python_package_list.py +40 -0
- devsper/tools/system/run_shell_command.py +51 -0
- devsper/tools/system/system_info.py +26 -0
- devsper/tools/tool_runner.py +122 -0
- devsper/tui/__init__.py +5 -0
- devsper/tui/activity_feed_view.py +73 -0
- devsper/tui/adaptive_tasks_view.py +75 -0
- devsper/tui/agent_role_view.py +35 -0
- devsper/tui/app.py +395 -0
- devsper/tui/dashboard_screen.py +290 -0
- devsper/tui/dev_view.py +99 -0
- devsper/tui/inject_screen.py +73 -0
- devsper/tui/knowledge_graph_view.py +46 -0
- devsper/tui/layout.py +43 -0
- devsper/tui/logs_view.py +83 -0
- devsper/tui/memory_view.py +58 -0
- devsper/tui/performance_view.py +33 -0
- devsper/tui/reasoning_graph_view.py +39 -0
- devsper/tui/results_view.py +139 -0
- devsper/tui/swarm_view.py +37 -0
- devsper/tui/task_detail_screen.py +55 -0
- devsper/tui/task_view.py +103 -0
- devsper/types/event.py +97 -0
- devsper/types/exceptions.py +21 -0
- devsper/types/swarm.py +41 -0
- devsper/types/task.py +80 -0
- devsper/upgrade/__init__.py +21 -0
- devsper/upgrade/changelog.py +124 -0
- devsper/upgrade/cli.py +145 -0
- devsper/upgrade/installer.py +103 -0
- devsper/upgrade/notifier.py +52 -0
- devsper/upgrade/version_check.py +121 -0
- devsper/utils/event_logger.py +88 -0
- devsper/utils/http.py +43 -0
- devsper/utils/models.py +54 -0
- devsper/visualization/__init__.py +5 -0
- devsper/visualization/dag_export.py +67 -0
- devsper/workflow/__init__.py +18 -0
- devsper/workflow/conditions.py +157 -0
- devsper/workflow/context.py +108 -0
- devsper/workflow/loader.py +156 -0
- devsper/workflow/resolver.py +109 -0
- devsper/workflow/runner.py +562 -0
- devsper/workflow/schema.py +63 -0
- devsper/workflow/validator.py +128 -0
- devsper-2.1.6.dist-info/METADATA +346 -0
- devsper-2.1.6.dist-info/RECORD +375 -0
- devsper-2.1.6.dist-info/WHEEL +4 -0
- devsper-2.1.6.dist-info/entry_points.txt +3 -0
- devsper-2.1.6.dist-info/licenses/LICENSE +639 -0
devsper/dev/scaffold.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repo scaffolder: generate repo structure from an architecture plan.
|
|
3
|
+
|
|
4
|
+
Structure: repo/backend/, frontend/, tests/, docker/, README.md
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ArchitecturePlan:
|
|
13
|
+
"""High-level architecture for the app to build."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
description: str
|
|
17
|
+
backend: str # e.g. "fastapi", "flask"
|
|
18
|
+
frontend: str # e.g. "react", "vanilla", "none"
|
|
19
|
+
features: list[str] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def scaffold_repo(
|
|
23
|
+
root: str | Path,
|
|
24
|
+
plan: ArchitecturePlan,
|
|
25
|
+
) -> list[str]:
|
|
26
|
+
"""
|
|
27
|
+
Create directory structure and minimal files based on architecture plan.
|
|
28
|
+
Returns list of created paths (relative to root).
|
|
29
|
+
"""
|
|
30
|
+
root = Path(root).resolve()
|
|
31
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
created: list[str] = []
|
|
33
|
+
|
|
34
|
+
# Directories
|
|
35
|
+
dirs = ["backend", "frontend", "tests", "docker"]
|
|
36
|
+
for d in dirs:
|
|
37
|
+
(root / d).mkdir(parents=True, exist_ok=True)
|
|
38
|
+
created.append(d + "/")
|
|
39
|
+
|
|
40
|
+
# README
|
|
41
|
+
readme = f"""# {plan.name}
|
|
42
|
+
|
|
43
|
+
{plan.description}
|
|
44
|
+
|
|
45
|
+
## Structure
|
|
46
|
+
|
|
47
|
+
- `backend/` — {plan.backend} API
|
|
48
|
+
- `frontend/` — {plan.frontend} UI
|
|
49
|
+
- `tests/` — integration and unit tests
|
|
50
|
+
- `docker/` — container config
|
|
51
|
+
|
|
52
|
+
## Run
|
|
53
|
+
|
|
54
|
+
### Backend
|
|
55
|
+
```bash
|
|
56
|
+
cd backend && pip install -r requirements.txt && python -m uvicorn main:app --reload
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Frontend (if applicable)
|
|
60
|
+
```bash
|
|
61
|
+
cd frontend && npm install && npm run dev
|
|
62
|
+
```
|
|
63
|
+
"""
|
|
64
|
+
(root / "README.md").write_text(readme, encoding="utf-8")
|
|
65
|
+
created.append("README.md")
|
|
66
|
+
|
|
67
|
+
# Backend scaffold based on plan.backend
|
|
68
|
+
backend_dir = root / "backend"
|
|
69
|
+
if "fastapi" in plan.backend.lower():
|
|
70
|
+
(backend_dir / "main.py").write_text(
|
|
71
|
+
'"""FastAPI app entrypoint."""\n\nfrom fastapi import FastAPI\n\napp = FastAPI(title="'
|
|
72
|
+
+ plan.name.replace('"', "'")
|
|
73
|
+
+ '")\n\n\n@app.get("/")\ndef root():\n return {"message": "Hello"}\n',
|
|
74
|
+
encoding="utf-8",
|
|
75
|
+
)
|
|
76
|
+
created.append("backend/main.py")
|
|
77
|
+
(backend_dir / "requirements.txt").write_text(
|
|
78
|
+
"fastapi>=0.100.0\nuvicorn[standard]>=0.22.0\n",
|
|
79
|
+
encoding="utf-8",
|
|
80
|
+
)
|
|
81
|
+
created.append("backend/requirements.txt")
|
|
82
|
+
elif "flask" in plan.backend.lower():
|
|
83
|
+
(backend_dir / "main.py").write_text(
|
|
84
|
+
'"""Flask app entrypoint."""\n\nfrom flask import Flask\n\napp = Flask(__name__)\n\n\n@app.route("/")\ndef root():\n return {"message": "Hello"}\n',
|
|
85
|
+
encoding="utf-8",
|
|
86
|
+
)
|
|
87
|
+
created.append("backend/main.py")
|
|
88
|
+
(backend_dir / "requirements.txt").write_text(
|
|
89
|
+
"flask>=3.0.0\n",
|
|
90
|
+
encoding="utf-8",
|
|
91
|
+
)
|
|
92
|
+
created.append("backend/requirements.txt")
|
|
93
|
+
else:
|
|
94
|
+
(backend_dir / "main.py").write_text(
|
|
95
|
+
'"""App entrypoint."""\n\n# TODO: implement\n',
|
|
96
|
+
encoding="utf-8",
|
|
97
|
+
)
|
|
98
|
+
(backend_dir / "requirements.txt").write_text(
|
|
99
|
+
"\n",
|
|
100
|
+
encoding="utf-8",
|
|
101
|
+
)
|
|
102
|
+
created.append("backend/main.py")
|
|
103
|
+
created.append("backend/requirements.txt")
|
|
104
|
+
|
|
105
|
+
# Tests
|
|
106
|
+
tests_dir = root / "tests"
|
|
107
|
+
(tests_dir / "__init__.py").write_text("", encoding="utf-8")
|
|
108
|
+
(tests_dir / "test_app.py").write_text(
|
|
109
|
+
'"""Basic tests."""\nimport pytest\n\n\ndef test_placeholder():\n assert True\n',
|
|
110
|
+
encoding="utf-8",
|
|
111
|
+
)
|
|
112
|
+
created.append("tests/__init__.py")
|
|
113
|
+
created.append("tests/test_app.py")
|
|
114
|
+
|
|
115
|
+
# Docker placeholder
|
|
116
|
+
(root / "docker" / "Dockerfile").write_text(
|
|
117
|
+
"# Dockerfile placeholder\nFROM python:3.12-slim\nWORKDIR /app\nCOPY backend/ .\nRUN pip install -r requirements.txt\nCMD [\"uvicorn\", \"main:app\", \"--host\", \"0.0.0.0\"]\n",
|
|
118
|
+
encoding="utf-8",
|
|
119
|
+
)
|
|
120
|
+
created.append("docker/Dockerfile")
|
|
121
|
+
|
|
122
|
+
return created
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local embedding service using sentence-transformers (all-MiniLM-L6-v2).
|
|
3
|
+
Falls back to provider embeddings (memory.embeddings.embed_text) if local model unavailable.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def embed(text: str) -> list[float]:
|
|
8
|
+
"""
|
|
9
|
+
Return embedding vector for text. Tries local sentence-transformers model first,
|
|
10
|
+
then falls back to provider embeddings (OpenAI/stub).
|
|
11
|
+
"""
|
|
12
|
+
if not text or not str(text).strip():
|
|
13
|
+
return _fallback_embed(" ")
|
|
14
|
+
vec = _local_embed(text)
|
|
15
|
+
if vec is not None:
|
|
16
|
+
return vec
|
|
17
|
+
return _fallback_embed(text)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _local_embed(text: str) -> list[float] | None:
|
|
21
|
+
"""Use sentence-transformers all-MiniLM-L6-v2 if available."""
|
|
22
|
+
try:
|
|
23
|
+
from sentence_transformers import SentenceTransformer
|
|
24
|
+
|
|
25
|
+
model = SentenceTransformer("all-MiniLM-L6-v2")
|
|
26
|
+
emb = model.encode(text[:8192], convert_to_numpy=True)
|
|
27
|
+
return emb.tolist()
|
|
28
|
+
except Exception:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _fallback_embed(text: str) -> list[float]:
|
|
33
|
+
"""Use provider embeddings (OpenAI or stub) from memory.embeddings."""
|
|
34
|
+
from devsper.memory.embeddings import embed_text
|
|
35
|
+
|
|
36
|
+
return embed_text(text)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Explainability: decision tree, rationale, simulation (v2.0)."""
|
|
2
|
+
|
|
3
|
+
from devsper.explainability.decision_tree import DecisionRecord, DecisionTreeBuilder, ToolConsideration
|
|
4
|
+
from devsper.explainability.rationale import RationaleGenerator
|
|
5
|
+
from devsper.explainability.simulation import SimulationMode, SimulationReport
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DecisionRecord",
|
|
9
|
+
"DecisionTreeBuilder",
|
|
10
|
+
"ToolConsideration",
|
|
11
|
+
"RationaleGenerator",
|
|
12
|
+
"SimulationMode",
|
|
13
|
+
"SimulationReport",
|
|
14
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Decision records per task for explainability."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ToolConsideration:
|
|
8
|
+
tool_name: str
|
|
9
|
+
similarity_score: float
|
|
10
|
+
reliability_score: float
|
|
11
|
+
blended_score: float
|
|
12
|
+
selected: bool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class DecisionRecord:
|
|
17
|
+
task_id: str
|
|
18
|
+
task_description: str
|
|
19
|
+
strategy_selected: str
|
|
20
|
+
strategy_reason: str
|
|
21
|
+
tools_considered: list[ToolConsideration] = field(default_factory=list)
|
|
22
|
+
tools_selected: list[str] = field(default_factory=list)
|
|
23
|
+
model_tier: str = ""
|
|
24
|
+
model_selected: str = ""
|
|
25
|
+
model_reason: str = ""
|
|
26
|
+
memory_records_used: int = 0
|
|
27
|
+
kg_context_injected: bool = False
|
|
28
|
+
critic_score: float | None = None
|
|
29
|
+
confidence: float = 0.0
|
|
30
|
+
rationale: str = ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DecisionTreeBuilder:
|
|
34
|
+
"""Build DecisionRecords from events in a run."""
|
|
35
|
+
|
|
36
|
+
def build_from_events(self, run_id: str, events_dir: str) -> list[DecisionRecord]:
|
|
37
|
+
"""Parse events from events_dir for this run_id and build one DecisionRecord per task."""
|
|
38
|
+
import os
|
|
39
|
+
import json
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
records: list[DecisionRecord] = []
|
|
42
|
+
path = Path(events_dir)
|
|
43
|
+
if not path.is_dir():
|
|
44
|
+
return records
|
|
45
|
+
event_files = list(path.glob("*.jsonl"))
|
|
46
|
+
if run_id:
|
|
47
|
+
exact = path / f"{run_id}.jsonl"
|
|
48
|
+
if exact.is_file():
|
|
49
|
+
event_files = [exact]
|
|
50
|
+
else:
|
|
51
|
+
event_files = [f for f in event_files if run_id in f.stem or run_id in f.name]
|
|
52
|
+
events: list[dict] = []
|
|
53
|
+
for f in event_files:
|
|
54
|
+
try:
|
|
55
|
+
with open(f, "r", encoding="utf-8") as fp:
|
|
56
|
+
for line in fp:
|
|
57
|
+
line = line.strip()
|
|
58
|
+
if not line:
|
|
59
|
+
continue
|
|
60
|
+
try:
|
|
61
|
+
events.append(json.loads(line))
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
task_ids = set()
|
|
67
|
+
for ev in events:
|
|
68
|
+
payload = ev.get("payload") or ev if isinstance(ev.get("payload"), dict) else {}
|
|
69
|
+
tid = payload.get("task_id") or ev.get("task_id")
|
|
70
|
+
if tid:
|
|
71
|
+
task_ids.add(tid)
|
|
72
|
+
for tid in sorted(task_ids):
|
|
73
|
+
task_events = [e for e in events if (e.get("payload") or {}).get("task_id") == tid or e.get("task_id") == tid]
|
|
74
|
+
model = ""
|
|
75
|
+
tier = ""
|
|
76
|
+
tools: list[str] = []
|
|
77
|
+
for e in task_events:
|
|
78
|
+
p = e.get("payload") or {}
|
|
79
|
+
if e.get("type") == "task_model_selected" or p.get("model"):
|
|
80
|
+
model = p.get("model") or model
|
|
81
|
+
tier = p.get("tier") or tier
|
|
82
|
+
if e.get("type") == "tool_called" or p.get("tool"):
|
|
83
|
+
t = p.get("tool")
|
|
84
|
+
if t and t not in tools:
|
|
85
|
+
tools.append(t)
|
|
86
|
+
desc = next((p.get("description") for e in task_events for p in [e.get("payload") or {}] if p.get("description")), tid)
|
|
87
|
+
rec = DecisionRecord(
|
|
88
|
+
task_id=tid,
|
|
89
|
+
task_description=desc or tid,
|
|
90
|
+
strategy_selected=tier or "default",
|
|
91
|
+
strategy_reason="from events",
|
|
92
|
+
tools_considered=[],
|
|
93
|
+
tools_selected=tools,
|
|
94
|
+
model_tier=tier or "default",
|
|
95
|
+
model_selected=model or "unknown",
|
|
96
|
+
model_reason="from events",
|
|
97
|
+
memory_records_used=0,
|
|
98
|
+
kg_context_injected=False,
|
|
99
|
+
critic_score=None,
|
|
100
|
+
confidence=0.8,
|
|
101
|
+
rationale=f"Task classified as {tier or 'default'}. Selected {len(tools)} tools ({', '.join(tools)}). Used {model or 'unknown'} ({tier or 'default'} tier). Confidence: 80%.",
|
|
102
|
+
)
|
|
103
|
+
records.append(rec)
|
|
104
|
+
return records
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Rationale generation (LLM or template) for decision records."""
|
|
2
|
+
|
|
3
|
+
from devsper.explainability.decision_tree import DecisionRecord
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RationaleGenerator:
|
|
7
|
+
"""Generate NL explanation for a DecisionRecord. Cached per task."""
|
|
8
|
+
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self._cache: dict[str, str] = {}
|
|
11
|
+
|
|
12
|
+
async def generate(self, record: DecisionRecord, model: str = "mock") -> str:
|
|
13
|
+
"""Use LLM to generate one-paragraph explanation. Cached per task_id."""
|
|
14
|
+
if record.task_id in self._cache:
|
|
15
|
+
return self._cache[record.task_id]
|
|
16
|
+
try:
|
|
17
|
+
from devsper.utils.models import generate
|
|
18
|
+
prompt = (
|
|
19
|
+
f"Explain in one short paragraph why this AI task was executed this way.\n"
|
|
20
|
+
f"Task: {record.task_description}\n"
|
|
21
|
+
f"Strategy: {record.strategy_selected}. Model: {record.model_selected}. "
|
|
22
|
+
f"Tools used: {', '.join(record.tools_selected)}. Confidence: {record.confidence:.0%}.\n"
|
|
23
|
+
f"Write 2-3 sentences only."
|
|
24
|
+
)
|
|
25
|
+
out = generate(model, prompt)
|
|
26
|
+
self._cache[record.task_id] = out or ""
|
|
27
|
+
return out or self.template_rationale(record)
|
|
28
|
+
except Exception:
|
|
29
|
+
return self.template_rationale(record)
|
|
30
|
+
|
|
31
|
+
def template_rationale(self, record: DecisionRecord) -> str:
|
|
32
|
+
"""Template-based fallback (no LLM)."""
|
|
33
|
+
return (
|
|
34
|
+
f"Task classified as {record.strategy_selected}. "
|
|
35
|
+
f"Selected {len(record.tools_selected)} tools ({', '.join(record.tools_selected) or 'none'}). "
|
|
36
|
+
f"Used {record.model_selected} ({record.model_tier} tier). "
|
|
37
|
+
f"Confidence: {record.confidence:.0%}."
|
|
38
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Simulation mode: dry-run planning and scheduling without LLM or tool execution."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class SimulationReport:
|
|
8
|
+
task_list: list[str] = field(default_factory=list)
|
|
9
|
+
estimated_cost: str = "N/A"
|
|
10
|
+
estimated_duration: str = "N/A"
|
|
11
|
+
tool_usage_plan: list[str] = field(default_factory=list)
|
|
12
|
+
model_tier_breakdown: dict = field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SimulationMode:
|
|
16
|
+
"""Dry-run: run planner and scheduler, build DecisionRecords, no LLM or tools."""
|
|
17
|
+
|
|
18
|
+
async def simulate(self, root_task: str) -> SimulationReport:
|
|
19
|
+
"""Run planning and scheduling only; return SimulationReport."""
|
|
20
|
+
from devsper.swarm.planner import Planner
|
|
21
|
+
from devsper.swarm.scheduler import Scheduler
|
|
22
|
+
from devsper.types.task import Task, TaskStatus
|
|
23
|
+
from devsper.explainability.decision_tree import DecisionTreeBuilder, DecisionRecord
|
|
24
|
+
from devsper.explainability.rationale import RationaleGenerator
|
|
25
|
+
planner = Planner(model_name="mock", event_log=None)
|
|
26
|
+
tasks = planner.plan(root_task)
|
|
27
|
+
if not tasks:
|
|
28
|
+
tasks = [Task(id="1", description=root_task, status=TaskStatus.PENDING)]
|
|
29
|
+
scheduler = Scheduler()
|
|
30
|
+
for t in tasks:
|
|
31
|
+
scheduler.add_task(t)
|
|
32
|
+
task_list = [t.description or t.id for t in scheduler.get_all_tasks()]
|
|
33
|
+
records: list[DecisionRecord] = []
|
|
34
|
+
gen = RationaleGenerator()
|
|
35
|
+
for t in scheduler.get_all_tasks():
|
|
36
|
+
rec = DecisionRecord(
|
|
37
|
+
task_id=t.id,
|
|
38
|
+
task_description=t.description or t.id,
|
|
39
|
+
strategy_selected="default",
|
|
40
|
+
strategy_reason="simulation",
|
|
41
|
+
tools_selected=[],
|
|
42
|
+
model_tier="default",
|
|
43
|
+
model_selected="mock",
|
|
44
|
+
model_reason="simulation",
|
|
45
|
+
confidence=0.5,
|
|
46
|
+
rationale="",
|
|
47
|
+
)
|
|
48
|
+
rec.rationale = gen.template_rationale(rec)
|
|
49
|
+
records.append(rec)
|
|
50
|
+
return SimulationReport(
|
|
51
|
+
task_list=task_list,
|
|
52
|
+
estimated_cost="N/A",
|
|
53
|
+
estimated_duration="N/A",
|
|
54
|
+
tool_usage_plan=[],
|
|
55
|
+
model_tier_breakdown={"default": len(task_list)},
|
|
56
|
+
)
|
devsper/hitl/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Human-in-the-Loop: escalation triggers, approval requests, and notification."""
|
|
2
|
+
|
|
3
|
+
from devsper.hitl.escalation import EscalationChecker, EscalationPolicy, EscalationTrigger
|
|
4
|
+
from devsper.hitl.approval import ApprovalRequest, ApprovalStore, ApprovalNotifier
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ApprovalNotifier",
|
|
8
|
+
"ApprovalRequest",
|
|
9
|
+
"ApprovalStore",
|
|
10
|
+
"EscalationChecker",
|
|
11
|
+
"EscalationPolicy",
|
|
12
|
+
"EscalationTrigger",
|
|
13
|
+
]
|
devsper/hitl/approval.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Approval store and notifier: persist pending approvals, notify approvers, resolve (approve/reject).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from devsper.hitl.escalation import EscalationPolicy, EscalationTrigger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ApprovalRequest:
|
|
17
|
+
request_id: str
|
|
18
|
+
task: object # Task
|
|
19
|
+
proposed_result: str
|
|
20
|
+
decision_record: object | None # DecisionRecord
|
|
21
|
+
trigger: EscalationTrigger
|
|
22
|
+
created_at: str
|
|
23
|
+
expires_at: str
|
|
24
|
+
status: Literal["pending", "approved", "rejected", "timeout"] = "pending"
|
|
25
|
+
reviewer_notes: str | None = None
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict:
|
|
28
|
+
task = self.task
|
|
29
|
+
task_dict = getattr(task, "to_dict", lambda: {"id": getattr(task, "id", ""), "description": getattr(task, "description", "")})()
|
|
30
|
+
dr = self.decision_record
|
|
31
|
+
dr_dict = None
|
|
32
|
+
if dr is not None and hasattr(dr, "__dict__"):
|
|
33
|
+
dr_dict = {"task_id": getattr(dr, "task_id", ""), "critic_score": getattr(dr, "critic_score", None), "confidence": getattr(dr, "confidence", 0)}
|
|
34
|
+
return {
|
|
35
|
+
"request_id": self.request_id,
|
|
36
|
+
"task": task_dict,
|
|
37
|
+
"proposed_result": self.proposed_result,
|
|
38
|
+
"decision_record": dr_dict,
|
|
39
|
+
"trigger": {"type": self.trigger.type, "threshold": self.trigger.threshold},
|
|
40
|
+
"created_at": self.created_at,
|
|
41
|
+
"expires_at": self.expires_at,
|
|
42
|
+
"status": self.status,
|
|
43
|
+
"reviewer_notes": self.reviewer_notes,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_dict(cls, data: dict) -> "ApprovalRequest":
|
|
48
|
+
from devsper.types.task import Task
|
|
49
|
+
task_data = data.get("task") or {}
|
|
50
|
+
task = Task.from_dict(task_data) if isinstance(task_data, dict) else task_data
|
|
51
|
+
tr = data.get("trigger") or {}
|
|
52
|
+
trigger = EscalationTrigger(type=tr.get("type", "confidence_below"), threshold=tr.get("threshold", 0.5))
|
|
53
|
+
return cls(
|
|
54
|
+
request_id=data.get("request_id", ""),
|
|
55
|
+
task=task,
|
|
56
|
+
proposed_result=data.get("proposed_result", ""),
|
|
57
|
+
decision_record=data.get("decision_record"),
|
|
58
|
+
trigger=trigger,
|
|
59
|
+
created_at=data.get("created_at", ""),
|
|
60
|
+
expires_at=data.get("expires_at", ""),
|
|
61
|
+
status=data.get("status", "pending"),
|
|
62
|
+
reviewer_notes=data.get("reviewer_notes"),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ApprovalStore:
|
|
67
|
+
"""Persist pending approvals under {data_dir}/approvals/{request_id}.json."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, data_dir: str = ".devsper") -> None:
|
|
70
|
+
self.data_dir = data_dir
|
|
71
|
+
self._approvals_dir = os.path.join(data_dir, "approvals")
|
|
72
|
+
os.makedirs(self._approvals_dir, exist_ok=True)
|
|
73
|
+
|
|
74
|
+
def _path(self, request_id: str) -> str:
|
|
75
|
+
return os.path.join(self._approvals_dir, f"{request_id}.json")
|
|
76
|
+
|
|
77
|
+
def list_pending(self) -> list[ApprovalRequest]:
|
|
78
|
+
"""Return all pending approval requests."""
|
|
79
|
+
out: list[ApprovalRequest] = []
|
|
80
|
+
for name in os.listdir(self._approvals_dir):
|
|
81
|
+
if name.endswith(".json"):
|
|
82
|
+
rid = name[:-5]
|
|
83
|
+
req = self.get(rid)
|
|
84
|
+
if req is not None and req.status == "pending":
|
|
85
|
+
out.append(req)
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
def get(self, request_id: str) -> ApprovalRequest | None:
|
|
89
|
+
"""Load one request by id."""
|
|
90
|
+
path = self._path(request_id)
|
|
91
|
+
if not os.path.isfile(path):
|
|
92
|
+
return None
|
|
93
|
+
try:
|
|
94
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
95
|
+
return ApprovalRequest.from_dict(json.load(f))
|
|
96
|
+
except Exception:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def save(self, request: ApprovalRequest) -> None:
|
|
100
|
+
"""Write request to disk."""
|
|
101
|
+
path = self._path(request.request_id)
|
|
102
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
103
|
+
json.dump(request.to_dict(), f, indent=0)
|
|
104
|
+
|
|
105
|
+
def resolve(self, request_id: str, approved: bool, notes: str = "") -> None:
|
|
106
|
+
"""Mark request approved or rejected and save."""
|
|
107
|
+
req = self.get(request_id)
|
|
108
|
+
if req is None:
|
|
109
|
+
return
|
|
110
|
+
req.status = "approved" if approved else "rejected"
|
|
111
|
+
req.reviewer_notes = notes or req.reviewer_notes
|
|
112
|
+
self.save(req)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ApprovalNotifier:
|
|
116
|
+
"""Send approval requests via configured channels (webhook, slack, email)."""
|
|
117
|
+
|
|
118
|
+
async def notify(self, request: ApprovalRequest, policy: EscalationPolicy) -> None:
|
|
119
|
+
"""For each approver in policy.approvers, parse scheme and dispatch."""
|
|
120
|
+
for approver in policy.approvers or []:
|
|
121
|
+
if approver.startswith("webhook://"):
|
|
122
|
+
await self._notify_webhook(request, approver[10:].strip())
|
|
123
|
+
elif approver.startswith("slack://"):
|
|
124
|
+
await self._notify_slack(request, approver[8:].strip())
|
|
125
|
+
elif approver.startswith("email://"):
|
|
126
|
+
self._notify_email(request, approver[8:].strip())
|
|
127
|
+
|
|
128
|
+
async def _notify_webhook(self, request: ApprovalRequest, url: str) -> None:
|
|
129
|
+
try:
|
|
130
|
+
import httpx
|
|
131
|
+
async with httpx.AsyncClient() as client:
|
|
132
|
+
await client.post(url, json=request.to_dict(), timeout=10.0)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
import sys
|
|
135
|
+
print(f"[hitl] webhook notification failed: {e}", file=sys.stderr)
|
|
136
|
+
|
|
137
|
+
async def _notify_slack(self, request: ApprovalRequest, webhook_or_channel: str) -> None:
|
|
138
|
+
try:
|
|
139
|
+
import httpx
|
|
140
|
+
payload = {
|
|
141
|
+
"text": f"Approval requested: {getattr(request.task, 'description', '')[:200]}...",
|
|
142
|
+
"blocks": [
|
|
143
|
+
{"type": "section", "text": {"type": "mrkdwn", "text": f"*Task:* {getattr(request.task, 'description', '')[:300]}"}},
|
|
144
|
+
{"type": "section", "text": {"type": "mrkdwn", "text": f"*Request ID:* `{request.request_id}`"}},
|
|
145
|
+
],
|
|
146
|
+
}
|
|
147
|
+
if webhook_or_channel.startswith("http"):
|
|
148
|
+
async with httpx.AsyncClient() as client:
|
|
149
|
+
await client.post(webhook_or_channel, json=payload, timeout=10.0)
|
|
150
|
+
else:
|
|
151
|
+
import sys
|
|
152
|
+
print("[hitl] slack:// URL must be webhook URL (https://hooks.slack.com/...)", file=sys.stderr)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
import sys
|
|
155
|
+
print(f"[hitl] slack notification failed: {e}", file=sys.stderr)
|
|
156
|
+
|
|
157
|
+
def _notify_email(self, request: ApprovalRequest, _address: str) -> None:
|
|
158
|
+
import sys
|
|
159
|
+
print("[hitl] email notifications require external SMTP config; printing to stdout", file=sys.stderr)
|
|
160
|
+
print(f"Approval requested: request_id={request.request_id} task={getattr(request.task, 'description', '')[:100]}")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Escalation policies and triggers: when to send a task for human approval.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
# Avoid circular import; Task, AgentResponse, DecisionRecord used at runtime
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from devsper.types.task import Task
|
|
13
|
+
from devsper.agents.agent import AgentResponse
|
|
14
|
+
from devsper.explainability.decision_tree import DecisionRecord
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
EscalationTriggerType = Literal[
|
|
18
|
+
"confidence_below",
|
|
19
|
+
"cost_above",
|
|
20
|
+
"tool_category",
|
|
21
|
+
"keyword_match",
|
|
22
|
+
"critic_score_below",
|
|
23
|
+
"sla_at_risk",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class EscalationTrigger:
|
|
29
|
+
type: EscalationTriggerType
|
|
30
|
+
threshold: float | str # number for numeric triggers, string for keyword/category
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class EscalationPolicy:
|
|
35
|
+
triggers: list[EscalationTrigger]
|
|
36
|
+
approvers: list[str] # email://, slack://, webhook://
|
|
37
|
+
timeout_seconds: int = 3600
|
|
38
|
+
on_timeout: Literal["auto_approve", "auto_reject", "escalate_further"] = "auto_approve"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class EscalationChecker:
|
|
42
|
+
"""Evaluate task + response + decision against configured triggers; return first match or None."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, policies: list[EscalationPolicy]) -> None:
|
|
45
|
+
self.policies = policies
|
|
46
|
+
self._triggers: list[tuple[EscalationTrigger, EscalationPolicy]] = []
|
|
47
|
+
for p in policies:
|
|
48
|
+
for t in p.triggers:
|
|
49
|
+
self._triggers.append((t, p))
|
|
50
|
+
|
|
51
|
+
def evaluate(
|
|
52
|
+
self,
|
|
53
|
+
task: "Task",
|
|
54
|
+
response: "AgentResponse",
|
|
55
|
+
decision: "DecisionRecord | None",
|
|
56
|
+
) -> tuple[EscalationTrigger, EscalationPolicy] | None:
|
|
57
|
+
"""Check all configured triggers. Return first (trigger, policy) match or None."""
|
|
58
|
+
for trigger, policy in self._triggers:
|
|
59
|
+
if self._matches(trigger, task, response, decision):
|
|
60
|
+
return (trigger, policy)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def _matches(
|
|
64
|
+
self,
|
|
65
|
+
trigger: EscalationTrigger,
|
|
66
|
+
task: "Task",
|
|
67
|
+
response: "AgentResponse",
|
|
68
|
+
decision: "DecisionRecord | None",
|
|
69
|
+
) -> bool:
|
|
70
|
+
t = trigger.type
|
|
71
|
+
th = trigger.threshold
|
|
72
|
+
if t == "confidence_below" and decision is not None:
|
|
73
|
+
return (decision.confidence or 0) < float(th)
|
|
74
|
+
if t == "cost_above":
|
|
75
|
+
# Approximate cost from tokens if available
|
|
76
|
+
cost = 0.0
|
|
77
|
+
if getattr(response, "tokens_used", None):
|
|
78
|
+
cost = (response.tokens_used or 0) * 1e-6 # rough
|
|
79
|
+
return cost > float(th)
|
|
80
|
+
if t == "tool_category":
|
|
81
|
+
tools = getattr(response, "tools_called", []) or []
|
|
82
|
+
# Compare category names if we had a category; use tool names as proxy
|
|
83
|
+
return isinstance(th, str) and th.lower() in [str(x).lower() for x in tools]
|
|
84
|
+
if t == "keyword_match" and isinstance(th, str):
|
|
85
|
+
text = (task.description or "") + " " + (response.result or "")
|
|
86
|
+
return th.lower() in text.lower()
|
|
87
|
+
if t == "critic_score_below" and decision is not None:
|
|
88
|
+
score = decision.critic_score
|
|
89
|
+
if score is not None:
|
|
90
|
+
return score < float(th)
|
|
91
|
+
return False
|
|
92
|
+
if t == "sla_at_risk":
|
|
93
|
+
# Would need SLA context; treat as no match if not applicable
|
|
94
|
+
return False
|
|
95
|
+
return False
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Swarm intelligence: task optimization, strategy selection, learning from runs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from devsper.intelligence.task_optimizer import TaskOptimizer
|
|
6
|
+
from devsper.intelligence.strategy_selector import StrategySelector
|
|
7
|
+
from devsper.intelligence.learning_engine import LearningEngine
|
|
8
|
+
|
|
9
|
+
__all__ = ["TaskOptimizer", "StrategySelector", "LearningEngine"]
|