summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers')
-rw-r--r--makima/src/server/handlers/directives.rs223
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()
+ }
+ }
+}