1use crate::model::RetrievalMode;
22use crate::pipeline::Pipeline;
23use crate::{RagError, Result};
24use axum::extract::{Path, Query, Request, State};
25use axum::http::{header, StatusCode};
26use axum::middleware::{self, Next};
27use axum::response::{IntoResponse, Response};
28use axum::routing::get;
29use axum::{Json, Router};
30use serde::Deserialize;
31use serde_json::json;
32use std::collections::HashSet;
33use std::str::FromStr;
34use std::sync::Arc;
35
36struct AppState {
37 pipeline: Pipeline,
38 keys: HashSet<String>,
39}
40
41pub fn router(pipeline: Pipeline, keys: Vec<String>) -> Result<Router> {
43 if keys.is_empty() {
44 return Err(RagError::config(
45 "RAG_API_KEYS must contain at least one key to start the REST API",
46 ));
47 }
48 let state = Arc::new(AppState {
49 pipeline,
50 keys: keys.into_iter().collect(),
51 });
52
53 let protected = Router::new()
54 .route("/api/stats", get(stats))
55 .route("/api/documents", get(list_documents))
56 .route("/api/documents/{id}", get(get_document))
57 .route("/api/search", get(search_get).post(search_post))
58 .layer(middleware::from_fn_with_state(state.clone(), auth));
59
60 Ok(Router::new()
61 .route("/health", get(|| async { Json(json!({"status": "ok"})) }))
62 .merge(protected)
63 .with_state(state))
64}
65
66pub async fn serve(pipeline: Pipeline, addr: &str, keys: Vec<String>) -> Result<()> {
68 let app = router(pipeline, keys)?;
69 let listener = tokio::net::TcpListener::bind(addr)
70 .await
71 .map_err(|e| RagError::config(format!("cannot bind {addr}: {e}")))?;
72 tracing::info!(%addr, "REST API listening");
73 axum::serve(listener, app)
74 .await
75 .map_err(|e| RagError::config(format!("server error: {e}")))
76}
77
78async fn auth(State(state): State<Arc<AppState>>, req: Request, next: Next) -> Response {
79 let headers = req.headers();
80 let provided = headers
81 .get("x-api-key")
82 .and_then(|v| v.to_str().ok())
83 .or_else(|| {
84 headers
85 .get(header::AUTHORIZATION)
86 .and_then(|v| v.to_str().ok())
87 .and_then(|v| v.strip_prefix("Bearer "))
88 });
89 match provided {
90 Some(key) if state.keys.contains(key) => next.run(req).await,
91 _ => err(StatusCode::UNAUTHORIZED, "invalid or missing API key").into_response(),
92 }
93}
94
95type ApiResult = std::result::Result<Response, (StatusCode, Json<serde_json::Value>)>;
96
97fn err(code: StatusCode, msg: impl std::fmt::Display) -> (StatusCode, Json<serde_json::Value>) {
98 (code, Json(json!({"error": msg.to_string()})))
99}
100
101fn internal(e: RagError) -> (StatusCode, Json<serde_json::Value>) {
102 err(StatusCode::INTERNAL_SERVER_ERROR, e)
103}
104
105async fn stats(State(state): State<Arc<AppState>>) -> ApiResult {
106 let store = state.pipeline.store();
107 let documents = store.count_documents().await.map_err(internal)?;
108 let chunks = store.count_chunks().await.map_err(internal)?;
109 Ok(Json(json!({"documents": documents, "chunks": chunks})).into_response())
110}
111
112async fn list_documents(State(state): State<Arc<AppState>>) -> ApiResult {
113 let docs = state
114 .pipeline
115 .store()
116 .list_documents()
117 .await
118 .map_err(internal)?;
119 Ok(Json(json!({"documents": docs})).into_response())
120}
121
122async fn get_document(State(state): State<Arc<AppState>>, Path(id): Path<String>) -> ApiResult {
123 let docs = state
124 .pipeline
125 .store()
126 .list_documents()
127 .await
128 .map_err(internal)?;
129 match docs.into_iter().find(|d| d.id == id) {
130 Some(doc) => Ok(Json(doc).into_response()),
131 None => Err(err(
132 StatusCode::NOT_FOUND,
133 format!("no document with id '{id}'"),
134 )),
135 }
136}
137
138#[derive(Debug, Deserialize)]
140struct SearchParams {
141 #[serde(alias = "q")]
143 query: String,
144 mode: Option<String>,
146 #[serde(alias = "k")]
148 top_k: Option<usize>,
149 #[serde(default)]
151 answer: bool,
152}
153
154async fn search_get(
155 State(state): State<Arc<AppState>>,
156 Query(params): Query<SearchParams>,
157) -> ApiResult {
158 run_search(state, params).await
159}
160
161async fn search_post(
162 State(state): State<Arc<AppState>>,
163 Json(params): Json<SearchParams>,
164) -> ApiResult {
165 run_search(state, params).await
166}
167
168async fn run_search(state: Arc<AppState>, params: SearchParams) -> ApiResult {
169 if params.query.trim().is_empty() {
170 return Err(err(StatusCode::BAD_REQUEST, "query must not be empty"));
171 }
172 let mode = match ¶ms.mode {
173 Some(m) => RetrievalMode::from_str(m).map_err(|e| err(StatusCode::BAD_REQUEST, e))?,
174 None => state.pipeline.config().retrieval_mode,
175 };
176 let k = params
177 .top_k
178 .unwrap_or(state.pipeline.config().top_k)
179 .clamp(1, 100);
180
181 if params.answer {
182 let a = state
183 .pipeline
184 .answer(¶ms.query, mode, k)
185 .await
186 .map_err(|e| match e {
187 RagError::Llm(_) => err(StatusCode::BAD_REQUEST, e),
188 other => internal(other),
189 })?;
190 return Ok(Json(json!({
191 "query": params.query,
192 "mode": mode.to_string(),
193 "answer": a.text,
194 "results": a.sources,
195 }))
196 .into_response());
197 }
198
199 let hits = state
200 .pipeline
201 .query(mode, ¶ms.query, k)
202 .await
203 .map_err(|e| match e {
204 RagError::Llm(_) => err(StatusCode::BAD_REQUEST, e),
205 other => internal(other),
206 })?;
207 Ok(Json(json!({
208 "query": params.query,
209 "mode": mode.to_string(),
210 "results": hits,
211 }))
212 .into_response())
213}