convergio_knowledge/
ext.rs1use std::path::PathBuf;
4use std::sync::{Arc, OnceLock};
5
6use convergio_db::pool::ConnPool;
7use convergio_types::events::{DomainEvent, EventFilter, EventKind};
8use convergio_types::extension::{
9 AppContext, Extension, Health, McpToolDef, Metric, Migration, ScheduledTask,
10};
11use convergio_types::manifest::{Capability, Manifest, ModuleKind};
12
13use crate::routes::{knowledge_routes, KnowledgeState};
14use crate::store::LanceVectorStore;
15
16pub struct KnowledgeExtension {
17 pool: ConnPool,
18 lance_path: PathBuf,
19 store: OnceLock<Option<Arc<LanceVectorStore>>>,
20}
21
22impl KnowledgeExtension {
23 pub fn new(pool: ConnPool) -> Self {
24 let lance_path = dirs::home_dir()
25 .unwrap_or_else(|| PathBuf::from("."))
26 .join(".convergio/data/knowledge.lance");
27 Self {
28 pool,
29 lance_path,
30 store: OnceLock::new(),
31 }
32 }
33
34 fn get_store(&self) -> Option<Arc<LanceVectorStore>> {
35 self.store
36 .get_or_init(|| {
37 let path = self.lance_path.clone();
38 let result = std::thread::spawn(move || {
40 let rt = tokio::runtime::Builder::new_current_thread()
41 .enable_all()
42 .build()
43 .ok()?;
44 rt.block_on(LanceVectorStore::open(&path)).ok()
45 })
46 .join()
47 .ok()
48 .flatten();
49 match result {
50 Some(s) => Some(Arc::new(s)),
51 None => {
52 tracing::warn!("lance store unavailable — knowledge search disabled");
53 None
54 }
55 }
56 })
57 .clone()
58 }
59
60 fn state(&self) -> Arc<KnowledgeState> {
61 Arc::new(KnowledgeState {
62 pool: self.pool.clone(),
63 store: self.get_store(),
64 })
65 }
66}
67
68impl Extension for KnowledgeExtension {
69 fn manifest(&self) -> Manifest {
70 Manifest {
71 id: "convergio-knowledge".to_string(),
72 description: "Vector knowledge store — semantic memory for agents via embeddings"
73 .to_string(),
74 version: env!("CARGO_PKG_VERSION").to_string(),
75 kind: ModuleKind::Platform,
76 provides: vec![
77 Capability {
78 name: "knowledge-vectors".to_string(),
79 version: "1.0.0".to_string(),
80 description: "Embedding-based semantic search over agent knowledge".to_string(),
81 },
82 Capability {
83 name: "knowledge-api".to_string(),
84 version: "1.0.0".to_string(),
85 description: "REST API for knowledge search and write".to_string(),
86 },
87 ],
88 requires: vec![],
89 agent_tools: vec![],
90 required_roles: vec!["orchestrator".into(), "all".into()],
91 }
92 }
93
94 fn migrations(&self) -> Vec<Migration> {
95 vec![]
96 }
97
98 fn routes(&self, _ctx: &AppContext) -> Option<axum::Router> {
99 Some(knowledge_routes(self.state()))
100 }
101
102 fn health(&self) -> Health {
103 let Some(store) = self.get_store() else {
104 return Health::Ok;
105 };
106 let ok = std::thread::spawn(move || {
107 let rt = tokio::runtime::Builder::new_current_thread()
108 .enable_all()
109 .build()
110 .ok()?;
111 rt.block_on(store.stats()).ok()
112 })
113 .join()
114 .ok()
115 .flatten();
116 if ok.is_some() {
117 Health::Ok
118 } else {
119 Health::Degraded {
120 reason: "lance stats failed".into(),
121 }
122 }
123 }
124
125 fn metrics(&self) -> Vec<Metric> {
126 let Some(store) = self.get_store() else {
127 return vec![];
128 };
129 let count = std::thread::spawn(move || {
130 let rt = tokio::runtime::Builder::new_current_thread()
131 .enable_all()
132 .build()
133 .ok()?;
134 rt.block_on(store.stats())
135 .ok()
136 .map(|s| s.total_entries as f64)
137 })
138 .join()
139 .ok()
140 .flatten()
141 .unwrap_or(0.0);
142 vec![Metric {
143 name: "knowledge_entries_total".to_string(),
144 value: count,
145 labels: vec![],
146 }]
147 }
148
149 fn subscriptions(&self) -> Vec<EventFilter> {
150 vec![EventFilter {
151 kind_prefix: Some("Task".to_string()),
152 org: None,
153 actor: None,
154 }]
155 }
156
157 fn on_event(&self, event: &DomainEvent) {
158 if let EventKind::TaskCompleted { task_id } = &event.kind {
159 if let Some(store) = self.get_store() {
160 crate::hooks::on_task_completed(&self.pool, store, *task_id)
161 };
162 }
163 }
164
165 fn scheduled_tasks(&self) -> Vec<ScheduledTask> {
166 vec![
167 ScheduledTask {
168 name: "knowledge-commit-sync",
169 cron: "*/15 * * * *",
170 },
171 ScheduledTask {
172 name: "knowledge-auto-prune",
173 cron: "0 3 * * *", },
175 ]
176 }
177
178 fn on_scheduled_task(&self, task_name: &str) {
179 if task_name == "knowledge-commit-sync" {
180 if let Some(store) = self.get_store() {
181 crate::hooks::sync_recent_commits(store)
182 };
183 }
184 if task_name == "knowledge-auto-prune" {
185 if let Some(store) = self.get_store() {
186 let store = store.clone();
187 tokio::spawn(async move {
188 let req = crate::pruning::PruneRequest {
189 max_age_days: None,
190 source_type: None,
191 dry_run: false,
192 };
193 let report = crate::pruning::prune(&store, &req).await;
194 tracing::info!(
195 stale = report.stale_removed,
196 dedup = report.duplicates_removed,
197 source_dedup = report.source_dedup_removed,
198 total = report.total_removed,
199 "auto-prune completed"
200 );
201 });
202 };
203 }
204 }
205
206 fn mcp_tools(&self) -> Vec<McpToolDef> {
207 crate::mcp_defs::knowledge_tools()
208 }
209}