use serde::{Deserialize, Serialize};
use crate::activity::Activity;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentChatRequest {
pub text: String,
#[serde(default)]
pub tenant: Option<String>,
#[serde(default)]
pub conversation_id: Option<String>,
#[serde(default)]
pub user_id: Option<String>,
#[serde(default)]
pub flow_id: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReplyView {
pub text: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentChatResponse {
pub replies: Vec<ReplyView>,
}
fn reply_text(activity: &Activity) -> String {
let payload = activity.payload();
if let Some(t) = payload.get("text").and_then(|v| v.as_str()) {
return t.to_string();
}
if let Some(t) = payload
.get("messages")
.and_then(|m| m.get(0))
.and_then(|m0| m0.get("text"))
.and_then(|v| v.as_str())
{
return t.to_string();
}
serde_json::to_string(payload).unwrap_or_default()
}
pub fn replies_to_response(activities: Vec<Activity>) -> AgentChatResponse {
let replies = activities
.iter()
.map(reply_text)
.filter(|t| !t.trim().is_empty())
.map(|text| ReplyView { text })
.collect();
AgentChatResponse { replies }
}
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::{Json, extract::State};
use crate::host::RunnerHost;
use crate::http::auth::AdminGuard;
use crate::runner::ServerState;
const DEFAULT_CONVERSATION: &str = "test-chat";
const DEFAULT_USER: &str = "test-chat-user";
async fn execute_chat(
host: &RunnerHost,
default_tenant: &str,
req: AgentChatRequest,
) -> Result<AgentChatResponse, (StatusCode, serde_json::Value)> {
let tenant = req
.tenant
.as_deref()
.map(str::to_string)
.unwrap_or_else(|| default_tenant.to_string());
let mut activity = Activity::text(req.text)
.in_conversation(
req.conversation_id
.unwrap_or_else(|| DEFAULT_CONVERSATION.to_string()),
)
.from_user(req.user_id.unwrap_or_else(|| DEFAULT_USER.to_string()));
if let Some(flow) = req.flow_id {
activity = activity.with_flow(flow);
}
match host.handle_activity(&tenant, activity).await {
Ok(activities) => Ok(replies_to_response(activities)),
Err(e) => {
let msg = format!("{e:#}");
let (code, error) = if msg.contains("not loaded") {
(StatusCode::NOT_FOUND, "tenant_not_loaded")
} else {
(StatusCode::INTERNAL_SERVER_ERROR, "agent_chat_failed")
};
Err((code, serde_json::json!({ "error": error, "message": msg })))
}
}
}
pub async fn agent_chat(
_guard: AdminGuard,
State(state): State<ServerState>,
Json(req): Json<AgentChatRequest>,
) -> impl IntoResponse {
match execute_chat(&state.host, state.routing.default_tenant(), req).await {
Ok(response) => (StatusCode::OK, Json(response)).into_response(),
Err((code, body)) => (code, Json(body)).into_response(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::activity::Activity;
use serde_json::json;
fn reply_with(payload: serde_json::Value) -> Activity {
Activity::from_output(payload, "demo")
}
#[test]
fn maps_text_payload_to_reply() {
let out = replies_to_response(vec![reply_with(json!({"text": "hello there"}))]);
assert_eq!(out.replies.len(), 1);
assert_eq!(out.replies[0].text, "hello there");
}
#[test]
fn maps_nested_messages_text() {
let out = replies_to_response(vec![reply_with(
json!({"messages": [{"text": "nested hi"}]}),
)]);
assert_eq!(out.replies[0].text, "nested hi");
}
#[test]
fn skips_empty_and_keeps_order() {
let out = replies_to_response(vec![
reply_with(json!({"text": ""})),
reply_with(json!({"text": "second"})),
]);
assert_eq!(out.replies.len(), 1);
assert_eq!(out.replies[0].text, "second");
}
#[test]
fn request_deserializes_camel_case() {
let r: AgentChatRequest = serde_json::from_value(json!({
"text": "hi", "conversationId": "c1", "userId": "u1"
}))
.unwrap();
assert_eq!(r.text, "hi");
assert_eq!(r.conversation_id.as_deref(), Some("c1"));
assert_eq!(r.user_id.as_deref(), Some("u1"));
}
#[tokio::test]
async fn agent_chat_unknown_tenant_maps_to_not_found() {
let host = crate::host::RunnerHost::for_test();
let req = AgentChatRequest {
text: "hello".into(),
tenant: Some("nope".into()),
conversation_id: None,
user_id: None,
flow_id: None,
};
let err = execute_chat(&host, "test", req)
.await
.expect_err("unknown tenant should fail");
assert_eq!(err.0, StatusCode::NOT_FOUND);
assert_eq!(err.1["error"], "tenant_not_loaded");
}
}