convergio_knowledge/
routes.rs1use std::sync::Arc;
9
10use axum::extract::{Path, State};
11use axum::routing::{delete, get, post};
12use axum::{Json, Router};
13use convergio_db::pool::ConnPool;
14use serde_json::{json, Value};
15
16use crate::store::LanceVectorStore;
17use crate::types::{SearchRequest, WriteRequest};
18
19pub struct KnowledgeState {
20 pub pool: ConnPool,
21 pub store: Option<Arc<LanceVectorStore>>,
22}
23
24pub fn knowledge_routes(state: Arc<KnowledgeState>) -> Router {
25 Router::new()
26 .route("/api/knowledge/search", post(handle_search))
27 .route("/api/knowledge/write", post(handle_write))
28 .route("/api/knowledge/stats", get(handle_stats))
29 .route("/api/knowledge/seed", post(handle_seed))
30 .route("/api/knowledge/prune", post(handle_prune))
31 .route("/api/knowledge/:id", delete(handle_delete))
32 .with_state(state)
33}
34
35async fn handle_search(
36 State(s): State<Arc<KnowledgeState>>,
37 Json(req): Json<SearchRequest>,
38) -> Json<Value> {
39 let embedding = crate::embedder::embed(&req.query).await;
40 let fetch_limit =
41 if req.org_id.is_some() || req.source_type.is_some() || req.project_id.is_some() {
42 req.limit * 3
43 } else {
44 req.limit
45 };
46
47 let Some(ref store) = s.store else {
48 return Json(json!({"results": [], "error": "knowledge store not available"}));
49 };
50 match store.search(&embedding, fetch_limit).await {
51 Ok(mut results) => {
52 if let Some(ref org) = req.org_id {
54 if req.federated {
55 results.retain(|r| {
56 r.entry.org_id.as_deref() == Some(org.as_str())
57 || r.entry.visibility == "public"
58 });
59 } else {
60 results.retain(|r| r.entry.org_id.as_deref() == Some(org.as_str()));
61 }
62 }
63 if let Some(ref st) = req.source_type {
64 results.retain(|r| r.entry.source_type == *st);
65 }
66 if let Some(ref proj) = req.project_id {
67 results.retain(|r| r.entry.project_id.as_deref() == Some(proj.as_str()));
68 }
69 results.retain(|r| r.score >= req.min_score);
71 let mut seen = std::collections::HashSet::new();
73 results.retain(|r| {
74 let key: String = r.entry.content.chars().take(200).collect();
75 seen.insert(key)
76 });
77 results.truncate(req.limit);
78 for r in &mut results {
80 if r.entry.content.len() > 200 {
81 r.entry.content = r.entry.content.chars().take(200).collect::<String>() + "…";
82 }
83 }
84 let count = results.len();
85 Json(json!({
86 "results": results,
87 "query": req.query,
88 "count": count,
89 "min_score": req.min_score,
90 }))
91 }
92 Err(e) => Json(json!({"error": e})),
93 }
94}
95
96async fn handle_write(
97 State(s): State<Arc<KnowledgeState>>,
98 Json(req): Json<WriteRequest>,
99) -> Json<Value> {
100 let Some(ref store) = s.store else {
101 return Json(json!({"error": "knowledge store not available"}));
102 };
103 let embedding = crate::embedder::embed(&req.content).await;
104
105 match store
106 .insert(
107 &req.content,
108 &req.source_type,
109 &req.source_id,
110 req.org_id.as_deref(),
111 req.agent_id.as_deref(),
112 req.project_id.as_deref(),
113 &req.visibility,
114 &embedding,
115 )
116 .await
117 {
118 Ok(id) => Json(json!({"id": id, "status": "stored"})),
119 Err(e) => Json(json!({"error": e})),
120 }
121}
122
123async fn handle_stats(State(s): State<Arc<KnowledgeState>>) -> Json<Value> {
124 let Some(ref store) = s.store else {
125 return Json(json!({"error": "knowledge store not available"}));
126 };
127 match store.stats().await {
128 Ok(stats) => Json(json!(stats)),
129 Err(e) => Json(json!({"error": e})),
130 }
131}
132
133async fn handle_delete(
134 State(s): State<Arc<KnowledgeState>>,
135 Path(id): Path<String>,
136) -> Json<Value> {
137 let Some(ref store) = s.store else {
138 return Json(json!({"error": "knowledge store not available"}));
139 };
140 match store.delete(&id).await {
141 Ok(true) => Json(json!({"deleted": true})),
142 Ok(false) => Json(json!({"error": "not found"})),
143 Err(e) => Json(json!({"error": e})),
144 }
145}
146
147async fn handle_seed(State(s): State<Arc<KnowledgeState>>) -> Json<Value> {
149 let Some(ref store) = s.store else {
150 return Json(json!({"error": "knowledge store not available"}));
151 };
152 let report = crate::seed::seed_baseline(store, &s.pool).await;
153 Json(json!({
154 "total": report.total,
155 "by_source": report.by_source,
156 "skipped": report.skipped,
157 "errors": report.errors
158 }))
159}
160
161async fn handle_prune(
163 State(s): State<Arc<KnowledgeState>>,
164 Json(req): Json<crate::pruning::PruneRequest>,
165) -> Json<Value> {
166 let Some(ref store) = s.store else {
167 return Json(json!({"error": "knowledge store not available"}));
168 };
169 let report = crate::pruning::prune(store, &req).await;
170 Json(json!(report))
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn search_request_deserializes() {
179 let json = r#"{"query": "agent task", "limit": 10}"#;
180 let req: SearchRequest = serde_json::from_str(json).unwrap();
181 assert_eq!(req.query, "agent task");
182 assert_eq!(req.limit, 10);
183 }
184
185 #[test]
186 fn write_request_deserializes() {
187 let json = r#"{"content": "test", "source_type": "doc", "source_id": "d1"}"#;
188 let req: WriteRequest = serde_json::from_str(json).unwrap();
189 assert_eq!(req.source_type, "doc");
190 assert!(req.org_id.is_none());
191 }
192}