|
|
|
@@ -3,6 +3,7 @@
|
|
|
|
from __future__ import annotations
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import difflib
|
|
|
|
import difflib
|
|
|
|
|
|
|
|
import json
|
|
|
|
import py_compile
|
|
|
|
import py_compile
|
|
|
|
import re
|
|
|
|
import re
|
|
|
|
import subprocess
|
|
|
|
import subprocess
|
|
|
|
@@ -14,18 +15,23 @@ try:
|
|
|
|
from .database_manager import DatabaseManager
|
|
|
|
from .database_manager import DatabaseManager
|
|
|
|
from .git_manager import GitManager
|
|
|
|
from .git_manager import GitManager
|
|
|
|
from .gitea import GiteaAPI
|
|
|
|
from .gitea import GiteaAPI
|
|
|
|
|
|
|
|
from .llm_service import LLMServiceClient
|
|
|
|
from .ui_manager import UIManager
|
|
|
|
from .ui_manager import UIManager
|
|
|
|
except ImportError:
|
|
|
|
except ImportError:
|
|
|
|
from config import settings
|
|
|
|
from config import settings
|
|
|
|
from agents.database_manager import DatabaseManager
|
|
|
|
from agents.database_manager import DatabaseManager
|
|
|
|
from agents.git_manager import GitManager
|
|
|
|
from agents.git_manager import GitManager
|
|
|
|
from agents.gitea import GiteaAPI
|
|
|
|
from agents.gitea import GiteaAPI
|
|
|
|
|
|
|
|
from agents.llm_service import LLMServiceClient
|
|
|
|
from agents.ui_manager import UIManager
|
|
|
|
from agents.ui_manager import UIManager
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentOrchestrator:
|
|
|
|
class AgentOrchestrator:
|
|
|
|
"""Orchestrates the software generation process with full audit trail."""
|
|
|
|
"""Orchestrates the software generation process with full audit trail."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
REMOTE_READY_REPOSITORY_MODES = {'project', 'onboarded'}
|
|
|
|
|
|
|
|
REMOTE_READY_REPOSITORY_STATUSES = {'created', 'exists', 'ready', 'onboarded'}
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
self,
|
|
|
|
project_id: str,
|
|
|
|
project_id: str,
|
|
|
|
@@ -77,6 +83,7 @@ class AgentOrchestrator:
|
|
|
|
self.branch_name = self._build_pr_branch_name(project_id)
|
|
|
|
self.branch_name = self._build_pr_branch_name(project_id)
|
|
|
|
self.active_pull_request = None
|
|
|
|
self.active_pull_request = None
|
|
|
|
self._gitea_username: str | None = None
|
|
|
|
self._gitea_username: str | None = None
|
|
|
|
|
|
|
|
existing_repository: dict | None = None
|
|
|
|
hinted_issue_number = (related_issue_hint or {}).get('number') if related_issue_hint else None
|
|
|
|
hinted_issue_number = (related_issue_hint or {}).get('number') if related_issue_hint else None
|
|
|
|
self.related_issue_number = hinted_issue_number if hinted_issue_number is not None else self._extract_issue_number(prompt_text)
|
|
|
|
self.related_issue_number = hinted_issue_number if hinted_issue_number is not None else self._extract_issue_number(prompt_text)
|
|
|
|
self.related_issue: dict | None = DatabaseManager._normalize_issue(related_issue_hint)
|
|
|
|
self.related_issue: dict | None = DatabaseManager._normalize_issue(related_issue_hint)
|
|
|
|
@@ -107,9 +114,12 @@ class AgentOrchestrator:
|
|
|
|
latest_ui = self.db_manager._get_latest_ui_snapshot_data(self.history.id)
|
|
|
|
latest_ui = self.db_manager._get_latest_ui_snapshot_data(self.history.id)
|
|
|
|
repository = latest_ui.get('repository') if isinstance(latest_ui, dict) else None
|
|
|
|
repository = latest_ui.get('repository') if isinstance(latest_ui, dict) else None
|
|
|
|
if isinstance(repository, dict) and repository:
|
|
|
|
if isinstance(repository, dict) and repository:
|
|
|
|
|
|
|
|
existing_repository = dict(repository)
|
|
|
|
self.repo_owner = repository.get('owner') or self.repo_owner
|
|
|
|
self.repo_owner = repository.get('owner') or self.repo_owner
|
|
|
|
self.repo_name = repository.get('name') or self.repo_name
|
|
|
|
self.repo_name = repository.get('name') or self.repo_name
|
|
|
|
self.repo_url = repository.get('url') or self.repo_url
|
|
|
|
self.repo_url = repository.get('url') or self.repo_url
|
|
|
|
|
|
|
|
git_state = latest_ui.get('git') if isinstance(latest_ui.get('git'), dict) else {}
|
|
|
|
|
|
|
|
self.branch_name = git_state.get('active_branch') or self.branch_name
|
|
|
|
if self.prompt_text:
|
|
|
|
if self.prompt_text:
|
|
|
|
self.prompt_audit = self.db_manager.log_prompt_submission(
|
|
|
|
self.prompt_audit = self.db_manager.log_prompt_submission(
|
|
|
|
history_id=self.history.id,
|
|
|
|
history_id=self.history.id,
|
|
|
|
@@ -126,18 +136,60 @@ class AgentOrchestrator:
|
|
|
|
self.ui_manager.ui_data["project_root"] = str(self.project_root)
|
|
|
|
self.ui_manager.ui_data["project_root"] = str(self.project_root)
|
|
|
|
self.ui_manager.ui_data["features"] = list(self.features)
|
|
|
|
self.ui_manager.ui_data["features"] = list(self.features)
|
|
|
|
self.ui_manager.ui_data["tech_stack"] = list(self.tech_stack)
|
|
|
|
self.ui_manager.ui_data["tech_stack"] = list(self.tech_stack)
|
|
|
|
self.ui_manager.ui_data["repository"] = {
|
|
|
|
repository_ui = {
|
|
|
|
"owner": self.repo_owner,
|
|
|
|
"owner": self.repo_owner,
|
|
|
|
"name": self.repo_name,
|
|
|
|
"name": self.repo_name,
|
|
|
|
"mode": "project" if settings.use_project_repositories else "shared",
|
|
|
|
"mode": "project" if settings.use_project_repositories else "shared",
|
|
|
|
"status": "pending" if settings.use_project_repositories else "shared",
|
|
|
|
"status": "pending" if settings.use_project_repositories else "shared",
|
|
|
|
"provider": "gitea",
|
|
|
|
"provider": "gitea",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if existing_repository:
|
|
|
|
|
|
|
|
repository_ui.update(existing_repository)
|
|
|
|
|
|
|
|
self.ui_manager.ui_data["repository"] = repository_ui
|
|
|
|
if self.related_issue:
|
|
|
|
if self.related_issue:
|
|
|
|
self.ui_manager.ui_data["related_issue"] = self.related_issue
|
|
|
|
self.ui_manager.ui_data["related_issue"] = self.related_issue
|
|
|
|
if self.active_pull_request:
|
|
|
|
if self.active_pull_request:
|
|
|
|
self.ui_manager.ui_data["pull_request"] = self.active_pull_request
|
|
|
|
self.ui_manager.ui_data["pull_request"] = self.active_pull_request
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _repository_supports_remote_delivery(self, repository: dict | None = None) -> bool:
|
|
|
|
|
|
|
|
"""Return whether repository metadata supports git push and PR delivery."""
|
|
|
|
|
|
|
|
repo = repository or self.ui_manager.ui_data.get('repository') or {}
|
|
|
|
|
|
|
|
return repo.get('mode') in self.REMOTE_READY_REPOSITORY_MODES and repo.get('status') in self.REMOTE_READY_REPOSITORY_STATUSES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _static_files(self) -> dict[str, str]:
|
|
|
|
|
|
|
|
"""Files that do not need prompt-specific generation."""
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
".gitignore": "__pycache__/\n*.pyc\n.venv/\n.pytest_cache/\n.mypy_cache/\n",
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fallback_generated_files(self) -> dict[str, str]:
|
|
|
|
|
|
|
|
"""Deterministic fallback files when LLM generation is unavailable."""
|
|
|
|
|
|
|
|
feature_section = "\n".join(f"- {feature}" for feature in self.features) or "- None specified"
|
|
|
|
|
|
|
|
tech_section = "\n".join(f"- {tech}" for tech in self.tech_stack) or "- Python"
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
"README.md": (
|
|
|
|
|
|
|
|
f"# {self.project_name}\n\n"
|
|
|
|
|
|
|
|
f"{self.description}\n\n"
|
|
|
|
|
|
|
|
"## Features\n"
|
|
|
|
|
|
|
|
f"{feature_section}\n\n"
|
|
|
|
|
|
|
|
"## Tech Stack\n"
|
|
|
|
|
|
|
|
f"{tech_section}\n"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"requirements.txt": "fastapi\nuvicorn\npytest\n",
|
|
|
|
|
|
|
|
"main.py": (
|
|
|
|
|
|
|
|
"from fastapi import FastAPI\n\n"
|
|
|
|
|
|
|
|
"app = FastAPI(title=\"Generated App\")\n\n"
|
|
|
|
|
|
|
|
"@app.get('/')\n"
|
|
|
|
|
|
|
|
"def read_root():\n"
|
|
|
|
|
|
|
|
f" return {{'name': '{self.project_name}', 'status': 'generated', 'features': {self.features!r}}}\n"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"tests/test_app.py": (
|
|
|
|
|
|
|
|
"from main import read_root\n\n"
|
|
|
|
|
|
|
|
"def test_read_root():\n"
|
|
|
|
|
|
|
|
f" assert read_root()['name'] == '{self.project_name}'\n"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def _build_pr_branch_name(self, project_id: str) -> str:
|
|
|
|
def _build_pr_branch_name(self, project_id: str) -> str:
|
|
|
|
"""Build a stable branch name used until the PR is merged."""
|
|
|
|
"""Build a stable branch name used until the PR is merged."""
|
|
|
|
return f"ai/{project_id}"
|
|
|
|
return f"ai/{project_id}"
|
|
|
|
@@ -158,7 +210,7 @@ class AgentOrchestrator:
|
|
|
|
"""Persist the current generation plan as an inspectable trace."""
|
|
|
|
"""Persist the current generation plan as an inspectable trace."""
|
|
|
|
if not self.db_manager or not self.history or not self.prompt_audit:
|
|
|
|
if not self.db_manager or not self.history or not self.prompt_audit:
|
|
|
|
return
|
|
|
|
return
|
|
|
|
planned_files = list(self._template_files().keys())
|
|
|
|
planned_files = list(self._static_files().keys()) + list(self._fallback_generated_files().keys())
|
|
|
|
self.db_manager.log_llm_trace(
|
|
|
|
self.db_manager.log_llm_trace(
|
|
|
|
project_id=self.project_id,
|
|
|
|
project_id=self.project_id,
|
|
|
|
history_id=self.history.id,
|
|
|
|
history_id=self.history.id,
|
|
|
|
@@ -188,6 +240,66 @@ class AgentOrchestrator:
|
|
|
|
fallback_used=False,
|
|
|
|
fallback_used=False,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_generated_files(self, content: str | None) -> dict[str, str]:
|
|
|
|
|
|
|
|
"""Parse an LLM file bundle response into relative-path/content pairs."""
|
|
|
|
|
|
|
|
if not content:
|
|
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
parsed = json.loads(content)
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
allowed_paths = set(self._fallback_generated_files().keys())
|
|
|
|
|
|
|
|
generated: dict[str, str] = {}
|
|
|
|
|
|
|
|
if isinstance(parsed, dict) and isinstance(parsed.get('files'), list):
|
|
|
|
|
|
|
|
for item in parsed['files']:
|
|
|
|
|
|
|
|
if not isinstance(item, dict):
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
path = str(item.get('path') or '').strip()
|
|
|
|
|
|
|
|
file_content = item.get('content')
|
|
|
|
|
|
|
|
if path in allowed_paths and isinstance(file_content, str) and file_content.strip():
|
|
|
|
|
|
|
|
generated[path] = file_content.rstrip() + "\n"
|
|
|
|
|
|
|
|
elif isinstance(parsed, dict):
|
|
|
|
|
|
|
|
for path, file_content in parsed.items():
|
|
|
|
|
|
|
|
if path in allowed_paths and isinstance(file_content, str) and file_content.strip():
|
|
|
|
|
|
|
|
generated[str(path)] = file_content.rstrip() + "\n"
|
|
|
|
|
|
|
|
return generated
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _generate_prompt_driven_files(self) -> tuple[dict[str, str], dict | None]:
|
|
|
|
|
|
|
|
"""Use the configured LLM to generate prompt-specific project files."""
|
|
|
|
|
|
|
|
fallback_files = self._fallback_generated_files()
|
|
|
|
|
|
|
|
system_prompt = (
|
|
|
|
|
|
|
|
'You generate small but concrete starter projects. '
|
|
|
|
|
|
|
|
'Return only JSON. Provide production-like but compact code that directly reflects the user request. '
|
|
|
|
|
|
|
|
'Include the files README.md, requirements.txt, main.py, and tests/test_app.py. '
|
|
|
|
|
|
|
|
'Use FastAPI for Python web requests unless the prompt clearly demands something else. '
|
|
|
|
|
|
|
|
'The test must verify a real behavior from main.py. '
|
|
|
|
|
|
|
|
'Do not wrap the JSON in markdown fences.'
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
user_prompt = (
|
|
|
|
|
|
|
|
f"Project name: {self.project_name}\n"
|
|
|
|
|
|
|
|
f"Description: {self.description}\n"
|
|
|
|
|
|
|
|
f"Original prompt: {self.prompt_text or self.description}\n"
|
|
|
|
|
|
|
|
f"Requested features: {json.dumps(self.features)}\n"
|
|
|
|
|
|
|
|
f"Preferred tech stack: {json.dumps(self.tech_stack)}\n"
|
|
|
|
|
|
|
|
f"Related issue: {json.dumps(self.related_issue) if self.related_issue else 'null'}\n\n"
|
|
|
|
|
|
|
|
"Return JSON shaped as {\"files\": [{\"path\": \"README.md\", \"content\": \"...\"}, ...]}."
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
content, trace = await LLMServiceClient().chat_with_trace(
|
|
|
|
|
|
|
|
stage='generation_plan',
|
|
|
|
|
|
|
|
system_prompt=system_prompt,
|
|
|
|
|
|
|
|
user_prompt=user_prompt,
|
|
|
|
|
|
|
|
tool_context_input={
|
|
|
|
|
|
|
|
'project_id': self.project_id,
|
|
|
|
|
|
|
|
'project_name': self.project_name,
|
|
|
|
|
|
|
|
'repository': self.ui_manager.ui_data.get('repository'),
|
|
|
|
|
|
|
|
'related_issue': self.related_issue,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
expect_json=True,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
generated_files = self._parse_generated_files(content)
|
|
|
|
|
|
|
|
merged_files = {**fallback_files, **generated_files}
|
|
|
|
|
|
|
|
return merged_files, trace
|
|
|
|
|
|
|
|
|
|
|
|
async def _sync_issue_context(self) -> None:
|
|
|
|
async def _sync_issue_context(self) -> None:
|
|
|
|
"""Sync repository issues and resolve a linked issue from the prompt when present."""
|
|
|
|
"""Sync repository issues and resolve a linked issue from the prompt when present."""
|
|
|
|
if not self.db_manager or not self.history:
|
|
|
|
if not self.db_manager or not self.history:
|
|
|
|
@@ -212,6 +324,14 @@ class AgentOrchestrator:
|
|
|
|
self.db_manager.attach_issue_to_prompt(self.prompt_audit.id, self.related_issue)
|
|
|
|
self.db_manager.attach_issue_to_prompt(self.prompt_audit.id, self.related_issue)
|
|
|
|
|
|
|
|
|
|
|
|
async def _ensure_remote_repository(self) -> None:
|
|
|
|
async def _ensure_remote_repository(self) -> None:
|
|
|
|
|
|
|
|
repository = self.ui_manager.ui_data.get("repository") or {}
|
|
|
|
|
|
|
|
if self._repository_supports_remote_delivery(repository):
|
|
|
|
|
|
|
|
repository.setdefault("provider", "gitea")
|
|
|
|
|
|
|
|
repository.setdefault("status", "ready")
|
|
|
|
|
|
|
|
if repository.get("url"):
|
|
|
|
|
|
|
|
self.repo_url = repository.get("url")
|
|
|
|
|
|
|
|
self.ui_manager.ui_data["repository"] = repository
|
|
|
|
|
|
|
|
return
|
|
|
|
if not settings.use_project_repositories:
|
|
|
|
if not settings.use_project_repositories:
|
|
|
|
self.ui_manager.ui_data["repository"]["status"] = "shared"
|
|
|
|
self.ui_manager.ui_data["repository"]["status"] = "shared"
|
|
|
|
if settings.gitea_repo:
|
|
|
|
if settings.gitea_repo:
|
|
|
|
@@ -303,9 +423,7 @@ class AgentOrchestrator:
|
|
|
|
async def _push_branch(self, branch: str) -> dict | None:
|
|
|
|
async def _push_branch(self, branch: str) -> dict | None:
|
|
|
|
"""Push a branch to the configured project repository when available."""
|
|
|
|
"""Push a branch to the configured project repository when available."""
|
|
|
|
repository = self.ui_manager.ui_data.get('repository') or {}
|
|
|
|
repository = self.ui_manager.ui_data.get('repository') or {}
|
|
|
|
if repository.get('mode') != 'project':
|
|
|
|
if not self._repository_supports_remote_delivery(repository):
|
|
|
|
return None
|
|
|
|
|
|
|
|
if repository.get('status') not in {'created', 'exists', 'ready'}:
|
|
|
|
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
if not settings.gitea_token or not self.repo_owner or not self.repo_name:
|
|
|
|
if not settings.gitea_token or not self.repo_owner or not self.repo_name:
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
@@ -352,7 +470,7 @@ class AgentOrchestrator:
|
|
|
|
self.ui_manager.ui_data['pull_request'] = self.active_pull_request
|
|
|
|
self.ui_manager.ui_data['pull_request'] = self.active_pull_request
|
|
|
|
return self.active_pull_request
|
|
|
|
return self.active_pull_request
|
|
|
|
repository = self.ui_manager.ui_data.get('repository') or {}
|
|
|
|
repository = self.ui_manager.ui_data.get('repository') or {}
|
|
|
|
if repository.get('mode') != 'project' or repository.get('status') not in {'created', 'exists', 'ready'}:
|
|
|
|
if not self._repository_supports_remote_delivery(repository):
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
title = f"AI delivery for {self.project_name}"
|
|
|
|
title = f"AI delivery for {self.project_name}"
|
|
|
|
@@ -393,9 +511,7 @@ class AgentOrchestrator:
|
|
|
|
async def _push_remote_commit(self, commit_hash: str, commit_message: str, changed_files: list[str], base_commit: str | None) -> dict | None:
|
|
|
|
async def _push_remote_commit(self, commit_hash: str, commit_message: str, changed_files: list[str], base_commit: str | None) -> dict | None:
|
|
|
|
"""Push the local commit to the provisioned Gitea repository and build browser links."""
|
|
|
|
"""Push the local commit to the provisioned Gitea repository and build browser links."""
|
|
|
|
repository = self.ui_manager.ui_data.get("repository") or {}
|
|
|
|
repository = self.ui_manager.ui_data.get("repository") or {}
|
|
|
|
if repository.get("mode") != "project":
|
|
|
|
if not self._repository_supports_remote_delivery(repository):
|
|
|
|
return None
|
|
|
|
|
|
|
|
if repository.get("status") not in {"created", "exists", "ready"}:
|
|
|
|
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
push_result = await self._push_branch(self.branch_name)
|
|
|
|
push_result = await self._push_branch(self.branch_name)
|
|
|
|
if push_result is None:
|
|
|
|
if push_result is None:
|
|
|
|
@@ -468,34 +584,6 @@ class AgentOrchestrator:
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _template_files(self) -> dict[str, str]:
|
|
|
|
|
|
|
|
feature_section = "\n".join(f"- {feature}" for feature in self.features) or "- None specified"
|
|
|
|
|
|
|
|
tech_section = "\n".join(f"- {tech}" for tech in self.tech_stack) or "- Python"
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
".gitignore": "__pycache__/\n*.pyc\n.venv/\n.pytest_cache/\n.mypy_cache/\n",
|
|
|
|
|
|
|
|
"README.md": (
|
|
|
|
|
|
|
|
f"# {self.project_name}\n\n"
|
|
|
|
|
|
|
|
f"{self.description}\n\n"
|
|
|
|
|
|
|
|
"## Features\n"
|
|
|
|
|
|
|
|
f"{feature_section}\n\n"
|
|
|
|
|
|
|
|
"## Tech Stack\n"
|
|
|
|
|
|
|
|
f"{tech_section}\n"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"requirements.txt": "fastapi\nuvicorn\npytest\n",
|
|
|
|
|
|
|
|
"main.py": (
|
|
|
|
|
|
|
|
"from fastapi import FastAPI\n\n"
|
|
|
|
|
|
|
|
"app = FastAPI(title=\"Generated App\")\n\n"
|
|
|
|
|
|
|
|
"@app.get('/')\n"
|
|
|
|
|
|
|
|
"def read_root():\n"
|
|
|
|
|
|
|
|
f" return {{'name': '{self.project_name}', 'status': 'generated', 'features': {self.features!r}}}\n"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"tests/test_app.py": (
|
|
|
|
|
|
|
|
"from main import read_root\n\n"
|
|
|
|
|
|
|
|
"def test_read_root():\n"
|
|
|
|
|
|
|
|
f" assert read_root()['name'] == '{self.project_name}'\n"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def run(self) -> dict:
|
|
|
|
async def run(self) -> dict:
|
|
|
|
"""Run the software generation process with full audit logging."""
|
|
|
|
"""Run the software generation process with full audit logging."""
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
@@ -585,18 +673,34 @@ class AgentOrchestrator:
|
|
|
|
async def _create_project_structure(self) -> None:
|
|
|
|
async def _create_project_structure(self) -> None:
|
|
|
|
"""Create initial project structure."""
|
|
|
|
"""Create initial project structure."""
|
|
|
|
self.project_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.project_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
for relative_path, content in self._template_files().items():
|
|
|
|
for relative_path, content in self._static_files().items():
|
|
|
|
if relative_path.startswith("main.py") or relative_path.startswith("tests/"):
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
self._write_file(relative_path, content)
|
|
|
|
self._write_file(relative_path, content)
|
|
|
|
self._append_log(f"Project structure created under {self.project_root}.")
|
|
|
|
self._append_log(f"Project structure created under {self.project_root}.")
|
|
|
|
|
|
|
|
|
|
|
|
async def _generate_code(self) -> None:
|
|
|
|
async def _generate_code(self) -> None:
|
|
|
|
"""Generate code using Ollama."""
|
|
|
|
"""Generate code using Ollama."""
|
|
|
|
for relative_path, content in self._template_files().items():
|
|
|
|
generated_files, trace = await self._generate_prompt_driven_files()
|
|
|
|
if relative_path in {"main.py", "tests/test_app.py"}:
|
|
|
|
for relative_path, content in generated_files.items():
|
|
|
|
self._write_file(relative_path, content)
|
|
|
|
self._write_file(relative_path, content)
|
|
|
|
self._append_log("Application entrypoint and smoke test generated.")
|
|
|
|
fallback_used = bool(trace and trace.get('fallback_used')) or trace is None
|
|
|
|
|
|
|
|
if self.db_manager and self.history and self.prompt_audit and trace:
|
|
|
|
|
|
|
|
self.db_manager.log_llm_trace(
|
|
|
|
|
|
|
|
project_id=self.project_id,
|
|
|
|
|
|
|
|
history_id=self.history.id,
|
|
|
|
|
|
|
|
prompt_id=self.prompt_audit.id,
|
|
|
|
|
|
|
|
stage='code_generation',
|
|
|
|
|
|
|
|
provider=trace.get('provider', 'ollama'),
|
|
|
|
|
|
|
|
model=trace.get('model', settings.OLLAMA_MODEL),
|
|
|
|
|
|
|
|
system_prompt=trace.get('system_prompt', ''),
|
|
|
|
|
|
|
|
user_prompt=trace.get('user_prompt', self.prompt_text or self.description),
|
|
|
|
|
|
|
|
assistant_response=trace.get('assistant_response', ''),
|
|
|
|
|
|
|
|
raw_response=trace.get('raw_response'),
|
|
|
|
|
|
|
|
fallback_used=fallback_used,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if fallback_used:
|
|
|
|
|
|
|
|
self._append_log('LLM code generation was unavailable; used deterministic scaffolding fallback.')
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
self._append_log('Application files generated from the prompt with the configured LLM.')
|
|
|
|
|
|
|
|
|
|
|
|
async def _run_tests(self) -> None:
|
|
|
|
async def _run_tests(self) -> None:
|
|
|
|
"""Run tests for the generated code."""
|
|
|
|
"""Run tests for the generated code."""
|
|
|
|
|