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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MetaPlanner: decomposes mega-tasks into sub-swarms with dependencies, SLAs, and priorities.
|
|
3
|
+
Coordinates execution across sub-swarms and monitors SLA breaches.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from devsper.utils.models import generate
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SLAConfig:
|
|
17
|
+
max_duration_seconds: int
|
|
18
|
+
max_cost_usd: float | None = None
|
|
19
|
+
min_quality_score: float | None = None # from critic scores
|
|
20
|
+
on_breach: Literal["cancel", "escalate", "continue"] = "continue"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SubSwarmSpec:
|
|
25
|
+
swarm_id: str
|
|
26
|
+
root_task: str
|
|
27
|
+
priority: int # 1 (highest) to 10 (lowest)
|
|
28
|
+
sla: SLAConfig
|
|
29
|
+
worker_count: int
|
|
30
|
+
model_override: str | None = None
|
|
31
|
+
depends_on: list[str] = field(default_factory=list) # other swarm_ids
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class SLABreach:
|
|
36
|
+
swarm_id: str
|
|
37
|
+
breach_type: Literal["duration", "cost", "quality"]
|
|
38
|
+
limit: float
|
|
39
|
+
actual: float
|
|
40
|
+
action_taken: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class MetaRunResult:
|
|
45
|
+
mega_task: str
|
|
46
|
+
sub_swarm_results: dict[str, dict] # swarm_id -> results
|
|
47
|
+
total_duration_seconds: float
|
|
48
|
+
total_cost_usd: float | None
|
|
49
|
+
sla_breaches: list[SLABreach]
|
|
50
|
+
final_synthesis: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _validate_specs(specs: list[SubSwarmSpec]) -> None:
|
|
54
|
+
"""Raise ValueError if priority out of range or depends_on has cycles."""
|
|
55
|
+
ids_ = {s.swarm_id for s in specs}
|
|
56
|
+
for s in specs:
|
|
57
|
+
if not (1 <= s.priority <= 10):
|
|
58
|
+
raise ValueError(f"SubSwarmSpec {s.swarm_id}: priority must be 1-10, got {s.priority}")
|
|
59
|
+
for dep in s.depends_on:
|
|
60
|
+
if dep not in ids_:
|
|
61
|
+
raise ValueError(f"SubSwarmSpec {s.swarm_id}: depends_on {dep!r} not in swarm ids")
|
|
62
|
+
# Cycle detection
|
|
63
|
+
graph: dict[str, list[str]] = {s.swarm_id: list(s.depends_on) for s in specs}
|
|
64
|
+
path = set()
|
|
65
|
+
visited = set()
|
|
66
|
+
|
|
67
|
+
def visit(n: str) -> None:
|
|
68
|
+
if n in path:
|
|
69
|
+
raise ValueError(f"Cycle in depends_on involving {n!r}")
|
|
70
|
+
if n in visited:
|
|
71
|
+
return
|
|
72
|
+
path.add(n)
|
|
73
|
+
for c in graph.get(n, []):
|
|
74
|
+
visit(c)
|
|
75
|
+
path.remove(n)
|
|
76
|
+
visited.add(n)
|
|
77
|
+
|
|
78
|
+
for n in graph:
|
|
79
|
+
if n not in visited:
|
|
80
|
+
visit(n)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _topological_order(specs: list[SubSwarmSpec]) -> list[SubSwarmSpec]:
|
|
84
|
+
"""Return specs in dependency order (dependencies first)."""
|
|
85
|
+
by_id = {s.swarm_id: s for s in specs}
|
|
86
|
+
in_degree = {s.swarm_id: 0 for s in specs}
|
|
87
|
+
for s in specs:
|
|
88
|
+
for dep in s.depends_on:
|
|
89
|
+
in_degree[s.swarm_id] += 1
|
|
90
|
+
from collections import deque
|
|
91
|
+
q: list[str] = [i for i, d in in_degree.items() if d == 0]
|
|
92
|
+
order: list[str] = []
|
|
93
|
+
while q:
|
|
94
|
+
n = q.pop(0)
|
|
95
|
+
order.append(n)
|
|
96
|
+
for s in specs:
|
|
97
|
+
if n in s.depends_on:
|
|
98
|
+
in_degree[s.swarm_id] -= 1
|
|
99
|
+
if in_degree[s.swarm_id] == 0:
|
|
100
|
+
q.append(s.swarm_id)
|
|
101
|
+
if len(order) != len(specs):
|
|
102
|
+
order = [s.swarm_id for s in sorted(specs, key=lambda x: (x.priority, x.swarm_id))]
|
|
103
|
+
return [by_id[i] for i in order if i in by_id]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class MetaPlanner:
|
|
107
|
+
"""Decomposes mega-tasks into sub-swarms and runs them with SLA monitoring."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, model_name: str = "mock", max_swarms: int = 20) -> None:
|
|
110
|
+
self.model_name = model_name
|
|
111
|
+
self.max_swarms = max_swarms
|
|
112
|
+
|
|
113
|
+
async def decompose(self, mega_task: str) -> list[SubSwarmSpec]:
|
|
114
|
+
"""LLM call: given mega_task, produce JSON list of SubSwarmSpecs. Validates no cycles and priority 1-10."""
|
|
115
|
+
prompt = f"""You are a meta-planner. Given the following mega-task, decompose it into independent or dependent sub-tasks, each to be run as a separate swarm.
|
|
116
|
+
|
|
117
|
+
Mega-task: {mega_task}
|
|
118
|
+
|
|
119
|
+
Respond with a JSON array of objects. Each object must have:
|
|
120
|
+
- swarm_id: string (short id, e.g. "research", "code", "review")
|
|
121
|
+
- root_task: string (the task this swarm will execute)
|
|
122
|
+
- priority: int (1 = highest, 10 = lowest)
|
|
123
|
+
- sla: object with max_duration_seconds (int), max_cost_usd (number or null), min_quality_score (number 0-1 or null), on_breach ("cancel"|"escalate"|"continue")
|
|
124
|
+
- worker_count: int (1-8)
|
|
125
|
+
- model_override: string or null
|
|
126
|
+
- depends_on: array of swarm_ids that must complete before this one (can be empty)
|
|
127
|
+
|
|
128
|
+
Ensure no circular dependencies. Return only the JSON array, no markdown."""
|
|
129
|
+
|
|
130
|
+
out = await asyncio.to_thread(generate, self.model_name, prompt)
|
|
131
|
+
text = (out or "").strip()
|
|
132
|
+
if "```" in text:
|
|
133
|
+
for part in text.split("```"):
|
|
134
|
+
part = part.strip()
|
|
135
|
+
if part.startswith("json") or part.startswith("["):
|
|
136
|
+
text = part[4:].strip() if part.startswith("json") else part
|
|
137
|
+
break
|
|
138
|
+
try:
|
|
139
|
+
raw = json.loads(text)
|
|
140
|
+
except json.JSONDecodeError:
|
|
141
|
+
raw = []
|
|
142
|
+
if not isinstance(raw, list):
|
|
143
|
+
raw = []
|
|
144
|
+
specs: list[SubSwarmSpec] = []
|
|
145
|
+
for i, item in enumerate(raw[: self.max_swarms]):
|
|
146
|
+
if not isinstance(item, dict):
|
|
147
|
+
continue
|
|
148
|
+
sla = item.get("sla") or {}
|
|
149
|
+
sla_config = SLAConfig(
|
|
150
|
+
max_duration_seconds=int(sla.get("max_duration_seconds", 300)),
|
|
151
|
+
max_cost_usd=float(sla["max_cost_usd"]) if sla.get("max_cost_usd") is not None else None,
|
|
152
|
+
min_quality_score=float(sla["min_quality_score"]) if sla.get("min_quality_score") is not None else None,
|
|
153
|
+
on_breach=(sla.get("on_breach") or "continue").lower()[:8],
|
|
154
|
+
)
|
|
155
|
+
if sla_config.on_breach not in ("cancel", "escalate", "continue"):
|
|
156
|
+
sla_config.on_breach = "continue"
|
|
157
|
+
spec = SubSwarmSpec(
|
|
158
|
+
swarm_id=str(item.get("swarm_id", f"swarm_{i}")),
|
|
159
|
+
root_task=str(item.get("root_task", mega_task)),
|
|
160
|
+
priority=int(item.get("priority", 5)),
|
|
161
|
+
sla=sla_config,
|
|
162
|
+
worker_count=max(1, min(8, int(item.get("worker_count", 2)))),
|
|
163
|
+
model_override=str(item["model_override"]) if item.get("model_override") else None,
|
|
164
|
+
depends_on=[str(d) for d in item.get("depends_on") or []],
|
|
165
|
+
)
|
|
166
|
+
specs.append(spec)
|
|
167
|
+
_validate_specs(specs)
|
|
168
|
+
return specs
|
|
169
|
+
|
|
170
|
+
async def run(self, mega_task: str, max_swarms: int | None = None, budget_usd: float | None = None) -> MetaRunResult:
|
|
171
|
+
"""Decompose -> build DAG -> run sub-swarms in order; monitor SLAs; return aggregated result with synthesis."""
|
|
172
|
+
max_n = max_swarms or self.max_swarms
|
|
173
|
+
specs = await self.decompose(mega_task)
|
|
174
|
+
specs = specs[:max_n]
|
|
175
|
+
ordered = _topological_order(specs)
|
|
176
|
+
sub_swarm_results: dict[str, dict] = {}
|
|
177
|
+
sla_breaches: list[SLABreach] = []
|
|
178
|
+
total_cost: float | None = None
|
|
179
|
+
start = datetime.now(timezone.utc)
|
|
180
|
+
|
|
181
|
+
from devsper.swarm.swarm import Swarm
|
|
182
|
+
from devsper.config import get_config
|
|
183
|
+
from devsper.utils.event_logger import EventLog
|
|
184
|
+
|
|
185
|
+
cfg = get_config()
|
|
186
|
+
event_log = EventLog(events_folder_path=cfg.events_dir)
|
|
187
|
+
|
|
188
|
+
for spec in ordered:
|
|
189
|
+
swarm_start = datetime.now(timezone.utc)
|
|
190
|
+
swarm = Swarm(
|
|
191
|
+
worker_count=spec.worker_count,
|
|
192
|
+
worker_model=spec.model_override or cfg.models.worker,
|
|
193
|
+
planner_model=cfg.models.planner,
|
|
194
|
+
event_log=event_log,
|
|
195
|
+
config=cfg,
|
|
196
|
+
)
|
|
197
|
+
try:
|
|
198
|
+
result = swarm.run(spec.root_task)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
result = {"error": str(e)}
|
|
201
|
+
elapsed = (datetime.now(timezone.utc) - swarm_start).total_seconds()
|
|
202
|
+
sub_swarm_results[spec.swarm_id] = result
|
|
203
|
+
|
|
204
|
+
if elapsed > spec.sla.max_duration_seconds:
|
|
205
|
+
breach = SLABreach(
|
|
206
|
+
swarm_id=spec.swarm_id,
|
|
207
|
+
breach_type="duration",
|
|
208
|
+
limit=float(spec.sla.max_duration_seconds),
|
|
209
|
+
actual=elapsed,
|
|
210
|
+
action_taken=spec.sla.on_breach,
|
|
211
|
+
)
|
|
212
|
+
sla_breaches.append(breach)
|
|
213
|
+
if spec.sla.on_breach == "cancel":
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
total_duration_seconds = (datetime.now(timezone.utc) - start).total_seconds()
|
|
217
|
+
|
|
218
|
+
# Final synthesis via LLM
|
|
219
|
+
results_summary = json.dumps({k: (v if isinstance(v, dict) else {"result": str(v)}) for k, v in sub_swarm_results.items()}, indent=0)[:6000]
|
|
220
|
+
synth_prompt = f"""Mega-task: {mega_task}
|
|
221
|
+
|
|
222
|
+
Sub-swarm results summary:
|
|
223
|
+
{results_summary}
|
|
224
|
+
|
|
225
|
+
SLA breaches (if any): {[f"{b.swarm_id}: {b.breach_type}" for b in sla_breaches]}
|
|
226
|
+
|
|
227
|
+
Write a short final synthesis (2-4 sentences) summarizing the overall outcome and any caveats."""
|
|
228
|
+
|
|
229
|
+
synth_out = await asyncio.to_thread(generate, self.model_name, synth_prompt)
|
|
230
|
+
final_synthesis = (synth_out or "").strip() or "No synthesis generated."
|
|
231
|
+
|
|
232
|
+
return MetaRunResult(
|
|
233
|
+
mega_task=mega_task,
|
|
234
|
+
sub_swarm_results=sub_swarm_results,
|
|
235
|
+
total_duration_seconds=total_duration_seconds,
|
|
236
|
+
total_cost_usd=total_cost,
|
|
237
|
+
sla_breaches=sla_breaches,
|
|
238
|
+
final_synthesis=final_synthesis,
|
|
239
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PriorityScheduler: extends Scheduler with priority-aware task ordering.
|
|
3
|
+
get_ready_tasks() returns tasks sorted by priority, then dependency impact, then estimated duration.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from devsper.swarm.scheduler import Scheduler
|
|
7
|
+
from devsper.types.task import Task, TaskStatus
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PriorityScheduler(Scheduler):
|
|
11
|
+
"""Scheduler that orders ready tasks by priority (1=highest), then by dependency impact, then shortest first."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, run_id: str = "") -> None:
|
|
14
|
+
super().__init__(run_id=run_id)
|
|
15
|
+
self._priority: dict[str, int] = {} # task_id -> priority (lower = higher priority)
|
|
16
|
+
|
|
17
|
+
def add_task(self, task: Task, priority: int = 5) -> None:
|
|
18
|
+
"""Add a single task with optional priority (1-10, default 5)."""
|
|
19
|
+
if not (1 <= priority <= 10):
|
|
20
|
+
priority = max(1, min(10, priority))
|
|
21
|
+
self._priority[task.id] = priority
|
|
22
|
+
super().add_tasks([task])
|
|
23
|
+
|
|
24
|
+
def add_tasks(self, tasks: list[Task]) -> None:
|
|
25
|
+
"""Add tasks; each task gets priority from task.priority if present, else 5."""
|
|
26
|
+
for t in tasks:
|
|
27
|
+
if t.id not in self._priority:
|
|
28
|
+
p = getattr(t, "priority", 5)
|
|
29
|
+
self._priority[t.id] = max(1, min(10, p)) if isinstance(p, int) else 5
|
|
30
|
+
super().add_tasks(tasks)
|
|
31
|
+
|
|
32
|
+
def bump_priority(self, task_id: str, new_priority: int) -> None:
|
|
33
|
+
"""Set priority for a task (e.g. for SLA escalation). Lower number = higher priority."""
|
|
34
|
+
if task_id in self._tasks:
|
|
35
|
+
self._priority[task_id] = max(1, min(10, new_priority))
|
|
36
|
+
|
|
37
|
+
def _priority_of(self, task_id: str) -> int:
|
|
38
|
+
return self._priority.get(task_id, 5)
|
|
39
|
+
|
|
40
|
+
def get_ready_tasks(self) -> list[Task]:
|
|
41
|
+
"""Return ready tasks sorted by: 1) priority (lower first), 2) dependencies satisfied count (unblock more first), 3) estimated duration (shortest first)."""
|
|
42
|
+
ready = super().get_ready_tasks()
|
|
43
|
+
if not ready:
|
|
44
|
+
return ready
|
|
45
|
+
|
|
46
|
+
def unblock_count(task_id: str) -> int:
|
|
47
|
+
return len(list(self._graph.successors(task_id)))
|
|
48
|
+
|
|
49
|
+
def estimated_duration(task: Task) -> float:
|
|
50
|
+
# Heuristic: length of description as proxy for duration
|
|
51
|
+
desc = task.description or ""
|
|
52
|
+
return float(len(desc))
|
|
53
|
+
|
|
54
|
+
ready.sort(
|
|
55
|
+
key=lambda t: (
|
|
56
|
+
self._priority_of(t.id),
|
|
57
|
+
-unblock_count(t.id), # higher successor count first
|
|
58
|
+
estimated_duration(t),
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
return ready
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Plugin system: discover and load tools via entry_points."""
|
|
2
|
+
|
|
3
|
+
from devsper.plugins.plugin_loader import load_plugins
|
|
4
|
+
from devsper.plugins.plugin_registry import (
|
|
5
|
+
PluginInfo,
|
|
6
|
+
clear_plugins,
|
|
7
|
+
get_plugin,
|
|
8
|
+
list_plugins,
|
|
9
|
+
register_plugin,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"load_plugins",
|
|
14
|
+
"PluginInfo",
|
|
15
|
+
"register_plugin",
|
|
16
|
+
"get_plugin",
|
|
17
|
+
"list_plugins",
|
|
18
|
+
"clear_plugins",
|
|
19
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Discover and load plugins via entry_points (devsper.plugins)."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import logging
|
|
5
|
+
from devsper.tools.base import Tool
|
|
6
|
+
from devsper.tools.registry import register
|
|
7
|
+
|
|
8
|
+
from devsper.plugins.plugin_registry import register_plugin
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
ENTRY_POINT_GROUP = "devsper.plugins"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_entry_points(group: str = ENTRY_POINT_GROUP):
|
|
16
|
+
try:
|
|
17
|
+
eps = importlib.metadata.entry_points(group=group)
|
|
18
|
+
except TypeError:
|
|
19
|
+
eps = importlib.metadata.entry_points().get(group, [])
|
|
20
|
+
return list(eps)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _invoke_plugin(ep: importlib.metadata.EntryPoint) -> list[str]:
|
|
24
|
+
"""
|
|
25
|
+
Load entry point and register tools. Entry point can be:
|
|
26
|
+
- A callable that returns a list of Tool instances
|
|
27
|
+
- A callable that takes no args and calls register() itself (returns None or list of tool names)
|
|
28
|
+
Returns list of tool names registered.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
fn = ep.load()
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.warning("Plugin %s failed to load: %s", ep.name, e)
|
|
34
|
+
return []
|
|
35
|
+
registered: list[str] = []
|
|
36
|
+
try:
|
|
37
|
+
result = fn()
|
|
38
|
+
if result is None:
|
|
39
|
+
return registered
|
|
40
|
+
if isinstance(result, list):
|
|
41
|
+
for item in result:
|
|
42
|
+
if isinstance(item, Tool):
|
|
43
|
+
register(item)
|
|
44
|
+
registered.append(item.name)
|
|
45
|
+
elif isinstance(item, str):
|
|
46
|
+
registered.append(item)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.warning("Plugin %s failed to run: %s", ep.name, e)
|
|
49
|
+
return registered
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_plugins(enabled: list[str] | None = None) -> list[str]:
|
|
53
|
+
"""
|
|
54
|
+
Discover plugins from entry_points and register their tools.
|
|
55
|
+
If enabled is provided, only load those plugin names; otherwise load all.
|
|
56
|
+
Returns list of plugin names that were loaded.
|
|
57
|
+
"""
|
|
58
|
+
loaded: list[str] = []
|
|
59
|
+
for ep in _load_entry_points():
|
|
60
|
+
if enabled is not None and ep.name not in enabled:
|
|
61
|
+
continue
|
|
62
|
+
try:
|
|
63
|
+
dist = ep.dist
|
|
64
|
+
version = dist.version if dist else ""
|
|
65
|
+
except Exception:
|
|
66
|
+
version = ""
|
|
67
|
+
tool_names = _invoke_plugin(ep)
|
|
68
|
+
register_plugin(ep.name, version=version, tools=tool_names)
|
|
69
|
+
loaded.append(ep.name)
|
|
70
|
+
return loaded
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Registry of loaded plugins: name, version, tools registered."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class PluginInfo:
|
|
8
|
+
name: str
|
|
9
|
+
version: str = ""
|
|
10
|
+
tools_registered: list[str] = field(default_factory=list)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_plugins: dict[str, PluginInfo] = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_plugin(name: str, version: str = "", tools: list[str] | None = None) -> None:
|
|
17
|
+
_plugins[name] = PluginInfo(
|
|
18
|
+
name=name,
|
|
19
|
+
version=version,
|
|
20
|
+
tools_registered=list(tools or []),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_plugin(name: str) -> PluginInfo | None:
|
|
25
|
+
return _plugins.get(name)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def list_plugins() -> list[PluginInfo]:
|
|
29
|
+
return list(_plugins.values())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def clear_plugins() -> None:
|
|
33
|
+
"""Clear registry (mainly for tests)."""
|
|
34
|
+
_plugins.clear()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Shared HTTP client and token helpers for the devsper package registry."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
REGISTRY_URL = os.environ.get(
|
|
8
|
+
"DEVSPER_REGISTRY_URL",
|
|
9
|
+
"https://registry.devsper.com",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
CREDENTIAL_SERVICE = "devsper_registry"
|
|
13
|
+
CREDENTIAL_USERNAME = "api_key"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_token() -> str | None:
|
|
17
|
+
"""Read stored API key from OS keychain, falling back to env var."""
|
|
18
|
+
try:
|
|
19
|
+
from devsper.credentials import get_credential
|
|
20
|
+
|
|
21
|
+
token = get_credential(CREDENTIAL_SERVICE, CREDENTIAL_USERNAME)
|
|
22
|
+
if token:
|
|
23
|
+
return token
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
return os.environ.get("DEVSPER_API_KEY")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_token(token: str) -> None:
|
|
30
|
+
"""Store API key in OS keychain."""
|
|
31
|
+
from devsper.credentials import set_credential
|
|
32
|
+
|
|
33
|
+
set_credential(CREDENTIAL_SERVICE, CREDENTIAL_USERNAME, token)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def delete_token() -> None:
|
|
37
|
+
"""Remove API key from OS keychain."""
|
|
38
|
+
try:
|
|
39
|
+
from devsper.credentials import delete_credential
|
|
40
|
+
|
|
41
|
+
delete_credential(CREDENTIAL_SERVICE, CREDENTIAL_USERNAME)
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def require_token() -> str:
|
|
47
|
+
"""Get token or exit with helpful message."""
|
|
48
|
+
token = get_token()
|
|
49
|
+
if not token:
|
|
50
|
+
from rich.console import Console
|
|
51
|
+
|
|
52
|
+
Console().print(
|
|
53
|
+
"[red]Not logged in.[/red] Run [bold]devsper reg login[/bold] first.\n"
|
|
54
|
+
"Or set [bold]DEVSPER_API_KEY[/bold] env var for CI."
|
|
55
|
+
)
|
|
56
|
+
raise SystemExit(1)
|
|
57
|
+
return token
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RegistryClient:
|
|
61
|
+
"""Thin HTTP wrapper for the devsper package registry API."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, token: str | None = None):
|
|
64
|
+
self.base = REGISTRY_URL.rstrip("/")
|
|
65
|
+
self.token = token
|
|
66
|
+
self._client = httpx.Client(timeout=30)
|
|
67
|
+
|
|
68
|
+
def _headers(self) -> dict:
|
|
69
|
+
h: dict[str, str] = {"Content-Type": "application/json"}
|
|
70
|
+
if self.token:
|
|
71
|
+
h["X-API-Key"] = self.token
|
|
72
|
+
return h
|
|
73
|
+
|
|
74
|
+
def get(self, path: str, **kwargs) -> httpx.Response:
|
|
75
|
+
return self._client.get(f"{self.base}{path}", headers=self._headers(), **kwargs)
|
|
76
|
+
|
|
77
|
+
def post(self, path: str, **kwargs) -> httpx.Response:
|
|
78
|
+
return self._client.post(
|
|
79
|
+
f"{self.base}{path}", headers=self._headers(), **kwargs
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def close(self) -> None:
|
|
83
|
+
self._client.close()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Provider adapters: base interface, router, and implementations."""
|
|
2
|
+
|
|
3
|
+
from devsper.providers.base import BaseProvider, MockProvider
|
|
4
|
+
from devsper.providers.router import ProviderRouter, get_router
|
|
5
|
+
from devsper.providers.openai import OpenAIProvider
|
|
6
|
+
from devsper.providers.anthropic import AnthropicProvider
|
|
7
|
+
from devsper.providers.gemini import GeminiProvider
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BaseProvider",
|
|
11
|
+
"MockProvider",
|
|
12
|
+
"ProviderRouter",
|
|
13
|
+
"get_router",
|
|
14
|
+
"OpenAIProvider",
|
|
15
|
+
"AnthropicProvider",
|
|
16
|
+
"GeminiProvider",
|
|
17
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Anthropic provider adapter using LangChain. Supports native Anthropic and Azure (Foundry) Claude."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Iterator
|
|
5
|
+
|
|
6
|
+
from langchain_anthropic import ChatAnthropic
|
|
7
|
+
from langchain_core.messages import HumanMessage
|
|
8
|
+
|
|
9
|
+
from devsper.providers.base import BaseProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _normalize_azure_base_url(url: str) -> str:
|
|
13
|
+
"""Strip trailing /messages so base URL ends with /v1; ChatAnthropic will append /messages."""
|
|
14
|
+
if not url:
|
|
15
|
+
return url
|
|
16
|
+
url = url.rstrip("/")
|
|
17
|
+
if url.endswith("/messages"):
|
|
18
|
+
return url[: -len("/messages")]
|
|
19
|
+
return url
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AnthropicProvider(BaseProvider):
|
|
23
|
+
"""
|
|
24
|
+
Anthropic API adapter. Uses ANTHROPIC_API_KEY (or pass api_key).
|
|
25
|
+
When AZURE_ANTHROPIC_* env vars are set, uses Azure Foundry Claude endpoint instead.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
api_key: str | None = None,
|
|
31
|
+
*,
|
|
32
|
+
azure: bool = False,
|
|
33
|
+
azure_endpoint: str | None = None,
|
|
34
|
+
azure_api_key: str | None = None,
|
|
35
|
+
azure_deployment: str | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.azure = azure or bool(
|
|
38
|
+
os.environ.get("AZURE_ANTHROPIC_ENDPOINT") or os.environ.get("AZURE_ANTHROPIC_API_KEY")
|
|
39
|
+
)
|
|
40
|
+
if self.azure:
|
|
41
|
+
self.azure_endpoint = _normalize_azure_base_url(
|
|
42
|
+
azure_endpoint or os.environ.get("AZURE_ANTHROPIC_ENDPOINT", "")
|
|
43
|
+
)
|
|
44
|
+
self.azure_api_key = azure_api_key or os.environ.get("AZURE_ANTHROPIC_API_KEY")
|
|
45
|
+
self.azure_deployment = (azure_deployment or os.environ.get("AZURE_ANTHROPIC_DEPLOYMENT_NAME") or "").strip()
|
|
46
|
+
if not self.azure_endpoint or not self.azure_api_key:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"Azure Anthropic requires AZURE_ANTHROPIC_ENDPOINT and AZURE_ANTHROPIC_API_KEY"
|
|
49
|
+
)
|
|
50
|
+
self.api_key = self.azure_api_key
|
|
51
|
+
else:
|
|
52
|
+
self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
|
|
53
|
+
self.azure_endpoint = ""
|
|
54
|
+
self.azure_api_key = ""
|
|
55
|
+
self.azure_deployment = ""
|
|
56
|
+
if not self.api_key:
|
|
57
|
+
raise ValueError("Anthropic requires api_key or ANTHROPIC_API_KEY")
|
|
58
|
+
|
|
59
|
+
def generate(self, model: str, prompt: str, stream: bool = False) -> str | Iterator[str]:
|
|
60
|
+
"""Call Anthropic (or Azure Claude) API and return the model output text."""
|
|
61
|
+
if self.azure:
|
|
62
|
+
deployment = (model or self.azure_deployment or "").strip() or self.azure_deployment
|
|
63
|
+
if not deployment:
|
|
64
|
+
raise ValueError("Azure Anthropic requires model name or AZURE_ANTHROPIC_DEPLOYMENT_NAME")
|
|
65
|
+
llm = ChatAnthropic(
|
|
66
|
+
model=deployment,
|
|
67
|
+
anthropic_api_url=self.azure_endpoint,
|
|
68
|
+
anthropic_api_key=self.azure_api_key,
|
|
69
|
+
temperature=0,
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
llm = ChatAnthropic(
|
|
73
|
+
model=model,
|
|
74
|
+
api_key=self.api_key,
|
|
75
|
+
temperature=0,
|
|
76
|
+
)
|
|
77
|
+
message = llm.invoke([HumanMessage(content=prompt)])
|
|
78
|
+
content = message.content
|
|
79
|
+
text = content if isinstance(content, str) else str(content)
|
|
80
|
+
if stream:
|
|
81
|
+
def _gen():
|
|
82
|
+
yield text
|
|
83
|
+
return _gen()
|
|
84
|
+
return text
|