1use std::sync::Mutex;
4
5use async_trait::async_trait;
6use serde_json::Value;
7use uuid::Uuid;
8
9use khive_brain_core::SectionPosteriorState;
10use khive_runtime::pack::{PackByIdResolver, PackRuntime};
11use khive_runtime::{KhiveRuntime, NamespaceToken, Resolved, RuntimeError, VerbRegistry};
12use khive_storage::types::{SqlStatement, SqlValue};
13use khive_types::{HandlerDef, Pack};
14
15use crate::knowledge::util::{atom_from_row, atom_to_json, domain_from_row, domain_to_json};
16use crate::knowledge::vamana;
17use crate::knowledge::KnowledgeHandlers;
18use crate::vocab::KNOWLEDGE_HANDLERS;
19
20pub struct KnowledgePack {
22 pub(crate) runtime: KhiveRuntime,
23 pub(crate) ann: vamana::SharedAnn,
24 pub(crate) section_posteriors: Mutex<SectionPosteriorState>,
25 pub(crate) brain_profile: Option<String>,
31}
32
33impl Pack for KnowledgePack {
34 const NAME: &'static str = "knowledge";
35 const NOTE_KINDS: &'static [&'static str] = &[];
36 const ENTITY_KINDS: &'static [&'static str] = &[];
37 const HANDLERS: &'static [HandlerDef] = &KNOWLEDGE_HANDLERS;
38 const REQUIRES: &'static [&'static str] = &["kg"];
39}
40
41impl KnowledgePack {
42 pub fn new(runtime: KhiveRuntime) -> Self {
44 let brain_profile = runtime.config().brain_profile.clone();
45 Self {
46 runtime,
47 ann: vamana::new_shared(),
48 section_posteriors: Mutex::new(SectionPosteriorState::new()),
49 brain_profile,
50 }
51 }
52}
53
54struct KnowledgePackFactory;
55
56impl khive_runtime::PackFactory for KnowledgePackFactory {
57 fn name(&self) -> &'static str {
58 "knowledge"
59 }
60
61 fn requires(&self) -> &'static [&'static str] {
62 &["kg"]
63 }
64
65 fn create(&self, runtime: KhiveRuntime) -> Box<dyn khive_runtime::PackRuntime> {
66 Box::new(KnowledgePack::new(runtime))
67 }
68
69 fn create_resolver(
70 &self,
71 runtime: KhiveRuntime,
72 ) -> Option<Box<dyn khive_runtime::PackByIdResolver>> {
73 Some(Box::new(KnowledgePack::new(runtime)))
74 }
75}
76
77inventory::submit! { khive_runtime::PackRegistration(&KnowledgePackFactory) }
78
79#[async_trait]
80impl PackRuntime for KnowledgePack {
81 fn name(&self) -> &str {
82 <KnowledgePack as Pack>::NAME
83 }
84
85 fn note_kinds(&self) -> &'static [&'static str] {
86 <KnowledgePack as Pack>::NOTE_KINDS
87 }
88
89 fn entity_kinds(&self) -> &'static [&'static str] {
90 <KnowledgePack as Pack>::ENTITY_KINDS
91 }
92
93 fn handlers(&self) -> &'static [HandlerDef] {
94 &KNOWLEDGE_HANDLERS
95 }
96
97 fn requires(&self) -> &'static [&'static str] {
98 <KnowledgePack as Pack>::REQUIRES
99 }
100
101 async fn warm(&self) {
102 crate::knowledge::vamana::warm_known_snapshots(&self.runtime, &self.ann).await;
103 if !self.runtime.default_embedder_name().is_empty() {
104 let runtime = self.runtime.clone();
105 tokio::spawn(async move {
106 let _ = runtime.embed("__khive_knowledge_warm__").await;
107 });
108 }
109 }
110
111 async fn dispatch(
112 &self,
113 verb: &str,
114 params: Value,
115 registry: &VerbRegistry,
116 token: &NamespaceToken,
117 ) -> Result<Value, RuntimeError> {
118 match verb {
119 "knowledge.upsert_atoms" => {
120 KnowledgeHandlers::upsert_atoms(&self.runtime, token, params).await
121 }
122 "knowledge.upsert_domains" => {
123 KnowledgeHandlers::upsert_domains(&self.runtime, token, params).await
124 }
125 "knowledge.get" => KnowledgeHandlers::get(&self.runtime, token, params).await,
126 "knowledge.list" => KnowledgeHandlers::list(&self.runtime, token, params).await,
127 "knowledge.delete_atoms" => {
128 KnowledgeHandlers::delete_atoms(&self.runtime, token, params).await
129 }
130 "knowledge.stats" => KnowledgeHandlers::stats(&self.runtime, token, params).await,
131 "knowledge.index" => {
132 KnowledgeHandlers::index(&self.runtime, token, params, &self.ann, None).await
133 }
134 "knowledge.fold" => KnowledgeHandlers::fold(&self.runtime, token, params).await,
135 "knowledge.search" => {
136 KnowledgeHandlers::search(&self.runtime, token, params, &self.ann).await
137 }
138 "knowledge.suggest" => {
139 KnowledgeHandlers::suggest(&self.runtime, token, params, &self.ann).await
140 }
141 "knowledge.compose" => {
142 let type_weights = self.resolve_compose_type_weights(registry, token).await;
143 KnowledgeHandlers::compose(&self.runtime, token, params, &self.ann, type_weights)
144 .await
145 }
146 "knowledge.edit" => {
147 KnowledgeHandlers::edit(&self.runtime, token, params, &self.ann).await
148 }
149 "knowledge.import" => {
150 KnowledgeHandlers::import(&self.runtime, token, params, &self.ann).await
151 }
152 "knowledge.challenge" => {
153 KnowledgeHandlers::challenge(&self.runtime, token, params).await
154 }
155 "knowledge.adjudicate" => {
156 KnowledgeHandlers::adjudicate(&self.runtime, token, params).await
157 }
158 "knowledge.learn" => self.handle_learn(token, params).await,
159 "knowledge.cite" => self.handle_cite(token, params).await,
160 "knowledge.topic" => self.handle_topic(token, params).await,
161 "knowledge.feedback" => self.handle_feedback(token, params, registry).await,
162 _ => Err(RuntimeError::InvalidInput(format!(
163 "knowledge pack does not handle verb {verb:?}"
164 ))),
165 }
166 }
167}
168
169#[async_trait]
170impl PackByIdResolver for KnowledgePack {
171 async fn resolve_by_id(&self, id: Uuid) -> Result<Option<Resolved>, RuntimeError> {
177 let sql = self.runtime.sql();
178 let id_str = id.to_string();
179
180 let mut reader = sql
181 .reader()
182 .await
183 .map_err(|e| RuntimeError::Internal(format!("knowledge resolve_by_id reader: {e}")))?;
184
185 let domain_row = reader
187 .query_row(SqlStatement {
188 sql: "SELECT id, namespace, slug, name, description, tags, members, \
189 created_at, updated_at, deleted_at \
190 FROM knowledge_domains WHERE id = ?1 AND deleted_at IS NULL LIMIT 1"
191 .into(),
192 params: vec![SqlValue::Text(id_str.clone())],
193 label: Some("knowledge.resolve_by_id.domain".into()),
194 })
195 .await
196 .map_err(|e| {
197 RuntimeError::Internal(format!("knowledge resolve_by_id domain query: {e}"))
198 })?;
199
200 if let Some(row) = domain_row {
201 if let Some(domain) = domain_from_row(&row) {
202 return Ok(Some(Resolved::PackRecord {
203 pack: "knowledge".into(),
204 kind: "domain".into(),
205 data: domain_to_json(&domain),
206 }));
207 }
208 }
209
210 let atom_row = reader
212 .query_row(SqlStatement {
213 sql: "SELECT id, namespace, slug, name, content, tags, properties, \
214 status, finalized, source_uri, source_type, created_at, updated_at, deleted_at \
215 FROM knowledge_atoms WHERE id = ?1 AND deleted_at IS NULL LIMIT 1"
216 .into(),
217 params: vec![SqlValue::Text(id_str)],
218 label: Some("knowledge.resolve_by_id.atom".into()),
219 })
220 .await
221 .map_err(|e| {
222 RuntimeError::Internal(format!("knowledge resolve_by_id atom query: {e}"))
223 })?;
224
225 if let Some(row) = atom_row {
226 if let Some(atom) = atom_from_row(&row) {
227 return Ok(Some(Resolved::PackRecord {
228 pack: "knowledge".into(),
229 kind: "atom".into(),
230 data: atom_to_json(&atom),
231 }));
232 }
233 }
234
235 Ok(None)
236 }
237
238 async fn resolve_by_id_including_deleted(
242 &self,
243 id: Uuid,
244 ) -> Result<Option<Resolved>, RuntimeError> {
245 let sql = self.runtime.sql();
246 let id_str = id.to_string();
247
248 let mut reader = sql.reader().await.map_err(|e| {
249 RuntimeError::Internal(format!(
250 "knowledge resolve_by_id_including_deleted reader: {e}"
251 ))
252 })?;
253
254 let domain_row = reader
255 .query_row(SqlStatement {
256 sql: "SELECT id, namespace, slug, name, description, tags, members, \
257 created_at, updated_at, deleted_at \
258 FROM knowledge_domains WHERE id = ?1 LIMIT 1"
259 .into(),
260 params: vec![SqlValue::Text(id_str.clone())],
261 label: Some("knowledge.resolve_by_id_incl_deleted.domain".into()),
262 })
263 .await
264 .map_err(|e| {
265 RuntimeError::Internal(format!(
266 "knowledge resolve_by_id_including_deleted domain query: {e}"
267 ))
268 })?;
269
270 if let Some(row) = domain_row {
271 if let Some(domain) = domain_from_row(&row) {
272 return Ok(Some(Resolved::PackRecord {
273 pack: "knowledge".into(),
274 kind: "domain".into(),
275 data: domain_to_json(&domain),
276 }));
277 }
278 }
279
280 let atom_row = reader
281 .query_row(SqlStatement {
282 sql: "SELECT id, namespace, slug, name, content, tags, properties, \
283 status, finalized, source_uri, source_type, created_at, updated_at, deleted_at \
284 FROM knowledge_atoms WHERE id = ?1 LIMIT 1"
285 .into(),
286 params: vec![SqlValue::Text(id_str)],
287 label: Some("knowledge.resolve_by_id_incl_deleted.atom".into()),
288 })
289 .await
290 .map_err(|e| {
291 RuntimeError::Internal(format!(
292 "knowledge resolve_by_id_including_deleted atom query: {e}"
293 ))
294 })?;
295
296 if let Some(row) = atom_row {
297 if let Some(atom) = atom_from_row(&row) {
298 return Ok(Some(Resolved::PackRecord {
299 pack: "knowledge".into(),
300 kind: "atom".into(),
301 data: atom_to_json(&atom),
302 }));
303 }
304 }
305
306 Ok(None)
307 }
308
309 async fn delete_by_id(&self, id: Uuid, hard: bool) -> Result<serde_json::Value, RuntimeError> {
315 let sql = self.runtime.sql();
316 let id_str = id.to_string();
317
318 let kind = match self.resolve_by_id_including_deleted(id).await? {
320 Some(Resolved::PackRecord { kind, .. }) => kind,
321 _ => {
322 return Err(RuntimeError::NotFound(format!(
323 "knowledge record not found: {id}"
324 )));
325 }
326 };
327
328 let now_us = std::time::SystemTime::now()
329 .duration_since(std::time::UNIX_EPOCH)
330 .unwrap_or_default()
331 .as_micros() as i64;
332
333 let mut writer = sql
334 .writer()
335 .await
336 .map_err(|e| RuntimeError::Internal(format!("knowledge delete_by_id writer: {e}")))?;
337
338 match kind.as_str() {
339 "domain" => {
340 if hard {
341 writer
345 .execute_batch(vec![
346 SqlStatement {
347 sql: "DELETE FROM knowledge_sections WHERE atom_id = ?1".into(),
348 params: vec![SqlValue::Text(id_str.clone())],
349 label: Some(
350 "knowledge.delete_by_id.domain_mirror_sections.hard".into(),
351 ),
352 },
353 SqlStatement {
354 sql: "DELETE FROM knowledge_atoms WHERE id = ?1".into(),
355 params: vec![SqlValue::Text(id_str.clone())],
356 label: Some("knowledge.delete_by_id.domain_mirror.hard".into()),
357 },
358 SqlStatement {
359 sql: "DELETE FROM knowledge_domains WHERE id = ?1".into(),
360 params: vec![SqlValue::Text(id_str.clone())],
361 label: Some("knowledge.delete_by_id.domain.hard".into()),
362 },
363 ])
364 .await
365 .map_err(|e| {
366 RuntimeError::Internal(format!(
367 "knowledge delete_by_id domain hard: {e}"
368 ))
369 })?;
370 } else {
371 writer
372 .execute(SqlStatement {
373 sql: "UPDATE knowledge_domains SET deleted_at = ?1 \
374 WHERE id = ?2 AND deleted_at IS NULL"
375 .into(),
376 params: vec![SqlValue::Integer(now_us), SqlValue::Text(id_str.clone())],
377 label: Some("knowledge.delete_by_id.domain.soft".into()),
378 })
379 .await
380 .map_err(|e| {
381 RuntimeError::Internal(format!(
382 "knowledge delete_by_id domain soft: {e}"
383 ))
384 })?;
385 writer
387 .execute(SqlStatement {
388 sql: "UPDATE knowledge_atoms SET deleted_at = ?1 \
389 WHERE id = ?2 AND deleted_at IS NULL"
390 .into(),
391 params: vec![SqlValue::Integer(now_us), SqlValue::Text(id_str.clone())],
392 label: Some("knowledge.delete_by_id.domain_mirror.soft".into()),
393 })
394 .await
395 .map_err(|e| {
396 RuntimeError::Internal(format!(
397 "knowledge delete_by_id domain mirror soft: {e}"
398 ))
399 })?;
400 }
401 }
402 "atom" => {
403 if hard {
404 writer
406 .execute_batch(vec![
407 SqlStatement {
408 sql: "DELETE FROM knowledge_sections WHERE atom_id = ?1".into(),
409 params: vec![SqlValue::Text(id_str.clone())],
410 label: Some("knowledge.delete_by_id.atom_sections.hard".into()),
411 },
412 SqlStatement {
413 sql: "DELETE FROM knowledge_atoms WHERE id = ?1".into(),
414 params: vec![SqlValue::Text(id_str.clone())],
415 label: Some("knowledge.delete_by_id.atom.hard".into()),
416 },
417 ])
418 .await
419 .map_err(|e| {
420 RuntimeError::Internal(format!("knowledge delete_by_id atom hard: {e}"))
421 })?;
422 } else {
423 writer
424 .execute(SqlStatement {
425 sql: "UPDATE knowledge_atoms SET deleted_at = ?1 \
426 WHERE id = ?2 AND deleted_at IS NULL"
427 .into(),
428 params: vec![SqlValue::Integer(now_us), SqlValue::Text(id_str.clone())],
429 label: Some("knowledge.delete_by_id.atom.soft".into()),
430 })
431 .await
432 .map_err(|e| {
433 RuntimeError::Internal(format!("knowledge delete_by_id atom soft: {e}"))
434 })?;
435 }
436 }
437 other => {
438 return Err(RuntimeError::Internal(format!(
439 "knowledge delete_by_id: unexpected kind {other:?}"
440 )));
441 }
442 }
443
444 Ok(serde_json::json!({
445 "deleted": true,
446 "id": id_str,
447 "kind": kind,
448 "hard": hard,
449 }))
450 }
451}