Files
tori/src/api/workflows.rs

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)
}