diff options
Diffstat (limited to 'makima/src/server')
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 223 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 2 | ||||
| -rw-r--r-- | makima/src/server/openapi.rs | 18 |
3 files changed, 233 insertions, 10 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() + } + } +} diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index 1a59e12..c8242ae 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -184,6 +184,8 @@ pub fn make_router(state: SharedState) -> Router { .route("/directives/{id}/start", post(directives::start_directive)) .route("/directives/{id}/chains", get(directives::list_chains)) .route("/directives/{id}/chains/{chain_id}", get(directives::get_chain)) + .route("/directives/{id}/steps/{step_id}/evaluate", post(directives::evaluate_step)) + .route("/directives/{id}/steps/{step_id}/evaluations", get(directives::list_evaluations)) // Contract supervisor resume endpoints .route("/contracts/{id}/supervisor/resume", post(mesh_supervisor::resume_supervisor)) .route("/contracts/{id}/supervisor/conversation/rewind", post(mesh_supervisor::rewind_conversation)) diff --git a/makima/src/server/openapi.rs b/makima/src/server/openapi.rs index 96c19e0..e680c07 100644 --- a/makima/src/server/openapi.rs +++ b/makima/src/server/openapi.rs @@ -4,17 +4,20 @@ use utoipa::OpenApi; use crate::db::models::{ AddLocalRepositoryRequest, AddRemoteRepositoryRequest, BranchInfo, BranchListResponse, - BranchTaskRequest, BranchTaskResponse, ChainStep, ChainWithSteps, ChangePhaseRequest, + BranchTaskRequest, BranchTaskResponse, ChainStep, ChainStepWithContract, ChainWithSteps, + ChangePhaseRequest, Contract, ContractChatHistoryResponse, ContractChatMessageRecord, ContractEvent, ContractListResponse, ContractRepository, ContractSummary, ContractWithRelations, CreateContractRequest, CreateDirectiveRequest, CreateFileRequest, CreateManagedRepositoryRequest, CreateTaskRequest, Daemon, DaemonDirectoriesResponse, - DaemonDirectory, DaemonListResponse, Directive, DirectiveChain, DirectiveListResponse, - DirectiveSummary, DirectiveWithChains, File, FileListResponse, FileSummary, + DaemonDirectory, DaemonListResponse, Directive, DirectiveChain, DirectiveEvaluation, + DirectiveEvent, DirectiveListResponse, DirectiveSummary, DirectiveWithChains, + EvaluationListResponse, File, FileListResponse, FileSummary, MergeCommitRequest, MergeCompleteCheckResponse, MergeResolveRequest, MergeResultResponse, MergeSkipRequest, MergeStartRequest, MergeStatusResponse, MeshChatConversation, MeshChatHistoryResponse, MeshChatMessageRecord, RepositoryHistoryEntry, - RepositoryHistoryListResponse, RepositorySuggestionsQuery, SendMessageRequest, Task, + RepositoryHistoryListResponse, RepositorySuggestionsQuery, SendMessageRequest, + StepContractSummary, Task, TaskEventListResponse, TaskListResponse, TaskSummary, TaskWithSubtasks, TranscriptEntry, UpdateContractRequest, UpdateDirectiveRequest, UpdateFileRequest, UpdateTaskRequest, }; @@ -114,6 +117,8 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage directives::start_directive, directives::list_chains, directives::get_chain, + directives::evaluate_step, + directives::list_evaluations, ), components( schemas( @@ -205,9 +210,14 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage DirectiveWithChains, DirectiveChain, ChainStep, + ChainStepWithContract, ChainWithSteps, + StepContractSummary, CreateDirectiveRequest, UpdateDirectiveRequest, + DirectiveEvaluation, + DirectiveEvent, + EvaluationListResponse, ) ), tags( |
