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,157 @@
|
|
|
1
|
+
"""Evaluate if: expressions safely (no eval)."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from devsper.workflow.context import WorkflowContext
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WorkflowConditionError(Exception):
|
|
10
|
+
"""Raised when a condition expression cannot be parsed or is invalid."""
|
|
11
|
+
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Pattern: steps.<id>.<field> <op> <value>
|
|
16
|
+
# id/field: word chars and underscore
|
|
17
|
+
# op: ==, !=, >=, <=, >, <, in, not in
|
|
18
|
+
# value: quoted string, int, float, true/false
|
|
19
|
+
_EXPR_PATTERN = re.compile(
|
|
20
|
+
r"^\s*steps\.([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*"
|
|
21
|
+
r"(==|!=|>=|<=|>|<|in|not\s+in)\s*(.+)\s*$",
|
|
22
|
+
re.IGNORECASE,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _parse_value(raw: str) -> Any:
|
|
27
|
+
raw = raw.strip()
|
|
28
|
+
if not raw:
|
|
29
|
+
raise WorkflowConditionError("Empty value in condition")
|
|
30
|
+
# List: [ ... ]
|
|
31
|
+
if raw.startswith("[") and raw.endswith("]"):
|
|
32
|
+
inner = raw[1:-1].strip()
|
|
33
|
+
if not inner:
|
|
34
|
+
return []
|
|
35
|
+
return [_parse_value(p.strip()) for p in _split_top_level(inner, ",")]
|
|
36
|
+
# Quoted string
|
|
37
|
+
if (raw.startswith("'") and raw.endswith("'")) or (
|
|
38
|
+
raw.startswith('"') and raw.endswith('"')
|
|
39
|
+
):
|
|
40
|
+
return raw[1:-1].replace("\\'", "'").replace('\\"', '"')
|
|
41
|
+
# Bool
|
|
42
|
+
if raw.lower() == "true":
|
|
43
|
+
return True
|
|
44
|
+
if raw.lower() == "false":
|
|
45
|
+
return False
|
|
46
|
+
# Int
|
|
47
|
+
try:
|
|
48
|
+
return int(raw)
|
|
49
|
+
except ValueError:
|
|
50
|
+
pass
|
|
51
|
+
# Float
|
|
52
|
+
try:
|
|
53
|
+
return float(raw)
|
|
54
|
+
except ValueError:
|
|
55
|
+
pass
|
|
56
|
+
raise WorkflowConditionError(f"Cannot parse value: {raw!r}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _split_top_level(s: str, sep: str) -> list[str]:
|
|
60
|
+
"""Split by sep only at top level (ignore inside quotes/brackets)."""
|
|
61
|
+
parts: list[str] = []
|
|
62
|
+
current: list[str] = []
|
|
63
|
+
depth = 0
|
|
64
|
+
in_quote = None
|
|
65
|
+
i = 0
|
|
66
|
+
while i < len(s):
|
|
67
|
+
c = s[i]
|
|
68
|
+
if in_quote:
|
|
69
|
+
if c == in_quote and (i == 0 or s[i - 1] != "\\"):
|
|
70
|
+
in_quote = None
|
|
71
|
+
current.append(c)
|
|
72
|
+
elif c in ("'", '"'):
|
|
73
|
+
in_quote = c
|
|
74
|
+
current.append(c)
|
|
75
|
+
elif c in ("[", "(", "{"):
|
|
76
|
+
depth += 1
|
|
77
|
+
current.append(c)
|
|
78
|
+
elif c in ("]", ")", "}"):
|
|
79
|
+
depth -= 1
|
|
80
|
+
current.append(c)
|
|
81
|
+
elif depth == 0 and s[i : i + len(sep)] == sep:
|
|
82
|
+
parts.append("".join(current))
|
|
83
|
+
current = []
|
|
84
|
+
i += len(sep) - 1
|
|
85
|
+
else:
|
|
86
|
+
current.append(c)
|
|
87
|
+
i += 1
|
|
88
|
+
parts.append("".join(current))
|
|
89
|
+
return parts
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def evaluate_condition(expression: str, context: WorkflowContext) -> bool:
|
|
93
|
+
"""
|
|
94
|
+
Parse expressions of the form: steps.<id>.<field> <op> <value>
|
|
95
|
+
Supported ops: ==, !=, >, <, >=, <=, in, not in
|
|
96
|
+
On missing step/field: return False (conservative — skip rather than crash).
|
|
97
|
+
On parse error: raise WorkflowConditionError.
|
|
98
|
+
"""
|
|
99
|
+
expression = expression.strip()
|
|
100
|
+
if not expression:
|
|
101
|
+
raise WorkflowConditionError("Empty condition expression")
|
|
102
|
+
|
|
103
|
+
m = _EXPR_PATTERN.match(expression)
|
|
104
|
+
if not m:
|
|
105
|
+
raise WorkflowConditionError(
|
|
106
|
+
f"Condition must match: steps.<step_id>.<field> <op> <value>. Got: {expression!r}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
step_id, field, op, value_str = m.group(1), m.group(2), m.group(3), m.group(4)
|
|
110
|
+
op = op.lower().replace(" ", "")
|
|
111
|
+
|
|
112
|
+
left = context.get_field(step_id, field)
|
|
113
|
+
# Missing step or field → conservative False
|
|
114
|
+
if left is None:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
right = _parse_value(value_str)
|
|
119
|
+
except WorkflowConditionError:
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
# Type coercion for comparison: if right is int/float and left is str, try to coerce left
|
|
123
|
+
if isinstance(right, (int, float)) and isinstance(left, str):
|
|
124
|
+
try:
|
|
125
|
+
if isinstance(right, int):
|
|
126
|
+
left = int(left)
|
|
127
|
+
else:
|
|
128
|
+
left = float(left)
|
|
129
|
+
except (ValueError, TypeError):
|
|
130
|
+
pass
|
|
131
|
+
if isinstance(right, bool) and isinstance(left, str):
|
|
132
|
+
left = left.lower() in ("true", "1", "yes")
|
|
133
|
+
|
|
134
|
+
if op == "==":
|
|
135
|
+
return left == right
|
|
136
|
+
if op == "!=":
|
|
137
|
+
return left != right
|
|
138
|
+
if op == ">":
|
|
139
|
+
return left > right # type: ignore[return-value]
|
|
140
|
+
if op == "<":
|
|
141
|
+
return left < right # type: ignore[return-value]
|
|
142
|
+
if op == ">=":
|
|
143
|
+
return left >= right # type: ignore[return-value]
|
|
144
|
+
if op == "<=":
|
|
145
|
+
return left <= right # type: ignore[return-value]
|
|
146
|
+
if op == "in":
|
|
147
|
+
if not isinstance(right, (list, tuple, str)):
|
|
148
|
+
raise WorkflowConditionError(f"Right side of 'in' must be list or str, got {type(right)}")
|
|
149
|
+
return left in right # type: ignore[operator]
|
|
150
|
+
if op == "notin":
|
|
151
|
+
if not isinstance(right, (list, tuple, str)):
|
|
152
|
+
raise WorkflowConditionError(
|
|
153
|
+
f"Right side of 'not in' must be list or str, got {type(right)}"
|
|
154
|
+
)
|
|
155
|
+
return left not in right # type: ignore[operator]
|
|
156
|
+
|
|
157
|
+
raise WorkflowConditionError(f"Unsupported operator: {op!r}")
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""WorkflowContext: typed output passing between steps."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WorkflowTemplateError(Exception):
|
|
10
|
+
"""Raised when a template reference cannot be resolved."""
|
|
11
|
+
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StepResult(BaseModel):
|
|
16
|
+
step_id: str
|
|
17
|
+
raw_result: str # full agent output
|
|
18
|
+
structured: dict | None = None # parsed output_schema result, if defined
|
|
19
|
+
skipped: bool = False
|
|
20
|
+
error: str | None = None
|
|
21
|
+
duration_seconds: float = 0.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WorkflowContext:
|
|
25
|
+
def __init__(self, inputs: dict[str, Any]) -> None:
|
|
26
|
+
self.inputs = inputs
|
|
27
|
+
self.steps: dict[str, StepResult] = {}
|
|
28
|
+
|
|
29
|
+
def record(self, step_id: str, result: StepResult) -> None:
|
|
30
|
+
self.steps[step_id] = result
|
|
31
|
+
|
|
32
|
+
def resolve_template(self, template: str) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Replace {input.field} with self.inputs[field].
|
|
35
|
+
Replace {steps.step_id.result} with self.steps[step_id].raw_result.
|
|
36
|
+
Replace {steps.step_id.field} with self.steps[step_id].structured[field].
|
|
37
|
+
Raise WorkflowTemplateError if reference not found.
|
|
38
|
+
"""
|
|
39
|
+
# Match {input.NAME} or {steps.STEP_ID.result} or {steps.STEP_ID.FIELD}
|
|
40
|
+
pattern = re.compile(
|
|
41
|
+
r"\{(\w+)\.([^}]+)\}"
|
|
42
|
+
) # group 1: input|steps, group 2: rest
|
|
43
|
+
|
|
44
|
+
def repl(match: re.Match[str]) -> str:
|
|
45
|
+
prefix, rest = match.group(1), match.group(2)
|
|
46
|
+
if prefix == "input":
|
|
47
|
+
if rest not in self.inputs:
|
|
48
|
+
raise WorkflowTemplateError(
|
|
49
|
+
f"Template references input.{rest} but input {rest!r} is not provided. "
|
|
50
|
+
f"Available: {list(self.inputs.keys())}"
|
|
51
|
+
)
|
|
52
|
+
val = self.inputs[rest]
|
|
53
|
+
return str(val) if val is not None else ""
|
|
54
|
+
if prefix == "steps":
|
|
55
|
+
part = rest.split(".", 1)
|
|
56
|
+
if len(part) != 2:
|
|
57
|
+
raise WorkflowTemplateError(
|
|
58
|
+
f"Invalid steps reference: {match.group(0)}. "
|
|
59
|
+
"Use steps.<step_id>.result or steps.<step_id>.<field>"
|
|
60
|
+
)
|
|
61
|
+
step_id, field = part[0], part[1]
|
|
62
|
+
if step_id not in self.steps:
|
|
63
|
+
raise WorkflowTemplateError(
|
|
64
|
+
f"Template references steps.{step_id}.{field} but step {step_id!r} "
|
|
65
|
+
f"has not run yet or does not exist. Available: {list(self.steps.keys())}"
|
|
66
|
+
)
|
|
67
|
+
sr = self.steps[step_id]
|
|
68
|
+
if field == "result":
|
|
69
|
+
return sr.raw_result or ""
|
|
70
|
+
if sr.structured is not None and field in sr.structured:
|
|
71
|
+
val = sr.structured[field]
|
|
72
|
+
return str(val) if val is not None else ""
|
|
73
|
+
raise WorkflowTemplateError(
|
|
74
|
+
f"Template references steps.{step_id}.{field} but step {step_id!r} "
|
|
75
|
+
f"has no structured field {field!r}. "
|
|
76
|
+
f"Available: result, or {list(sr.structured.keys()) if sr.structured else []}"
|
|
77
|
+
)
|
|
78
|
+
raise WorkflowTemplateError(f"Unknown template prefix: {prefix!r}")
|
|
79
|
+
|
|
80
|
+
return pattern.sub(repl, template)
|
|
81
|
+
|
|
82
|
+
def get_field(self, step_id: str, field: str) -> Any:
|
|
83
|
+
"""Used by condition evaluator. Returns None if step/field missing."""
|
|
84
|
+
if step_id not in self.steps:
|
|
85
|
+
return None
|
|
86
|
+
sr = self.steps[step_id]
|
|
87
|
+
if field == "result":
|
|
88
|
+
return sr.raw_result
|
|
89
|
+
if sr.structured is not None and field in sr.structured:
|
|
90
|
+
return sr.structured[field]
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def to_summary(self) -> dict[str, Any]:
|
|
94
|
+
"""Serializable summary for output/replay."""
|
|
95
|
+
steps_summary: dict[str, dict[str, Any]] = {}
|
|
96
|
+
for sid, sr in self.steps.items():
|
|
97
|
+
steps_summary[sid] = {
|
|
98
|
+
"step_id": sr.step_id,
|
|
99
|
+
"skipped": sr.skipped,
|
|
100
|
+
"error": sr.error,
|
|
101
|
+
"duration_seconds": sr.duration_seconds,
|
|
102
|
+
"has_result": bool(sr.raw_result),
|
|
103
|
+
"has_structured": sr.structured is not None,
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
"inputs": dict(self.inputs),
|
|
107
|
+
"steps": steps_summary,
|
|
108
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Load workflow definitions from workflow.devsper.toml or devsper.toml."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import tomllib
|
|
8
|
+
|
|
9
|
+
from devsper.workflow.schema import (
|
|
10
|
+
OutputField,
|
|
11
|
+
StepCondition,
|
|
12
|
+
WorkflowDefinition,
|
|
13
|
+
WorkflowStep,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _find_workflow_file() -> Path | None:
|
|
18
|
+
cwd = Path.cwd()
|
|
19
|
+
for base in (cwd, cwd.parent):
|
|
20
|
+
for name in ("workflow.devsper.toml", "devsper.toml"):
|
|
21
|
+
p = base / name
|
|
22
|
+
if p.is_file():
|
|
23
|
+
return p
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_toml(path: Path) -> dict:
|
|
28
|
+
if not path.is_file():
|
|
29
|
+
return {}
|
|
30
|
+
try:
|
|
31
|
+
with open(path, "rb") as f:
|
|
32
|
+
return tomllib.load(f)
|
|
33
|
+
except Exception:
|
|
34
|
+
return {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _workflow_from_legacy_steps(steps: list[str]) -> WorkflowDefinition:
|
|
38
|
+
"""Wrap a list of task strings into a WorkflowDefinition with auto-generated step ids."""
|
|
39
|
+
workflow_steps = []
|
|
40
|
+
for i, task in enumerate(steps):
|
|
41
|
+
step_id = f"step_{secrets.token_hex(3)}"
|
|
42
|
+
workflow_steps.append(
|
|
43
|
+
WorkflowStep(
|
|
44
|
+
id=step_id,
|
|
45
|
+
task=task,
|
|
46
|
+
depends_on=[workflow_steps[-1].id] if workflow_steps else [],
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
return WorkflowDefinition(
|
|
50
|
+
name="legacy",
|
|
51
|
+
description="Legacy workflow from list of steps",
|
|
52
|
+
version="1.0",
|
|
53
|
+
steps=workflow_steps,
|
|
54
|
+
inputs=[],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _step_dict_to_model(d: dict[str, Any]) -> WorkflowStep:
|
|
59
|
+
"""Convert a raw step dict (from TOML) to WorkflowStep."""
|
|
60
|
+
# TOML may have "if" as key; Pydantic expects alias "if" -> if_
|
|
61
|
+
raw = dict(d)
|
|
62
|
+
if "if" in raw and isinstance(raw["if"], dict):
|
|
63
|
+
raw["if"] = StepCondition(expression=raw["if"].get("expression", ""))
|
|
64
|
+
elif "if" in raw and isinstance(raw["if"], str):
|
|
65
|
+
raw["if"] = StepCondition(expression=raw["if"])
|
|
66
|
+
if "output_schema" in raw and isinstance(raw["output_schema"], list):
|
|
67
|
+
raw["output_schema"] = [
|
|
68
|
+
OutputField(**f) if isinstance(f, dict) else f for f in raw["output_schema"]
|
|
69
|
+
]
|
|
70
|
+
return WorkflowStep(**raw)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _workflow_dict_to_definition(name: str, raw: dict[str, Any]) -> WorkflowDefinition:
|
|
74
|
+
"""Convert raw workflow dict to WorkflowDefinition."""
|
|
75
|
+
steps_raw = raw.get("steps") or []
|
|
76
|
+
if not steps_raw:
|
|
77
|
+
return WorkflowDefinition(
|
|
78
|
+
name=name,
|
|
79
|
+
description=raw.get("description"),
|
|
80
|
+
version=str(raw.get("version", "1.0")),
|
|
81
|
+
steps=[],
|
|
82
|
+
inputs=raw.get("inputs") or [],
|
|
83
|
+
)
|
|
84
|
+
# Legacy: list of strings
|
|
85
|
+
if all(isinstance(s, str) for s in steps_raw):
|
|
86
|
+
wf = _workflow_from_legacy_steps(steps_raw)
|
|
87
|
+
wf = WorkflowDefinition(
|
|
88
|
+
name=name,
|
|
89
|
+
description=raw.get("description") or wf.description,
|
|
90
|
+
version=str(raw.get("version", "1.0")),
|
|
91
|
+
steps=wf.steps,
|
|
92
|
+
inputs=raw.get("inputs") or [],
|
|
93
|
+
)
|
|
94
|
+
return wf
|
|
95
|
+
# New format: list of step dicts
|
|
96
|
+
steps = []
|
|
97
|
+
for s in steps_raw:
|
|
98
|
+
if isinstance(s, dict):
|
|
99
|
+
steps.append(_step_dict_to_model(s))
|
|
100
|
+
else:
|
|
101
|
+
steps.append(s)
|
|
102
|
+
inputs_raw = raw.get("inputs") or []
|
|
103
|
+
inputs = [OutputField(**f) if isinstance(f, dict) else f for f in inputs_raw]
|
|
104
|
+
return WorkflowDefinition(
|
|
105
|
+
name=name,
|
|
106
|
+
description=raw.get("description"),
|
|
107
|
+
version=str(raw.get("version", "1.0")),
|
|
108
|
+
steps=steps,
|
|
109
|
+
inputs=inputs,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def load_workflow(name: str, config_path: Path | None = None) -> WorkflowDefinition | None:
|
|
114
|
+
"""
|
|
115
|
+
Load workflow by name. Returns WorkflowDefinition (Pydantic model).
|
|
116
|
+
If config_path is given, read that file; else discover workflow.devsper.toml / devsper.toml.
|
|
117
|
+
Legacy: if steps are a list of strings, they are wrapped in a WorkflowDefinition with
|
|
118
|
+
auto-generated step ids and sequential dependencies.
|
|
119
|
+
"""
|
|
120
|
+
path = config_path or _find_workflow_file()
|
|
121
|
+
if not path:
|
|
122
|
+
return None
|
|
123
|
+
data = _load_toml(path)
|
|
124
|
+
raw: dict[str, Any] | None = None
|
|
125
|
+
# Single [workflow] section
|
|
126
|
+
wf = data.get("workflow")
|
|
127
|
+
if isinstance(wf, dict) and wf.get("name") == name:
|
|
128
|
+
raw = {"name": name, **wf}
|
|
129
|
+
if raw is None:
|
|
130
|
+
for key, val in data.items():
|
|
131
|
+
if key.startswith("workflow.") and isinstance(val, dict):
|
|
132
|
+
if val.get("name") == name or key == f"workflow.{name}":
|
|
133
|
+
raw = {"name": name, **val}
|
|
134
|
+
break
|
|
135
|
+
if not raw:
|
|
136
|
+
return None
|
|
137
|
+
return _workflow_dict_to_definition(name, raw)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def list_workflows(config_path: Path | None = None) -> list[str]:
|
|
141
|
+
"""Return list of workflow identifiers (name or key suffix) that load_workflow() accepts."""
|
|
142
|
+
path = config_path or _find_workflow_file()
|
|
143
|
+
if not path:
|
|
144
|
+
return []
|
|
145
|
+
data = _load_toml(path)
|
|
146
|
+
names: list[str] = []
|
|
147
|
+
wf = data.get("workflow")
|
|
148
|
+
if isinstance(wf, dict) and wf.get("name"):
|
|
149
|
+
names.append(str(wf["name"]))
|
|
150
|
+
for key, val in data.items():
|
|
151
|
+
if key.startswith("workflow.") and isinstance(val, dict):
|
|
152
|
+
# Use key suffix so load_workflow(key_suffix) works
|
|
153
|
+
key_suffix = key.removeprefix("workflow.")
|
|
154
|
+
if key_suffix and key_suffix not in names:
|
|
155
|
+
names.append(key_suffix)
|
|
156
|
+
return list(dict.fromkeys(names))
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Resolve step dependencies into execution order (waves)."""
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
|
|
5
|
+
from devsper.workflow.schema import WorkflowStep
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WorkflowCycleError(Exception):
|
|
9
|
+
"""Raised when depends_on forms a cycle."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, cycle: list[str]) -> None:
|
|
12
|
+
self.cycle = cycle
|
|
13
|
+
super().__init__(f"Dependency cycle: {' -> '.join(cycle)}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_execution_order(steps: list[WorkflowStep]) -> list[list[WorkflowStep]]:
|
|
17
|
+
"""
|
|
18
|
+
Topological sort of steps using depends_on edges.
|
|
19
|
+
Returns list of "waves"; steps in the same wave can run in parallel.
|
|
20
|
+
Steps with no depends_on are in wave 0.
|
|
21
|
+
Raises WorkflowCycleError with the cycle path if a cycle is detected.
|
|
22
|
+
"""
|
|
23
|
+
step_map = {s.id: s for s in steps}
|
|
24
|
+
# in_degree[s] = number of dependencies of s (steps s waits for)
|
|
25
|
+
in_degree = {s.id: len(s.depends_on) for s in steps}
|
|
26
|
+
# dependants[dep] = list of step ids that depend on dep
|
|
27
|
+
dependants: dict[str, list[str]] = {s.id: [] for s in steps}
|
|
28
|
+
for s in steps:
|
|
29
|
+
for dep in s.depends_on:
|
|
30
|
+
dependants[dep].append(s.id)
|
|
31
|
+
|
|
32
|
+
waves: list[list[WorkflowStep]] = []
|
|
33
|
+
queue: deque[str] = deque(sid for sid, d in in_degree.items() if d == 0)
|
|
34
|
+
remaining = set(step_map.keys())
|
|
35
|
+
|
|
36
|
+
while queue:
|
|
37
|
+
wave_ids: list[str] = list(queue)
|
|
38
|
+
queue.clear()
|
|
39
|
+
for sid in wave_ids:
|
|
40
|
+
remaining.discard(sid)
|
|
41
|
+
for nxt in dependants[sid]:
|
|
42
|
+
in_degree[nxt] -= 1
|
|
43
|
+
if in_degree[nxt] == 0:
|
|
44
|
+
queue.append(nxt)
|
|
45
|
+
waves.append([step_map[sid] for sid in wave_ids])
|
|
46
|
+
|
|
47
|
+
if remaining:
|
|
48
|
+
# Cycle: find one
|
|
49
|
+
cycle = _find_cycle(steps)
|
|
50
|
+
raise WorkflowCycleError(cycle)
|
|
51
|
+
|
|
52
|
+
return waves
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _find_cycle(steps: list[WorkflowStep]) -> list[str]:
|
|
56
|
+
"""Return one cycle as list of step ids."""
|
|
57
|
+
step_map = {s.id: s for s in steps}
|
|
58
|
+
visited: set[str] = set()
|
|
59
|
+
path: list[str] = []
|
|
60
|
+
path_set: set[str] = set()
|
|
61
|
+
cycle_start: str | None = None
|
|
62
|
+
|
|
63
|
+
def dfs(sid: str) -> bool:
|
|
64
|
+
nonlocal cycle_start
|
|
65
|
+
visited.add(sid)
|
|
66
|
+
path.append(sid)
|
|
67
|
+
path_set.add(sid)
|
|
68
|
+
step = step_map.get(sid)
|
|
69
|
+
if step:
|
|
70
|
+
for dep in step.depends_on:
|
|
71
|
+
if dep not in visited:
|
|
72
|
+
if dfs(dep):
|
|
73
|
+
return True
|
|
74
|
+
elif dep in path_set:
|
|
75
|
+
cycle_start = dep
|
|
76
|
+
return True
|
|
77
|
+
path.pop()
|
|
78
|
+
path_set.discard(sid)
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
for sid in step_map:
|
|
82
|
+
if sid not in visited and dfs(sid):
|
|
83
|
+
# Build cycle from path: from cycle_start to end of path
|
|
84
|
+
try:
|
|
85
|
+
idx = path.index(cycle_start)
|
|
86
|
+
return path[idx:] + [cycle_start]
|
|
87
|
+
except (ValueError, TypeError):
|
|
88
|
+
return path + [path[0]] if path else list(step_map.keys())[:1]
|
|
89
|
+
|
|
90
|
+
return list(step_map.keys())[:1]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def validate_dag(steps: list[WorkflowStep]) -> list[str]:
|
|
94
|
+
"""Return list of error strings (used by validator)."""
|
|
95
|
+
errors: list[str] = []
|
|
96
|
+
step_ids = {s.id for s in steps}
|
|
97
|
+
|
|
98
|
+
for s in steps:
|
|
99
|
+
for dep in s.depends_on:
|
|
100
|
+
if dep not in step_ids:
|
|
101
|
+
errors.append(f"Step {s.id!r} depends_on unknown step {dep!r}")
|
|
102
|
+
|
|
103
|
+
if not errors:
|
|
104
|
+
try:
|
|
105
|
+
build_execution_order(steps)
|
|
106
|
+
except WorkflowCycleError as e:
|
|
107
|
+
errors.append(str(e))
|
|
108
|
+
|
|
109
|
+
return errors
|