use std::sync::Arc;
use axum::{Extension, Router};
use http::StatusCode;
use toolkit::api::OpenApiRegistry;
use toolkit::api::operation_builder::{
CORE_GLOBAL_BASE_LICENSE_FEATURE, LicenseFeature, OperationBuilder,
};
use crate::api::rest::WebhookEmitter;
use crate::api::rest::dto::{
CreateSessionRequestDto, ExportAcceptedDto, MessageDto, MessageListDto, ReactionListDto,
ReactionRequestDto, RecreateMessageRequestDto, RegisterSessionTypeRequestDto, SearchRequestDto,
SearchResultsDto, SendMessageRequestDto, SessionDto, SessionTypeDto, ShareRequestDto,
ShareResponseDto, SharedSessionDto, StreamingEventDto, SwitchSessionTypeRequestDto,
VariantListDto,
};
use crate::api::rest::handlers;
use crate::domain::ports::StreamEventBuffer;
use crate::domain::service::{
ExportService, IntelligenceService, MessageService, ReactionService, SearchService,
SessionService, VariantService,
};
const API_TAG: &str = "Chat Engine";
pub(crate) struct ChatEngineLicense;
impl AsRef<str> for ChatEngineLicense {
fn as_ref(&self) -> &'static str {
CORE_GLOBAL_BASE_LICENSE_FEATURE
}
}
impl LicenseFeature for ChatEngineLicense {}
#[derive(Clone)]
pub struct ChatEngineServices {
pub sessions: Arc<SessionService>,
pub messages: Arc<MessageService>,
pub variants: Arc<VariantService>,
pub reactions: Arc<ReactionService>,
pub search: Arc<SearchService>,
pub intelligence: Arc<IntelligenceService>,
pub export: Arc<ExportService>,
}
pub fn register_routes(
router: Router,
openapi: &dyn OpenApiRegistry,
services: ChatEngineServices,
webhooks: Arc<dyn WebhookEmitter>,
stream_buffer: Arc<dyn StreamEventBuffer>,
enable_search: bool,
) -> Router {
let mut router = router;
router = OperationBuilder::post("/chat-engine/v1/session-types")
.operation_id("chat_engine.session_type.register")
.summary("Register a session type and bind it to a backend plugin")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.json_request::<RegisterSessionTypeRequestDto>(openapi, "Session type registration")
.handler(handlers::session_types::register_session_type)
.json_response_with_schema::<SessionTypeDto>(
openapi,
StatusCode::CREATED,
"Registered session type",
)
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::get("/chat-engine/v1/session-types")
.operation_id("chat_engine.session_type.list")
.summary("List registered session types")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.handler(handlers::session_types::list_session_types)
.json_response_with_schema::<Vec<SessionTypeDto>>(
openapi,
StatusCode::OK,
"Session type list",
)
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::get("/chat-engine/v1/session-types/{id}")
.operation_id("chat_engine.session_type.get")
.summary("Get a session type")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Session type UUID")
.handler(handlers::session_types::get_session_type)
.json_response_with_schema::<SessionTypeDto>(openapi, StatusCode::OK, "Session type")
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::post("/chat-engine/v1/sessions")
.operation_id("chat_engine.session.create")
.summary("Create a chat session")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.json_request::<CreateSessionRequestDto>(openapi, "Session creation parameters")
.handler(handlers::sessions::create_session)
.json_response_with_schema::<SessionDto>(openapi, StatusCode::CREATED, "Created session")
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::get("/chat-engine/v1/sessions/{id}")
.operation_id("chat_engine.session.get")
.summary("Get a session")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Session UUID")
.handler(handlers::sessions::get_session)
.json_response_with_schema::<SessionDto>(openapi, StatusCode::OK, "Session details")
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::delete("/chat-engine/v1/sessions/{id}")
.operation_id("chat_engine.session.delete")
.summary("Delete a session (soft or hard)")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Session UUID")
.query_param_typed(
"hard",
false,
"When true, perform a cascading hard delete",
"boolean",
)
.handler(handlers::sessions::delete_session)
.json_response_with_schema::<SessionDto>(openapi, StatusCode::OK, "Soft-deleted session")
.no_content_response(StatusCode::NO_CONTENT, "Hard delete completed")
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::post("/chat-engine/v1/sessions/{id}/switch-type")
.operation_id("chat_engine.session.switch_type")
.summary("Switch the session type of an existing session")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Session UUID")
.json_request::<SwitchSessionTypeRequestDto>(openapi, "Target session type")
.handler(handlers::variants::switch_session_type)
.json_response_with_schema::<SessionDto>(openapi, StatusCode::OK, "Updated session")
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::post("/chat-engine/v1/sessions/{id}/export")
.operation_id("chat_engine.session.export")
.summary("Export a session (returns a download URL)")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Session UUID")
.query_param_typed(
"format",
false,
"Export format (`json` or `markdown`)",
"string",
)
.query_param_typed(
"include_plugin_metadata",
false,
"Include plugin-defined per-message metadata in the export",
"boolean",
)
.handler(handlers::export::export_session)
.json_response_with_schema::<ExportAcceptedDto>(
openapi,
StatusCode::ACCEPTED,
"Export accepted",
)
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::post("/chat-engine/v1/sessions/{id}/share")
.operation_id("chat_engine.session.share")
.summary("Generate a share link for a session")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Session UUID")
.json_request::<ShareRequestDto>(openapi, "Share options")
.handler(handlers::export::create_share)
.json_response_with_schema::<ShareResponseDto>(
openapi,
StatusCode::CREATED,
"Share link issued",
)
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::post("/chat-engine/v1/shared/{share_token}")
.operation_id("chat_engine.session.access_shared")
.summary("Access a session via a public share token")
.tag(API_TAG)
.public()
.path_param("share_token", "Opaque share token (bearer secret)")
.handler(handlers::export::access_shared)
.json_response_with_schema::<SharedSessionDto>(
openapi,
StatusCode::OK,
"Shared session payload",
)
.problem_response(
openapi,
StatusCode::GONE,
"Share token has expired or been revoked",
)
.standard_errors(openapi)
.register(router, openapi);
if enable_search {
router = OperationBuilder::post("/chat-engine/v1/sessions/{id}/search")
.operation_id("chat_engine.session.search")
.summary("Search inside a single session")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Session UUID")
.json_request::<SearchRequestDto>(openapi, "Search query")
.handler(handlers::glue::search_in_session)
.json_response_with_schema::<SearchResultsDto>(
openapi,
StatusCode::OK,
"Search results",
)
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::post("/chat-engine/v1/sessions/search")
.operation_id("chat_engine.sessions.search")
.summary("Search across all sessions for the current user")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.json_request::<SearchRequestDto>(openapi, "Search query")
.handler(handlers::glue::search_across_sessions)
.json_response_with_schema::<SearchResultsDto>(
openapi,
StatusCode::OK,
"Search results",
)
.standard_errors(openapi)
.register(router, openapi);
}
router = OperationBuilder::post("/chat-engine/v1/sessions/{id}/summarize")
.operation_id("chat_engine.session.summarize")
.summary("Trigger an asynchronous session summary")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Session UUID")
.handler(handlers::glue::summarize_session)
.json_response_with_schema::<StreamingEventDto>(
openapi,
StatusCode::OK,
"SSE typed delta stream of message.start/message.text.delta/message.complete/message.error events (text/event-stream)",
)
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::post("/chat-engine/v1/sessions/{id}/messages")
.operation_id("chat_engine.message.send")
.summary("Send a message and stream the assistant response as NDJSON")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Session UUID")
.json_request::<SendMessageRequestDto>(openapi, "Message payload")
.handler(handlers::glue::send_message_in_session)
.json_response_with_schema::<StreamingEventDto>(
openapi,
StatusCode::OK,
"SSE typed delta stream of message.start/message.text.delta/message.complete/message.error events (text/event-stream)",
)
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::get("/chat-engine/v1/sessions/{id}/messages")
.operation_id("chat_engine.message.list")
.summary("List messages on the active path of a session")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Session UUID")
.query_param_typed(
"parent_message_id",
false,
"Optional parent message UUID for partial listings",
"string",
)
.handler(handlers::glue::list_messages)
.json_response_with_schema::<MessageListDto>(openapi, StatusCode::OK, "Message list")
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::get("/chat-engine/v1/messages/{id}")
.operation_id("chat_engine.message.get")
.summary("Get a single message")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Message UUID")
.handler(handlers::glue::get_message)
.json_response_with_schema::<MessageDto>(openapi, StatusCode::OK, "Message details")
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::get("/chat-engine/v1/messages/{id}/stream")
.operation_id("chat_engine.message.stream")
.summary("Resume an assistant message's SSE delta stream (Last-Event-ID)")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Message UUID")
.handler(handlers::glue::resume_message_stream)
.json_response_with_schema::<StreamingEventDto>(
openapi,
StatusCode::OK,
"SSE delta stream replayed from Last-Event-ID then live-tailed (text/event-stream)",
)
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::delete("/chat-engine/v1/messages/{id}")
.operation_id("chat_engine.message.delete")
.summary("Delete a message and its descendants (cascade)")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Message UUID")
.handler(handlers::messages::delete_message)
.json_response_with_schema::<MessageDto>(openapi, StatusCode::OK, "Cascade deletion result")
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::post("/chat-engine/v1/messages/{id}/recreate")
.operation_id("chat_engine.message.recreate")
.summary("Recreate an assistant variant (NDJSON stream)")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Message UUID")
.json_request::<RecreateMessageRequestDto>(openapi, "Recreate options")
.handler(handlers::glue::recreate_message)
.json_response_with_schema::<StreamingEventDto>(
openapi,
StatusCode::OK,
"SSE typed delta stream of message.start/message.text.delta/message.complete/message.error events (text/event-stream)",
)
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::get("/chat-engine/v1/messages/{id}/variants")
.operation_id("chat_engine.message.variants")
.summary("List variants for a message")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Message UUID")
.handler(handlers::glue::list_variants)
.json_response_with_schema::<VariantListDto>(openapi, StatusCode::OK, "Variant list")
.standard_errors(openapi)
.register(router, openapi);
router = OperationBuilder::post("/chat-engine/v1/messages/{id}/reactions")
.operation_id("chat_engine.message.react")
.summary("Set or update a reaction on a message")
.tag(API_TAG)
.authenticated()
.require_license_features([&ChatEngineLicense])
.path_param("id", "Message UUID")
.json_request::<ReactionRequestDto>(openapi, "Reaction payload")
.handler(handlers::glue::set_reaction)
.json_response_with_schema::<ReactionListDto>(
openapi,
StatusCode::OK,
"Updated reaction list",
)
.standard_errors(openapi)
.register(router, openapi);
router
.layer(Extension(services.sessions))
.layer(Extension(services.messages))
.layer(Extension(services.variants))
.layer(Extension(services.reactions))
.layer(Extension(services.search))
.layer(Extension(services.intelligence))
.layer(Extension(services.export))
.layer(Extension(webhooks))
.layer(Extension(stream_buffer))
}