feat(topology): persist comparison runs + history
Save each comparison and let users reload past ones.
- migration 0008: topology_runs (workspace-scoped; full comparison as JSONB).
- cm-db repo::topology_runs (insert / list_recent / get) + regenerated .sqlx.
- cm-api: compare persists best-effort (never loses the LLM result on a DB
hiccup); GET /api/topology-runs (recent) + GET /api/topology-runs/{id}.
Integration test asserts persist → list → get.
- frontend: "Recent comparisons" list on the Compare tab; click to reload a
saved run. e2e p8 green (39 suite); offline build + clippy clean.
Server self-migrates at boot (cm_db::MIGRATOR), so 0008 applies on deploy.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
654ec0f511
commit
7baf2082d0
@@ -3,11 +3,13 @@
|
||||
//! back the topology builder UI. Running/comparing topologies is a later,
|
||||
//! provider-backed endpoint.
|
||||
|
||||
use axum::extract::State;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::Json;
|
||||
use cm_orchestrator::{compare, Comparison, JudgeScorer, ProviderExecutor};
|
||||
use cm_topology::{build, classify, heuristics, Classification, TopologyGraph, TopologyKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{ApiError, AppState, Authed};
|
||||
|
||||
@@ -85,7 +87,7 @@ pub struct CompareRequest {
|
||||
/// provider for both execution (tool-free turns) and the LLM judge.
|
||||
pub async fn compare_topologies(
|
||||
State(state): State<AppState>,
|
||||
_auth: Authed,
|
||||
Authed(user): Authed,
|
||||
Json(req): Json<CompareRequest>,
|
||||
) -> Result<Json<Comparison>, ApiError> {
|
||||
let provider = state.runtime.provider();
|
||||
@@ -96,5 +98,66 @@ pub async fn compare_topologies(
|
||||
let cmp = compare(&req.graphs, &req.task, &executor, &scorer)
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal)?;
|
||||
|
||||
// Best-effort persistence: never lose the (expensive) result on a DB hiccup.
|
||||
if let Ok(value) = serde_json::to_value(&cmp) {
|
||||
let _ = cm_db::repo::topology_runs::insert(
|
||||
&state.pool,
|
||||
Uuid::now_v7(),
|
||||
user.workspace_id,
|
||||
&req.task,
|
||||
&value,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(Json(cmp))
|
||||
}
|
||||
|
||||
/// A saved comparison run, summarized.
|
||||
#[derive(Serialize)]
|
||||
pub struct RunSummary {
|
||||
pub id: String,
|
||||
pub task: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
/// `GET /api/topology-runs` — recent saved comparison runs for the workspace.
|
||||
pub async fn list_runs(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
) -> Result<Json<Vec<RunSummary>>, ApiError> {
|
||||
let rows = cm_db::repo::topology_runs::list_recent(&state.pool, user.workspace_id, 20).await?;
|
||||
let out = rows
|
||||
.into_iter()
|
||||
.map(|r| RunSummary {
|
||||
id: r.id.to_string(),
|
||||
task: r.task,
|
||||
created_at: r.created_at.format(&Rfc3339).unwrap_or_default(),
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(out))
|
||||
}
|
||||
|
||||
/// A full saved comparison run.
|
||||
#[derive(Serialize)]
|
||||
pub struct RunDetail {
|
||||
pub id: String,
|
||||
pub task: String,
|
||||
pub created_at: String,
|
||||
pub comparison: serde_json::Value,
|
||||
}
|
||||
|
||||
/// `GET /api/topology-runs/{id}` — a single saved comparison run.
|
||||
pub async fn get_run(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<RunDetail>, ApiError> {
|
||||
let run = cm_db::repo::topology_runs::get(&state.pool, id, user.workspace_id).await?;
|
||||
Ok(Json(RunDetail {
|
||||
id: run.id.to_string(),
|
||||
task: run.task,
|
||||
created_at: run.created_at.format(&Rfc3339).unwrap_or_default(),
|
||||
comparison: run.comparison,
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user