use super::SharedAgentStatus;
use crate::agents::{AgentConfig, ChatCapable};
use axum::{
Router,
extract::State,
http::{StatusCode, header},
response::{Html, IntoResponse, Json},
routing::{get, post},
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::sync::Arc;
use tracing::{error, info};
#[derive(Clone)]
struct AppState {
status: SharedAgentStatus,
chat_agent: Option<Arc<dyn ChatCapable>>,
agent_config: AgentConfig,
}
pub struct StatusServer;
impl StatusServer {
pub async fn run(
port: u16,
status: SharedAgentStatus,
chat_agent: Option<Arc<dyn ChatCapable>>,
agent_config: AgentConfig,
api_error_telemetry: Option<Arc<crate::api_error_middleware::ApiErrorTelemetry>>,
) {
let state = AppState {
status,
chat_agent,
agent_config,
};
let app = Router::new()
.route("/", get(dashboard_page))
.route("/api/status", get(status_json))
.route("/api/config", get(config_json))
.route("/api/chat", post(chat_handler))
.with_state(state)
.layer(axum::middleware::from_fn_with_state(
api_error_telemetry,
crate::api_error_middleware::api_error_telemetry_middleware,
));
let addr = SocketAddr::from(([127, 0, 0, 1], port));
info!("Agent status dashboard → http://{}/", addr);
let listener = match tokio::net::TcpListener::bind(addr).await {
Ok(l) => l,
Err(e) => {
error!("Failed to bind status server on port {}: {}", port, e);
return;
}
};
if let Err(e) = axum::serve(listener, app).await {
error!("Status server error: {}", e);
}
}
}
async fn dashboard_page() -> impl IntoResponse {
(
[(header::CACHE_CONTROL, "no-store")],
Html(include_str!("status.html")),
)
}
async fn status_json(State(state): State<AppState>) -> Json<super::AgentStatusSnapshot> {
let snap = state.status.read().await;
Json(snap.clone())
}
async fn config_json(State(state): State<AppState>) -> Json<crate::agents::AgentConfig> {
Json(state.agent_config.clone())
}
#[derive(Deserialize)]
struct ChatMessage {
role: String, content: String,
}
#[derive(Deserialize)]
struct ChatRequest {
messages: Vec<ChatMessage>,
}
#[derive(Serialize)]
struct ChatResponse {
response: String,
}
async fn chat_handler(
State(state): State<AppState>,
Json(req): Json<ChatRequest>,
) -> impl IntoResponse {
use async_openai::types::{
ChatCompletionRequestAssistantMessage, ChatCompletionRequestMessage,
ChatCompletionRequestUserMessage,
};
let Some(ref chat_agent) = state.chat_agent else {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ChatResponse {
response: "Chat not available — agent does not implement ChatCapable.".into(),
}),
)
.into_response();
};
let messages: Vec<ChatCompletionRequestMessage> = req
.messages
.into_iter()
.map(|m| match m.role.as_str() {
"assistant" => ChatCompletionRequestAssistantMessage {
content: Some(
async_openai::types::ChatCompletionRequestAssistantMessageContent::Text(
m.content,
),
),
..Default::default()
}
.into(),
_ => ChatCompletionRequestUserMessage {
content: async_openai::types::ChatCompletionRequestUserMessageContent::Text(
m.content,
),
..Default::default()
}
.into(),
})
.collect();
match chat_agent.chat(messages).await {
Ok(response) => (StatusCode::OK, Json(ChatResponse { response })).into_response(),
Err(e) => {
error!("Chat error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ChatResponse {
response: format!("Error: {}", e),
}),
)
.into_response()
}
}
}