diff options
| author | soryu <soryu@soryu.co> | 2026-02-07 18:27:54 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-07 18:27:54 +0000 |
| commit | 97e21c8296ec5f91912d56980ebf3b18a1ca3507 (patch) | |
| tree | 3650e2eb62ab5b387006563ce64139aa7688da5f /makima/src/server/handlers | |
| parent | 8f757f561eeb397aaea70d7c10d41445cc5e50b5 (diff) | |
| download | soryu-97e21c8296ec5f91912d56980ebf3b18a1ca3507.tar.gz soryu-97e21c8296ec5f91912d56980ebf3b18a1ca3507.zip | |
Add directive monitor contracts
Diffstat (limited to 'makima/src/server/handlers')
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 223 |
1 files changed, 217 insertions, 6 deletions
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs index a877c6b..65f32d5 100644 --- a/makima/src/server/handlers/directives.rs +++ b/makima/src/server/handlers/directives.rs @@ -8,9 +8,12 @@ use axum::{ }; use uuid::Uuid; +use std::collections::HashMap; + use crate::db::models::{ - ChainStep, ChainWithSteps, CreateDirectiveRequest, Directive, DirectiveChain, - DirectiveListResponse, DirectiveWithChains, UpdateDirectiveRequest, + ChainStep, ChainStepWithContract, ChainWithSteps, CreateDirectiveRequest, Directive, + DirectiveChain, DirectiveListResponse, DirectiveWithChains, EvaluationListResponse, + StepContractSummary, UpdateDirectiveRequest, }; use crate::db::repository::{self, RepositoryError}; use crate::orchestration; @@ -123,8 +126,8 @@ pub async fn get_directive( }; // Build chains with steps - let mut chains_with_steps = Vec::new(); - for chain in chains { + let mut all_steps_by_chain = Vec::new(); + for chain in &chains { let steps = match repository::list_steps_for_chain(pool, chain.id).await { Ok(s) => s, Err(e) => { @@ -132,11 +135,61 @@ pub async fn get_directive( Vec::new() } }; - chains_with_steps.push(ChainWithSteps { chain, steps }); + all_steps_by_chain.push(steps); + } + + // Collect all contract IDs (from steps + orchestrator) + let mut contract_ids: Vec<Uuid> = all_steps_by_chain + .iter() + .flat_map(|steps| steps.iter().filter_map(|s| s.contract_id)) + .collect(); + if let Some(orch_id) = directive.orchestrator_contract_id { + contract_ids.push(orch_id); } + // Batch fetch contract summaries + let mut summary_map: HashMap<Uuid, StepContractSummary> = if contract_ids.is_empty() { + HashMap::new() + } else { + match repository::get_contract_summaries_batch(pool, &contract_ids).await { + Ok(summaries) => summaries.into_iter().map(|s| (s.id, s)).collect(), + Err(e) => { + tracing::warn!("Failed to fetch contract summaries: {}", e); + HashMap::new() + } + } + }; + + // Build enriched chains + let chains_with_steps: Vec<ChainWithSteps> = chains + .into_iter() + .zip(all_steps_by_chain.into_iter()) + .map(|(chain, steps)| { + let enriched_steps = steps + .into_iter() + .map(|step| { + let contract_summary = + step.contract_id.and_then(|id| summary_map.remove(&id)); + ChainStepWithContract { + step, + contract_summary, + } + }) + .collect(); + ChainWithSteps { + chain, + steps: enriched_steps, + } + }) + .collect(); + + let orchestrator_contract_summary = directive + .orchestrator_contract_id + .and_then(|id| summary_map.remove(&id)); + Json(DirectiveWithChains { directive, + orchestrator_contract_summary, chains: chains_with_steps, }) .into_response() @@ -454,7 +507,37 @@ pub async fn get_chain( } }; - Json(ChainWithSteps { chain, steps }).into_response() + // Collect contract IDs from steps + let contract_ids: Vec<Uuid> = steps.iter().filter_map(|s| s.contract_id).collect(); + + let mut summary_map: HashMap<Uuid, StepContractSummary> = if contract_ids.is_empty() { + HashMap::new() + } else { + match repository::get_contract_summaries_batch(pool, &contract_ids).await { + Ok(summaries) => summaries.into_iter().map(|s| (s.id, s)).collect(), + Err(e) => { + tracing::warn!("Failed to fetch contract summaries: {}", e); + HashMap::new() + } + } + }; + + let enriched_steps = steps + .into_iter() + .map(|step| { + let contract_summary = step.contract_id.and_then(|id| summary_map.remove(&id)); + ChainStepWithContract { + step, + contract_summary, + } + }) + .collect(); + + Json(ChainWithSteps { + chain, + steps: enriched_steps, + }) + .into_response() } /// Start a directive: create a planning contract and begin orchestration. @@ -513,3 +596,131 @@ pub async fn start_directive( } } } + +/// Trigger a manual evaluation for a step. +#[utoipa::path( + post, + path = "/api/v1/directives/{id}/steps/{step_id}/evaluate", + params( + ("id" = Uuid, Path, description = "Directive ID"), + ("step_id" = Uuid, Path, description = "Step ID") + ), + responses( + (status = 200, description = "Evaluation triggered", body = ChainStep), + (status = 400, description = "Step cannot be evaluated", body = ApiError), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Directives" +)] +pub async fn evaluate_step( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path((id, step_id)): Path<(Uuid, Uuid)>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + match orchestration::directive::trigger_manual_evaluation(pool, &state, auth.owner_id, id, step_id).await { + Ok(step) => Json(step).into_response(), + Err(e) if e.contains("not found") || e.contains("Not found") => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", e)), + ) + .into_response(), + Err(e) if e.contains("hasn't been executed") || e.contains("no active chain") => ( + StatusCode::BAD_REQUEST, + Json(ApiError::new("INVALID_STATE", e)), + ) + .into_response(), + Err(e) => { + tracing::error!("Failed to trigger evaluation for step {}: {}", step_id, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("EVALUATION_FAILED", e)), + ) + .into_response() + } + } +} + +/// List evaluations for a step. +#[utoipa::path( + get, + path = "/api/v1/directives/{id}/steps/{step_id}/evaluations", + params( + ("id" = Uuid, Path, description = "Directive ID"), + ("step_id" = Uuid, Path, description = "Step ID") + ), + responses( + (status = 200, description = "List of evaluations", body = EvaluationListResponse), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Directives" +)] +pub async fn list_evaluations( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path((id, step_id)): Path<(Uuid, Uuid)>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Verify directive exists and belongs to owner + match repository::get_directive_for_owner(pool, id, auth.owner_id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Directive not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get directive {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + } + + match repository::list_evaluations_for_step(pool, step_id).await { + Ok(evaluations) => { + let total = evaluations.len() as i64; + Json(EvaluationListResponse { evaluations, total }).into_response() + } + Err(e) => { + tracing::error!("Failed to list evaluations for step {}: {}", step_id, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} |
