use std::sync::Arc;
use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde::{Deserialize, Serialize};
use orca_ai::ops::{ChatTurn, chat};
use orca_core::types::WorkloadStatus;
use crate::state::AppState;
#[derive(Deserialize)]
pub(crate) struct AskRequest {
pub question: String,
#[serde(default)]
pub history: Vec<ChatTurn>,
}
#[derive(Serialize)]
pub(crate) struct AskResponse {
pub response: String,
}
pub(crate) async fn ask(
State(state): State<Arc<AppState>>,
Json(req): Json<AskRequest>,
) -> impl IntoResponse {
let Some(ai) = state.cluster_config.ai.as_ref() else {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(serde_json::json!({
"error": "AI is not configured; add an [ai] block to cluster.toml"
})),
)
.into_response();
};
if req.question.trim().is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "question must not be empty" })),
)
.into_response();
}
let status_text = render_status_text(&state).await;
match chat(ai, &req.history, &req.question, &status_text, "").await {
Ok(response) => Json(AskResponse { response }).into_response(),
Err(e) => (
StatusCode::BAD_GATEWAY,
Json(serde_json::json!({ "error": format!("AI request failed: {e}") })),
)
.into_response(),
}
}
async fn render_status_text(state: &AppState) -> String {
let services = state.services.read().await;
let nodes = state.registered_nodes.read().await;
let mut out = String::with_capacity(1024);
out.push_str(&format!(
"Cluster: {}\nNodes: {}\n\n",
state.cluster_config.cluster.name,
nodes.len()
));
out.push_str("Services:\n");
for svc in services.values() {
let running = svc
.instances
.iter()
.filter(|i| matches!(i.status, WorkloadStatus::Running))
.count();
out.push_str(&format!(
"- {} [{}/{}] desired={} ",
svc.config.name, running, svc.desired_replicas, svc.desired_replicas
));
if let Some(img) = &svc.config.image {
out.push_str(&format!("image={img} "));
}
if let Some(p) = svc.config.port {
out.push_str(&format!("port={p}"));
}
out.push('\n');
}
out
}