249 lines
7.1 KiB
Rust
249 lines
7.1 KiB
Rust
use std::sync::Arc;
|
|
use axum::{
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
routing::get,
|
|
Json, Router,
|
|
};
|
|
use serde::Deserialize;
|
|
use crate::AppState;
|
|
use crate::agent::{AgentEvent, PlanStepInfo};
|
|
use crate::db::{Workflow, ExecutionLogEntry, Comment, LlmCallLogEntry};
|
|
use crate::state::AgentState;
|
|
use crate::template;
|
|
use super::{ApiResult, db_err};
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct ReportResponse {
|
|
project_id: String,
|
|
report: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct CreateWorkflow {
|
|
pub requirement: String,
|
|
#[serde(default)]
|
|
pub template_id: Option<String>,
|
|
#[serde(default)]
|
|
pub worker: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct CreateComment {
|
|
pub content: String,
|
|
}
|
|
|
|
pub fn router(state: Arc<AppState>) -> Router {
|
|
Router::new()
|
|
.route("/projects/{id}/workflows", get(list_workflows).post(create_workflow))
|
|
.route("/workflows/{id}/steps", get(list_steps))
|
|
.route("/workflows/{id}/comments", get(list_comments).post(create_comment))
|
|
.route("/workflows/{id}/report", get(get_report))
|
|
.route("/workflows/{id}/plan", get(get_plan))
|
|
.route("/workflows/{id}/llm-calls", get(list_llm_calls))
|
|
.route("/templates", get(list_templates))
|
|
.with_state(state)
|
|
}
|
|
|
|
async fn list_workflows(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(project_id): Path<String>,
|
|
) -> ApiResult<Vec<Workflow>> {
|
|
sqlx::query_as::<_, Workflow>(
|
|
"SELECT * FROM workflows WHERE project_id = ? ORDER BY created_at DESC"
|
|
)
|
|
.bind(&project_id)
|
|
.fetch_all(&state.db.pool)
|
|
.await
|
|
.map(Json)
|
|
.map_err(db_err)
|
|
}
|
|
|
|
async fn create_workflow(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(project_id): Path<String>,
|
|
Json(input): Json<CreateWorkflow>,
|
|
) -> ApiResult<Workflow> {
|
|
let id = uuid::Uuid::new_v4().to_string();
|
|
let workflow = sqlx::query_as::<_, Workflow>(
|
|
"INSERT INTO workflows (id, project_id, requirement) VALUES (?, ?, ?) RETURNING *"
|
|
)
|
|
.bind(&id)
|
|
.bind(&project_id)
|
|
.bind(&input.requirement)
|
|
.fetch_one(&state.db.pool)
|
|
.await
|
|
.map_err(db_err)?;
|
|
|
|
state.agent_mgr.send_event(&project_id, AgentEvent::NewRequirement {
|
|
workflow_id: workflow.id.clone(),
|
|
requirement: workflow.requirement.clone(),
|
|
template_id: input.template_id,
|
|
worker: input.worker,
|
|
}).await;
|
|
|
|
Ok(Json(workflow))
|
|
}
|
|
|
|
/// Convert a JSON string to a simple YAML-like representation for display.
|
|
/// Falls back to the original string if it's not a JSON object.
|
|
fn json_to_yaml(input: &str) -> String {
|
|
let obj: serde_json::Map<String, serde_json::Value> = match serde_json::from_str(input) {
|
|
Ok(v) => v,
|
|
Err(_) => return input.to_string(),
|
|
};
|
|
let mut lines = Vec::new();
|
|
for (k, v) in &obj {
|
|
match v {
|
|
serde_json::Value::String(s) if s.contains('\n') => {
|
|
lines.push(format!("{}: |", k));
|
|
for line in s.lines() {
|
|
lines.push(format!(" {}", line));
|
|
}
|
|
}
|
|
serde_json::Value::String(s) => lines.push(format!("{}: {}", k, s)),
|
|
other => lines.push(format!("{}: {}", k, other)),
|
|
}
|
|
}
|
|
lines.join("\n")
|
|
}
|
|
|
|
async fn list_steps(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workflow_id): Path<String>,
|
|
) -> ApiResult<Vec<ExecutionLogEntry>> {
|
|
sqlx::query_as::<_, ExecutionLogEntry>(
|
|
"SELECT * FROM execution_log WHERE workflow_id = ? ORDER BY created_at"
|
|
)
|
|
.bind(&workflow_id)
|
|
.fetch_all(&state.db.pool)
|
|
.await
|
|
.map(|entries| Json(entries.into_iter().map(|mut e| {
|
|
e.tool_input = json_to_yaml(&e.tool_input);
|
|
e
|
|
}).collect()))
|
|
.map_err(db_err)
|
|
}
|
|
|
|
async fn list_comments(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workflow_id): Path<String>,
|
|
) -> ApiResult<Vec<Comment>> {
|
|
sqlx::query_as::<_, Comment>(
|
|
"SELECT * FROM comments WHERE workflow_id = ? ORDER BY created_at"
|
|
)
|
|
.bind(&workflow_id)
|
|
.fetch_all(&state.db.pool)
|
|
.await
|
|
.map(Json)
|
|
.map_err(db_err)
|
|
}
|
|
|
|
async fn create_comment(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workflow_id): Path<String>,
|
|
Json(input): Json<CreateComment>,
|
|
) -> ApiResult<Comment> {
|
|
let id = uuid::Uuid::new_v4().to_string();
|
|
let comment = sqlx::query_as::<_, Comment>(
|
|
"INSERT INTO comments (id, workflow_id, content) VALUES (?, ?, ?) RETURNING *"
|
|
)
|
|
.bind(&id)
|
|
.bind(&workflow_id)
|
|
.bind(&input.content)
|
|
.fetch_one(&state.db.pool)
|
|
.await
|
|
.map_err(db_err)?;
|
|
|
|
// Notify agent about the comment
|
|
if let Ok(Some(wf)) = sqlx::query_as::<_, Workflow>(
|
|
"SELECT * FROM workflows WHERE id = ?"
|
|
)
|
|
.bind(&workflow_id)
|
|
.fetch_optional(&state.db.pool)
|
|
.await
|
|
{
|
|
state.agent_mgr.send_event(&wf.project_id, AgentEvent::Comment {
|
|
workflow_id: workflow_id.clone(),
|
|
content: input.content,
|
|
}).await;
|
|
}
|
|
|
|
Ok(Json(comment))
|
|
}
|
|
|
|
async fn get_report(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workflow_id): Path<String>,
|
|
) -> Result<Json<ReportResponse>, Response> {
|
|
let wf = sqlx::query_as::<_, Workflow>(
|
|
"SELECT * FROM workflows WHERE id = ?"
|
|
)
|
|
.bind(&workflow_id)
|
|
.fetch_optional(&state.db.pool)
|
|
.await
|
|
.map_err(db_err)?;
|
|
|
|
let wf = match wf {
|
|
Some(w) => w,
|
|
None => return Err((StatusCode::NOT_FOUND, "Workflow not found").into_response()),
|
|
};
|
|
|
|
// Read report.md from workspace
|
|
let workspace_root = if std::path::Path::new("/app/data/workspaces").is_dir() {
|
|
"/app/data/workspaces"
|
|
} else {
|
|
"data/workspaces"
|
|
};
|
|
let report_path = format!("{}/{}/report.md", workspace_root, wf.project_id);
|
|
let report = tokio::fs::read_to_string(&report_path).await.unwrap_or_default();
|
|
|
|
if report.is_empty() {
|
|
return Err((StatusCode::NOT_FOUND, "report.md not found").into_response());
|
|
}
|
|
|
|
Ok(Json(ReportResponse { project_id: wf.project_id, report }))
|
|
}
|
|
|
|
async fn get_plan(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workflow_id): Path<String>,
|
|
) -> ApiResult<Vec<PlanStepInfo>> {
|
|
let snapshot_json: Option<String> = sqlx::query_scalar(
|
|
"SELECT state_json FROM agent_state_snapshots WHERE workflow_id = ? ORDER BY created_at DESC LIMIT 1"
|
|
)
|
|
.bind(&workflow_id)
|
|
.fetch_optional(&state.db.pool)
|
|
.await
|
|
.map_err(db_err)?;
|
|
|
|
if let Some(json) = snapshot_json {
|
|
if let Ok(agent_state) = serde_json::from_str::<AgentState>(&json) {
|
|
return Ok(Json(crate::agent::plan_infos_from_state(&agent_state)));
|
|
}
|
|
}
|
|
|
|
Ok(Json(vec![]))
|
|
}
|
|
|
|
async fn list_llm_calls(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(workflow_id): Path<String>,
|
|
) -> ApiResult<Vec<LlmCallLogEntry>> {
|
|
sqlx::query_as::<_, LlmCallLogEntry>(
|
|
"SELECT * FROM llm_call_log WHERE workflow_id = ? ORDER BY created_at"
|
|
)
|
|
.bind(&workflow_id)
|
|
.fetch_all(&state.db.pool)
|
|
.await
|
|
.map(Json)
|
|
.map_err(db_err)
|
|
}
|
|
|
|
async fn list_templates(
|
|
State(state): State<Arc<AppState>>,
|
|
) -> Json<Vec<template::TemplateListItem>> {
|
|
Json(template::list_all_templates(state.config.template_repo.as_ref()).await)
|
|
}
|