feat: better history data, refs NOISSUE
This commit is contained in:
@@ -27,19 +27,23 @@ from sqlalchemy.orm import Session
|
||||
try:
|
||||
from . import __version__, frontend
|
||||
from . import database as database_module
|
||||
from .agents.change_summary import ChangeSummaryGenerator
|
||||
from .agents.database_manager import DatabaseManager
|
||||
from .agents.request_interpreter import RequestInterpreter
|
||||
from .agents.orchestrator import AgentOrchestrator
|
||||
from .agents.n8n_setup import N8NSetupAgent
|
||||
from .agents.prompt_workflow import PromptWorkflowManager
|
||||
from .agents.ui_manager import UIManager
|
||||
from .models import ProjectHistory, ProjectLog, SystemLog
|
||||
except ImportError:
|
||||
import frontend
|
||||
import database as database_module
|
||||
from agents.change_summary import ChangeSummaryGenerator
|
||||
from agents.database_manager import DatabaseManager
|
||||
from agents.request_interpreter import RequestInterpreter
|
||||
from agents.orchestrator import AgentOrchestrator
|
||||
from agents.n8n_setup import N8NSetupAgent
|
||||
from agents.prompt_workflow import PromptWorkflowManager
|
||||
from agents.ui_manager import UIManager
|
||||
from models import ProjectHistory, ProjectLog, SystemLog
|
||||
|
||||
@@ -90,6 +94,15 @@ class FreeformSoftwareRequest(BaseModel):
|
||||
chat_type: str | None = None
|
||||
|
||||
|
||||
class GiteaRepositoryOnboardRequest(BaseModel):
|
||||
"""Request body for onboarding a manually created Gitea repository."""
|
||||
|
||||
repo_name: str = Field(min_length=1, max_length=255)
|
||||
owner: str | None = None
|
||||
sync_commits: bool = True
|
||||
commit_limit: int = Field(default=25, ge=1, le=200)
|
||||
|
||||
|
||||
def _build_project_id(name: str) -> str:
|
||||
"""Create a stable project id from the requested name."""
|
||||
slug = PROJECT_ID_PATTERN.sub("-", name.strip().lower()).strip("-") or "project"
|
||||
@@ -164,7 +177,27 @@ async def _run_generation(
|
||||
"""Run the shared generation pipeline for a structured request."""
|
||||
database_module.init_db()
|
||||
|
||||
project_id = _build_project_id(request.name)
|
||||
manager = DatabaseManager(db)
|
||||
reusable_history = manager.get_latest_project_by_name(request.name)
|
||||
if reusable_history and database_module.settings.gitea_url and database_module.settings.gitea_token:
|
||||
try:
|
||||
from .agents.gitea import GiteaAPI
|
||||
except ImportError:
|
||||
from agents.gitea import GiteaAPI
|
||||
manager.sync_pull_request_states(
|
||||
GiteaAPI(
|
||||
token=database_module.settings.GITEA_TOKEN,
|
||||
base_url=database_module.settings.GITEA_URL,
|
||||
owner=database_module.settings.GITEA_OWNER,
|
||||
repo=database_module.settings.GITEA_REPO or '',
|
||||
),
|
||||
project_id=reusable_history.project_id,
|
||||
)
|
||||
if reusable_history and manager.get_open_pull_request(project_id=reusable_history.project_id):
|
||||
project_id = reusable_history.project_id
|
||||
else:
|
||||
project_id = _build_project_id(request.name)
|
||||
reusable_history = None
|
||||
resolved_prompt_text = prompt_text or _compose_prompt_text(request)
|
||||
orchestrator = AgentOrchestrator(
|
||||
project_id=project_id,
|
||||
@@ -175,6 +208,7 @@ async def _run_generation(
|
||||
db=db,
|
||||
prompt_text=resolved_prompt_text,
|
||||
prompt_actor=prompt_actor,
|
||||
existing_history=reusable_history,
|
||||
)
|
||||
result = await orchestrator.run()
|
||||
|
||||
@@ -195,7 +229,43 @@ async def _run_generation(
|
||||
response_data['project_root'] = result.get('project_root', str(_project_root(project_id)))
|
||||
response_data['changed_files'] = result.get('changed_files', [])
|
||||
response_data['repository'] = result.get('repository')
|
||||
return {'status': result['status'], 'data': response_data}
|
||||
response_data['pull_request'] = result.get('pull_request') or manager.get_open_pull_request(project_id=project_id)
|
||||
summary_context = {
|
||||
'name': response_data['name'],
|
||||
'description': response_data['description'],
|
||||
'features': response_data['features'],
|
||||
'tech_stack': response_data['tech_stack'],
|
||||
'changed_files': response_data['changed_files'],
|
||||
'repository_url': (
|
||||
(response_data.get('repository') or {}).get('url')
|
||||
if isinstance(response_data.get('repository'), dict)
|
||||
and (response_data.get('repository') or {}).get('status') in {'created', 'exists', 'ready', 'shared'}
|
||||
else None
|
||||
),
|
||||
'repository_status': (response_data.get('repository') or {}).get('status') if isinstance(response_data.get('repository'), dict) else None,
|
||||
'pull_request_url': (response_data.get('pull_request') or {}).get('pr_url') if isinstance(response_data.get('pull_request'), dict) else None,
|
||||
'pull_request_state': (response_data.get('pull_request') or {}).get('pr_state') if isinstance(response_data.get('pull_request'), dict) else None,
|
||||
'message': response_data.get('message'),
|
||||
'logs': [log.get('message', '') for log in response_data.get('logs', []) if isinstance(log, dict)],
|
||||
}
|
||||
summary_message, summary_trace = await ChangeSummaryGenerator().summarize_with_trace(summary_context)
|
||||
if orchestrator.db_manager and orchestrator.history and orchestrator.prompt_audit:
|
||||
orchestrator.db_manager.log_llm_trace(
|
||||
project_id=project_id,
|
||||
history_id=orchestrator.history.id,
|
||||
prompt_id=orchestrator.prompt_audit.id,
|
||||
stage=summary_trace['stage'],
|
||||
provider=summary_trace['provider'],
|
||||
model=summary_trace['model'],
|
||||
system_prompt=summary_trace['system_prompt'],
|
||||
user_prompt=summary_trace['user_prompt'],
|
||||
assistant_response=summary_trace['assistant_response'],
|
||||
raw_response=summary_trace.get('raw_response'),
|
||||
fallback_used=summary_trace.get('fallback_used', False),
|
||||
)
|
||||
response_data['summary_message'] = summary_message
|
||||
response_data['pull_request'] = result.get('pull_request') or manager.get_open_pull_request(project_id=project_id)
|
||||
return {'status': result['status'], 'data': response_data, 'summary_message': summary_message}
|
||||
|
||||
|
||||
def _project_root(project_id: str) -> Path:
|
||||
@@ -203,6 +273,22 @@ def _project_root(project_id: str) -> Path:
|
||||
return database_module.settings.projects_root / project_id
|
||||
|
||||
|
||||
def _create_gitea_api():
|
||||
"""Create a configured Gitea client or raise an HTTP error if unavailable."""
|
||||
if not database_module.settings.gitea_url or not database_module.settings.gitea_token:
|
||||
raise HTTPException(status_code=400, detail='Gitea integration is not configured')
|
||||
try:
|
||||
from .agents.gitea import GiteaAPI
|
||||
except ImportError:
|
||||
from agents.gitea import GiteaAPI
|
||||
return GiteaAPI(
|
||||
token=database_module.settings.GITEA_TOKEN,
|
||||
base_url=database_module.settings.GITEA_URL,
|
||||
owner=database_module.settings.GITEA_OWNER,
|
||||
repo=database_module.settings.GITEA_REPO or '',
|
||||
)
|
||||
|
||||
|
||||
def _resolve_n8n_api_url(explicit_url: str | None = None) -> str:
|
||||
"""Resolve the effective n8n API URL from explicit input or settings."""
|
||||
if explicit_url and explicit_url.strip():
|
||||
@@ -234,8 +320,16 @@ def read_api_info():
|
||||
'/audit/system/logs',
|
||||
'/audit/prompts',
|
||||
'/audit/changes',
|
||||
'/audit/commit-context',
|
||||
'/audit/timeline',
|
||||
'/audit/llm-traces',
|
||||
'/audit/pull-requests',
|
||||
'/audit/lineage',
|
||||
'/audit/correlations',
|
||||
'/projects/{project_id}/prompts/{prompt_id}/undo',
|
||||
'/projects/{project_id}/sync-repository',
|
||||
'/gitea/repos',
|
||||
'/gitea/repos/onboard',
|
||||
'/n8n/health',
|
||||
'/n8n/setup',
|
||||
],
|
||||
@@ -279,7 +373,7 @@ async def generate_software_from_text(request: FreeformSoftwareRequest, db: DbSe
|
||||
},
|
||||
}
|
||||
|
||||
interpreted = await RequestInterpreter().interpret(request.prompt_text)
|
||||
interpreted, interpretation_trace = await RequestInterpreter().interpret_with_trace(request.prompt_text)
|
||||
structured_request = SoftwareRequest(**interpreted)
|
||||
response = await _run_generation(
|
||||
structured_request,
|
||||
@@ -287,7 +381,26 @@ async def generate_software_from_text(request: FreeformSoftwareRequest, db: DbSe
|
||||
prompt_text=request.prompt_text,
|
||||
prompt_actor=request.source,
|
||||
)
|
||||
project_data = response.get('data', {})
|
||||
if project_data.get('history_id') is not None:
|
||||
manager = DatabaseManager(db)
|
||||
prompts = manager.get_prompt_events(project_id=project_data.get('project_id'))
|
||||
prompt_id = prompts[0]['id'] if prompts else None
|
||||
manager.log_llm_trace(
|
||||
project_id=project_data.get('project_id'),
|
||||
history_id=project_data.get('history_id'),
|
||||
prompt_id=prompt_id,
|
||||
stage=interpretation_trace['stage'],
|
||||
provider=interpretation_trace['provider'],
|
||||
model=interpretation_trace['model'],
|
||||
system_prompt=interpretation_trace['system_prompt'],
|
||||
user_prompt=interpretation_trace['user_prompt'],
|
||||
assistant_response=interpretation_trace['assistant_response'],
|
||||
raw_response=interpretation_trace.get('raw_response'),
|
||||
fallback_used=interpretation_trace.get('fallback_used', False),
|
||||
)
|
||||
response['interpreted_request'] = interpreted
|
||||
response['llm_trace'] = interpretation_trace
|
||||
response['source'] = {
|
||||
'type': request.source,
|
||||
'chat_id': request.chat_id,
|
||||
@@ -343,6 +456,54 @@ def get_code_change_audit(db: DbSession, project_id: str | None = Query(default=
|
||||
return {'changes': [_serialize_audit_item(item) for item in manager.get_code_changes(project_id=project_id)]}
|
||||
|
||||
|
||||
@app.get('/audit/commit-context')
|
||||
def get_commit_context_audit(
|
||||
db: DbSession,
|
||||
commit_hash: str = Query(min_length=4),
|
||||
project_id: str | None = Query(default=None),
|
||||
branch_scope: str | None = Query(default=None, pattern='^(main|pr|manual)?$'),
|
||||
):
|
||||
"""Return the recorded context explaining how a commit came to be."""
|
||||
manager = DatabaseManager(db)
|
||||
context = manager.get_commit_context(commit_hash=commit_hash, project_id=project_id, branch_scope=branch_scope)
|
||||
if context is None:
|
||||
raise HTTPException(status_code=404, detail='Commit context not found')
|
||||
return context
|
||||
|
||||
|
||||
@app.get('/audit/timeline')
|
||||
def get_project_timeline_audit(
|
||||
db: DbSession,
|
||||
project_id: str = Query(min_length=1),
|
||||
branch_scope: str | None = Query(default=None, pattern='^(main|pr|manual)?$'),
|
||||
):
|
||||
"""Return the mixed audit timeline for one project."""
|
||||
manager = DatabaseManager(db)
|
||||
return {'timeline': manager.get_project_timeline(project_id=project_id, branch_scope=branch_scope)}
|
||||
|
||||
|
||||
@app.get('/audit/llm-traces')
|
||||
def get_llm_trace_audit(
|
||||
db: DbSession,
|
||||
project_id: str | None = Query(default=None),
|
||||
prompt_id: int | None = Query(default=None),
|
||||
stage: str | None = Query(default=None),
|
||||
model: str | None = Query(default=None),
|
||||
search: str | None = Query(default=None),
|
||||
):
|
||||
"""Return persisted LLM traces."""
|
||||
manager = DatabaseManager(db)
|
||||
return {
|
||||
'llm_traces': manager.get_llm_traces(
|
||||
project_id=project_id,
|
||||
prompt_id=prompt_id,
|
||||
stage=stage,
|
||||
model=model,
|
||||
search_query=search,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@app.get('/audit/lineage')
|
||||
def get_prompt_change_lineage(db: DbSession, project_id: str | None = Query(default=None)):
|
||||
"""Return explicit prompt-to-code lineage rows."""
|
||||
@@ -357,6 +518,84 @@ def get_prompt_change_correlations(db: DbSession, project_id: str | None = Query
|
||||
return {'correlations': manager.get_prompt_change_correlations(project_id=project_id)}
|
||||
|
||||
|
||||
@app.get('/audit/pull-requests')
|
||||
def get_pull_request_audit(db: DbSession, project_id: str | None = Query(default=None), open_only: bool = Query(default=False)):
|
||||
"""Return tracked pull requests for generated projects."""
|
||||
manager = DatabaseManager(db)
|
||||
return {'pull_requests': manager.get_pull_requests(project_id=project_id, only_open=open_only)}
|
||||
|
||||
|
||||
@app.post('/projects/{project_id}/prompts/{prompt_id}/undo')
|
||||
async def undo_prompt_changes(project_id: str, prompt_id: int, db: DbSession):
|
||||
"""Undo all changes associated with a specific prompt."""
|
||||
result = await PromptWorkflowManager(db).undo_prompt(project_id=project_id, prompt_id=prompt_id)
|
||||
if result.get('status') == 'error':
|
||||
raise HTTPException(status_code=400, detail=result.get('message', 'Undo failed'))
|
||||
return result
|
||||
|
||||
|
||||
@app.post('/projects/{project_id}/sync-repository')
|
||||
def sync_project_repository(project_id: str, db: DbSession, commit_limit: int = Query(default=25, ge=1, le=200)):
|
||||
"""Import recent repository activity from Gitea for a tracked project."""
|
||||
manager = DatabaseManager(db)
|
||||
result = manager.sync_repository_activity(project_id=project_id, gitea_api=_create_gitea_api(), commit_limit=commit_limit)
|
||||
if result.get('status') == 'error':
|
||||
raise HTTPException(status_code=400, detail=result.get('message', 'Repository sync failed'))
|
||||
return result
|
||||
|
||||
|
||||
@app.get('/gitea/repos')
|
||||
def list_gitea_repositories(db: DbSession, owner: str | None = Query(default=None)):
|
||||
"""List repositories in the configured Gitea organization and whether they are already onboarded."""
|
||||
gitea_api = _create_gitea_api()
|
||||
resolved_owner = owner or database_module.settings.gitea_owner
|
||||
repos = gitea_api.list_repositories_sync(owner=resolved_owner)
|
||||
if isinstance(repos, dict) and repos.get('error'):
|
||||
raise HTTPException(status_code=502, detail=repos.get('error'))
|
||||
manager = DatabaseManager(db)
|
||||
items = []
|
||||
for repo in repos if isinstance(repos, list) else []:
|
||||
tracked_project = manager.get_project_by_repository(resolved_owner, repo.get('name', ''))
|
||||
items.append(
|
||||
{
|
||||
'name': repo.get('name'),
|
||||
'full_name': repo.get('full_name') or f"{resolved_owner}/{repo.get('name')}",
|
||||
'description': repo.get('description'),
|
||||
'html_url': repo.get('html_url'),
|
||||
'clone_url': repo.get('clone_url'),
|
||||
'default_branch': repo.get('default_branch'),
|
||||
'private': bool(repo.get('private', False)),
|
||||
'onboarded': tracked_project is not None,
|
||||
'project_id': tracked_project.project_id if tracked_project is not None else None,
|
||||
}
|
||||
)
|
||||
return {'repositories': items}
|
||||
|
||||
|
||||
@app.post('/gitea/repos/onboard')
|
||||
async def onboard_gitea_repository(request: GiteaRepositoryOnboardRequest, db: DbSession):
|
||||
"""Onboard a manually created Gitea repository into the factory dashboard."""
|
||||
gitea_api = _create_gitea_api()
|
||||
owner = request.owner or database_module.settings.gitea_owner
|
||||
repo = await gitea_api.get_repo_info(owner=owner, repo=request.repo_name)
|
||||
if isinstance(repo, dict) and repo.get('error'):
|
||||
raise HTTPException(status_code=404, detail=repo.get('error'))
|
||||
manager = DatabaseManager(db)
|
||||
onboarded = manager.onboard_repository(owner=owner, repo_name=request.repo_name, repository_data=repo)
|
||||
sync_result = None
|
||||
if request.sync_commits:
|
||||
sync_result = manager.sync_repository_activity(
|
||||
project_id=onboarded['project_id'],
|
||||
gitea_api=gitea_api,
|
||||
commit_limit=request.commit_limit,
|
||||
)
|
||||
return {
|
||||
'status': 'success',
|
||||
'onboarded': onboarded,
|
||||
'sync_result': sync_result,
|
||||
}
|
||||
|
||||
|
||||
@app.get('/audit/logs')
|
||||
def get_audit_logs(db: DbSession):
|
||||
"""Return all project logs ordered newest first."""
|
||||
|
||||
Reference in New Issue
Block a user