NOT-OMEGA commited on
Commit
2b81bff
·
verified ·
1 Parent(s): 1448ed8

Delete backend

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. backend/__init__.py +0 -0
  2. backend/agents/__init__.py +0 -1
  3. backend/agents/code_agent.py +0 -95
  4. backend/agents/diagnosis_agent.py +0 -81
  5. backend/agents/planner.py +0 -91
  6. backend/agents/repair_agent.py +0 -101
  7. backend/agents/task_handlers.py +0 -101
  8. backend/agents/test_agent.py +0 -94
  9. backend/analysis/__init__.py +0 -1
  10. backend/analysis/static_analyzer.py +0 -110
  11. backend/api/__init__.py +0 -0
  12. backend/api/auth.py +0 -70
  13. backend/api/providers.py +0 -36
  14. backend/api/schemas.py +0 -146
  15. backend/api/settings.py +0 -73
  16. backend/api/workflows.py +0 -370
  17. backend/core/__init__.py +0 -0
  18. backend/core/config.py +0 -59
  19. backend/core/dependencies.py +0 -50
  20. backend/core/encryption.py +0 -30
  21. backend/core/security.py +0 -36
  22. backend/db/__init__.py +0 -0
  23. backend/db/migrations/env.py +0 -52
  24. backend/db/migrations/versions/001_initial_schema.py +0 -138
  25. backend/db/migrations/versions/__init__.py +0 -1
  26. backend/db/models/__init__.py +0 -150
  27. backend/db/session.py +0 -50
  28. backend/events/__init__.py +0 -1
  29. backend/events/publisher.py +0 -40
  30. backend/execution/__init__.py +0 -1
  31. backend/execution/docker_runner.py +0 -265
  32. backend/execution/result_parser.py +0 -40
  33. backend/execution/sandbox.py +0 -38
  34. backend/execution/verification.py +0 -79
  35. backend/llm/__init__.py +0 -0
  36. backend/llm/base.py +0 -48
  37. backend/llm/providers/__init__.py +0 -20
  38. backend/llm/providers/gemini_provider.py +0 -98
  39. backend/llm/providers/mock_provider.py +0 -56
  40. backend/llm/providers/openai_provider.py +0 -99
  41. backend/llm/types.py +0 -21
  42. backend/main.py +0 -98
  43. backend/memory/__init__.py +0 -0
  44. backend/memory/attempt_repository.py +0 -63
  45. backend/memory/task_repository.py +0 -99
  46. backend/memory/workflow_repository.py +0 -84
  47. backend/orchestration/__init__.py +0 -1
  48. backend/orchestration/dag_engine.py +0 -174
  49. backend/orchestration/dag_schema.py +0 -42
  50. backend/orchestration/dag_validator.py +0 -85
backend/__init__.py DELETED
File without changes
backend/agents/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Agent modules — planner and task handlers."""
 
 
backend/agents/code_agent.py DELETED
@@ -1,95 +0,0 @@
1
- from typing import Any
2
-
3
- from pydantic import BaseModel, Field
4
-
5
-
6
- class GeneratedFile(BaseModel):
7
- path: str
8
- content: str
9
- language: str = "python"
10
-
11
-
12
- class CodeGenerationResult(BaseModel):
13
- files: dict[str, dict[str, str]]
14
- status: str = "generated"
15
-
16
- @classmethod
17
- def from_llm_payload(cls, payload: dict[str, Any]) -> "CodeGenerationResult":
18
- files: dict[str, dict[str, str]] = {}
19
- for item in payload.get("files", []):
20
- path = item["path"].lstrip("/").replace("\\", "/")
21
- files[path] = {
22
- "content": item["content"],
23
- "language": item.get("language", "python"),
24
- }
25
- if not files:
26
- raise ValueError("Code generation produced no files")
27
- return cls(files=files)
28
-
29
-
30
- CODE_GENERATION_SCHEMA: dict[str, Any] = {
31
- "type": "object",
32
- "properties": {
33
- "files": {
34
- "type": "array",
35
- "items": {
36
- "type": "object",
37
- "properties": {
38
- "path": {"type": "string"},
39
- "content": {"type": "string"},
40
- "language": {"type": "string"},
41
- },
42
- "required": ["path", "content"],
43
- },
44
- }
45
- },
46
- "required": ["files"],
47
- }
48
-
49
- CODE_GENERATION_SYSTEM_PROMPT = """You are a code generation agent.
50
- Generate a complete, runnable Python project for the user request.
51
- Return JSON with a files array. Each file must include path, content, and language.
52
-
53
- Requirements:
54
- - Use a flat or package layout that pytest can import (e.g. calculator.py or calculator/__init__.py).
55
- - Include requirements.txt with only third-party packages (pytest is pre-installed in the sandbox).
56
- - For stdlib-only projects, requirements.txt may be empty or contain only comments.
57
- - Do not include tests — a separate agent will generate them.
58
- Return only valid JSON."""
59
-
60
-
61
- from backend.llm.base import LLMProvider
62
- from backend.orchestration.errors import TaskExecutionError
63
- from backend.orchestration.task_context import TaskContext
64
-
65
-
66
- class CodeGenerationAgent:
67
- def __init__(self, llm: LLMProvider) -> None:
68
- self.llm = llm
69
-
70
- async def generate(self, context: TaskContext) -> dict[str, Any]:
71
- prompt = (
72
- f"User request:\n{context.user_prompt}\n\n"
73
- "Generate the implementation files for this project."
74
- )
75
- try:
76
- structured = await self.llm.generate_structured(
77
- prompt,
78
- schema=CODE_GENERATION_SCHEMA,
79
- system_prompt=CODE_GENERATION_SYSTEM_PROMPT,
80
- temperature=0.2,
81
- )
82
- result = CodeGenerationResult.from_llm_payload(structured)
83
- return result.model_dump()
84
- except ValueError as exc:
85
- raise TaskExecutionError(str(exc), error_type="CODE_GENERATION_ERROR") from exc
86
- except Exception as exc:
87
- raise TaskExecutionError(
88
- f"Code generation failed: {exc}",
89
- error_type="CODE_GENERATION_ERROR",
90
- ) from exc
91
-
92
-
93
- async def run_code_generation(context: TaskContext) -> dict[str, Any]:
94
- agent = CodeGenerationAgent(context.llm)
95
- return await agent.generate(context)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/agents/diagnosis_agent.py DELETED
@@ -1,81 +0,0 @@
1
- from typing import Any
2
-
3
- from pydantic import BaseModel
4
-
5
- from backend.llm.base import LLMProvider
6
- from backend.orchestration.failure_classifier import ClassifiedFailure
7
- from backend.orchestration.task_context import TaskContext
8
-
9
-
10
- class DiagnosisResult(BaseModel):
11
- error_type: str
12
- root_cause: str
13
- recommended_fix: str
14
- affected_files: list[str] = []
15
-
16
- def to_dict(self) -> dict[str, Any]:
17
- return self.model_dump()
18
-
19
-
20
- DIAGNOSIS_SCHEMA: dict[str, Any] = {
21
- "type": "object",
22
- "properties": {
23
- "error_type": {"type": "string"},
24
- "root_cause": {"type": "string"},
25
- "recommended_fix": {"type": "string"},
26
- "affected_files": {"type": "array", "items": {"type": "string"}},
27
- },
28
- "required": ["error_type", "root_cause", "recommended_fix", "affected_files"],
29
- }
30
-
31
- DIAGNOSIS_SYSTEM_PROMPT = """You are a failure diagnosis agent.
32
- Analyze the workflow failure context and return structured JSON with root cause and fix recommendation.
33
- Return only valid JSON."""
34
-
35
-
36
- class DiagnosisAgent:
37
- def __init__(self, llm: LLMProvider) -> None:
38
- self.llm = llm
39
-
40
- async def diagnose(
41
- self,
42
- *,
43
- user_prompt: str,
44
- failure: ClassifiedFailure,
45
- code_files: dict[str, dict[str, str]],
46
- test_files: dict[str, dict[str, str]],
47
- stdout: str = "",
48
- stderr: str = "",
49
- previous_attempts: list[dict[str, Any]] | None = None,
50
- ) -> DiagnosisResult:
51
- code_listing = "\n".join(
52
- f"--- {path} ---\n{meta['content']}" for path, meta in code_files.items()
53
- )
54
- test_listing = "\n".join(
55
- f"--- {path} ---\n{meta['content']}" for path, meta in test_files.items()
56
- )
57
- prompt = (
58
- f"User request:\n{user_prompt}\n\n"
59
- f"Failure category: {failure.category.value}\n"
60
- f"Failure message: {failure.message}\n"
61
- f"stdout:\n{stdout}\n\nstderr:\n{stderr}\n\n"
62
- f"Code files:\n{code_listing}\n\nTest files:\n{test_listing}\n\n"
63
- f"Previous attempts: {len(previous_attempts or [])}\n"
64
- "Diagnose the failure."
65
- )
66
- structured = await self.llm.generate_structured(
67
- prompt,
68
- schema=DIAGNOSIS_SCHEMA,
69
- system_prompt=DIAGNOSIS_SYSTEM_PROMPT,
70
- temperature=0.0,
71
- )
72
- return DiagnosisResult.model_validate(structured)
73
-
74
-
75
- async def stub_diagnose(failure: ClassifiedFailure) -> DiagnosisResult:
76
- return DiagnosisResult(
77
- error_type=failure.category.value,
78
- root_cause=f"Detected {failure.category.value}: {failure.message}",
79
- recommended_fix="Apply automated repair to resolve the reported failure.",
80
- affected_files=[],
81
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/agents/planner.py DELETED
@@ -1,91 +0,0 @@
1
- import json
2
- from typing import Any
3
-
4
- from backend.llm.base import LLMProvider
5
- from backend.orchestration.dag_schema import DAGPlan, DAGTaskDefinition, TaskType
6
- from backend.orchestration.errors import PlannerError
7
-
8
- PLANNER_SYSTEM_PROMPT = """You are a software workflow planner.
9
- Given a user coding request, produce a dependency-aware execution DAG as JSON.
10
-
11
- Rules:
12
- - Return ONLY valid JSON matching the schema.
13
- - Use unique task ids (short labels like A, B, C or descriptive ids).
14
- - Task types must be one of: code_generation, test_generation, static_analysis, execute.
15
- - Typical flow: code_generation first, then test_generation and static_analysis in parallel, then execute depending on both.
16
- - depends_on must list task ids that must complete before this task runs.
17
- - Do not create cycles.
18
- """
19
-
20
- PLANNER_OUTPUT_SCHEMA: dict[str, Any] = {
21
- "type": "object",
22
- "properties": {
23
- "tasks": {
24
- "type": "array",
25
- "items": {
26
- "type": "object",
27
- "properties": {
28
- "id": {"type": "string"},
29
- "type": {
30
- "type": "string",
31
- "enum": [t.value for t in TaskType],
32
- },
33
- "depends_on": {
34
- "type": "array",
35
- "items": {"type": "string"},
36
- },
37
- },
38
- "required": ["id", "type", "depends_on"],
39
- },
40
- }
41
- },
42
- "required": ["tasks"],
43
- }
44
-
45
-
46
- class PlannerAgent:
47
- """Generates a structured DAG plan from a user prompt."""
48
-
49
- def __init__(self, llm: LLMProvider) -> None:
50
- self.llm = llm
51
-
52
- async def create_plan(self, user_prompt: str, *, context: str | None = None) -> DAGPlan:
53
- prompt = user_prompt
54
- if context:
55
- prompt = f"Relevant context:\n{context}\n\nUser request:\n{user_prompt}"
56
-
57
- try:
58
- structured = await self.llm.generate_structured(
59
- prompt,
60
- schema=PLANNER_OUTPUT_SCHEMA,
61
- system_prompt=PLANNER_SYSTEM_PROMPT,
62
- temperature=0.0,
63
- )
64
- return self.parse_plan(structured)
65
- except PlannerError:
66
- raise
67
- except Exception as exc:
68
- raise PlannerError(f"Planner LLM call failed: {exc}") from exc
69
-
70
- @staticmethod
71
- def parse_plan(data: dict[str, Any]) -> DAGPlan:
72
- try:
73
- tasks = [
74
- DAGTaskDefinition(
75
- id=task["id"],
76
- type=TaskType(task["type"]),
77
- depends_on=task.get("depends_on", []),
78
- )
79
- for task in data["tasks"]
80
- ]
81
- return DAGPlan(tasks=tasks)
82
- except (KeyError, TypeError, ValueError) as exc:
83
- raise PlannerError(f"Invalid planner output structure: {exc}") from exc
84
-
85
- @staticmethod
86
- def parse_plan_json(raw_json: str) -> DAGPlan:
87
- try:
88
- data = json.loads(raw_json)
89
- except json.JSONDecodeError as exc:
90
- raise PlannerError(f"Planner output is not valid JSON: {exc}") from exc
91
- return PlannerAgent.parse_plan(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/agents/repair_agent.py DELETED
@@ -1,101 +0,0 @@
1
- from typing import Any
2
-
3
- from pydantic import BaseModel
4
-
5
- from backend.agents.code_agent import CodeGenerationResult
6
- from backend.llm.base import LLMProvider
7
- from backend.agents.diagnosis_agent import DiagnosisResult
8
-
9
-
10
- class RepairResult(BaseModel):
11
- files: dict[str, dict[str, str]]
12
- repair_summary: str
13
- status: str = "repaired"
14
-
15
- def to_dict(self) -> dict[str, Any]:
16
- return {
17
- "files": self.files,
18
- "repair_summary": self.repair_summary,
19
- "status": self.status,
20
- }
21
-
22
-
23
- REPAIR_SCHEMA: dict[str, Any] = {
24
- "type": "object",
25
- "properties": {
26
- "files": {
27
- "type": "array",
28
- "items": {
29
- "type": "object",
30
- "properties": {
31
- "path": {"type": "string"},
32
- "content": {"type": "string"},
33
- "language": {"type": "string"},
34
- },
35
- "required": ["path", "content"],
36
- },
37
- },
38
- "repair_summary": {"type": "string"},
39
- },
40
- "required": ["files", "repair_summary"],
41
- }
42
-
43
- REPAIR_SYSTEM_PROMPT = """You are a code repair agent.
44
- Given the diagnosis and current project files, produce repaired files as JSON.
45
- Return only valid JSON with files array and repair_summary."""
46
-
47
-
48
- class RepairAgent:
49
- def __init__(self, llm: LLMProvider) -> None:
50
- self.llm = llm
51
-
52
- async def repair(
53
- self,
54
- *,
55
- user_prompt: str,
56
- diagnosis: DiagnosisResult,
57
- code_files: dict[str, dict[str, str]],
58
- test_files: dict[str, dict[str, str]],
59
- ) -> RepairResult:
60
- all_files = {**code_files, **test_files}
61
- listing = "\n".join(
62
- f"--- {path} ---\n{meta['content']}" for path, meta in all_files.items()
63
- )
64
- prompt = (
65
- f"User request:\n{user_prompt}\n\n"
66
- f"Diagnosis:\n{diagnosis.model_dump_json()}\n\n"
67
- f"Current files:\n{listing}\n\n"
68
- "Produce repaired project files."
69
- )
70
- structured = await self.llm.generate_structured(
71
- prompt,
72
- schema=REPAIR_SCHEMA,
73
- system_prompt=REPAIR_SYSTEM_PROMPT,
74
- temperature=0.1,
75
- )
76
- generated = CodeGenerationResult.from_llm_payload(structured)
77
- return RepairResult(
78
- files=generated.files,
79
- repair_summary=structured.get("repair_summary", "Repaired project files"),
80
- )
81
-
82
-
83
- def stub_repair(
84
- code_files: dict[str, dict[str, str]],
85
- test_files: dict[str, dict[str, str]],
86
- *,
87
- fix_code: bool = True,
88
- ) -> RepairResult:
89
- """Stub repair that fixes a common broken add() implementation for tests."""
90
- files = {**code_files, **test_files}
91
- if fix_code and "main.py" in files:
92
- content = files["main.py"]["content"]
93
- if "return a - b" in content or "return a+b-1" in content:
94
- files["main.py"] = {
95
- **files["main.py"],
96
- "content": "def add(a, b):\n return a + b\n",
97
- }
98
- return RepairResult(
99
- files=files,
100
- repair_summary="Stub repair applied",
101
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/agents/task_handlers.py DELETED
@@ -1,101 +0,0 @@
1
- import asyncio
2
- from collections.abc import Awaitable, Callable
3
- from typing import Any
4
-
5
- from backend.agents.code_agent import run_code_generation
6
- from backend.agents.test_agent import run_test_generation
7
- from backend.analysis.static_analyzer import StaticAnalyzer
8
- from backend.execution.verification import run_verification
9
- from backend.orchestration.dag_schema import TaskType
10
- from backend.orchestration.errors import TaskExecutionError
11
- from backend.orchestration.task_context import TaskContext
12
-
13
- TaskHandler = Callable[[TaskContext], Awaitable[dict[str, Any]]]
14
-
15
- STUB_CODE_FILES = {
16
- "main.py": {
17
- "content": "def add(a, b):\n return a - b\n",
18
- "language": "python",
19
- },
20
- "requirements.txt": {"content": "pytest\n", "language": "text"},
21
- }
22
-
23
- STUB_TEST_FILES = {
24
- "tests/test_main.py": {
25
- "content": "from main import add\n\ndef test_add():\n assert add(1, 2) == 3\n",
26
- "language": "python",
27
- }
28
- }
29
-
30
-
31
- async def _handle_code_generation(context: TaskContext) -> dict[str, Any]:
32
- if context.use_stubs:
33
- return await _stub_handler(context, TaskType.CODE_GENERATION)
34
- return await run_code_generation(context)
35
-
36
-
37
- async def _handle_test_generation(context: TaskContext) -> dict[str, Any]:
38
- if context.use_stubs:
39
- return await _stub_handler(context, TaskType.TEST_GENERATION)
40
- return await run_test_generation(context)
41
-
42
-
43
- async def _handle_static_analysis(context: TaskContext) -> dict[str, Any]:
44
- if context.use_stubs:
45
- return await _stub_handler(context, TaskType.STATIC_ANALYSIS)
46
- files = context.generated_files
47
- if not files:
48
- raise TaskExecutionError(
49
- "No generated code available for static analysis",
50
- error_type="STATIC_ANALYSIS_ERROR",
51
- )
52
- analyzer = StaticAnalyzer()
53
- result = analyzer.analyze_files(files)
54
- if result.status == "FAIL":
55
- raise TaskExecutionError(
56
- "Static analysis failed",
57
- error_type="STATIC_ANALYSIS_ERROR",
58
- )
59
- return result.to_dict()
60
-
61
-
62
- async def _handle_execute(context: TaskContext) -> dict[str, Any]:
63
- if context.use_stubs and not context.stub_execute_calls_docker:
64
- return await _stub_handler(context, TaskType.EXECUTE)
65
- return await run_verification(context)
66
-
67
-
68
- async def _stub_handler(context: TaskContext, task_type: TaskType) -> dict[str, Any]:
69
- await asyncio.sleep(context.stub_delay)
70
- if context.fail_task == task_type.value:
71
- raise TaskExecutionError(
72
- f"{task_type.value} failed",
73
- error_type=f"{task_type.value.upper()}_ERROR",
74
- )
75
- if task_type == TaskType.CODE_GENERATION:
76
- return {"status": "generated", "files": dict(STUB_CODE_FILES)}
77
- if task_type == TaskType.TEST_GENERATION:
78
- return {"status": "generated", "files": dict(STUB_TEST_FILES)}
79
- if task_type == TaskType.STATIC_ANALYSIS:
80
- return {"status": "PASS", "errors": [], "warnings": []}
81
- if task_type == TaskType.EXECUTE:
82
- return {"status": "PASS", "stdout": "", "stderr": "", "exit_code": 0}
83
- return {"status": "planned", "files": {}, "task_type": task_type.value}
84
-
85
-
86
- TASK_HANDLERS: dict[TaskType, TaskHandler] = {
87
- TaskType.CODE_GENERATION: _handle_code_generation,
88
- TaskType.TEST_GENERATION: _handle_test_generation,
89
- TaskType.STATIC_ANALYSIS: _handle_static_analysis,
90
- TaskType.EXECUTE: _handle_execute,
91
- }
92
-
93
-
94
- def get_task_handler(task_type: TaskType) -> TaskHandler:
95
- handler = TASK_HANDLERS.get(task_type)
96
- if handler is None:
97
- raise TaskExecutionError(
98
- f"No handler registered for task type '{task_type}'",
99
- error_type="TASK_EXECUTION_ERROR",
100
- )
101
- return handler
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/agents/test_agent.py DELETED
@@ -1,94 +0,0 @@
1
- from typing import Any
2
-
3
- from pydantic import BaseModel
4
-
5
- from backend.llm.base import LLMProvider
6
- from backend.orchestration.errors import TaskExecutionError
7
- from backend.orchestration.task_context import TaskContext
8
-
9
-
10
- class TestGenerationResult(BaseModel):
11
- files: dict[str, dict[str, str]]
12
- status: str = "generated"
13
-
14
- @classmethod
15
- def from_llm_payload(cls, payload: dict[str, Any]) -> "TestGenerationResult":
16
- files: dict[str, dict[str, str]] = {}
17
- for item in payload.get("files", []):
18
- path = item["path"].lstrip("/").replace("\\", "/")
19
- files[path] = {
20
- "content": item["content"],
21
- "language": item.get("language", "python"),
22
- }
23
- if not files:
24
- raise ValueError("Test generation produced no files")
25
- return cls(files=files)
26
-
27
-
28
- TEST_GENERATION_SCHEMA: dict[str, Any] = {
29
- "type": "object",
30
- "properties": {
31
- "files": {
32
- "type": "array",
33
- "items": {
34
- "type": "object",
35
- "properties": {
36
- "path": {"type": "string"},
37
- "content": {"type": "string"},
38
- "language": {"type": "string"},
39
- },
40
- "required": ["path", "content"],
41
- },
42
- }
43
- },
44
- "required": ["files"],
45
- }
46
-
47
- TEST_GENERATION_SYSTEM_PROMPT = """You are a test generation agent.
48
- Given generated source code, produce pytest test files.
49
- Return JSON with a files array (paths under tests/).
50
- Do not modify implementation files.
51
- Return only valid JSON."""
52
-
53
-
54
- class TestGenerationAgent:
55
- def __init__(self, llm: LLMProvider) -> None:
56
- self.llm = llm
57
-
58
- async def generate(self, context: TaskContext) -> dict[str, Any]:
59
- code_files = context.generated_files
60
- if not code_files:
61
- raise TaskExecutionError(
62
- "No generated code available for test generation",
63
- error_type="TEST_GENERATION_ERROR",
64
- )
65
-
66
- file_listing = "\n\n".join(
67
- f"--- {path} ---\n{meta['content']}" for path, meta in code_files.items()
68
- )
69
- prompt = (
70
- f"User request:\n{context.user_prompt}\n\n"
71
- f"Generated source files:\n{file_listing}\n\n"
72
- "Generate pytest tests for the implementation."
73
- )
74
- try:
75
- structured = await self.llm.generate_structured(
76
- prompt,
77
- schema=TEST_GENERATION_SCHEMA,
78
- system_prompt=TEST_GENERATION_SYSTEM_PROMPT,
79
- temperature=0.2,
80
- )
81
- result = TestGenerationResult.from_llm_payload(structured)
82
- return result.model_dump()
83
- except ValueError as exc:
84
- raise TaskExecutionError(str(exc), error_type="TEST_GENERATION_ERROR") from exc
85
- except Exception as exc:
86
- raise TaskExecutionError(
87
- f"Test generation failed: {exc}",
88
- error_type="TEST_GENERATION_ERROR",
89
- ) from exc
90
-
91
-
92
- async def run_test_generation(context: TaskContext) -> dict[str, Any]:
93
- agent = TestGenerationAgent(context.llm)
94
- return await agent.generate(context)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/analysis/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Static analysis — implemented in Phase 2+."""
 
 
backend/analysis/static_analyzer.py DELETED
@@ -1,110 +0,0 @@
1
- import ast
2
- import re
3
- from dataclasses import dataclass, field
4
- from pathlib import Path
5
- from typing import Any
6
-
7
- from backend.orchestration.errors import TaskExecutionError
8
- from backend.orchestration.task_context import TaskContext
9
-
10
-
11
- @dataclass
12
- class AnalysisIssue:
13
- file: str
14
- line: int
15
- message: str
16
-
17
-
18
- @dataclass
19
- class StaticAnalysisResult:
20
- status: str
21
- errors: list[dict[str, Any]] = field(default_factory=list)
22
- warnings: list[dict[str, Any]] = field(default_factory=list)
23
-
24
- def to_dict(self) -> dict[str, Any]:
25
- return {
26
- "status": self.status,
27
- "errors": self.errors,
28
- "warnings": self.warnings,
29
- }
30
-
31
-
32
- class StaticAnalyzer:
33
- """Analyze generated Python files without executing them."""
34
-
35
- def analyze_files(self, files: dict[str, dict[str, str]]) -> StaticAnalysisResult:
36
- errors: list[dict[str, Any]] = []
37
- warnings: list[dict[str, Any]] = []
38
-
39
- python_files = {
40
- path: meta["content"]
41
- for path, meta in files.items()
42
- if path.endswith(".py")
43
- }
44
-
45
- if not python_files:
46
- errors.append(
47
- {"file": "", "line": 0, "message": "No Python source files found to analyze"}
48
- )
49
- return StaticAnalysisResult(status="FAIL", errors=errors, warnings=warnings)
50
-
51
- for path, content in python_files.items():
52
- syntax_issues = self._check_syntax(path, content)
53
- errors.extend(syntax_issues)
54
- warnings.extend(self._check_imports(path, content))
55
-
56
- status = "FAIL" if errors else "PASS"
57
- return StaticAnalysisResult(status=status, errors=errors, warnings=warnings)
58
-
59
- def _check_syntax(self, path: str, content: str) -> list[dict[str, Any]]:
60
- try:
61
- ast.parse(content, filename=path)
62
- return []
63
- except SyntaxError as exc:
64
- return [
65
- {
66
- "file": path,
67
- "line": exc.lineno or 0,
68
- "message": f"Syntax error: {exc.msg}",
69
- }
70
- ]
71
-
72
- def _check_imports(self, path: str, content: str) -> list[dict[str, Any]]:
73
- warnings: list[dict[str, Any]] = []
74
- for line_no, line in enumerate(content.splitlines(), start=1):
75
- stripped = line.strip()
76
- if stripped.startswith("import ") or stripped.startswith("from "):
77
- if re.search(r"\b(os\.system|subprocess\.|eval\(|exec\()", stripped):
78
- warnings.append(
79
- {
80
- "file": path,
81
- "line": line_no,
82
- "message": "Suspicious import or call pattern detected",
83
- }
84
- )
85
- return warnings
86
-
87
-
88
- async def run_static_analysis(context: TaskContext) -> dict[str, Any]:
89
- files = context.all_project_files
90
- if not files:
91
- raise TaskExecutionError(
92
- "No generated files available for static analysis",
93
- error_type="STATIC_ANALYSIS_ERROR",
94
- )
95
- analyzer = StaticAnalyzer()
96
- result = analyzer.analyze_files(files)
97
- if result.status == "FAIL":
98
- raise TaskExecutionError(
99
- "Static analysis failed",
100
- error_type="STATIC_ANALYSIS_ERROR",
101
- )
102
- return result.to_dict()
103
-
104
-
105
- def analyze_workspace(workspace: Path) -> StaticAnalysisResult:
106
- files: dict[str, dict[str, str]] = {}
107
- for path in workspace.rglob("*.py"):
108
- relative = path.relative_to(workspace).as_posix()
109
- files[relative] = {"content": path.read_text(encoding="utf-8"), "language": "python"}
110
- return StaticAnalyzer().analyze_files(files)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/api/__init__.py DELETED
File without changes
backend/api/auth.py DELETED
@@ -1,70 +0,0 @@
1
- from fastapi import APIRouter, Depends, HTTPException, status
2
-
3
- from backend.api.schemas import LoginRequest, SignupRequest, TokenResponse, UserResponse
4
- from backend.core.dependencies import get_current_user, get_repository
5
- from backend.core.security import create_access_token, hash_password, verify_password
6
- from backend.db.models import User
7
- from backend.memory.workflow_repository import WorkflowRepository
8
-
9
- router = APIRouter(prefix="/auth", tags=["auth"])
10
-
11
-
12
- @router.post("/signup", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
13
- async def signup(
14
- payload: SignupRequest,
15
- repo: WorkflowRepository = Depends(get_repository),
16
- ) -> User:
17
- if await repo.get_user_by_email(payload.email):
18
- raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Email already registered")
19
- if await repo.get_user_by_username(payload.username):
20
- raise HTTPException(
21
- status_code=status.HTTP_409_CONFLICT, detail="Username already taken"
22
- )
23
-
24
- user = await repo.create_user(
25
- email=payload.email,
26
- username=payload.username,
27
- password_hash=hash_password(payload.password),
28
- )
29
- return user
30
-
31
-
32
- @router.post("/login", response_model=TokenResponse)
33
- async def login(
34
- payload: LoginRequest,
35
- repo: WorkflowRepository = Depends(get_repository),
36
- ) -> TokenResponse:
37
- user = await repo.get_user_by_email(payload.email)
38
- if user is None or not verify_password(payload.password, user.password_hash):
39
- raise HTTPException(
40
- status_code=status.HTTP_401_UNAUTHORIZED,
41
- detail="Invalid email or password",
42
- )
43
-
44
- token = create_access_token(str(user.id))
45
- return TokenResponse(access_token=token)
46
-
47
-
48
- @router.post("/guest", response_model=TokenResponse)
49
- async def guest_login(
50
- repo: WorkflowRepository = Depends(get_repository),
51
- ) -> TokenResponse:
52
- import uuid
53
- guest_id = str(uuid.uuid4())
54
- guest_email = f"guest-{guest_id}@autoforge.local"
55
- guest_username = f"Guest_{guest_id[:8]}"
56
-
57
- # Auto-create the guest user with a random password
58
- user = await repo.create_user(
59
- email=guest_email,
60
- username=guest_username,
61
- password_hash=hash_password(guest_id),
62
- )
63
-
64
- token = create_access_token(str(user.id))
65
- return TokenResponse(access_token=token)
66
-
67
-
68
- @router.get("/me", response_model=UserResponse)
69
- async def me(current_user: User = Depends(get_current_user)) -> User:
70
- return current_user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/api/providers.py DELETED
@@ -1,36 +0,0 @@
1
- from fastapi import APIRouter
2
-
3
- from backend.api.schemas import ProviderInfo, ProviderListResponse
4
- from backend.llm.providers import list_supported_providers
5
-
6
- router = APIRouter(prefix="/settings", tags=["settings"])
7
-
8
- OPENAI_MODELS = [
9
- "gpt-4o",
10
- "gpt-4o-mini",
11
- "gpt-4.1",
12
- "gpt-4.1-mini",
13
- ]
14
-
15
- GEMINI_MODELS = [
16
- "gemini-3.1-pro",
17
- "gemini-3.1-flash",
18
- ]
19
-
20
- PROVIDER_MODELS: dict[str, list[str]] = {
21
- "openai": OPENAI_MODELS,
22
- "gemini": GEMINI_MODELS,
23
- }
24
-
25
-
26
- @router.get("/providers", response_model=ProviderListResponse)
27
- async def list_providers() -> ProviderListResponse:
28
- providers = [
29
- ProviderInfo(
30
- id=provider_id,
31
- name=provider_id.replace("_", " ").title(),
32
- models=PROVIDER_MODELS.get(provider_id, []),
33
- )
34
- for provider_id in list_supported_providers()
35
- ]
36
- return ProviderListResponse(providers=providers)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/api/schemas.py DELETED
@@ -1,146 +0,0 @@
1
- from datetime import datetime
2
-
3
- from pydantic import BaseModel, ConfigDict, EmailStr, Field
4
-
5
-
6
- class SignupRequest(BaseModel):
7
- email: EmailStr
8
- username: str = Field(min_length=3, max_length=100)
9
- password: str = Field(min_length=8, max_length=128)
10
-
11
-
12
- class LoginRequest(BaseModel):
13
- email: EmailStr
14
- password: str
15
-
16
-
17
- class TokenResponse(BaseModel):
18
- access_token: str
19
- token_type: str = "bearer"
20
-
21
-
22
- class UserResponse(BaseModel):
23
- model_config = ConfigDict(from_attributes=True)
24
-
25
- id: int
26
- email: str
27
- username: str
28
- created_at: datetime
29
-
30
-
31
- class WorkflowCreateRequest(BaseModel):
32
- user_prompt: str = Field(min_length=1)
33
- llm_provider: str = Field(min_length=1, max_length=64)
34
- llm_model: str = Field(min_length=1, max_length=128)
35
- api_key: str | None = Field(default=None, min_length=1)
36
-
37
-
38
- class WorkflowResponse(BaseModel):
39
- model_config = ConfigDict(from_attributes=True)
40
-
41
- workflow_id: str
42
- user_prompt: str
43
- status: str
44
- llm_provider: str | None
45
- llm_model: str | None
46
- planner_output: dict | None
47
- created_at: datetime
48
- completed_at: datetime | None
49
-
50
-
51
- class WorkflowListResponse(BaseModel):
52
- workflows: list[WorkflowResponse]
53
- total: int
54
-
55
-
56
- class ApiCredentialCreateRequest(BaseModel):
57
- provider: str = Field(min_length=1, max_length=64)
58
- api_key: str = Field(min_length=1)
59
-
60
-
61
- class ApiCredentialResponse(BaseModel):
62
- model_config = ConfigDict(from_attributes=True)
63
-
64
- id: int
65
- provider: str
66
- api_key_hint: str
67
- created_at: datetime
68
- updated_at: datetime
69
-
70
-
71
- class HealthResponse(BaseModel):
72
- status: str
73
- database: str
74
-
75
-
76
- class TaskResponse(BaseModel):
77
- model_config = ConfigDict(from_attributes=True)
78
-
79
- task_key: str
80
- task_type: str
81
- status: str
82
- depends_on: list[str] | None
83
- output: dict | None
84
- started_at: datetime | None
85
- completed_at: datetime | None
86
-
87
-
88
- class TaskListResponse(BaseModel):
89
- tasks: list[TaskResponse]
90
- total: int
91
-
92
-
93
- class EventResponse(BaseModel):
94
- model_config = ConfigDict(from_attributes=True, populate_by_name=True)
95
-
96
- id: int
97
- event_type: str
98
- message: str
99
- task_id: int | None
100
- metadata: dict | None = Field(default=None, validation_alias="event_metadata")
101
- timestamp: datetime
102
-
103
-
104
- class AttemptResponse(BaseModel):
105
- model_config = ConfigDict(from_attributes=True)
106
-
107
- id: int
108
- task_id: int | None
109
- attempt_number: int
110
- generated_code: dict | None
111
- generated_tests: dict | None
112
- static_analysis_result: dict | None
113
- stdout: str | None
114
- stderr: str | None
115
- exit_code: int | None
116
- result: str | None
117
- error: dict | None
118
- created_at: datetime
119
-
120
-
121
- class AttemptListResponse(BaseModel):
122
- attempts: list[AttemptResponse]
123
- total: int
124
-
125
-
126
- class WorkflowStartRequest(BaseModel):
127
- api_key: str | None = Field(default=None, min_length=1)
128
-
129
-
130
- class ProviderInfo(BaseModel):
131
- id: str
132
- name: str
133
- models: list[str]
134
-
135
-
136
- class ProviderListResponse(BaseModel):
137
- providers: list[ProviderInfo]
138
-
139
-
140
- class EventListResponse(BaseModel):
141
- events: list[EventResponse]
142
- total: int
143
-
144
-
145
- class WorkflowStartResponse(WorkflowResponse):
146
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/api/settings.py DELETED
@@ -1,73 +0,0 @@
1
- from fastapi import APIRouter, Depends, HTTPException, status
2
- from sqlalchemy import select
3
-
4
- from backend.api.schemas import ApiCredentialCreateRequest, ApiCredentialResponse
5
- from backend.core.dependencies import get_current_user, get_repository
6
- from backend.core.encryption import encrypt_value, redact_secret
7
- from backend.db.models import ApiCredential, User
8
- from backend.db.session import get_db
9
- from sqlalchemy.ext.asyncio import AsyncSession
10
-
11
- router = APIRouter(prefix="/settings/api-credentials", tags=["settings"])
12
-
13
-
14
- @router.post("", response_model=ApiCredentialResponse, status_code=status.HTTP_201_CREATED)
15
- async def create_api_credential(
16
- payload: ApiCredentialCreateRequest,
17
- current_user: User = Depends(get_current_user),
18
- db: AsyncSession = Depends(get_db),
19
- ) -> ApiCredentialResponse:
20
- credential = ApiCredential(
21
- user_id=current_user.id,
22
- provider=payload.provider.lower().strip(),
23
- encrypted_api_key=encrypt_value(payload.api_key),
24
- )
25
- db.add(credential)
26
- await db.flush()
27
- await db.refresh(credential)
28
- return ApiCredentialResponse(
29
- id=credential.id,
30
- provider=credential.provider,
31
- api_key_hint=redact_secret(payload.api_key),
32
- created_at=credential.created_at,
33
- updated_at=credential.updated_at,
34
- )
35
-
36
-
37
- @router.get("", response_model=list[ApiCredentialResponse])
38
- async def list_api_credentials(
39
- current_user: User = Depends(get_current_user),
40
- db: AsyncSession = Depends(get_db),
41
- ) -> list[ApiCredentialResponse]:
42
- result = await db.execute(
43
- select(ApiCredential).where(ApiCredential.user_id == current_user.id)
44
- )
45
- credentials = result.scalars().all()
46
- return [
47
- ApiCredentialResponse(
48
- id=cred.id,
49
- provider=cred.provider,
50
- api_key_hint="********",
51
- created_at=cred.created_at,
52
- updated_at=cred.updated_at,
53
- )
54
- for cred in credentials
55
- ]
56
-
57
-
58
- @router.delete("/{credential_id}", status_code=status.HTTP_204_NO_CONTENT)
59
- async def delete_api_credential(
60
- credential_id: int,
61
- current_user: User = Depends(get_current_user),
62
- db: AsyncSession = Depends(get_db),
63
- ) -> None:
64
- result = await db.execute(
65
- select(ApiCredential).where(
66
- ApiCredential.id == credential_id,
67
- ApiCredential.user_id == current_user.id,
68
- )
69
- )
70
- credential = result.scalar_one_or_none()
71
- if credential is None:
72
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Credential not found")
73
- await db.delete(credential)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/api/workflows.py DELETED
@@ -1,370 +0,0 @@
1
- import asyncio
2
- import json
3
- import logging
4
-
5
- from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
6
- from fastapi.responses import StreamingResponse
7
- from sqlalchemy import select
8
- from sqlalchemy.ext.asyncio import AsyncSession
9
-
10
- from backend.api.schemas import (
11
- AttemptListResponse,
12
- AttemptResponse,
13
- EventListResponse,
14
- EventResponse,
15
- TaskListResponse,
16
- TaskResponse,
17
- WorkflowCreateRequest,
18
- WorkflowListResponse,
19
- WorkflowResponse,
20
- WorkflowStartRequest,
21
- WorkflowStartResponse,
22
- )
23
- from backend.core.dependencies import get_current_user, get_repository
24
- from backend.core.encryption import decrypt_value
25
- from backend.core.security import decode_access_token
26
- from backend.db.models import ApiCredential, Attempt, Event, User, Workflow, WorkflowStatus
27
- from backend.db.session import AsyncSessionLocal, get_db
28
- from backend.events.publisher import EventPublisher
29
- from backend.llm.providers import get_llm_provider
30
- from backend.memory.workflow_repository import WorkflowRepository
31
- from backend.orchestration.workflow_runner import WorkflowRunner
32
-
33
- logger = logging.getLogger(__name__)
34
- router = APIRouter(prefix="/workflows", tags=["workflows"])
35
-
36
- TERMINAL_WORKFLOW_STATUSES = {
37
- WorkflowStatus.SUCCESS,
38
- WorkflowStatus.FAILED,
39
- WorkflowStatus.CANCELLED,
40
- }
41
-
42
-
43
- def _to_workflow_response(workflow) -> WorkflowResponse:
44
- return WorkflowResponse(
45
- workflow_id=workflow.workflow_id,
46
- user_prompt=workflow.user_prompt,
47
- status=workflow.status,
48
- llm_provider=workflow.llm_provider,
49
- llm_model=workflow.llm_model,
50
- planner_output=workflow.planner_output,
51
- created_at=workflow.created_at,
52
- completed_at=workflow.completed_at,
53
- )
54
-
55
-
56
- async def _resolve_api_key(
57
- db: AsyncSession,
58
- user_id: int,
59
- provider: str,
60
- request_api_key: str | None,
61
- ) -> str:
62
- if request_api_key:
63
- return request_api_key
64
-
65
- result = await db.execute(
66
- select(ApiCredential).where(
67
- ApiCredential.user_id == user_id,
68
- ApiCredential.provider == provider.lower().strip(),
69
- )
70
- )
71
- credential = result.scalar_one_or_none()
72
- if credential is None:
73
- raise HTTPException(
74
- status_code=status.HTTP_400_BAD_REQUEST,
75
- detail="API key required. Provide api_key or store credentials in settings.",
76
- )
77
- return decrypt_value(credential.encrypted_api_key)
78
-
79
-
80
- async def _execute_workflow_background(
81
- workflow_id: str,
82
- user_id: int,
83
- api_key: str | None,
84
- ) -> None:
85
- async with AsyncSessionLocal() as db:
86
- try:
87
- repo = WorkflowRepository(db)
88
- workflow = await repo.get_workflow_by_public_id(workflow_id, user_id)
89
- if workflow is None:
90
- return
91
- llm = get_llm_provider(
92
- workflow.llm_provider,
93
- api_key=api_key,
94
- model=workflow.llm_model,
95
- )
96
- import shutil
97
-
98
- has_docker = shutil.which("docker") is not None
99
- if not has_docker:
100
- raise RuntimeError("DOCKER_UNAVAILABLE: Sandbox unavailable — Docker is required to execute generated code.")
101
-
102
- runner = WorkflowRunner(db)
103
- await runner.run(workflow, llm)
104
- await db.commit()
105
- except Exception as exc:
106
- logger.exception("Background workflow execution failed for %s", workflow_id)
107
- await db.rollback()
108
- try:
109
- repo = WorkflowRepository(db)
110
- workflow = await repo.get_workflow_by_public_id(workflow_id, user_id)
111
- if workflow and workflow.status == WorkflowStatus.RUNNING:
112
- runner = WorkflowRunner(db)
113
- await runner._fail_workflow(
114
- workflow,
115
- message=str(exc),
116
- error_type="WORKFLOW_EXECUTION_ERROR",
117
- )
118
- await db.commit()
119
- except Exception:
120
- logger.exception("Failed to mark workflow %s as failed", workflow_id)
121
- await db.rollback()
122
-
123
-
124
- @router.post("", response_model=WorkflowResponse, status_code=status.HTTP_201_CREATED)
125
- async def create_workflow(
126
- payload: WorkflowCreateRequest,
127
- current_user: User = Depends(get_current_user),
128
- repo: WorkflowRepository = Depends(get_repository),
129
- db: AsyncSession = Depends(get_db),
130
- ) -> WorkflowResponse:
131
- workflow = await repo.create_workflow(
132
- user_id=current_user.id,
133
- user_prompt=payload.user_prompt,
134
- llm_provider=payload.llm_provider,
135
- llm_model=payload.llm_model,
136
- )
137
- if payload.api_key:
138
- existing = await db.execute(
139
- select(ApiCredential).where(
140
- ApiCredential.user_id == current_user.id,
141
- ApiCredential.provider == payload.llm_provider.lower().strip(),
142
- )
143
- )
144
- if existing.scalar_one_or_none() is None:
145
- from backend.core.encryption import encrypt_value
146
-
147
- db.add(
148
- ApiCredential(
149
- user_id=current_user.id,
150
- provider=payload.llm_provider.lower().strip(),
151
- encrypted_api_key=encrypt_value(payload.api_key),
152
- )
153
- )
154
- return _to_workflow_response(workflow)
155
-
156
-
157
- @router.post("/{workflow_id}/start", response_model=WorkflowStartResponse, status_code=status.HTTP_202_ACCEPTED)
158
- async def start_workflow(
159
- workflow_id: str,
160
- payload: WorkflowStartRequest,
161
- background_tasks: BackgroundTasks,
162
- current_user: User = Depends(get_current_user),
163
- repo: WorkflowRepository = Depends(get_repository),
164
- db: AsyncSession = Depends(get_db),
165
- ) -> WorkflowStartResponse:
166
- workflow = await repo.get_workflow_by_public_id(workflow_id, current_user.id)
167
- if workflow is None:
168
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workflow not found")
169
- if workflow.status not in {WorkflowStatus.CREATED, WorkflowStatus.FAILED}:
170
- raise HTTPException(
171
- status_code=status.HTTP_409_CONFLICT,
172
- detail=f"Workflow cannot be started from status {workflow.status}",
173
- )
174
- if not workflow.llm_provider or not workflow.llm_model:
175
- raise HTTPException(
176
- status_code=status.HTTP_400_BAD_REQUEST,
177
- detail="Workflow is missing LLM provider configuration",
178
- )
179
-
180
- resolved_key = await _resolve_api_key(
181
- db,
182
- current_user.id,
183
- workflow.llm_provider,
184
- payload.api_key,
185
- )
186
-
187
- workflow.status = WorkflowStatus.RUNNING
188
- await db.flush()
189
-
190
- background_tasks.add_task(
191
- _execute_workflow_background,
192
- workflow_id,
193
- current_user.id,
194
- resolved_key,
195
- )
196
- return _to_workflow_response(workflow)
197
-
198
-
199
- @router.get("", response_model=WorkflowListResponse)
200
- async def list_workflows(
201
- current_user: User = Depends(get_current_user),
202
- repo: WorkflowRepository = Depends(get_repository),
203
- ) -> WorkflowListResponse:
204
- workflows = await repo.list_workflows_for_user(current_user.id)
205
- items = [_to_workflow_response(wf) for wf in workflows]
206
- return WorkflowListResponse(workflows=items, total=len(items))
207
-
208
-
209
- @router.get("/{workflow_id}", response_model=WorkflowResponse)
210
- async def get_workflow(
211
- workflow_id: str,
212
- current_user: User = Depends(get_current_user),
213
- repo: WorkflowRepository = Depends(get_repository),
214
- ) -> WorkflowResponse:
215
- workflow = await repo.get_workflow_by_public_id(workflow_id, current_user.id)
216
- if workflow is None:
217
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workflow not found")
218
- return _to_workflow_response(workflow)
219
-
220
-
221
- @router.get("/{workflow_id}/tasks", response_model=TaskListResponse)
222
- async def list_workflow_tasks(
223
- workflow_id: str,
224
- current_user: User = Depends(get_current_user),
225
- repo: WorkflowRepository = Depends(get_repository),
226
- db: AsyncSession = Depends(get_db),
227
- ) -> TaskListResponse:
228
- from backend.memory.task_repository import TaskRepository
229
-
230
- workflow = await repo.get_workflow_by_public_id(workflow_id, current_user.id)
231
- if workflow is None:
232
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workflow not found")
233
-
234
- tasks = await TaskRepository(db).get_tasks_for_workflow(workflow.id)
235
- items = [
236
- TaskResponse(
237
- task_key=task.task_key,
238
- task_type=task.task_type,
239
- status=task.status,
240
- depends_on=task.depends_on,
241
- output=task.output,
242
- started_at=task.started_at,
243
- completed_at=task.completed_at,
244
- )
245
- for task in tasks
246
- ]
247
- return TaskListResponse(tasks=items, total=len(items))
248
-
249
-
250
- @router.get("/{workflow_id}/events", response_model=EventListResponse)
251
- async def list_workflow_events(
252
- workflow_id: str,
253
- after_id: int = 0,
254
- current_user: User = Depends(get_current_user),
255
- repo: WorkflowRepository = Depends(get_repository),
256
- db: AsyncSession = Depends(get_db),
257
- ) -> EventListResponse:
258
- workflow = await repo.get_workflow_by_public_id(workflow_id, current_user.id)
259
- if workflow is None:
260
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workflow not found")
261
-
262
- query = (
263
- select(Event)
264
- .where(Event.workflow_id == workflow.id, Event.id > after_id)
265
- .order_by(Event.timestamp.asc())
266
- )
267
- events = (await db.execute(query)).scalars().all()
268
- items = [
269
- EventResponse(
270
- id=event.id,
271
- event_type=event.event_type,
272
- message=event.message,
273
- task_id=event.task_id,
274
- event_metadata=event.event_metadata,
275
- timestamp=event.timestamp,
276
- )
277
- for event in events
278
- ]
279
- return EventListResponse(events=items, total=len(items))
280
-
281
-
282
- @router.get("/{workflow_id}/events/stream")
283
- async def stream_workflow_events(
284
- workflow_id: str,
285
- token: str = Query(...),
286
- after_id: int = Query(0),
287
- db: AsyncSession = Depends(get_db),
288
- ):
289
- user_id_str = decode_access_token(token)
290
- if user_id_str is None:
291
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
292
- user_id = int(user_id_str)
293
- repo = WorkflowRepository(db)
294
- workflow = await repo.get_workflow_by_public_id(workflow_id, user_id)
295
- if workflow is None:
296
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workflow not found")
297
-
298
- async def event_generator():
299
- cursor = after_id
300
- idle_ticks = 0
301
- while True:
302
- result = await db.execute(
303
- select(Event)
304
- .where(Event.workflow_id == workflow.id, Event.id > cursor)
305
- .order_by(Event.id.asc())
306
- )
307
- new_events = result.scalars().all()
308
- if new_events:
309
- idle_ticks = 0
310
- for event in new_events:
311
- cursor = event.id
312
- payload = {
313
- "id": event.id,
314
- "event_type": event.event_type,
315
- "message": event.message,
316
- "task_id": event.task_id,
317
- "metadata": event.event_metadata,
318
- "timestamp": event.timestamp.isoformat(),
319
- }
320
- yield f"data: {json.dumps(payload)}\n\n"
321
- else:
322
- idle_ticks += 1
323
-
324
- wf_result = await db.execute(
325
- select(Workflow.status).where(Workflow.id == workflow.id)
326
- )
327
- current_status = wf_result.scalar_one_or_none()
328
- if current_status in TERMINAL_WORKFLOW_STATUSES and idle_ticks >= 3:
329
- yield f"data: {json.dumps({'event_type': 'STREAM_END', 'message': 'Workflow finished'})}\n\n"
330
- break
331
- await asyncio.sleep(1)
332
-
333
- return StreamingResponse(event_generator(), media_type="text/event-stream")
334
-
335
-
336
- @router.get("/{workflow_id}/attempts", response_model=AttemptListResponse)
337
- async def list_workflow_attempts(
338
- workflow_id: str,
339
- current_user: User = Depends(get_current_user),
340
- repo: WorkflowRepository = Depends(get_repository),
341
- db: AsyncSession = Depends(get_db),
342
- ) -> AttemptListResponse:
343
- workflow = await repo.get_workflow_by_public_id(workflow_id, current_user.id)
344
- if workflow is None:
345
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workflow not found")
346
-
347
- result = await db.execute(
348
- select(Attempt)
349
- .where(Attempt.workflow_id == workflow.id)
350
- .order_by(Attempt.attempt_number.asc(), Attempt.id.asc())
351
- )
352
- attempts = result.scalars().all()
353
- items = [
354
- AttemptResponse(
355
- id=attempt.id,
356
- task_id=attempt.task_id,
357
- attempt_number=attempt.attempt_number,
358
- generated_code=attempt.generated_code,
359
- generated_tests=attempt.generated_tests,
360
- static_analysis_result=attempt.static_analysis_result,
361
- stdout=attempt.stdout,
362
- stderr=attempt.stderr,
363
- exit_code=attempt.exit_code,
364
- result=attempt.result,
365
- error=attempt.error,
366
- created_at=attempt.created_at,
367
- )
368
- for attempt in attempts
369
- ]
370
- return AttemptListResponse(attempts=items, total=len(items))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/core/__init__.py DELETED
File without changes
backend/core/config.py DELETED
@@ -1,59 +0,0 @@
1
- import os
2
- import tempfile
3
- from functools import lru_cache
4
-
5
- from pydantic import field_validator
6
- from pydantic_settings import BaseSettings, SettingsConfigDict
7
-
8
-
9
- def _default_workspace_root() -> str:
10
- base = os.path.join(tempfile.gettempdir(), "autoforge", "workspaces")
11
- os.makedirs(base, exist_ok=True)
12
- return base
13
-
14
-
15
- class Settings(BaseSettings):
16
- model_config = SettingsConfigDict(
17
- env_file=".env",
18
- env_file_encoding="utf-8",
19
- case_sensitive=False,
20
- extra="ignore",
21
- )
22
-
23
- database_url: str = "sqlite+aiosqlite:////tmp/autoforge.db"
24
-
25
- @field_validator("database_url", mode="before")
26
- @classmethod
27
- def normalize_database_url(cls, v: str | None) -> str:
28
- if not v or not isinstance(v, str) or not v.strip():
29
- return "sqlite+aiosqlite:////tmp/autoforge.db"
30
- v = v.strip()
31
- # If someone accidentally pasted an HTTP/HTTPS URL (e.g. dashboard URL)
32
- if v.startswith("http://") or v.startswith("https://"):
33
- return "sqlite+aiosqlite:////tmp/autoforge.db"
34
- if v.startswith("postgres://"):
35
- return v.replace("postgres://", "postgresql+asyncpg://", 1)
36
- if v.startswith("postgresql://") and not v.startswith("postgresql+asyncpg://"):
37
- return v.replace("postgresql://", "postgresql+asyncpg://", 1)
38
- if not (v.startswith("sqlite") or v.startswith("postgresql")):
39
- return "sqlite+aiosqlite:////tmp/autoforge.db"
40
- return v
41
- secret_key: str = "change-me-in-production-use-a-long-random-string"
42
- encryption_key: str = "change-me-32-byte-base64-fernet-key-here="
43
- algorithm: str = "HS256"
44
- access_token_expire_minutes: int = 60 * 24
45
- log_level: str = "INFO"
46
- docker_host: str = "unix:///var/run/docker.sock"
47
- max_retries: int = 4
48
- workspace_root: str = _default_workspace_root()
49
- default_timeout: int = 300
50
- cors_origins: str = "http://localhost:3000,http://localhost:5173"
51
- docker_image: str = "autoforge-sandbox:3.12"
52
- docker_memory_limit: str = "512m"
53
- docker_cpu_limit: float = 1.0
54
- docker_network_disabled: bool = True
55
-
56
-
57
- @lru_cache
58
- def get_settings() -> Settings:
59
- return Settings()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/core/dependencies.py DELETED
@@ -1,50 +0,0 @@
1
- from fastapi import Depends, HTTPException, status
2
- from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
3
- from sqlalchemy.ext.asyncio import AsyncSession
4
-
5
- from backend.core.security import decode_access_token
6
- from backend.db.models import User
7
- from backend.db.session import get_db
8
- from backend.memory.workflow_repository import WorkflowRepository
9
-
10
- security = HTTPBearer(auto_error=False)
11
-
12
-
13
- async def get_repository(db: AsyncSession = Depends(get_db)) -> WorkflowRepository:
14
- return WorkflowRepository(db)
15
-
16
-
17
- async def get_current_user(
18
- credentials: HTTPAuthorizationCredentials | None = Depends(security),
19
- repo: WorkflowRepository = Depends(get_repository),
20
- ) -> User:
21
- if credentials is None or not credentials.credentials:
22
- raise HTTPException(
23
- status_code=status.HTTP_401_UNAUTHORIZED,
24
- detail="Not authenticated",
25
- headers={"WWW-Authenticate": "Bearer"},
26
- )
27
-
28
- user_id_str = decode_access_token(credentials.credentials)
29
- if user_id_str is None:
30
- raise HTTPException(
31
- status_code=status.HTTP_401_UNAUTHORIZED,
32
- detail="Invalid or expired token",
33
- headers={"WWW-Authenticate": "Bearer"},
34
- )
35
-
36
- try:
37
- user_id = int(user_id_str)
38
- except ValueError as exc:
39
- raise HTTPException(
40
- status_code=status.HTTP_401_UNAUTHORIZED,
41
- detail="Invalid token subject",
42
- ) from exc
43
-
44
- user = await repo.get_user_by_id(user_id)
45
- if user is None:
46
- raise HTTPException(
47
- status_code=status.HTTP_401_UNAUTHORIZED,
48
- detail="User not found",
49
- )
50
- return user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/core/encryption.py DELETED
@@ -1,30 +0,0 @@
1
- import base64
2
- import hashlib
3
-
4
- from cryptography.fernet import Fernet, InvalidToken
5
-
6
- from backend.core.config import get_settings
7
-
8
-
9
- def _fernet() -> Fernet:
10
- settings = get_settings()
11
- key_material = hashlib.sha256(settings.encryption_key.encode()).digest()
12
- fernet_key = base64.urlsafe_b64encode(key_material)
13
- return Fernet(fernet_key)
14
-
15
-
16
- def encrypt_value(value: str) -> str:
17
- return _fernet().encrypt(value.encode()).decode()
18
-
19
-
20
- def decrypt_value(encrypted_value: str) -> str:
21
- try:
22
- return _fernet().decrypt(encrypted_value.encode()).decode()
23
- except InvalidToken as exc:
24
- raise ValueError("Failed to decrypt credential") from exc
25
-
26
-
27
- def redact_secret(value: str, visible_chars: int = 4) -> str:
28
- if len(value) <= visible_chars:
29
- return "*" * len(value)
30
- return f"{'*' * (len(value) - visible_chars)}{value[-visible_chars:]}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/core/security.py DELETED
@@ -1,36 +0,0 @@
1
- from datetime import UTC, datetime, timedelta
2
- from typing import Any
3
-
4
- from jose import JWTError, jwt
5
- from passlib.context import CryptContext
6
-
7
- from backend.core.config import get_settings
8
-
9
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
10
-
11
-
12
- def hash_password(password: str) -> str:
13
- return pwd_context.hash(password)
14
-
15
-
16
- def verify_password(plain_password: str, hashed_password: str) -> bool:
17
- return pwd_context.verify(plain_password, hashed_password)
18
-
19
-
20
- def create_access_token(subject: str, expires_delta: timedelta | None = None) -> str:
21
- settings = get_settings()
22
- expire = datetime.now(UTC) + (
23
- expires_delta or timedelta(minutes=settings.access_token_expire_minutes)
24
- )
25
- payload: dict[str, Any] = {"sub": subject, "exp": expire}
26
- return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)
27
-
28
-
29
- def decode_access_token(token: str) -> str | None:
30
- settings = get_settings()
31
- try:
32
- payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
33
- subject = payload.get("sub")
34
- return str(subject) if subject else None
35
- except JWTError:
36
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/db/__init__.py DELETED
File without changes
backend/db/migrations/env.py DELETED
@@ -1,52 +0,0 @@
1
- from logging.config import fileConfig
2
-
3
- from alembic import context
4
- from sqlalchemy import engine_from_config, pool
5
-
6
- from backend.core.config import get_settings
7
- from backend.db.models import Base
8
-
9
- config = context.config
10
- if config.config_file_name is not None:
11
- fileConfig(config.config_file_name)
12
-
13
- target_metadata = Base.metadata
14
-
15
-
16
- def get_url() -> str:
17
- return get_settings().database_url.replace("+asyncpg", "")
18
-
19
-
20
- def run_migrations_offline() -> None:
21
- url = get_url()
22
- context.configure(
23
- url=url,
24
- target_metadata=target_metadata,
25
- literal_binds=True,
26
- dialect_opts={"paramstyle": "named"},
27
- )
28
-
29
- with context.begin_transaction():
30
- context.run_migrations()
31
-
32
-
33
- def run_migrations_online() -> None:
34
- configuration = config.get_section(config.config_ini_section) or {}
35
- configuration["sqlalchemy.url"] = get_url()
36
- connectable = engine_from_config(
37
- configuration,
38
- prefix="sqlalchemy.",
39
- poolclass=pool.NullPool,
40
- )
41
-
42
- with connectable.connect() as connection:
43
- context.configure(connection=connection, target_metadata=target_metadata)
44
-
45
- with context.begin_transaction():
46
- context.run_migrations()
47
-
48
-
49
- if context.is_offline_mode():
50
- run_migrations_offline()
51
- else:
52
- run_migrations_online()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/db/migrations/versions/001_initial_schema.py DELETED
@@ -1,138 +0,0 @@
1
- """Initial schema
2
-
3
- Revision ID: 001_initial
4
- Revises:
5
- Create Date: 2026-08-29
6
- """
7
-
8
- from typing import Sequence, Union
9
-
10
- import sqlalchemy as sa
11
- from alembic import op
12
- from sqlalchemy.dialects import postgresql
13
-
14
- revision: str = "001_initial"
15
- down_revision: Union[str, None] = None
16
- branch_labels: Union[str, Sequence[str], None] = None
17
- depends_on: Union[str, Sequence[str], None] = None
18
-
19
-
20
- def upgrade() -> None:
21
- op.create_table(
22
- "users",
23
- sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
24
- sa.Column("email", sa.String(length=255), nullable=False),
25
- sa.Column("username", sa.String(length=100), nullable=False),
26
- sa.Column("password_hash", sa.String(length=255), nullable=False),
27
- sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
28
- sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
29
- sa.PrimaryKeyConstraint("id"),
30
- )
31
- op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
32
- op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True)
33
-
34
- op.create_table(
35
- "workflows",
36
- sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
37
- sa.Column("workflow_id", sa.String(length=64), nullable=False),
38
- sa.Column("user_id", sa.Integer(), nullable=False),
39
- sa.Column("user_prompt", sa.Text(), nullable=False),
40
- sa.Column("status", sa.String(length=32), nullable=False),
41
- sa.Column("planner_output", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
42
- sa.Column("llm_provider", sa.String(length=64), nullable=True),
43
- sa.Column("llm_model", sa.String(length=128), nullable=True),
44
- sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
45
- sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
46
- sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
47
- sa.PrimaryKeyConstraint("id"),
48
- )
49
- op.create_index(op.f("ix_workflows_user_id"), "workflows", ["user_id"], unique=False)
50
- op.create_index(op.f("ix_workflows_workflow_id"), "workflows", ["workflow_id"], unique=True)
51
-
52
- op.create_table(
53
- "api_credentials",
54
- sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
55
- sa.Column("user_id", sa.Integer(), nullable=False),
56
- sa.Column("provider", sa.String(length=64), nullable=False),
57
- sa.Column("encrypted_api_key", sa.Text(), nullable=False),
58
- sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
59
- sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
60
- sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
61
- sa.PrimaryKeyConstraint("id"),
62
- )
63
- op.create_index(op.f("ix_api_credentials_user_id"), "api_credentials", ["user_id"], unique=False)
64
-
65
- op.create_table(
66
- "tasks",
67
- sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
68
- sa.Column("workflow_id", sa.Integer(), nullable=False),
69
- sa.Column("task_key", sa.String(length=64), nullable=False),
70
- sa.Column("task_type", sa.String(length=64), nullable=False),
71
- sa.Column("status", sa.String(length=32), nullable=False),
72
- sa.Column("depends_on", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
73
- sa.Column("output", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
74
- sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
75
- sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
76
- sa.ForeignKeyConstraint(["workflow_id"], ["workflows.id"]),
77
- sa.PrimaryKeyConstraint("id"),
78
- )
79
- op.create_index(op.f("ix_tasks_workflow_id"), "tasks", ["workflow_id"], unique=False)
80
-
81
- op.create_table(
82
- "attempts",
83
- sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
84
- sa.Column("workflow_id", sa.Integer(), nullable=False),
85
- sa.Column("task_id", sa.Integer(), nullable=True),
86
- sa.Column("attempt_number", sa.Integer(), nullable=False),
87
- sa.Column("generated_code", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
88
- sa.Column("generated_tests", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
89
- sa.Column("static_analysis_result", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
90
- sa.Column("stdout", sa.Text(), nullable=True),
91
- sa.Column("stderr", sa.Text(), nullable=True),
92
- sa.Column("exit_code", sa.Integer(), nullable=True),
93
- sa.Column("result", sa.String(length=32), nullable=True),
94
- sa.Column("error", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
95
- sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
96
- sa.ForeignKeyConstraint(["task_id"], ["tasks.id"]),
97
- sa.ForeignKeyConstraint(["workflow_id"], ["workflows.id"]),
98
- sa.PrimaryKeyConstraint("id"),
99
- )
100
- op.create_index(op.f("ix_attempts_task_id"), "attempts", ["task_id"], unique=False)
101
- op.create_index(op.f("ix_attempts_workflow_id"), "attempts", ["workflow_id"], unique=False)
102
-
103
- op.create_table(
104
- "events",
105
- sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
106
- sa.Column("workflow_id", sa.Integer(), nullable=False),
107
- sa.Column("task_id", sa.Integer(), nullable=True),
108
- sa.Column("event_type", sa.String(length=64), nullable=False),
109
- sa.Column("message", sa.Text(), nullable=False),
110
- sa.Column("metadata", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
111
- sa.Column("timestamp", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
112
- sa.ForeignKeyConstraint(["task_id"], ["tasks.id"]),
113
- sa.ForeignKeyConstraint(["workflow_id"], ["workflows.id"]),
114
- sa.PrimaryKeyConstraint("id"),
115
- )
116
- op.create_index(op.f("ix_events_event_type"), "events", ["event_type"], unique=False)
117
- op.create_index(op.f("ix_events_timestamp"), "events", ["timestamp"], unique=False)
118
- op.create_index(op.f("ix_events_workflow_id"), "events", ["workflow_id"], unique=False)
119
-
120
-
121
- def downgrade() -> None:
122
- op.drop_index(op.f("ix_events_workflow_id"), table_name="events")
123
- op.drop_index(op.f("ix_events_timestamp"), table_name="events")
124
- op.drop_index(op.f("ix_events_event_type"), table_name="events")
125
- op.drop_table("events")
126
- op.drop_index(op.f("ix_attempts_workflow_id"), table_name="attempts")
127
- op.drop_index(op.f("ix_attempts_task_id"), table_name="attempts")
128
- op.drop_table("attempts")
129
- op.drop_index(op.f("ix_tasks_workflow_id"), table_name="tasks")
130
- op.drop_table("tasks")
131
- op.drop_index(op.f("ix_api_credentials_user_id"), table_name="api_credentials")
132
- op.drop_table("api_credentials")
133
- op.drop_index(op.f("ix_workflows_workflow_id"), table_name="workflows")
134
- op.drop_index(op.f("ix_workflows_user_id"), table_name="workflows")
135
- op.drop_table("workflows")
136
- op.drop_index(op.f("ix_users_username"), table_name="users")
137
- op.drop_index(op.f("ix_users_email"), table_name="users")
138
- op.drop_table("users")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/db/migrations/versions/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Alembic migrations for AutoForge."""
 
 
backend/db/models/__init__.py DELETED
@@ -1,150 +0,0 @@
1
- from datetime import datetime
2
- from enum import StrEnum
3
-
4
- from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func, JSON
5
- from sqlalchemy.dialects.postgresql import JSONB
6
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
7
-
8
- JSON_TYPE = JSON().with_variant(JSONB, "postgresql")
9
-
10
-
11
- class Base(DeclarativeBase):
12
- pass
13
-
14
-
15
- class WorkflowStatus(StrEnum):
16
- CREATED = "CREATED"
17
- RUNNING = "RUNNING"
18
- SUCCESS = "SUCCESS"
19
- FAILED = "FAILED"
20
- CANCELLED = "CANCELLED"
21
- CANCELLING = "CANCELLING"
22
-
23
-
24
- class TaskStatus(StrEnum):
25
- CREATED = "CREATED"
26
- READY = "READY"
27
- RUNNING = "RUNNING"
28
- SUCCESS = "SUCCESS"
29
- FAILED = "FAILED"
30
- RETRYING = "RETRYING"
31
- CANCELLED = "CANCELLED"
32
-
33
-
34
- class User(Base):
35
- __tablename__ = "users"
36
-
37
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
38
- email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
39
- username: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True)
40
- password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
41
- created_at: Mapped[datetime] = mapped_column(
42
- DateTime(timezone=True), server_default=func.now(), nullable=False
43
- )
44
- updated_at: Mapped[datetime] = mapped_column(
45
- DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
46
- )
47
-
48
- workflows: Mapped[list["Workflow"]] = relationship(back_populates="user")
49
- api_credentials: Mapped[list["ApiCredential"]] = relationship(back_populates="user")
50
-
51
-
52
- class Workflow(Base):
53
- __tablename__ = "workflows"
54
-
55
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
56
- workflow_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
57
- user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
58
- user_prompt: Mapped[str] = mapped_column(Text, nullable=False)
59
- status: Mapped[str] = mapped_column(String(32), nullable=False, default=WorkflowStatus.CREATED)
60
- planner_output: Mapped[dict | None] = mapped_column(JSON_TYPE, nullable=True)
61
- llm_provider: Mapped[str | None] = mapped_column(String(64), nullable=True)
62
- llm_model: Mapped[str | None] = mapped_column(String(128), nullable=True)
63
- created_at: Mapped[datetime] = mapped_column(
64
- DateTime(timezone=True), server_default=func.now(), nullable=False
65
- )
66
- completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
67
-
68
- user: Mapped["User"] = relationship(back_populates="workflows")
69
- tasks: Mapped[list["Task"]] = relationship(back_populates="workflow", cascade="all, delete-orphan")
70
- attempts: Mapped[list["Attempt"]] = relationship(
71
- back_populates="workflow", cascade="all, delete-orphan"
72
- )
73
- events: Mapped[list["Event"]] = relationship(
74
- back_populates="workflow", cascade="all, delete-orphan"
75
- )
76
-
77
-
78
- class Task(Base):
79
- __tablename__ = "tasks"
80
-
81
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
82
- workflow_id: Mapped[int] = mapped_column(ForeignKey("workflows.id"), nullable=False, index=True)
83
- task_key: Mapped[str] = mapped_column(String(64), nullable=False)
84
- task_type: Mapped[str] = mapped_column(String(64), nullable=False)
85
- status: Mapped[str] = mapped_column(String(32), nullable=False, default=TaskStatus.CREATED)
86
- depends_on: Mapped[list | None] = mapped_column(JSON_TYPE, nullable=True)
87
- output: Mapped[dict | None] = mapped_column(JSON_TYPE, nullable=True)
88
- started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
89
- completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
90
-
91
- workflow: Mapped["Workflow"] = relationship(back_populates="tasks")
92
- attempts: Mapped[list["Attempt"]] = relationship(back_populates="task")
93
- events: Mapped[list["Event"]] = relationship(back_populates="task")
94
-
95
-
96
- class Attempt(Base):
97
- __tablename__ = "attempts"
98
-
99
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
100
- workflow_id: Mapped[int] = mapped_column(ForeignKey("workflows.id"), nullable=False, index=True)
101
- task_id: Mapped[int | None] = mapped_column(ForeignKey("tasks.id"), nullable=True, index=True)
102
- attempt_number: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
103
- generated_code: Mapped[dict | None] = mapped_column(JSON_TYPE, nullable=True)
104
- generated_tests: Mapped[dict | None] = mapped_column(JSON_TYPE, nullable=True)
105
- static_analysis_result: Mapped[dict | None] = mapped_column(JSON_TYPE, nullable=True)
106
- stdout: Mapped[str | None] = mapped_column(Text, nullable=True)
107
- stderr: Mapped[str | None] = mapped_column(Text, nullable=True)
108
- exit_code: Mapped[int | None] = mapped_column(Integer, nullable=True)
109
- result: Mapped[str | None] = mapped_column(String(32), nullable=True)
110
- error: Mapped[dict | None] = mapped_column(JSON_TYPE, nullable=True)
111
- created_at: Mapped[datetime] = mapped_column(
112
- DateTime(timezone=True), server_default=func.now(), nullable=False
113
- )
114
-
115
- workflow: Mapped["Workflow"] = relationship(back_populates="attempts")
116
- task: Mapped["Task | None"] = relationship(back_populates="attempts")
117
-
118
-
119
- class Event(Base):
120
- __tablename__ = "events"
121
-
122
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
123
- workflow_id: Mapped[int] = mapped_column(ForeignKey("workflows.id"), nullable=False, index=True)
124
- task_id: Mapped[int | None] = mapped_column(ForeignKey("tasks.id"), nullable=True, index=True)
125
- event_type: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
126
- message: Mapped[str] = mapped_column(Text, nullable=False)
127
- event_metadata: Mapped[dict | None] = mapped_column("metadata", JSON_TYPE, nullable=True)
128
- timestamp: Mapped[datetime] = mapped_column(
129
- DateTime(timezone=True), server_default=func.now(), nullable=False, index=True
130
- )
131
-
132
- workflow: Mapped["Workflow"] = relationship(back_populates="events")
133
- task: Mapped["Task | None"] = relationship(back_populates="events")
134
-
135
-
136
- class ApiCredential(Base):
137
- __tablename__ = "api_credentials"
138
-
139
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
140
- user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
141
- provider: Mapped[str] = mapped_column(String(64), nullable=False)
142
- encrypted_api_key: Mapped[str] = mapped_column(Text, nullable=False)
143
- created_at: Mapped[datetime] = mapped_column(
144
- DateTime(timezone=True), server_default=func.now(), nullable=False
145
- )
146
- updated_at: Mapped[datetime] = mapped_column(
147
- DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
148
- )
149
-
150
- user: Mapped["User"] = relationship(back_populates="api_credentials")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/db/session.py DELETED
@@ -1,50 +0,0 @@
1
- import logging
2
- from collections.abc import AsyncGenerator
3
-
4
- from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
5
-
6
- from backend.core.config import get_settings
7
-
8
- logger = logging.getLogger(__name__)
9
- settings = get_settings()
10
-
11
-
12
- def _create_engine_safely(url: str):
13
- try:
14
- if url.startswith("sqlite"):
15
- return create_async_engine(
16
- url,
17
- echo=False,
18
- connect_args={"check_same_thread": False},
19
- )
20
- return create_async_engine(
21
- url,
22
- echo=False,
23
- pool_pre_ping=True,
24
- )
25
- except Exception as exc:
26
- logger.error("Failed to create engine for %s, falling back to SQLite: %s", url, exc)
27
- return create_async_engine(
28
- "sqlite+aiosqlite:////tmp/autoforge.db",
29
- echo=False,
30
- connect_args={"check_same_thread": False},
31
- )
32
-
33
-
34
- engine = _create_engine_safely(settings.database_url)
35
-
36
- AsyncSessionLocal = async_sessionmaker(
37
- engine,
38
- class_=AsyncSession,
39
- expire_on_commit=False,
40
- )
41
-
42
-
43
- async def get_db() -> AsyncGenerator[AsyncSession, None]:
44
- async with AsyncSessionLocal() as session:
45
- try:
46
- yield session
47
- await session.commit()
48
- except Exception:
49
- await session.rollback()
50
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/events/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Event publishing utilities."""
 
 
backend/events/publisher.py DELETED
@@ -1,40 +0,0 @@
1
- from sqlalchemy import select
2
- from sqlalchemy.ext.asyncio import AsyncSession
3
-
4
- from backend.db.models import Event
5
-
6
-
7
- class EventPublisher:
8
- """Persists workflow events to PostgreSQL."""
9
-
10
- def __init__(self, db: AsyncSession) -> None:
11
- self.db = db
12
-
13
- async def publish(
14
- self,
15
- *,
16
- workflow_db_id: int,
17
- public_workflow_id: str,
18
- event_type: str,
19
- message: str,
20
- task_id: int | None = None,
21
- task_key: str | None = None,
22
- metadata: dict | None = None,
23
- ) -> Event:
24
- event_metadata = {"workflow_id": public_workflow_id}
25
- if task_key:
26
- event_metadata["task_key"] = task_key
27
- if metadata:
28
- event_metadata.update(metadata)
29
-
30
- event = Event(
31
- workflow_id=workflow_db_id,
32
- task_id=task_id,
33
- event_type=event_type,
34
- message=message,
35
- event_metadata=event_metadata,
36
- )
37
- self.db.add(event)
38
- await self.db.flush()
39
- await self.db.refresh(event)
40
- return event
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/execution/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Docker sandbox execution — implemented in Phase 2+."""
 
 
backend/execution/docker_runner.py DELETED
@@ -1,265 +0,0 @@
1
- import asyncio
2
- import os
3
- import time
4
- import uuid
5
- from abc import ABC, abstractmethod
6
- from pathlib import Path, PureWindowsPath
7
-
8
- from backend.core.config import get_settings
9
- from backend.execution.sandbox import ExecutionResult
10
-
11
-
12
- def _docker_volume_path(workspace: Path) -> str:
13
- """Convert a host path to a Docker-compatible volume mount source."""
14
- resolved = workspace.resolve()
15
- if os.name == "nt":
16
- path = PureWindowsPath(resolved)
17
- drive = path.drive.rstrip(":").lower()
18
- rest = str(path.relative_to(path.anchor)).replace("\\", "/")
19
- return f"/{drive}/{rest}"
20
- return resolved.as_posix()
21
-
22
-
23
- def _build_container_test_command(workspace: Path, *, network_disabled: bool) -> str:
24
- requirements = workspace / "requirements.txt"
25
- if (
26
- not network_disabled
27
- and requirements.exists()
28
- and requirements.read_text(encoding="utf-8").strip()
29
- ):
30
- return "pip install --no-cache-dir -r requirements.txt -q && python -m pytest -v"
31
- return "python -m pytest -v"
32
-
33
-
34
- class DockerRunnerBase(ABC):
35
- @abstractmethod
36
- async def run_tests(
37
- self,
38
- workspace: Path,
39
- *,
40
- timeout: int | None = None,
41
- ) -> ExecutionResult:
42
- """Run project tests inside an isolated container."""
43
-
44
- @abstractmethod
45
- async def cleanup(self, container_id: str | None = None) -> None:
46
- """Remove container resources."""
47
-
48
-
49
- class DockerCliRunner(DockerRunnerBase):
50
- """Execute generated projects via the Docker CLI — never on the host."""
51
-
52
- def __init__(
53
- self,
54
- *,
55
- docker_host: str | None = None,
56
- image: str | None = None,
57
- memory_limit: str | None = None,
58
- cpu_limit: float | None = None,
59
- network_disabled: bool | None = None,
60
- ) -> None:
61
- settings = get_settings()
62
- self.docker_host = docker_host or settings.docker_host
63
- self.image = image or settings.docker_image
64
- self.memory_limit = memory_limit or settings.docker_memory_limit
65
- self.cpu_limit = cpu_limit or settings.docker_cpu_limit
66
- self.network_disabled = (
67
- network_disabled
68
- if network_disabled is not None
69
- else settings.docker_network_disabled
70
- )
71
- self.default_timeout = settings.default_timeout
72
- self._active_containers: set[str] = set()
73
-
74
- async def run_tests(
75
- self,
76
- workspace: Path,
77
- *,
78
- timeout: int | None = None,
79
- ) -> ExecutionResult:
80
- timeout = timeout or self.default_timeout
81
- container_name = f"autoforge_{uuid.uuid4().hex[:12]}"
82
- workspace_mount = _docker_volume_path(workspace)
83
- test_command = _build_container_test_command(workspace, network_disabled=self.network_disabled)
84
-
85
- command = [
86
- "docker",
87
- "run",
88
- "--name",
89
- container_name,
90
- "--rm",
91
- "--memory",
92
- self.memory_limit,
93
- "--cpus",
94
- str(self.cpu_limit),
95
- "-v",
96
- f"{workspace_mount}:/workspace:ro",
97
- "-w",
98
- "/workspace",
99
- ]
100
- if self.network_disabled:
101
- command.append("--network")
102
- command.append("none")
103
-
104
- command.extend([self.image, "bash", "-lc", test_command])
105
-
106
- start = time.perf_counter()
107
- timed_out = False
108
- stdout = ""
109
- stderr = ""
110
- exit_code = 1
111
- container_id = container_name
112
-
113
- try:
114
- process = await asyncio.create_subprocess_exec(
115
- *command,
116
- stdout=asyncio.subprocess.PIPE,
117
- stderr=asyncio.subprocess.PIPE,
118
- )
119
- self._active_containers.add(container_name)
120
- try:
121
- stdout_bytes, stderr_bytes = await asyncio.wait_for(
122
- process.communicate(),
123
- timeout=timeout,
124
- )
125
- stdout = stdout_bytes.decode("utf-8", errors="replace")
126
- stderr = stderr_bytes.decode("utf-8", errors="replace")
127
- exit_code = process.returncode or 0
128
- except asyncio.TimeoutError:
129
- timed_out = True
130
- stderr = f"Execution timed out after {timeout} seconds"
131
- exit_code = 124
132
- await self._force_remove_container(container_name)
133
- finally:
134
- self._active_containers.discard(container_name)
135
- except FileNotFoundError as exc:
136
- stderr = "Docker CLI not found. Generated code cannot run without Docker."
137
- exit_code = 127
138
- return ExecutionResult(
139
- stdout=stdout,
140
- stderr=stderr,
141
- exit_code=exit_code,
142
- execution_time=time.perf_counter() - start,
143
- timed_out=False,
144
- container_id=container_id,
145
- container_build_status="failed",
146
- container_start_status="failed",
147
- )
148
-
149
- elapsed = time.perf_counter() - start
150
- return ExecutionResult(
151
- stdout=stdout,
152
- stderr=stderr,
153
- exit_code=exit_code,
154
- execution_time=elapsed,
155
- timed_out=timed_out,
156
- container_id=container_id,
157
- container_build_status="success",
158
- container_start_status="started" if not timed_out else "timeout",
159
- )
160
-
161
- async def cleanup(self, container_id: str | None = None) -> None:
162
- if container_id:
163
- await self._force_remove_container(container_id)
164
- return
165
- for cid in list(self._active_containers):
166
- await self._force_remove_container(cid)
167
-
168
- async def _force_remove_container(self, container_name: str) -> None:
169
- process = await asyncio.create_subprocess_exec(
170
- "docker",
171
- "rm",
172
- "-f",
173
- container_name,
174
- stdout=asyncio.subprocess.DEVNULL,
175
- stderr=asyncio.subprocess.DEVNULL,
176
- )
177
- await process.wait()
178
- self._active_containers.discard(container_name)
179
-
180
-
181
- class FakeDockerRunner(DockerRunnerBase):
182
- """Test double that simulates container execution without Docker."""
183
-
184
- def __init__(
185
- self,
186
- *,
187
- exit_code: int = 0,
188
- stdout: str = "===== 2 passed in 0.01s =====",
189
- stderr: str = "",
190
- timed_out: bool = False,
191
- delay: float = 0,
192
- should_fail_start: bool = False,
193
- ) -> None:
194
- self.exit_code = exit_code
195
- self.stdout = stdout
196
- self.stderr = stderr
197
- self.timed_out = timed_out
198
- self.delay = delay
199
- self.should_fail_start = should_fail_start
200
- self.fail_remaining = 0
201
- self.containers_started: list[str] = []
202
- self.cleaned_up: list[str] = []
203
-
204
- def with_failures(self, count: int) -> "FakeDockerRunner":
205
- self.fail_remaining = count
206
- return self
207
-
208
- async def run_tests(
209
- self,
210
- workspace: Path,
211
- *,
212
- timeout: int | None = None,
213
- ) -> ExecutionResult:
214
- if self.should_fail_start:
215
- return ExecutionResult(
216
- stdout="",
217
- stderr="Docker build failed",
218
- exit_code=1,
219
- execution_time=0,
220
- container_build_status="failed",
221
- container_start_status="failed",
222
- )
223
-
224
- if self.delay:
225
- await asyncio.sleep(self.delay)
226
-
227
- container_id = f"fake_{uuid.uuid4().hex[:8]}"
228
- self.containers_started.append(container_id)
229
-
230
- if self.fail_remaining > 0:
231
- self.fail_remaining -= 1
232
- return ExecutionResult(
233
- stdout="===== 1 failed in 0.01s =====",
234
- stderr="AssertionError: test failed",
235
- exit_code=1,
236
- execution_time=self.delay,
237
- container_id=container_id,
238
- container_build_status="success",
239
- container_start_status="started",
240
- )
241
-
242
- if self.timed_out:
243
- return ExecutionResult(
244
- stdout="",
245
- stderr="Execution timed out",
246
- exit_code=124,
247
- execution_time=float(timeout or 1),
248
- timed_out=True,
249
- container_id=container_id,
250
- container_build_status="success",
251
- container_start_status="timeout",
252
- )
253
-
254
- return ExecutionResult(
255
- stdout=self.stdout,
256
- stderr=self.stderr,
257
- exit_code=self.exit_code,
258
- execution_time=self.delay,
259
- container_id=container_id,
260
- container_build_status="success",
261
- container_start_status="started",
262
- )
263
-
264
- async def cleanup(self, container_id: str | None = None) -> None:
265
- self.cleaned_up.append(container_id or "all")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/execution/result_parser.py DELETED
@@ -1,40 +0,0 @@
1
- import re
2
- from dataclasses import dataclass
3
-
4
-
5
- @dataclass
6
- class TestSummary:
7
- passed: int
8
- failed: int
9
- total: int
10
-
11
- @property
12
- def success(self) -> bool:
13
- return self.failed == 0 and self.total > 0
14
-
15
-
16
- def parse_pytest_output(stdout: str, stderr: str) -> TestSummary:
17
- combined = f"{stdout}\n{stderr}"
18
- match = re.search(r"=+\s+(\d+) passed(?:,\s+(\d+) failed)?", combined)
19
- if match:
20
- passed = int(match.group(1))
21
- failed = int(match.group(2) or 0)
22
- return TestSummary(passed=passed, failed=failed, total=passed + failed)
23
-
24
- failed_match = re.search(r"=+\s+(\d+) failed", combined)
25
- if failed_match:
26
- failed = int(failed_match.group(1))
27
- return TestSummary(passed=0, failed=failed, total=failed)
28
-
29
- return TestSummary(passed=0, failed=0, total=0)
30
-
31
-
32
- def parse_verification_result(stdout: str, stderr: str, exit_code: int) -> dict:
33
- summary = parse_pytest_output(stdout, stderr)
34
- return {
35
- "tests_passed": summary.passed,
36
- "tests_failed": summary.failed,
37
- "tests_total": summary.total,
38
- "verification_passed": exit_code == 0 and summary.success,
39
- "exit_code": exit_code,
40
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/execution/sandbox.py DELETED
@@ -1,38 +0,0 @@
1
- from dataclasses import dataclass
2
- from pathlib import Path
3
-
4
-
5
- @dataclass
6
- class ExecutionResult:
7
- stdout: str
8
- stderr: str
9
- exit_code: int
10
- execution_time: float
11
- timed_out: bool = False
12
- container_id: str | None = None
13
- container_build_status: str = "skipped"
14
- container_start_status: str = "unknown"
15
-
16
- @property
17
- def success(self) -> bool:
18
- return self.exit_code == 0 and not self.timed_out
19
-
20
-
21
- def write_project_files(workspace: Path, files: dict[str, dict[str, str]]) -> None:
22
- workspace.mkdir(parents=True, exist_ok=True)
23
- for rel_path, meta in files.items():
24
- target = workspace / rel_path
25
- target.parent.mkdir(parents=True, exist_ok=True)
26
- target.write_text(meta["content"], encoding="utf-8")
27
-
28
-
29
- def cleanup_workspace(workspace: Path) -> None:
30
- if not workspace.exists():
31
- return
32
- for path in sorted(workspace.rglob("*"), reverse=True):
33
- if path.is_file():
34
- path.unlink()
35
- elif path.is_dir():
36
- path.rmdir()
37
- if workspace.exists():
38
- workspace.rmdir()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/execution/verification.py DELETED
@@ -1,79 +0,0 @@
1
- from pathlib import Path
2
-
3
- from backend.execution.docker_runner import DockerRunnerBase
4
- from backend.execution.result_parser import parse_verification_result
5
- from backend.execution.sandbox import ExecutionResult, write_project_files
6
- from backend.orchestration.errors import TaskExecutionError
7
- from backend.orchestration.task_context import TaskContext
8
-
9
-
10
- class VerificationPipeline:
11
- """Code → Static Analysis gate → Docker → Test Execution → Verification."""
12
-
13
- def __init__(self, docker_runner: DockerRunnerBase) -> None:
14
- self.docker_runner = docker_runner
15
-
16
- async def run(self, context: TaskContext) -> dict:
17
- files = context.all_project_files
18
- if not files:
19
- raise TaskExecutionError(
20
- "No project files available for verification",
21
- error_type="RUNTIME_ERROR",
22
- )
23
-
24
- static_result = context.task_outputs_by_type.get("static_analysis")
25
- if static_result and static_result.get("status") != "PASS":
26
- raise TaskExecutionError(
27
- "Static analysis did not pass; skipping execution",
28
- error_type="STATIC_ANALYSIS_ERROR",
29
- )
30
-
31
- workspace = context.workspace_path / "verify"
32
- write_project_files(workspace, files)
33
-
34
- execution: ExecutionResult | None = None
35
- try:
36
- execution = await self.docker_runner.run_tests(workspace)
37
- verification = parse_verification_result(
38
- execution.stdout,
39
- execution.stderr,
40
- execution.exit_code,
41
- )
42
-
43
- if execution.timed_out:
44
- raise TaskExecutionError(
45
- "Docker execution timed out",
46
- error_type="TIMEOUT",
47
- )
48
-
49
- if not verification["verification_passed"]:
50
- raise TaskExecutionError(
51
- f"Verification failed: {verification['tests_failed']} test(s) failed",
52
- error_type="TEST_FAILURE",
53
- )
54
-
55
- return {
56
- "status": "PASS",
57
- "stdout": execution.stdout,
58
- "stderr": execution.stderr,
59
- "exit_code": execution.exit_code,
60
- "execution_time": execution.execution_time,
61
- "timed_out": execution.timed_out,
62
- "container_build_status": execution.container_build_status,
63
- "container_start_status": execution.container_start_status,
64
- "verification": verification,
65
- }
66
- finally:
67
- await self.docker_runner.cleanup(
68
- execution.container_id if execution else None
69
- )
70
-
71
-
72
- async def run_verification(context: TaskContext) -> dict:
73
- docker_runner = context.docker_runner
74
- if docker_runner is None:
75
- from backend.execution.docker_runner import DockerCliRunner
76
-
77
- docker_runner = DockerCliRunner()
78
- pipeline = VerificationPipeline(docker_runner)
79
- return await pipeline.run(context)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/llm/__init__.py DELETED
File without changes
backend/llm/base.py DELETED
@@ -1,48 +0,0 @@
1
- import json
2
- from abc import ABC, abstractmethod
3
- from typing import Any
4
-
5
- from backend.llm.types import LLMCostEstimate, LLMResponse
6
-
7
-
8
- class LLMProvider(ABC):
9
- """Abstract LLM provider interface used by agents and orchestration."""
10
-
11
- provider_name: str
12
-
13
- @abstractmethod
14
- async def generate(
15
- self,
16
- prompt: str,
17
- *,
18
- system_prompt: str | None = None,
19
- temperature: float = 0.2,
20
- max_tokens: int | None = None,
21
- ) -> LLMResponse:
22
- """Generate a text completion from the provider."""
23
-
24
- @abstractmethod
25
- async def generate_structured(
26
- self,
27
- prompt: str,
28
- *,
29
- schema: dict[str, Any],
30
- system_prompt: str | None = None,
31
- temperature: float = 0.0,
32
- ) -> dict[str, Any]:
33
- """Generate structured JSON output validated against a schema description."""
34
-
35
- @abstractmethod
36
- def estimate_cost(self, input_tokens: int, output_tokens: int) -> LLMCostEstimate:
37
- """Estimate API cost for a token usage pair."""
38
-
39
- def _parse_json_content(self, content: str) -> dict[str, Any]:
40
- text = content.strip()
41
- if text.startswith("```"):
42
- lines = text.splitlines()
43
- if lines and lines[0].startswith("```"):
44
- lines = lines[1:]
45
- if lines and lines[-1].strip() == "```":
46
- lines = lines[:-1]
47
- text = "\n".join(lines).strip()
48
- return json.loads(text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/llm/providers/__init__.py DELETED
@@ -1,20 +0,0 @@
1
- from backend.llm.base import LLMProvider
2
- from backend.llm.providers.openai_provider import OpenAIProvider
3
- from backend.llm.providers.gemini_provider import GeminiProvider
4
-
5
- SUPPORTED_PROVIDERS: dict[str, type[LLMProvider]] = {
6
- "openai": OpenAIProvider,
7
- "gemini": GeminiProvider,
8
- }
9
-
10
-
11
- def get_llm_provider(provider: str, api_key: str, model: str) -> LLMProvider:
12
- provider_key = provider.lower().strip()
13
- if provider_key not in SUPPORTED_PROVIDERS:
14
- supported = ", ".join(sorted(SUPPORTED_PROVIDERS))
15
- raise ValueError(f"Unsupported LLM provider '{provider}'. Supported: {supported}")
16
- return SUPPORTED_PROVIDERS[provider_key](api_key=api_key, model=model)
17
-
18
-
19
- def list_supported_providers() -> list[str]:
20
- return sorted(SUPPORTED_PROVIDERS.keys())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/llm/providers/gemini_provider.py DELETED
@@ -1,98 +0,0 @@
1
- import httpx
2
-
3
- from backend.llm.base import LLMProvider
4
- from backend.llm.types import LLMCostEstimate, LLMResponse
5
-
6
- _GEMINI_PRICING: dict[str, dict[str, float]] = {
7
- "gemini-3.1-pro": {"input": 1.25, "output": 5.00},
8
- "gemini-3.1-flash": {"input": 0.075, "output": 0.30},
9
- }
10
-
11
-
12
- class GeminiProvider(LLMProvider):
13
- provider_name = "gemini"
14
-
15
- def __init__(self, api_key: str, model: str) -> None:
16
- self.api_key = api_key
17
- self.model = model
18
- self.base_url = "https://generativelanguage.googleapis.com/v1beta"
19
-
20
- async def generate(
21
- self,
22
- prompt: str,
23
- *,
24
- system_prompt: str | None = None,
25
- temperature: float = 0.2,
26
- max_tokens: int | None = None,
27
- ) -> LLMResponse:
28
- payload: dict = {
29
- "contents": [{"role": "user", "parts": [{"text": prompt}]}],
30
- "generationConfig": {
31
- "temperature": temperature,
32
- }
33
- }
34
-
35
- if system_prompt:
36
- payload["systemInstruction"] = {"parts": [{"text": system_prompt}]}
37
-
38
- if max_tokens is not None:
39
- payload["generationConfig"]["maxOutputTokens"] = max_tokens
40
-
41
- async with httpx.AsyncClient(timeout=120.0) as client:
42
- response = await client.post(
43
- f"{self.base_url}/models/{self.model}:generateContent",
44
- params={"key": self.api_key},
45
- headers={"Content-Type": "application/json"},
46
- json=payload,
47
- )
48
- response.raise_for_status()
49
- data = response.json()
50
-
51
- try:
52
- choice = data["candidates"][0]["content"]["parts"][0]["text"]
53
- except (KeyError, IndexError):
54
- choice = ""
55
-
56
- usage = data.get("usageMetadata", {})
57
- return LLMResponse(
58
- content=choice,
59
- input_tokens=int(usage.get("promptTokenCount", 0)),
60
- output_tokens=int(usage.get("candidatesTokenCount", 0)),
61
- model=self.model,
62
- provider=self.provider_name,
63
- raw=data,
64
- )
65
-
66
- async def generate_structured(
67
- self,
68
- prompt: str,
69
- *,
70
- schema: dict,
71
- system_prompt: str | None = None,
72
- temperature: float = 0.0,
73
- ) -> dict:
74
- schema_instruction = (
75
- "Respond with valid JSON only that matches this schema description:\n"
76
- f"{schema}"
77
- )
78
- combined_system = (
79
- f"{system_prompt}\n\n{schema_instruction}" if system_prompt else schema_instruction
80
- )
81
- response = await self.generate(
82
- prompt,
83
- system_prompt=combined_system,
84
- temperature=temperature,
85
- )
86
- return self._parse_json_content(response.content)
87
-
88
- def estimate_cost(self, input_tokens: int, output_tokens: int) -> LLMCostEstimate:
89
- pricing = _GEMINI_PRICING.get(self.model, {"input": 1.0, "output": 3.0})
90
- input_cost = (input_tokens / 1_000_000) * pricing["input"]
91
- output_cost = (output_tokens / 1_000_000) * pricing["output"]
92
- return LLMCostEstimate(
93
- input_tokens=input_tokens,
94
- output_tokens=output_tokens,
95
- estimated_cost_usd=round(input_cost + output_cost, 6),
96
- provider=self.provider_name,
97
- model=self.model,
98
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/llm/providers/mock_provider.py DELETED
@@ -1,56 +0,0 @@
1
- from typing import Any
2
-
3
- from backend.llm.base import LLMProvider
4
- from backend.llm.types import LLMCostEstimate, LLMResponse
5
-
6
-
7
- class MockLLMProvider(LLMProvider):
8
- """Test double for LLM provider interactions."""
9
-
10
- provider_name = "mock"
11
-
12
- def __init__(
13
- self,
14
- *,
15
- structured_response: dict[str, Any] | None = None,
16
- text_response: str = "mock response",
17
- model: str = "mock-model",
18
- ) -> None:
19
- self.structured_response = structured_response or {"tasks": []}
20
- self.text_response = text_response
21
- self.model = model
22
-
23
- async def generate(
24
- self,
25
- prompt: str,
26
- *,
27
- system_prompt: str | None = None,
28
- temperature: float = 0.2,
29
- max_tokens: int | None = None,
30
- ) -> LLMResponse:
31
- return LLMResponse(
32
- content=self.text_response,
33
- input_tokens=10,
34
- output_tokens=20,
35
- model=self.model,
36
- provider=self.provider_name,
37
- )
38
-
39
- async def generate_structured(
40
- self,
41
- prompt: str,
42
- *,
43
- schema: dict[str, Any],
44
- system_prompt: str | None = None,
45
- temperature: float = 0.0,
46
- ) -> dict[str, Any]:
47
- return self.structured_response
48
-
49
- def estimate_cost(self, input_tokens: int, output_tokens: int) -> LLMCostEstimate:
50
- return LLMCostEstimate(
51
- input_tokens=input_tokens,
52
- output_tokens=output_tokens,
53
- estimated_cost_usd=0.0,
54
- provider=self.provider_name,
55
- model=self.model,
56
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/llm/providers/openai_provider.py DELETED
@@ -1,99 +0,0 @@
1
- import httpx
2
-
3
- from backend.llm.base import LLMProvider
4
- from backend.llm.types import LLMCostEstimate, LLMResponse
5
-
6
- # Approximate OpenAI pricing per 1M tokens (USD) — used for estimates only.
7
- _OPENAI_PRICING: dict[str, dict[str, float]] = {
8
- "gpt-4o": {"input": 2.50, "output": 10.00},
9
- "gpt-4o-mini": {"input": 0.15, "output": 0.60},
10
- "gpt-4.1": {"input": 2.00, "output": 8.00},
11
- "gpt-4.1-mini": {"input": 0.40, "output": 1.60},
12
- }
13
-
14
-
15
- class OpenAIProvider(LLMProvider):
16
- provider_name = "openai"
17
-
18
- def __init__(self, api_key: str, model: str, base_url: str = "https://api.openai.com/v1") -> None:
19
- self.api_key = api_key
20
- self.model = model
21
- self.base_url = base_url.rstrip("/")
22
-
23
- async def generate(
24
- self,
25
- prompt: str,
26
- *,
27
- system_prompt: str | None = None,
28
- temperature: float = 0.2,
29
- max_tokens: int | None = None,
30
- ) -> LLMResponse:
31
- messages: list[dict[str, str]] = []
32
- if system_prompt:
33
- messages.append({"role": "system", "content": system_prompt})
34
- messages.append({"role": "user", "content": prompt})
35
-
36
- payload: dict = {
37
- "model": self.model,
38
- "messages": messages,
39
- "temperature": temperature,
40
- }
41
- if max_tokens is not None:
42
- payload["max_tokens"] = max_tokens
43
-
44
- async with httpx.AsyncClient(timeout=120.0) as client:
45
- response = await client.post(
46
- f"{self.base_url}/chat/completions",
47
- headers={
48
- "Authorization": f"Bearer {self.api_key}",
49
- "Content-Type": "application/json",
50
- },
51
- json=payload,
52
- )
53
- response.raise_for_status()
54
- data = response.json()
55
-
56
- choice = data["choices"][0]["message"]["content"]
57
- usage = data.get("usage", {})
58
- return LLMResponse(
59
- content=choice,
60
- input_tokens=int(usage.get("prompt_tokens", 0)),
61
- output_tokens=int(usage.get("completion_tokens", 0)),
62
- model=self.model,
63
- provider=self.provider_name,
64
- raw=data,
65
- )
66
-
67
- async def generate_structured(
68
- self,
69
- prompt: str,
70
- *,
71
- schema: dict,
72
- system_prompt: str | None = None,
73
- temperature: float = 0.0,
74
- ) -> dict:
75
- schema_instruction = (
76
- "Respond with valid JSON only that matches this schema description:\n"
77
- f"{schema}"
78
- )
79
- combined_system = (
80
- f"{system_prompt}\n\n{schema_instruction}" if system_prompt else schema_instruction
81
- )
82
- response = await self.generate(
83
- prompt,
84
- system_prompt=combined_system,
85
- temperature=temperature,
86
- )
87
- return self._parse_json_content(response.content)
88
-
89
- def estimate_cost(self, input_tokens: int, output_tokens: int) -> LLMCostEstimate:
90
- pricing = _OPENAI_PRICING.get(self.model, {"input": 1.0, "output": 3.0})
91
- input_cost = (input_tokens / 1_000_000) * pricing["input"]
92
- output_cost = (output_tokens / 1_000_000) * pricing["output"]
93
- return LLMCostEstimate(
94
- input_tokens=input_tokens,
95
- output_tokens=output_tokens,
96
- estimated_cost_usd=round(input_cost + output_cost, 6),
97
- provider=self.provider_name,
98
- model=self.model,
99
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/llm/types.py DELETED
@@ -1,21 +0,0 @@
1
- from dataclasses import dataclass
2
- from typing import Any
3
-
4
-
5
- @dataclass
6
- class LLMResponse:
7
- content: str
8
- input_tokens: int
9
- output_tokens: int
10
- model: str
11
- provider: str
12
- raw: dict[str, Any] | None = None
13
-
14
-
15
- @dataclass
16
- class LLMCostEstimate:
17
- input_tokens: int
18
- output_tokens: int
19
- estimated_cost_usd: float
20
- provider: str
21
- model: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/main.py DELETED
@@ -1,98 +0,0 @@
1
- import logging
2
- from contextlib import asynccontextmanager
3
-
4
- from fastapi import FastAPI
5
- from fastapi.middleware.cors import CORSMiddleware
6
- from sqlalchemy import text
7
-
8
- from backend.api.auth import router as auth_router
9
- from backend.api.providers import router as providers_router
10
- from backend.api.settings import router as settings_router
11
- from backend.api.workflows import router as workflows_router
12
- from backend.api.schemas import HealthResponse
13
- from backend.core.config import get_settings
14
- from backend.db.models import Base
15
- from backend.db.session import engine
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- @asynccontextmanager
21
- async def lifespan(app: FastAPI):
22
- settings = get_settings()
23
- logging.basicConfig(level=settings.log_level)
24
- logger.info("Starting AutoForge backend")
25
-
26
- # Automatically initialize database schema for Hugging Face Spaces
27
- async with engine.begin() as conn:
28
- await conn.run_sync(Base.metadata.create_all)
29
-
30
- yield
31
- await engine.dispose()
32
- logger.info("AutoForge backend stopped")
33
-
34
-
35
- def create_app() -> FastAPI:
36
- settings = get_settings()
37
- app = FastAPI(
38
- title="AutoForge API",
39
- description="Autonomous DAG-based code generation and verification platform",
40
- version="0.1.0",
41
- lifespan=lifespan,
42
- )
43
-
44
- origins = [origin.strip() for origin in settings.cors_origins.split(",") if origin.strip()]
45
- app.add_middleware(
46
- CORSMiddleware,
47
- allow_origins=origins,
48
- allow_credentials=True,
49
- allow_methods=["*"],
50
- allow_headers=["*"],
51
- )
52
-
53
- from fastapi import APIRouter
54
- from fastapi.staticfiles import StaticFiles
55
- from fastapi.responses import FileResponse
56
- import os
57
-
58
- api_router = APIRouter(prefix="/api")
59
- api_router.include_router(auth_router)
60
- api_router.include_router(workflows_router)
61
- api_router.include_router(settings_router)
62
- api_router.include_router(providers_router)
63
-
64
- @api_router.get("/health", response_model=HealthResponse, tags=["health"])
65
- async def health_check() -> HealthResponse:
66
- db_status = "disconnected"
67
- try:
68
- async with engine.connect() as conn:
69
- await conn.execute(text("SELECT 1"))
70
- db_status = "connected"
71
- except Exception as exc:
72
- logger.warning("Database health check failed: %s", exc)
73
-
74
- return HealthResponse(
75
- status="ok" if db_status == "connected" else "degraded",
76
- database=db_status,
77
- )
78
-
79
- app.include_router(api_router)
80
-
81
- # Serve static frontend if it exists
82
- frontend_dir = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist")
83
- if os.path.isdir(frontend_dir):
84
- app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dir, "assets")), name="assets")
85
-
86
- @app.get("/{full_path:path}")
87
- async def serve_frontend(full_path: str):
88
- # If a specific file is requested in root (like favicon.ico), try serving it
89
- requested_file = os.path.join(frontend_dir, full_path)
90
- if os.path.isfile(requested_file):
91
- return FileResponse(requested_file)
92
- # Otherwise, fall back to index.html for SPA routing
93
- return FileResponse(os.path.join(frontend_dir, "index.html"))
94
-
95
- return app
96
-
97
-
98
- app = create_app()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/memory/__init__.py DELETED
File without changes
backend/memory/attempt_repository.py DELETED
@@ -1,63 +0,0 @@
1
- from sqlalchemy import func, select
2
- from sqlalchemy.ext.asyncio import AsyncSession
3
-
4
- from backend.db.models import Attempt
5
-
6
-
7
- class AttemptRepository:
8
- def __init__(self, db: AsyncSession) -> None:
9
- self.db = db
10
-
11
- async def get_next_attempt_number(self, workflow_id: int) -> int:
12
- result = await self.db.execute(
13
- select(func.max(Attempt.attempt_number)).where(Attempt.workflow_id == workflow_id)
14
- )
15
- current_max = result.scalar()
16
- return int(current_max or 0) + 1
17
-
18
- async def list_attempts_for_workflow(self, workflow_id: int) -> list[Attempt]:
19
- result = await self.db.execute(
20
- select(Attempt)
21
- .where(Attempt.workflow_id == workflow_id)
22
- .order_by(Attempt.attempt_number.asc(), Attempt.id.asc())
23
- )
24
- return list(result.scalars().all())
25
-
26
- async def count_attempts(self, workflow_id: int) -> int:
27
- result = await self.db.execute(
28
- select(func.count()).select_from(Attempt).where(Attempt.workflow_id == workflow_id)
29
- )
30
- return int(result.scalar() or 0)
31
-
32
- async def create_attempt(
33
- self,
34
- *,
35
- workflow_id: int,
36
- task_id: int,
37
- attempt_number: int = 1,
38
- generated_code: dict | None = None,
39
- generated_tests: dict | None = None,
40
- static_analysis_result: dict | None = None,
41
- stdout: str | None = None,
42
- stderr: str | None = None,
43
- exit_code: int | None = None,
44
- result: str | None = None,
45
- error: dict | None = None,
46
- ) -> Attempt:
47
- attempt = Attempt(
48
- workflow_id=workflow_id,
49
- task_id=task_id,
50
- attempt_number=attempt_number,
51
- generated_code=generated_code,
52
- generated_tests=generated_tests,
53
- static_analysis_result=static_analysis_result,
54
- stdout=stdout,
55
- stderr=stderr,
56
- exit_code=exit_code,
57
- result=result,
58
- error=error,
59
- )
60
- self.db.add(attempt)
61
- await self.db.flush()
62
- await self.db.refresh(attempt)
63
- return attempt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/memory/task_repository.py DELETED
@@ -1,99 +0,0 @@
1
- from datetime import UTC, datetime
2
-
3
- from sqlalchemy import select
4
- from sqlalchemy.ext.asyncio import AsyncSession
5
-
6
- from backend.db.models import Task, TaskStatus, Workflow, WorkflowStatus
7
- from backend.orchestration.dag_schema import DAGPlan
8
- from backend.orchestration.state_machine import validate_transition
9
-
10
-
11
- class TaskRepository:
12
- def __init__(self, db: AsyncSession) -> None:
13
- self.db = db
14
-
15
- async def create_tasks_from_plan(
16
- self, workflow: Workflow, plan: DAGPlan
17
- ) -> list[Task]:
18
- tasks: list[Task] = []
19
- for task_def in plan.tasks:
20
- task = Task(
21
- workflow_id=workflow.id,
22
- task_key=task_def.id,
23
- task_type=task_def.type.value,
24
- status=TaskStatus.CREATED,
25
- depends_on=task_def.depends_on,
26
- )
27
- self.db.add(task)
28
- tasks.append(task)
29
- await self.db.flush()
30
- for task in tasks:
31
- await self.db.refresh(task)
32
- return tasks
33
-
34
- async def get_tasks_for_workflow(self, workflow_db_id: int) -> list[Task]:
35
- result = await self.db.execute(
36
- select(Task)
37
- .where(Task.workflow_id == workflow_db_id)
38
- .order_by(Task.id.asc())
39
- )
40
- return list(result.scalars().all())
41
-
42
- async def get_task_by_key(self, workflow_db_id: int, task_key: str) -> Task | None:
43
- result = await self.db.execute(
44
- select(Task).where(
45
- Task.workflow_id == workflow_db_id,
46
- Task.task_key == task_key,
47
- )
48
- )
49
- return result.scalar_one_or_none()
50
-
51
- async def get_task_by_type(self, workflow_db_id: int, task_type: str) -> Task | None:
52
- result = await self.db.execute(
53
- select(Task).where(
54
- Task.workflow_id == workflow_db_id,
55
- Task.task_type == task_type,
56
- )
57
- )
58
- return result.scalar_one_or_none()
59
-
60
- async def transition_task(
61
- self,
62
- task: Task,
63
- target_status: TaskStatus | str,
64
- *,
65
- output: dict | None = None,
66
- set_started: bool = False,
67
- set_completed: bool = False,
68
- ) -> Task:
69
- validate_transition(task.status, target_status)
70
- task.status = str(target_status)
71
- if output is not None:
72
- task.output = output
73
- if set_started and task.started_at is None:
74
- task.started_at = datetime.now(UTC)
75
- if set_completed:
76
- task.completed_at = datetime.now(UTC)
77
- await self.db.flush()
78
- await self.db.refresh(task)
79
- return task
80
-
81
- async def mark_workflow_status(
82
- self,
83
- workflow: Workflow,
84
- status: WorkflowStatus | str,
85
- *,
86
- completed: bool = False,
87
- ) -> Workflow:
88
- workflow.status = str(status)
89
- if completed:
90
- workflow.completed_at = datetime.now(UTC)
91
- await self.db.flush()
92
- await self.db.refresh(workflow)
93
- return workflow
94
-
95
- async def save_planner_output(self, workflow: Workflow, planner_output: dict) -> Workflow:
96
- workflow.planner_output = planner_output
97
- await self.db.flush()
98
- await self.db.refresh(workflow)
99
- return workflow
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/memory/workflow_repository.py DELETED
@@ -1,84 +0,0 @@
1
- import uuid
2
- from datetime import datetime
3
-
4
- from sqlalchemy import select
5
- from sqlalchemy.ext.asyncio import AsyncSession
6
- from sqlalchemy.orm import selectinload
7
-
8
- from backend.db.models import Event, User, Workflow, WorkflowStatus
9
-
10
-
11
- def generate_workflow_id() -> str:
12
- return f"wf_{uuid.uuid4().hex[:12]}"
13
-
14
-
15
- class WorkflowRepository:
16
- def __init__(self, db: AsyncSession) -> None:
17
- self.db = db
18
-
19
- async def create_workflow(
20
- self,
21
- *,
22
- user_id: int,
23
- user_prompt: str,
24
- llm_provider: str | None = None,
25
- llm_model: str | None = None,
26
- ) -> Workflow:
27
- workflow = Workflow(
28
- workflow_id=generate_workflow_id(),
29
- user_id=user_id,
30
- user_prompt=user_prompt,
31
- status=WorkflowStatus.CREATED,
32
- llm_provider=llm_provider,
33
- llm_model=llm_model,
34
- )
35
- self.db.add(workflow)
36
- await self.db.flush()
37
-
38
- event = Event(
39
- workflow_id=workflow.id,
40
- event_type="WORKFLOW_STARTED",
41
- message="Workflow created",
42
- event_metadata={"workflow_id": workflow.workflow_id},
43
- )
44
- self.db.add(event)
45
- await self.db.flush()
46
- await self.db.refresh(workflow)
47
- return workflow
48
-
49
- async def get_workflow_by_public_id(
50
- self, workflow_id: str, user_id: int
51
- ) -> Workflow | None:
52
- result = await self.db.execute(
53
- select(Workflow)
54
- .where(Workflow.workflow_id == workflow_id, Workflow.user_id == user_id)
55
- .options(selectinload(Workflow.events))
56
- )
57
- return result.scalar_one_or_none()
58
-
59
- async def list_workflows_for_user(self, user_id: int) -> list[Workflow]:
60
- result = await self.db.execute(
61
- select(Workflow)
62
- .where(Workflow.user_id == user_id)
63
- .order_by(Workflow.created_at.desc())
64
- )
65
- return list(result.scalars().all())
66
-
67
- async def get_user_by_email(self, email: str) -> User | None:
68
- result = await self.db.execute(select(User).where(User.email == email))
69
- return result.scalar_one_or_none()
70
-
71
- async def get_user_by_username(self, username: str) -> User | None:
72
- result = await self.db.execute(select(User).where(User.username == username))
73
- return result.scalar_one_or_none()
74
-
75
- async def get_user_by_id(self, user_id: int) -> User | None:
76
- result = await self.db.execute(select(User).where(User.id == user_id))
77
- return result.scalar_one_or_none()
78
-
79
- async def create_user(self, *, email: str, username: str, password_hash: str) -> User:
80
- user = User(email=email, username=username, password_hash=password_hash)
81
- self.db.add(user)
82
- await self.db.flush()
83
- await self.db.refresh(user)
84
- return user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/orchestration/__init__.py DELETED
@@ -1 +0,0 @@
1
- """DAG engine — validation, scheduling, execution, and workflow orchestration."""
 
 
backend/orchestration/dag_engine.py DELETED
@@ -1,174 +0,0 @@
1
- import asyncio
2
- from dataclasses import dataclass, field
3
- from datetime import UTC, datetime
4
- from typing import Any
5
-
6
- from backend.agents.task_handlers import get_task_handler
7
- from backend.orchestration.dag_schema import DAGPlan, DAGTaskDefinition, TaskType
8
- from backend.orchestration.errors import TaskExecutionError
9
- from backend.orchestration.scheduler import get_ready_task_ids
10
- from backend.orchestration.task_context import TaskContext
11
-
12
-
13
- @dataclass
14
- class TaskRunRecord:
15
- task_key: str
16
- task_type: TaskType
17
- status: str
18
- output: dict[str, Any] | None = None
19
- error: dict[str, Any] | None = None
20
- started_at: datetime | None = None
21
- completed_at: datetime | None = None
22
-
23
-
24
- @dataclass
25
- class DAGExecutionResult:
26
- success: bool
27
- task_results: dict[str, TaskRunRecord] = field(default_factory=dict)
28
- error: str | None = None
29
- task_outputs_by_type: dict[str, dict[str, Any]] = field(default_factory=dict)
30
- failed_task_type: str | None = None
31
- failure_error: dict[str, Any] | None = None
32
-
33
-
34
- class DAGExecutor:
35
- """Executes a validated DAG with parallel execution of independent tasks."""
36
-
37
- def __init__(
38
- self,
39
- *,
40
- base_context: dict[str, Any] | None = None,
41
- on_task_started: Any | None = None,
42
- on_task_completed: Any | None = None,
43
- on_task_failed: Any | None = None,
44
- ) -> None:
45
- self.base_context = base_context or {}
46
- self.on_task_started = on_task_started
47
- self.on_task_completed = on_task_completed
48
- self.on_task_failed = on_task_failed
49
- self.task_outputs: dict[str, dict[str, Any]] = {}
50
- self.task_outputs_by_type: dict[str, dict[str, Any]] = {}
51
-
52
- async def execute(self, plan: DAGPlan) -> DAGExecutionResult:
53
- completed: set[str] = set()
54
- results: dict[str, TaskRunRecord] = {
55
- task.id: TaskRunRecord(task_key=task.id, task_type=task.type, status="CREATED")
56
- for task in plan.tasks
57
- }
58
-
59
- while len(completed) < len(plan.tasks):
60
- ready_ids = get_ready_task_ids(plan, completed)
61
- if not ready_ids:
62
- return DAGExecutionResult(
63
- success=False,
64
- task_results=results,
65
- error="Deadlock detected: no ready tasks but DAG is incomplete",
66
- )
67
-
68
- layer_outcome = await self._execute_layer(plan, ready_ids, results)
69
- if not layer_outcome.success:
70
- return layer_outcome
71
-
72
- completed.update(ready_ids)
73
-
74
- return DAGExecutionResult(
75
- success=True,
76
- task_results=results,
77
- task_outputs_by_type=dict(self.task_outputs_by_type),
78
- )
79
-
80
- async def _execute_layer(
81
- self,
82
- plan: DAGPlan,
83
- ready_ids: list[str],
84
- results: dict[str, TaskRunRecord],
85
- ) -> DAGExecutionResult:
86
- outcomes = await asyncio.gather(
87
- *[self._run_task(plan, task_id, results) for task_id in ready_ids],
88
- return_exceptions=True,
89
- )
90
-
91
- for outcome in outcomes:
92
- if isinstance(outcome, DAGExecutionResult):
93
- return outcome
94
- if isinstance(outcome, Exception):
95
- return DAGExecutionResult(
96
- success=False,
97
- task_results=results,
98
- error=str(outcome),
99
- )
100
-
101
- return DAGExecutionResult(success=True, task_results=results)
102
-
103
- def _build_context(self, task_def: DAGTaskDefinition) -> TaskContext:
104
- return TaskContext(
105
- workflow_id=self.base_context["workflow_id"],
106
- user_prompt=self.base_context["user_prompt"],
107
- llm=self.base_context["llm"],
108
- workspace_path=self.base_context["workspace_path"],
109
- task_def=task_def,
110
- task_outputs=dict(self.task_outputs),
111
- task_outputs_by_type=dict(self.task_outputs_by_type),
112
- docker_runner=self.base_context.get("docker_runner"),
113
- use_stubs=self.base_context.get("use_stubs", False),
114
- fail_task=self.base_context.get("fail_task"),
115
- stub_delay=self.base_context.get("stub_delay", 0),
116
- stub_execute_calls_docker=self.base_context.get("stub_execute_calls_docker", False),
117
- )
118
-
119
- async def _run_task(
120
- self,
121
- plan: DAGPlan,
122
- task_id: str,
123
- results: dict[str, TaskRunRecord],
124
- ) -> None | DAGExecutionResult:
125
- task_def = plan.task_map()[task_id]
126
- record = results[task_id]
127
- record.status = "RUNNING"
128
- record.started_at = datetime.now(UTC)
129
-
130
- if self.on_task_started:
131
- await self.on_task_started(task_def)
132
-
133
- context = self._build_context(task_def)
134
-
135
- try:
136
- handler = get_task_handler(task_def.type)
137
- output = await handler(context)
138
- record.output = output
139
- record.status = "SUCCESS"
140
- record.completed_at = datetime.now(UTC)
141
- self.task_outputs[task_id] = output
142
- self.task_outputs_by_type[task_def.type.value] = output
143
- if self.on_task_completed:
144
- await self.on_task_completed(task_def, output)
145
- except TaskExecutionError as exc:
146
- record.status = "FAILED"
147
- record.completed_at = datetime.now(UTC)
148
- record.error = {"error_type": exc.error_type, "message": exc.message}
149
- if self.on_task_failed:
150
- await self.on_task_failed(task_def, record.error)
151
- return DAGExecutionResult(
152
- success=False,
153
- task_results=results,
154
- error=exc.message,
155
- task_outputs_by_type=dict(self.task_outputs_by_type),
156
- failed_task_type=task_def.type.value,
157
- failure_error=record.error,
158
- )
159
- except Exception as exc:
160
- record.status = "FAILED"
161
- record.completed_at = datetime.now(UTC)
162
- record.error = {"error_type": "TASK_EXECUTION_ERROR", "message": str(exc)}
163
- if self.on_task_failed:
164
- await self.on_task_failed(task_def, record.error)
165
- return DAGExecutionResult(
166
- success=False,
167
- task_results=results,
168
- error=str(exc),
169
- task_outputs_by_type=dict(self.task_outputs_by_type),
170
- failed_task_type=task_def.type.value,
171
- failure_error=record.error,
172
- )
173
-
174
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/orchestration/dag_schema.py DELETED
@@ -1,42 +0,0 @@
1
- from enum import StrEnum
2
-
3
- from pydantic import BaseModel, Field, field_validator
4
-
5
-
6
- class TaskType(StrEnum):
7
- CODE_GENERATION = "code_generation"
8
- TEST_GENERATION = "test_generation"
9
- STATIC_ANALYSIS = "static_analysis"
10
- EXECUTE = "execute"
11
-
12
-
13
- VALID_TASK_TYPES = frozenset(TaskType)
14
-
15
-
16
- class DAGTaskDefinition(BaseModel):
17
- id: str = Field(min_length=1, max_length=64)
18
- type: TaskType
19
- depends_on: list[str] = Field(default_factory=list)
20
-
21
- @field_validator("id")
22
- @classmethod
23
- def normalize_id(cls, value: str) -> str:
24
- normalized = value.strip()
25
- if not normalized:
26
- raise ValueError("Task id cannot be empty")
27
- return normalized
28
-
29
- @field_validator("depends_on")
30
- @classmethod
31
- def normalize_dependencies(cls, value: list[str]) -> list[str]:
32
- return [dep.strip() for dep in value if dep.strip()]
33
-
34
-
35
- class DAGPlan(BaseModel):
36
- tasks: list[DAGTaskDefinition] = Field(min_length=1)
37
-
38
- def task_ids(self) -> set[str]:
39
- return {task.id for task in self.tasks}
40
-
41
- def task_map(self) -> dict[str, DAGTaskDefinition]:
42
- return {task.id: task for task in self.tasks}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/orchestration/dag_validator.py DELETED
@@ -1,85 +0,0 @@
1
- class DAGValidationError(Exception):
2
- """Raised when a planner-produced DAG fails validation."""
3
-
4
- def __init__(self, errors: list[str]) -> None:
5
- self.errors = errors
6
- message = "; ".join(errors)
7
- super().__init__(message)
8
-
9
-
10
- def validate_dag(plan: "DAGPlan") -> None:
11
- """Validate DAG structure, references, and acyclicity."""
12
- from backend.orchestration.dag_schema import DAGPlan as DAGPlanModel
13
-
14
- if not isinstance(plan, DAGPlanModel):
15
- raise TypeError("plan must be a DAGPlan instance")
16
-
17
- errors: list[str] = []
18
- task_ids = plan.task_ids()
19
-
20
- if len(task_ids) != len(plan.tasks):
21
- errors.append("Duplicate task ids are not allowed")
22
-
23
- for task in plan.tasks:
24
- for dependency in task.depends_on:
25
- if dependency == task.id:
26
- errors.append(f"Task '{task.id}' cannot depend on itself")
27
- elif dependency not in task_ids:
28
- errors.append(
29
- f"Task '{task.id}' depends on unknown task '{dependency}'"
30
- )
31
-
32
- if errors:
33
- raise DAGValidationError(errors)
34
-
35
- if _has_cycle(plan):
36
- raise DAGValidationError(["DAG contains a cycle"])
37
-
38
-
39
- def _has_cycle(plan: "DAGPlan") -> bool:
40
- graph = {task.id: list(task.depends_on) for task in plan.tasks}
41
- visited: set[str] = set()
42
- stack: set[str] = set()
43
-
44
- def visit(node: str) -> bool:
45
- if node in stack:
46
- return True
47
- if node in visited:
48
- return False
49
- visited.add(node)
50
- stack.add(node)
51
- for dependency in graph[node]:
52
- if visit(dependency):
53
- return True
54
- stack.remove(node)
55
- return False
56
-
57
- return any(visit(task_id) for task_id in graph)
58
-
59
-
60
- def topological_order(plan: "DAGPlan") -> list[str]:
61
- """Return a valid topological ordering of task ids."""
62
- validate_dag(plan)
63
-
64
- in_degree = {task.id: 0 for task in plan.tasks}
65
- dependents: dict[str, list[str]] = {task.id: [] for task in plan.tasks}
66
-
67
- for task in plan.tasks:
68
- for dependency in task.depends_on:
69
- in_degree[task.id] += 1
70
- dependents[dependency].append(task.id)
71
-
72
- ready = [task_id for task_id, degree in in_degree.items() if degree == 0]
73
- order: list[str] = []
74
-
75
- while ready:
76
- current = ready.pop(0)
77
- order.append(current)
78
- for dependent in dependents[current]:
79
- in_degree[dependent] -= 1
80
- if in_degree[dependent] == 0:
81
- ready.append(dependent)
82
-
83
- if len(order) != len(plan.tasks):
84
- raise DAGValidationError(["DAG contains a cycle"])
85
- return order