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/builder.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Autonomous Application Builder: build a working repo from an app description.
|
|
3
|
+
|
|
4
|
+
Flow:
|
|
5
|
+
1) architecture design
|
|
6
|
+
2) repo scaffold
|
|
7
|
+
3) module implementation
|
|
8
|
+
4) test generation
|
|
9
|
+
5) test execution
|
|
10
|
+
6) debugging loop
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from devsper.dev.scaffold import ArchitecturePlan, scaffold_repo
|
|
16
|
+
from devsper.dev.sandbox import Sandbox, SandboxLimits
|
|
17
|
+
from devsper.dev.debugger import debug_loop
|
|
18
|
+
from devsper.dev.repo_index import RepoIndex
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _design_architecture(description: str) -> ArchitecturePlan:
|
|
22
|
+
"""Produce an architecture plan from app description (heuristic + optional LLM)."""
|
|
23
|
+
desc_lower = (description or "").lower()
|
|
24
|
+
name = (description or "app").strip()[:50].replace("/", "-")
|
|
25
|
+
backend = "fastapi"
|
|
26
|
+
frontend = "none"
|
|
27
|
+
if "flask" in desc_lower:
|
|
28
|
+
backend = "flask"
|
|
29
|
+
if "react" in desc_lower or "frontend" in desc_lower or "ui" in desc_lower:
|
|
30
|
+
frontend = "react"
|
|
31
|
+
if "todo" in desc_lower:
|
|
32
|
+
name = "Todo App"
|
|
33
|
+
return ArchitecturePlan(
|
|
34
|
+
name=name or "App",
|
|
35
|
+
description=description or "Generated application",
|
|
36
|
+
backend=backend,
|
|
37
|
+
frontend=frontend,
|
|
38
|
+
features=[],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _implement_modules(sandbox: Sandbox, plan: ArchitecturePlan, description: str) -> None:
|
|
43
|
+
"""Generate and write main backend module(s)."""
|
|
44
|
+
if "fastapi" in plan.backend.lower() and "todo" in plan.name.lower():
|
|
45
|
+
main_py = '''"""FastAPI Todo API."""
|
|
46
|
+
from fastapi import FastAPI, HTTPException
|
|
47
|
+
from pydantic import BaseModel
|
|
48
|
+
from typing import List
|
|
49
|
+
|
|
50
|
+
app = FastAPI(title="Todo API")
|
|
51
|
+
|
|
52
|
+
class TodoItem(BaseModel):
|
|
53
|
+
id: int
|
|
54
|
+
title: str
|
|
55
|
+
done: bool = False
|
|
56
|
+
|
|
57
|
+
todos: List[TodoItem] = []
|
|
58
|
+
next_id = 1
|
|
59
|
+
|
|
60
|
+
@app.get("/")
|
|
61
|
+
def root():
|
|
62
|
+
return {"message": "Todo API", "docs": "/docs"}
|
|
63
|
+
|
|
64
|
+
@app.get("/todos")
|
|
65
|
+
def list_todos():
|
|
66
|
+
return {"todos": [t.model_dump() for t in todos]}
|
|
67
|
+
|
|
68
|
+
@app.post("/todos")
|
|
69
|
+
def create_todo(title: str):
|
|
70
|
+
global next_id
|
|
71
|
+
item = TodoItem(id=next_id, title=title, done=False)
|
|
72
|
+
next_id += 1
|
|
73
|
+
todos.append(item)
|
|
74
|
+
return item.model_dump()
|
|
75
|
+
|
|
76
|
+
@app.patch("/todos/{item_id}")
|
|
77
|
+
def toggle_todo(item_id: int):
|
|
78
|
+
for t in todos:
|
|
79
|
+
if t.id == item_id:
|
|
80
|
+
t.done = not t.done
|
|
81
|
+
return t.model_dump()
|
|
82
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
83
|
+
|
|
84
|
+
@app.delete("/todos/{item_id}")
|
|
85
|
+
def delete_todo(item_id: int):
|
|
86
|
+
global todos
|
|
87
|
+
for i, t in enumerate(todos):
|
|
88
|
+
if t.id == item_id:
|
|
89
|
+
todos.pop(i)
|
|
90
|
+
return {"ok": True}
|
|
91
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
92
|
+
'''
|
|
93
|
+
sandbox.write_file("backend/main.py", main_py)
|
|
94
|
+
elif "fastapi" in plan.backend.lower():
|
|
95
|
+
# Generic FastAPI placeholder already from scaffold; leave or minimal enhance
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _generate_tests(sandbox: Sandbox, plan: ArchitecturePlan) -> None:
|
|
100
|
+
"""Write basic tests."""
|
|
101
|
+
if "todo" in (plan.name or "").lower():
|
|
102
|
+
test_py = '''"""Tests for Todo API."""
|
|
103
|
+
import pytest
|
|
104
|
+
from fastapi.testclient import TestClient
|
|
105
|
+
|
|
106
|
+
# Import app from backend
|
|
107
|
+
import sys
|
|
108
|
+
from pathlib import Path
|
|
109
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "backend"))
|
|
110
|
+
from main import app
|
|
111
|
+
|
|
112
|
+
client = TestClient(app)
|
|
113
|
+
|
|
114
|
+
def test_root():
|
|
115
|
+
r = client.get("/")
|
|
116
|
+
assert r.status_code == 200
|
|
117
|
+
assert "message" in r.json()
|
|
118
|
+
|
|
119
|
+
def test_list_todos_empty():
|
|
120
|
+
r = client.get("/todos")
|
|
121
|
+
assert r.status_code == 200
|
|
122
|
+
assert r.json()["todos"] == []
|
|
123
|
+
|
|
124
|
+
def test_create_and_list_todo():
|
|
125
|
+
r = client.post("/todos?title=test task")
|
|
126
|
+
assert r.status_code == 200
|
|
127
|
+
data = r.json()
|
|
128
|
+
assert data["title"] == "test task"
|
|
129
|
+
assert data["done"] is False
|
|
130
|
+
r2 = client.get("/todos")
|
|
131
|
+
assert len(r2.json()["todos"]) >= 1
|
|
132
|
+
'''
|
|
133
|
+
sandbox.write_file("tests/test_app.py", test_py)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def run_build(
|
|
137
|
+
app_description: str,
|
|
138
|
+
output_dir: str | Path,
|
|
139
|
+
*,
|
|
140
|
+
timeout_seconds: int = 300,
|
|
141
|
+
max_debug_iterations: int = 3,
|
|
142
|
+
) -> dict:
|
|
143
|
+
"""
|
|
144
|
+
Build a working repository from an app description.
|
|
145
|
+
|
|
146
|
+
Steps: architecture design, scaffold, implement, generate tests,
|
|
147
|
+
install deps, run tests, debug loop until pass or limit.
|
|
148
|
+
|
|
149
|
+
Returns dict with keys: success, plan, created_paths, test_passed, debug_result, repo_path.
|
|
150
|
+
"""
|
|
151
|
+
output_dir = Path(output_dir).resolve()
|
|
152
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
|
|
154
|
+
plan = _design_architecture(app_description)
|
|
155
|
+
created_paths = scaffold_repo(output_dir, plan)
|
|
156
|
+
|
|
157
|
+
limits = SandboxLimits(timeout_seconds=timeout_seconds)
|
|
158
|
+
sandbox = Sandbox(output_dir, limits=limits)
|
|
159
|
+
|
|
160
|
+
_implement_modules(sandbox, plan, app_description)
|
|
161
|
+
_generate_tests(sandbox, plan)
|
|
162
|
+
|
|
163
|
+
install_result = sandbox.install_dependencies(backend=True, frontend=False)
|
|
164
|
+
|
|
165
|
+
def get_fix(stdout: str, stderr: str) -> str:
|
|
166
|
+
try:
|
|
167
|
+
from devsper.utils.models import generate
|
|
168
|
+
from devsper.config import get_config
|
|
169
|
+
cfg = get_config()
|
|
170
|
+
model = getattr(cfg.swarm, "worker_model", "gpt-4o") or "gpt-4o"
|
|
171
|
+
prompt = (
|
|
172
|
+
"The following test run failed. Suggest a single file fix as:\n"
|
|
173
|
+
"FILE: <relative path>\nCONTENT: <full file content>\n\n"
|
|
174
|
+
"Stdout:\n" + (stdout or "")[:2000] + "\n\nStderr:\n" + (stderr or "")[:1000]
|
|
175
|
+
)
|
|
176
|
+
return generate(model, prompt)
|
|
177
|
+
except Exception:
|
|
178
|
+
return ""
|
|
179
|
+
|
|
180
|
+
debug_result = debug_loop(
|
|
181
|
+
sandbox,
|
|
182
|
+
max_iterations=max_debug_iterations,
|
|
183
|
+
test_path="tests",
|
|
184
|
+
get_fix=get_fix,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
"success": debug_result.passed,
|
|
189
|
+
"plan": plan,
|
|
190
|
+
"created_paths": created_paths,
|
|
191
|
+
"install_success": install_result.success,
|
|
192
|
+
"test_passed": debug_result.passed,
|
|
193
|
+
"debug_result": debug_result,
|
|
194
|
+
"repo_path": str(output_dir),
|
|
195
|
+
}
|
devsper/dev/debugger.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test execution loop: run tests, detect errors, spawn fix tasks, patch code.
|
|
3
|
+
Loop until tests pass or max iterations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
from devsper.dev.sandbox import Sandbox, SandboxResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class DebugResult:
|
|
14
|
+
"""Result of the debug loop."""
|
|
15
|
+
|
|
16
|
+
passed: bool
|
|
17
|
+
iterations: int
|
|
18
|
+
last_stdout: str = ""
|
|
19
|
+
last_stderr: str = ""
|
|
20
|
+
fixes_applied: list[str] = field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def debug_loop(
|
|
24
|
+
sandbox: Sandbox,
|
|
25
|
+
max_iterations: int = 5,
|
|
26
|
+
test_path: str = "tests",
|
|
27
|
+
get_fix: Callable[[str, str], str] | None = None,
|
|
28
|
+
) -> DebugResult:
|
|
29
|
+
"""
|
|
30
|
+
Run tests in sandbox; on failure, call get_fix(stdout, stderr) to get patch/code,
|
|
31
|
+
apply it (e.g. via sandbox.write_file), then re-run. Loop until tests pass or
|
|
32
|
+
max_iterations. If get_fix is None, no fixes are applied (just re-run).
|
|
33
|
+
"""
|
|
34
|
+
fixes: list[str] = []
|
|
35
|
+
last_stdout = ""
|
|
36
|
+
last_stderr = ""
|
|
37
|
+
|
|
38
|
+
for iteration in range(max_iterations):
|
|
39
|
+
result: SandboxResult = sandbox.run_tests(path=test_path)
|
|
40
|
+
last_stdout = result.stdout
|
|
41
|
+
last_stderr = result.stderr or ""
|
|
42
|
+
|
|
43
|
+
if result.success:
|
|
44
|
+
return DebugResult(
|
|
45
|
+
passed=True,
|
|
46
|
+
iterations=iteration + 1,
|
|
47
|
+
last_stdout=last_stdout,
|
|
48
|
+
last_stderr=last_stderr,
|
|
49
|
+
fixes_applied=fixes,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if get_fix and not result.timed_out:
|
|
53
|
+
fix_instruction = get_fix(last_stdout, last_stderr)
|
|
54
|
+
if fix_instruction and fix_instruction.strip():
|
|
55
|
+
fixes.append(fix_instruction[:500])
|
|
56
|
+
# get_fix may return "path: <path>\\ncontent: <content>" or similar
|
|
57
|
+
# A real implementation would parse and call sandbox.write_file;
|
|
58
|
+
# for now we only record that a fix was requested.
|
|
59
|
+
# Caller can pass a get_fix that actually writes files.
|
|
60
|
+
if _apply_fix_instruction(sandbox, fix_instruction):
|
|
61
|
+
continue
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
return DebugResult(
|
|
65
|
+
passed=False,
|
|
66
|
+
iterations=max_iterations,
|
|
67
|
+
last_stdout=last_stdout,
|
|
68
|
+
last_stderr=last_stderr,
|
|
69
|
+
fixes_applied=fixes,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _apply_fix_instruction(sandbox: Sandbox, instruction: str) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
Try to parse a fix instruction (e.g. "FILE: path\\nCONTENT: ...") and apply via sandbox.
|
|
76
|
+
Returns True if something was applied.
|
|
77
|
+
"""
|
|
78
|
+
lines = instruction.strip().split("\n")
|
|
79
|
+
path = None
|
|
80
|
+
content_lines: list[str] = []
|
|
81
|
+
in_content = False
|
|
82
|
+
for line in lines:
|
|
83
|
+
if line.upper().startswith("FILE:") or line.upper().startswith("PATH:"):
|
|
84
|
+
path = line.split(":", 1)[1].strip()
|
|
85
|
+
in_content = False
|
|
86
|
+
elif line.upper().startswith("CONTENT:") or line.upper().startswith("CODE:"):
|
|
87
|
+
in_content = True
|
|
88
|
+
content_lines.append(line.split(":", 1)[1] if ":" in line else "")
|
|
89
|
+
elif in_content and path:
|
|
90
|
+
content_lines.append(line)
|
|
91
|
+
if path and content_lines:
|
|
92
|
+
content = "\n".join(content_lines)
|
|
93
|
+
r = sandbox.write_file(path, content)
|
|
94
|
+
return r.success
|
|
95
|
+
return False
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Codebase index for dev agents: AST parsing, dependency graph, symbol search.
|
|
3
|
+
|
|
4
|
+
Reuses patterns from devsper.tools.code_intelligence; provides a single API
|
|
5
|
+
for agents to understand the generated codebase.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class SymbolInfo:
|
|
15
|
+
"""A top-level symbol (function or class) in a file."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
kind: str # "function" or "class"
|
|
19
|
+
file: str
|
|
20
|
+
line: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class DependencyEdge:
|
|
25
|
+
"""Module A depends on module B (import)."""
|
|
26
|
+
|
|
27
|
+
from_module: str
|
|
28
|
+
to_module: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class RepoIndex:
|
|
33
|
+
"""
|
|
34
|
+
Index of a repo: files, symbols, dependencies.
|
|
35
|
+
Built via AST parsing; supports dependency graph and symbol search.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
root: Path
|
|
39
|
+
files: list[str] = field(default_factory=list)
|
|
40
|
+
symbols: list[SymbolInfo] = field(default_factory=list)
|
|
41
|
+
edges: list[DependencyEdge] = field(default_factory=list)
|
|
42
|
+
docstrings: dict[str, str] = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
def _rel_path(self, p: Path) -> str:
|
|
45
|
+
return str(p.relative_to(self.root)).replace("\\", "/")
|
|
46
|
+
|
|
47
|
+
def _imports(self, p: Path) -> list[str]:
|
|
48
|
+
out: list[str] = []
|
|
49
|
+
try:
|
|
50
|
+
tree = ast.parse(p.read_text(encoding="utf-8", errors="replace"))
|
|
51
|
+
for node in ast.walk(tree):
|
|
52
|
+
if isinstance(node, ast.Import):
|
|
53
|
+
for alias in node.names:
|
|
54
|
+
out.append(alias.name.split(".")[0])
|
|
55
|
+
elif isinstance(node, ast.ImportFrom):
|
|
56
|
+
if node.module:
|
|
57
|
+
out.append(node.module.split(".")[0])
|
|
58
|
+
except (SyntaxError, OSError):
|
|
59
|
+
pass
|
|
60
|
+
return list(dict.fromkeys(out))
|
|
61
|
+
|
|
62
|
+
def _extract_symbols(self, p: Path) -> list[SymbolInfo]:
|
|
63
|
+
syms: list[SymbolInfo] = []
|
|
64
|
+
try:
|
|
65
|
+
tree = ast.parse(p.read_text(encoding="utf-8", errors="replace"))
|
|
66
|
+
doc = ast.get_docstring(tree)
|
|
67
|
+
if doc:
|
|
68
|
+
self.docstrings[self._rel_path(p)] = doc[:500]
|
|
69
|
+
for node in ast.walk(tree):
|
|
70
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
71
|
+
if node.col_offset == 0:
|
|
72
|
+
kind = "class" if isinstance(node, ast.ClassDef) else "function"
|
|
73
|
+
syms.append(
|
|
74
|
+
SymbolInfo(
|
|
75
|
+
name=node.name,
|
|
76
|
+
kind=kind,
|
|
77
|
+
file=self._rel_path(p),
|
|
78
|
+
line=node.lineno or 0,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
except (SyntaxError, OSError):
|
|
82
|
+
pass
|
|
83
|
+
return syms
|
|
84
|
+
|
|
85
|
+
def build(self, max_files: int = 200) -> None:
|
|
86
|
+
"""Build index from repo root: parse AST, collect symbols and dependencies."""
|
|
87
|
+
self.files = []
|
|
88
|
+
self.symbols = []
|
|
89
|
+
self.edges = []
|
|
90
|
+
self.docstrings = {}
|
|
91
|
+
count = 0
|
|
92
|
+
for p in self.root.rglob("*.py"):
|
|
93
|
+
if count >= max_files:
|
|
94
|
+
break
|
|
95
|
+
if not p.is_file() or "__pycache__" in p.parts:
|
|
96
|
+
continue
|
|
97
|
+
count += 1
|
|
98
|
+
rel = self._rel_path(p)
|
|
99
|
+
self.files.append(rel)
|
|
100
|
+
mod = rel.replace(".py", "").replace("/", ".")
|
|
101
|
+
for imp in self._imports(p):
|
|
102
|
+
self.edges.append(DependencyEdge(from_module=mod, to_module=imp))
|
|
103
|
+
self.symbols.extend(self._extract_symbols(p))
|
|
104
|
+
|
|
105
|
+
def dependency_graph(self) -> dict[str, list[str]]:
|
|
106
|
+
"""Return adjacency list: module -> list of imported modules."""
|
|
107
|
+
out: dict[str, list[str]] = {}
|
|
108
|
+
for e in self.edges:
|
|
109
|
+
out.setdefault(e.from_module, []).append(e.to_module)
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
def symbol_search(self, name: str) -> list[SymbolInfo]:
|
|
113
|
+
"""Find symbols whose name contains the given string."""
|
|
114
|
+
name_lower = name.lower()
|
|
115
|
+
return [s for s in self.symbols if name_lower in s.name.lower()]
|
|
116
|
+
|
|
117
|
+
def file_search(self, substring: str) -> list[str]:
|
|
118
|
+
"""Find files whose path contains the given string."""
|
|
119
|
+
sub = substring.lower()
|
|
120
|
+
return [f for f in self.files if sub in f.lower()]
|
|
121
|
+
|
|
122
|
+
def summary(self) -> str:
|
|
123
|
+
"""Human-readable summary for agents."""
|
|
124
|
+
lines = [
|
|
125
|
+
f"Root: {self.root}",
|
|
126
|
+
f"Files: {len(self.files)}",
|
|
127
|
+
f"Symbols: {len(self.symbols)}",
|
|
128
|
+
f"Dependencies: {len(self.edges)} edges",
|
|
129
|
+
"",
|
|
130
|
+
"Top-level files:",
|
|
131
|
+
]
|
|
132
|
+
for f in sorted(self.files)[:30]:
|
|
133
|
+
lines.append(f" {f}")
|
|
134
|
+
lines.append("")
|
|
135
|
+
lines.append("Symbols (sample):")
|
|
136
|
+
for s in self.symbols[:40]:
|
|
137
|
+
lines.append(f" {s.kind} {s.name} @ {s.file}:{s.line}")
|
|
138
|
+
return "\n".join(lines)
|
devsper/dev/sandbox.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code sandbox: write files, run shell commands, run tests, install dependencies.
|
|
3
|
+
|
|
4
|
+
Enforces timeout and resource limits for safe autonomous execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import shlex
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
try:
|
|
13
|
+
import resource
|
|
14
|
+
except ImportError:
|
|
15
|
+
resource = None # Windows has no resource module
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class SandboxResult:
|
|
21
|
+
"""Result of a sandboxed command or operation."""
|
|
22
|
+
|
|
23
|
+
success: bool
|
|
24
|
+
stdout: str = ""
|
|
25
|
+
stderr: str = ""
|
|
26
|
+
returncode: int = 0
|
|
27
|
+
timed_out: bool = False
|
|
28
|
+
error: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class SandboxLimits:
|
|
33
|
+
"""Resource limits for sandbox execution."""
|
|
34
|
+
|
|
35
|
+
timeout_seconds: int = 300
|
|
36
|
+
max_memory_mb: int | None = 100
|
|
37
|
+
max_cpu_seconds: int | None = 60 # soft CPU time
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Sandbox:
|
|
41
|
+
"""
|
|
42
|
+
Isolated execution environment with timeout and resource limits.
|
|
43
|
+
|
|
44
|
+
Capabilities: write files, run shell commands, run tests, install dependencies.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, root: str | Path, limits: SandboxLimits | None = None):
|
|
48
|
+
self.root = Path(root).resolve()
|
|
49
|
+
self.limits = limits or SandboxLimits()
|
|
50
|
+
|
|
51
|
+
def _cwd(self) -> Path:
|
|
52
|
+
if not self.root.exists():
|
|
53
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
return self.root
|
|
55
|
+
|
|
56
|
+
def write_file(self, path: str, content: str) -> SandboxResult:
|
|
57
|
+
"""Write content to a file under sandbox root. Creates parent dirs."""
|
|
58
|
+
try:
|
|
59
|
+
p = (self.root / path).resolve()
|
|
60
|
+
if not str(p).startswith(str(self.root)):
|
|
61
|
+
return SandboxResult(
|
|
62
|
+
success=False,
|
|
63
|
+
error="Path escapes sandbox root",
|
|
64
|
+
)
|
|
65
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
p.write_text(content, encoding="utf-8")
|
|
67
|
+
return SandboxResult(success=True, stdout=f"Wrote {len(content)} chars to {path}")
|
|
68
|
+
except Exception as e:
|
|
69
|
+
return SandboxResult(success=False, error=str(e))
|
|
70
|
+
|
|
71
|
+
def read_file(self, path: str) -> SandboxResult:
|
|
72
|
+
"""Read file content under sandbox root."""
|
|
73
|
+
try:
|
|
74
|
+
p = (self.root / path).resolve()
|
|
75
|
+
if not str(p).startswith(str(self.root)) or not p.is_file():
|
|
76
|
+
return SandboxResult(success=False, error=f"File not in sandbox or missing: {path}")
|
|
77
|
+
content = p.read_text(encoding="utf-8", errors="replace")
|
|
78
|
+
return SandboxResult(success=True, stdout=content)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
return SandboxResult(success=False, error=str(e))
|
|
81
|
+
|
|
82
|
+
def run(
|
|
83
|
+
self,
|
|
84
|
+
command: str | list[str],
|
|
85
|
+
cwd: str | Path | None = None,
|
|
86
|
+
env: dict | None = None,
|
|
87
|
+
timeout_seconds: int | None = None,
|
|
88
|
+
) -> SandboxResult:
|
|
89
|
+
"""
|
|
90
|
+
Run a shell command in the sandbox with timeout and optional resource limits.
|
|
91
|
+
"""
|
|
92
|
+
timeout = timeout_seconds or self.limits.timeout_seconds
|
|
93
|
+
work_dir = Path(cwd) if cwd else self._cwd()
|
|
94
|
+
if not work_dir.exists():
|
|
95
|
+
work_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
full_env = os.environ.copy()
|
|
97
|
+
if env:
|
|
98
|
+
full_env.update(env)
|
|
99
|
+
|
|
100
|
+
if isinstance(command, str):
|
|
101
|
+
cmd_list = shlex.split(command)
|
|
102
|
+
else:
|
|
103
|
+
cmd_list = list(command)
|
|
104
|
+
|
|
105
|
+
def _set_limits() -> None:
|
|
106
|
+
if resource is None:
|
|
107
|
+
return
|
|
108
|
+
if self.limits.max_memory_mb is not None:
|
|
109
|
+
try:
|
|
110
|
+
resource.setrlimit(
|
|
111
|
+
resource.RLIMIT_AS,
|
|
112
|
+
(self.limits.max_memory_mb * 1024 * 1024, -1),
|
|
113
|
+
)
|
|
114
|
+
except (ValueError, OSError):
|
|
115
|
+
pass
|
|
116
|
+
if self.limits.max_cpu_seconds is not None:
|
|
117
|
+
try:
|
|
118
|
+
resource.setrlimit(
|
|
119
|
+
resource.RLIMIT_CPU,
|
|
120
|
+
(self.limits.max_cpu_seconds, -1),
|
|
121
|
+
)
|
|
122
|
+
except (ValueError, OSError):
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
proc = subprocess.run(
|
|
127
|
+
cmd_list,
|
|
128
|
+
cwd=str(work_dir),
|
|
129
|
+
env=full_env,
|
|
130
|
+
capture_output=True,
|
|
131
|
+
text=True,
|
|
132
|
+
timeout=timeout,
|
|
133
|
+
preexec_fn=_set_limits if os.name != "nt" else None,
|
|
134
|
+
)
|
|
135
|
+
out = (proc.stdout or "") + (
|
|
136
|
+
"\n--- stderr ---\n" + (proc.stderr or "") if proc.stderr else ""
|
|
137
|
+
)
|
|
138
|
+
return SandboxResult(
|
|
139
|
+
success=proc.returncode == 0,
|
|
140
|
+
stdout=out.strip(),
|
|
141
|
+
stderr=proc.stderr or "",
|
|
142
|
+
returncode=proc.returncode,
|
|
143
|
+
)
|
|
144
|
+
except subprocess.TimeoutExpired:
|
|
145
|
+
return SandboxResult(
|
|
146
|
+
success=False,
|
|
147
|
+
timed_out=True,
|
|
148
|
+
error=f"Command timed out after {timeout}s",
|
|
149
|
+
)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
return SandboxResult(success=False, error=str(e))
|
|
152
|
+
|
|
153
|
+
def install_dependencies(self, backend: bool = True, frontend: bool = False) -> SandboxResult:
|
|
154
|
+
"""Install backend (pip) and optionally frontend (npm) dependencies."""
|
|
155
|
+
results: list[str] = []
|
|
156
|
+
if backend:
|
|
157
|
+
req = self.root / "backend" / "requirements.txt"
|
|
158
|
+
if not req.exists():
|
|
159
|
+
req = self.root / "requirements.txt"
|
|
160
|
+
if req.exists():
|
|
161
|
+
# Prefer uv when available (e.g. venvs without pip); else pip
|
|
162
|
+
r = self.run(
|
|
163
|
+
["uv", "pip", "install", "-r", str(req)],
|
|
164
|
+
timeout_seconds=120,
|
|
165
|
+
)
|
|
166
|
+
if not r.success and r.error:
|
|
167
|
+
python = os.environ.get("PYTHON") or sys.executable or "python3"
|
|
168
|
+
r = self.run(
|
|
169
|
+
[python, "-m", "pip", "install", "-r", str(req)],
|
|
170
|
+
timeout_seconds=120,
|
|
171
|
+
)
|
|
172
|
+
results.append(f"pip: success={r.success}" + (f" {r.error}" if r.error else ""))
|
|
173
|
+
if frontend:
|
|
174
|
+
pkg = self.root / "frontend" / "package.json"
|
|
175
|
+
if pkg.exists():
|
|
176
|
+
r = self.run("npm install", cwd=str(self.root / "frontend"), timeout_seconds=120)
|
|
177
|
+
results.append(f"npm: success={r.success}" + (f" {r.error}" if r.error else ""))
|
|
178
|
+
return SandboxResult(
|
|
179
|
+
success=all("success=True" in s for s in results) or not results,
|
|
180
|
+
stdout="\n".join(results) or "No dependency files found",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def run_tests(self, path: str = "tests") -> SandboxResult:
|
|
184
|
+
"""Run tests (pytest for backend, or path to test dir)."""
|
|
185
|
+
test_dir = self.root / path
|
|
186
|
+
if not test_dir.exists():
|
|
187
|
+
test_dir = self.root / "backend" / "tests"
|
|
188
|
+
if not test_dir.exists():
|
|
189
|
+
test_dir = self.root
|
|
190
|
+
env = os.environ.copy()
|
|
191
|
+
backend_dir = self.root / "backend"
|
|
192
|
+
if backend_dir.exists():
|
|
193
|
+
env["PYTHONPATH"] = str(backend_dir) + os.pathsep + env.get("PYTHONPATH", "")
|
|
194
|
+
python = os.environ.get("PYTHON") or sys.executable or "python3"
|
|
195
|
+
cmd = [
|
|
196
|
+
python,
|
|
197
|
+
"-m",
|
|
198
|
+
"pytest",
|
|
199
|
+
str(test_dir),
|
|
200
|
+
"-v",
|
|
201
|
+
"--tb=short",
|
|
202
|
+
]
|
|
203
|
+
return self.run(cmd, timeout_seconds=60, env=env)
|