2026-08-15 13:50:24 +07:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-08-15 20:56:56 +07:00
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
from django.http import JsonResponse
|
|
|
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
|
|
|
from django.urls import reverse
|
|
|
|
|
from django.utils import timezone
|
|
|
|
|
from django.views.decorators.http import require_POST
|
2026-08-15 13:50:24 +07:00
|
|
|
|
2026-08-15 20:03:59 +07:00
|
|
|
from agents.control_room import AgentControlRoomService
|
2026-08-15 20:56:56 +07:00
|
|
|
from agents.lifecycle import ExplorerService
|
|
|
|
|
from agents.roadmap import RoadmapService
|
|
|
|
|
from agents.scenario_lab import ScenarioLabService
|
2026-08-28 23:51:02 +07:00
|
|
|
from control_plane.authoring.checkpoints import open_story_checkpointer
|
|
|
|
|
from control_plane.authoring.runner import StoryWorkflowRunner
|
|
|
|
|
from control_plane.authoring.services import DjangoStoryWorkflowServices
|
|
|
|
|
from control_plane.authoring.workflow import build_story_workflow
|
2026-08-15 13:50:24 +07:00
|
|
|
from control_plane.events.models import Event
|
2026-08-15 20:56:56 +07:00
|
|
|
from control_plane.projects.models import Decision, ExplorationOpportunity, Project, RoadmapItem, ScenarioFinding, ScenarioSuite, StewardFinding, Task
|
|
|
|
|
from control_plane.projects.ui_services import ControlPlaneUIService
|
|
|
|
|
from graph.bootstrap import champion_project_exploration_graph_v1
|
|
|
|
|
from graph.langgraph_runtime import LangGraphRuntime
|
|
|
|
|
from graph.lifecycle import exploration_registry
|
|
|
|
|
from graph.models import GraphApproval, GraphApprovalStatus, GraphRun, GraphRunStatus
|
2026-08-28 23:51:02 +07:00
|
|
|
from model_router.providers import providers_from_resources
|
|
|
|
|
from model_router.router import ModelRouter
|
2026-08-15 20:56:56 +07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
ui = ControlPlaneUIService()
|
2026-08-15 13:50:24 +07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def dashboard(request):
|
2026-08-15 20:56:56 +07:00
|
|
|
return render(request, "control_plane/dashboard.html", ui.dashboard())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def projects(request):
|
|
|
|
|
return render(request, "control_plane/projects.html", {"rows": ui.project_list()})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def project_workspace(request, project_id):
|
|
|
|
|
project = get_object_or_404(Project, id=project_id)
|
|
|
|
|
return render(request, "control_plane/project_workspace.html", ui.project_workspace(project))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def project_brain(request, project_id):
|
|
|
|
|
project = get_object_or_404(Project, id=project_id)
|
|
|
|
|
if request.method == "POST":
|
|
|
|
|
Decision.objects.create(project=project, decision_type="PROJECT_BRAIN_NOTE", decision=request.POST.get("message", ""), reason="User-authored Project Brain interaction from UI", actor="ui")
|
|
|
|
|
return redirect("project_brain", project_id=project.id)
|
|
|
|
|
return render(request, "control_plane/project_brain.html", ui.project_brain(project))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def project_archaeologist(request, project_id):
|
|
|
|
|
project = get_object_or_404(Project, id=project_id)
|
|
|
|
|
return render(request, "control_plane/archaeologist.html", ui.archaeologist(project))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def graph_run_detail(request, graph_run_id):
|
|
|
|
|
graph_run = get_object_or_404(GraphRun, id=graph_run_id)
|
|
|
|
|
template = "control_plane/partials/graph_run_status.html" if request.headers.get("HX-Request") else "control_plane/graph_run.html"
|
|
|
|
|
return render(request, template, ui.graph_run_detail(graph_run))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def graph_run_json(request, graph_run_id):
|
|
|
|
|
graph_run = get_object_or_404(GraphRun, id=graph_run_id)
|
|
|
|
|
return JsonResponse({"id": graph_run.id, "status": graph_run.status, "current_node": graph_run.current_node, "nodes": list(graph_run.node_runs.values("node_id", "visit_index", "status", "failure_evidence", "telemetry")), "edges": list(graph_run.edge_traversals.values("source_node", "target_node", "result", "condition"))})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def project_dag_json(request, project_id):
|
|
|
|
|
project = get_object_or_404(Project, id=project_id)
|
|
|
|
|
return JsonResponse({"project": str(project.id), "milestones": list(project.milestones.values("id", "key", "title", "status", "order")), "features": list(project.features.values("id", "milestone_id", "title", "status")), "tasks": list(project.tasks.values("id", "milestone_id", "feature_id", "goal", "status", "priority")), "dependencies": list(project.tasks.values("id", "dependency_edges__depends_on_id"))})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def task_detail(request, task_id):
|
|
|
|
|
task = get_object_or_404(Task, id=task_id)
|
|
|
|
|
return render(request, "control_plane/task.html", ui.task_detail(task))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def steward(request):
|
|
|
|
|
return render(request, "control_plane/steward.html", ui.steward())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def project_steward(request, project_id):
|
|
|
|
|
project = get_object_or_404(Project, id=project_id)
|
|
|
|
|
return render(request, "control_plane/steward.html", {"project": project, **ui.steward(project)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def explore(request, project_id=None):
|
|
|
|
|
project = get_object_or_404(Project, id=project_id) if project_id else None
|
|
|
|
|
opportunities = ExplorationOpportunity.objects.select_related("project", "exploration").order_by("-composite_score", "-created_at")
|
|
|
|
|
if project:
|
|
|
|
|
opportunities = opportunities.filter(project=project)
|
|
|
|
|
return render(request, "control_plane/explore.html", {"project": project, "opportunities": opportunities[:100]})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@require_POST
|
|
|
|
|
def run_explore(request, project_id):
|
|
|
|
|
project = get_object_or_404(Project, id=project_id)
|
|
|
|
|
service = ExplorerService()
|
|
|
|
|
exploration = service.start_exploration(project, prompt="UI Explore run")
|
|
|
|
|
version = champion_project_exploration_graph_v1()
|
|
|
|
|
graph_run = GraphRun.objects.create(execution_graph_version=version, project=project, current_node=version.graph_spec["entry"], metadata={"exploration_id": str(exploration.id), "source": "ui"})
|
|
|
|
|
LangGraphRuntime(exploration_registry(service)).run_until_terminal_or_paused(graph_run)
|
|
|
|
|
return redirect("graph_run_detail", graph_run_id=graph_run.id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@require_POST
|
|
|
|
|
def opportunity_action(request, opportunity_id):
|
|
|
|
|
opportunity = get_object_or_404(ExplorationOpportunity, id=opportunity_id)
|
|
|
|
|
action = request.POST.get("action")
|
|
|
|
|
service = ExplorerService()
|
|
|
|
|
if action == "extend":
|
|
|
|
|
service.convert_to_extension(opportunity)
|
|
|
|
|
elif action == "evolve":
|
|
|
|
|
service.convert_to_evolution(opportunity, baseline_measurement={"metric": "value", "value": 100})
|
|
|
|
|
elif action == "roadmap":
|
|
|
|
|
RoadmapService().upsert_item(opportunity.project, title=opportunity.title, description=opportunity.description, source="EXPLORE", source_ref={"exploration_opportunity_id": str(opportunity.id)}, rationale=opportunity.rationale, evidence=opportunity.evidence, horizon="NEXT", category=opportunity.opportunity_type, target_action=opportunity.recommended_action if opportunity.recommended_action in ["EXTEND", "EVOLVE", "REPAIR", "INVESTIGATE"] else "NONE", scores={"value": opportunity.value_score, "effort": opportunity.effort_score, "risk": opportunity.risk_score, "confidence": opportunity.confidence, "strategic_fit": opportunity.strategic_fit, "technical_fit": opportunity.technical_fit})
|
|
|
|
|
elif action == "defer":
|
|
|
|
|
service.defer(opportunity)
|
|
|
|
|
elif action == "reject":
|
|
|
|
|
service.reject(opportunity)
|
|
|
|
|
return redirect(request.META.get("HTTP_REFERER") or reverse("explore"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def roadmap(request, project_id=None):
|
|
|
|
|
project = get_object_or_404(Project, id=project_id) if project_id else None
|
|
|
|
|
return render(request, "control_plane/roadmap.html", {"project": project, "board": ui.roadmap_board(project)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@require_POST
|
|
|
|
|
def roadmap_action(request, item_id):
|
|
|
|
|
item = get_object_or_404(RoadmapItem, id=item_id)
|
|
|
|
|
action = request.POST.get("action")
|
|
|
|
|
service = RoadmapService()
|
|
|
|
|
if action in ["NOW", "NEXT", "LATER", "EXPLORING"]:
|
|
|
|
|
item.horizon = action
|
|
|
|
|
item.save(update_fields=["horizon", "updated_at"])
|
|
|
|
|
elif action == "defer":
|
|
|
|
|
item.status = "DEFERRED"
|
|
|
|
|
item.save(update_fields=["status", "updated_at"])
|
|
|
|
|
elif action == "reject":
|
|
|
|
|
item.status = "REJECTED"
|
|
|
|
|
item.save(update_fields=["status", "updated_at"])
|
|
|
|
|
elif action == "extend":
|
|
|
|
|
service.convert_to_extension(item)
|
|
|
|
|
elif action == "evolve":
|
|
|
|
|
service.convert_to_evolution(item, baseline_measurement={"metric": "value", "value": 100})
|
|
|
|
|
return redirect(request.META.get("HTTP_REFERER") or reverse("roadmap"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def scenarios(request, project_id=None):
|
|
|
|
|
project = get_object_or_404(Project, id=project_id) if project_id else None
|
|
|
|
|
return render(request, "control_plane/scenarios.html", {"project": project, **ui.scenario_lab(project)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@require_POST
|
|
|
|
|
def scenario_finding_action(request, finding_id):
|
|
|
|
|
finding = get_object_or_404(ScenarioFinding, id=finding_id)
|
|
|
|
|
action = request.POST.get("action")
|
|
|
|
|
if action == "roadmap":
|
|
|
|
|
ScenarioLabService().route_finding(finding)
|
|
|
|
|
return redirect(request.META.get("HTTP_REFERER") or reverse("scenarios"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def progeny(request):
|
|
|
|
|
return render(request, "control_plane/progeny.html", ui.progeny())
|
2026-08-15 20:03:59 +07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def agent_control_room(request):
|
|
|
|
|
service = AgentControlRoomService()
|
|
|
|
|
agents = service.list_agents()
|
|
|
|
|
for agent in agents:
|
|
|
|
|
champion_id = agent.get("champion_version")
|
|
|
|
|
agent["health"] = service.get_agent_health(champion_id) if champion_id else {"status": "WATCH", "reasons": ["No champion version."]}
|
2026-08-15 20:56:56 +07:00
|
|
|
agent["performance"] = service.get_agent_performance(champion_id) if champion_id else {}
|
|
|
|
|
return render(request, "control_plane/agents.html", {"agents": agents, "teams": service.list_teams()})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def agent_detail(request, version_id):
|
|
|
|
|
service = AgentControlRoomService()
|
|
|
|
|
return render(request, "control_plane/agent_detail.html", {"version": service.get_agent_version(version_id), "performance": service.get_agent_performance(version_id), "health": service.get_agent_health(version_id), "usage": service.get_agent_usage(version_id), "progeny": service.get_agent_progeny(version_id), "challengers": service.get_agent_challengers(service.get_agent_version(version_id)["agent_id"])})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def agent_performance_json(request, version_id):
|
|
|
|
|
return JsonResponse(AgentControlRoomService().get_agent_performance(version_id, window=request.GET.get("window", "lifetime")))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resources(request):
|
|
|
|
|
return render(request, "control_plane/resources.html", ui.resources())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def approvals(request):
|
|
|
|
|
return render(request, "control_plane/approvals.html", ui.approvals())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@require_POST
|
|
|
|
|
def approval_action(request, approval_id):
|
|
|
|
|
approval = get_object_or_404(GraphApproval, id=approval_id)
|
|
|
|
|
action = request.POST.get("action")
|
2026-08-28 23:51:02 +07:00
|
|
|
graph_run = approval.graph_run
|
|
|
|
|
if graph_run.execution_graph_version.graph.name == "story_authoring":
|
|
|
|
|
decision = {
|
|
|
|
|
"action": "approve" if action == "approve" else "request_revision",
|
|
|
|
|
"actor": "ui",
|
|
|
|
|
"notes": request.POST.get("notes", "").strip(),
|
|
|
|
|
}
|
|
|
|
|
try:
|
|
|
|
|
with open_story_checkpointer() as checkpointer:
|
|
|
|
|
services = DjangoStoryWorkflowServices(
|
|
|
|
|
ModelRouter(providers_from_resources(), persist_requests=True)
|
|
|
|
|
)
|
|
|
|
|
workflow = build_story_workflow(services, checkpointer)
|
|
|
|
|
StoryWorkflowRunner(workflow).resume(graph_run.id, decision)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
graph_run.failure_reason = f"UI approval resume failed: {exc}"
|
|
|
|
|
graph_run.save(update_fields=["failure_reason", "updated_at"])
|
|
|
|
|
return redirect("graph_run_detail", graph_run_id=graph_run.id)
|
|
|
|
|
|
2026-08-15 20:56:56 +07:00
|
|
|
approval.status = GraphApprovalStatus.APPROVED if action == "approve" else GraphApprovalStatus.REJECTED
|
|
|
|
|
approval.decided_by = "ui"
|
|
|
|
|
approval.decided_at = timezone.now()
|
|
|
|
|
approval.save(update_fields=["status", "decided_by", "decided_at", "updated_at"])
|
|
|
|
|
if approval.status == GraphApprovalStatus.APPROVED:
|
|
|
|
|
graph_run.status = GraphRunStatus.RUNNING
|
|
|
|
|
graph_run.failure_reason = ""
|
|
|
|
|
graph_run.save(update_fields=["status", "failure_reason", "updated_at"])
|
|
|
|
|
self_resume_graph(graph_run)
|
|
|
|
|
return redirect("graph_run_detail", graph_run_id=graph_run.id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def activity(request):
|
|
|
|
|
project = Project.objects.filter(id=request.GET.get("project")).first() if request.GET.get("project") else None
|
|
|
|
|
return render(request, "control_plane/activity.html", {"project": project, **ui.activity(project)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def self_resume_graph(graph_run: GraphRun) -> None:
|
|
|
|
|
name = graph_run.execution_graph_version.graph.name
|
|
|
|
|
try:
|
|
|
|
|
if name == "project_exploration":
|
|
|
|
|
LangGraphRuntime(exploration_registry(ExplorerService())).run_until_terminal_or_paused(graph_run)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
graph_run.failure_reason = f"UI approval saved; automatic resume failed: {exc}"
|
|
|
|
|
graph_run.save(update_fields=["failure_reason", "updated_at"])
|