1use std::sync::Arc;
9use std::time::SystemTime;
10
11use serde::Deserialize;
12
13use crate::context::manager::ContextManager as KnowledgeContextManager;
14use crate::context::types::*;
15use crate::reasoning::conversation::{Conversation, MessageRole};
16use crate::reasoning::inference::ToolDefinition;
17use crate::types::AgentId;
18
19#[derive(Debug, Clone)]
21pub struct KnowledgeConfig {
22 pub max_context_items: usize,
24 pub relevance_threshold: f32,
26 pub auto_persist: bool,
28}
29
30impl Default for KnowledgeConfig {
31 fn default() -> Self {
32 Self {
33 max_context_items: 5,
34 relevance_threshold: 0.3,
35 auto_persist: true,
36 }
37 }
38}
39
40pub struct KnowledgeBridge {
42 context_manager: Arc<dyn KnowledgeContextManager>,
43 config: KnowledgeConfig,
44}
45
46impl KnowledgeBridge {
47 pub fn new(context_manager: Arc<dyn KnowledgeContextManager>, config: KnowledgeConfig) -> Self {
48 Self {
49 context_manager,
50 config,
51 }
52 }
53
54 pub async fn inject_context(
59 &self,
60 agent_id: &AgentId,
61 conversation: &mut Conversation,
62 ) -> Result<usize, ContextError> {
63 let search_terms = extract_search_terms(conversation);
65 if search_terms.is_empty() {
66 return Ok(0);
67 }
68
69 let query = ContextQuery {
71 query_type: QueryType::Hybrid,
72 search_terms: search_terms.clone(),
73 time_range: None,
74 memory_types: vec![],
75 relevance_threshold: self.config.relevance_threshold,
76 max_results: self.config.max_context_items,
77 include_embeddings: false,
78 };
79
80 let context_items = self.context_manager.query_context(*agent_id, query).await?;
81
82 let search_query = search_terms.join(" ");
84 let knowledge_items = self
85 .context_manager
86 .search_knowledge(*agent_id, &search_query, self.config.max_context_items)
87 .await?;
88
89 let mut lines = Vec::new();
91
92 for item in &context_items {
93 lines.push(format!(
94 "- [memory, relevance={:.2}] {}",
95 item.relevance_score, item.content
96 ));
97 }
98
99 for item in &knowledge_items {
100 lines.push(format!(
101 "- [knowledge/{:?}, confidence={:.2}] {}",
102 item.knowledge_type, item.confidence, item.content
103 ));
104 }
105
106 let total_items = context_items.len() + knowledge_items.len();
107
108 if !lines.is_empty() {
109 let context_text = format!(
110 "The following relevant knowledge and context was retrieved for this conversation:\n{}",
111 lines.join("\n")
112 );
113 conversation.inject_knowledge_context(context_text);
114 }
115
116 Ok(total_items)
117 }
118
119 pub async fn persist_learnings(
122 &self,
123 agent_id: &AgentId,
124 conversation: &Conversation,
125 ) -> Result<(), ContextError> {
126 let assistant_messages: Vec<&str> = conversation
128 .messages()
129 .iter()
130 .filter(|m| m.role == MessageRole::Assistant && !m.content.is_empty())
131 .map(|m| m.content.as_str())
132 .collect();
133
134 if assistant_messages.is_empty() {
135 return Ok(());
136 }
137
138 let summary = if assistant_messages.len() == 1 {
139 assistant_messages[0].to_string()
140 } else {
141 let combined = assistant_messages.join("\n---\n");
143 if combined.len() > 2000 {
144 format!("{}...", &combined[..2000])
145 } else {
146 combined
147 }
148 };
149
150 let memory_update = MemoryUpdate {
151 operation: UpdateOperation::Add,
152 target: MemoryTarget::Working("last_conversation_summary".to_string()),
153 data: serde_json::Value::String(summary),
154 };
155
156 self.context_manager
157 .update_memory(*agent_id, vec![memory_update])
158 .await
159 }
160
161 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
163 vec![recall_tool_def(), store_tool_def()]
164 }
165
166 pub async fn handle_tool_call(
168 &self,
169 agent_id: &AgentId,
170 tool_name: &str,
171 arguments: &str,
172 ) -> Result<String, String> {
173 match tool_name {
174 "recall_knowledge" => self.handle_recall(agent_id, arguments).await,
175 "store_knowledge" => self.handle_store(agent_id, arguments).await,
176 _ => Err(format!("Unknown knowledge tool: {}", tool_name)),
177 }
178 }
179
180 pub fn is_knowledge_tool(tool_name: &str) -> bool {
182 matches!(tool_name, "recall_knowledge" | "store_knowledge")
183 }
184
185 async fn handle_recall(&self, agent_id: &AgentId, arguments: &str) -> Result<String, String> {
186 #[derive(Deserialize)]
187 struct RecallArgs {
188 query: String,
189 #[serde(default = "default_limit")]
190 limit: usize,
191 #[cfg(feature = "orga-adaptive")]
192 #[serde(default)]
193 directory: Option<String>,
194 #[cfg(feature = "orga-adaptive")]
195 #[serde(default)]
196 scope: Option<String>,
197 }
198 fn default_limit() -> usize {
199 5
200 }
201
202 let args: RecallArgs =
203 serde_json::from_str(arguments).map_err(|e| format!("Invalid arguments: {}", e))?;
204
205 #[cfg(feature = "orga-adaptive")]
207 {
208 if let (Some(ref dir), Some(ref scope)) = (&args.directory, &args.scope) {
209 if scope == "conventions" {
210 return self
211 .retrieve_scoped_conventions(agent_id, &args.query, dir, args.limit)
212 .await;
213 }
214 }
215 }
216
217 let items = self
218 .context_manager
219 .search_knowledge(*agent_id, &args.query, args.limit)
220 .await
221 .map_err(|e| format!("Knowledge search failed: {}", e))?;
222
223 if items.is_empty() {
224 return Ok("No relevant knowledge found.".to_string());
225 }
226
227 let mut lines = Vec::new();
228 for item in &items {
229 lines.push(format!(
230 "- [{:?}, confidence={:.2}] {}",
231 item.knowledge_type, item.confidence, item.content
232 ));
233 }
234 Ok(lines.join("\n"))
235 }
236
237 #[cfg(feature = "orga-adaptive")]
240 async fn retrieve_scoped_conventions(
241 &self,
242 agent_id: &AgentId,
243 language: &str,
244 directory: &str,
245 limit: usize,
246 ) -> Result<String, String> {
247 let mut all_items = Vec::new();
248 let mut seen_content = std::collections::HashSet::new();
249
250 let mut current_dir = std::path::PathBuf::from(directory);
252 loop {
253 let dir_query = format!("{} conventions {}", language, current_dir.display());
254 if let Ok(items) = self
255 .context_manager
256 .search_knowledge(*agent_id, &dir_query, limit)
257 .await
258 {
259 for item in items {
260 if seen_content.insert(item.content.clone()) {
261 all_items.push(item);
262 }
263 }
264 }
265
266 if !current_dir.pop() {
267 break;
268 }
269 }
270
271 let lang_query = format!("{} conventions", language);
273 if let Ok(items) = self
274 .context_manager
275 .search_knowledge(*agent_id, &lang_query, limit)
276 .await
277 {
278 for item in items {
279 if seen_content.insert(item.content.clone()) {
280 all_items.push(item);
281 }
282 }
283 }
284
285 all_items.truncate(limit);
287
288 if all_items.is_empty() {
289 return Ok("No relevant conventions found.".to_string());
290 }
291
292 let mut lines = Vec::new();
293 for item in &all_items {
294 lines.push(format!(
295 "- [{:?}, confidence={:.2}] {}",
296 item.knowledge_type, item.confidence, item.content
297 ));
298 }
299 Ok(lines.join("\n"))
300 }
301
302 async fn handle_store(&self, agent_id: &AgentId, arguments: &str) -> Result<String, String> {
303 #[derive(Deserialize)]
304 struct StoreArgs {
305 subject: String,
306 predicate: String,
307 object: String,
308 #[serde(default = "default_confidence")]
309 confidence: f32,
310 }
311 fn default_confidence() -> f32 {
312 0.8
313 }
314
315 let args: StoreArgs =
316 serde_json::from_str(arguments).map_err(|e| format!("Invalid arguments: {}", e))?;
317
318 let fact = KnowledgeFact {
319 id: KnowledgeId::new(),
320 subject: args.subject.clone(),
321 predicate: args.predicate.clone(),
322 object: args.object.clone(),
323 confidence: args.confidence,
324 source: KnowledgeSource::Experience,
325 created_at: SystemTime::now(),
326 verified: false,
327 };
328
329 let knowledge_id = self
330 .context_manager
331 .add_knowledge(*agent_id, Knowledge::Fact(fact))
332 .await
333 .map_err(|e| format!("Failed to store knowledge: {}", e))?;
334
335 Ok(format!(
336 "Stored fact: {} {} {} (id: {})",
337 args.subject, args.predicate, args.object, knowledge_id.0
338 ))
339 }
340}
341
342#[cfg(not(feature = "orga-adaptive"))]
343fn recall_tool_def() -> ToolDefinition {
344 ToolDefinition {
345 name: "recall_knowledge".to_string(),
346 description: "Search the agent's knowledge base for relevant information. Use this to recall facts, procedures, or patterns that may help with the current task.".to_string(),
347 parameters: serde_json::json!({
348 "type": "object",
349 "properties": {
350 "query": {
351 "type": "string",
352 "description": "The search query to find relevant knowledge"
353 },
354 "limit": {
355 "type": "integer",
356 "description": "Maximum number of results to return (default: 5)",
357 "default": 5
358 }
359 },
360 "required": ["query"]
361 }),
362 }
363}
364
365#[cfg(feature = "orga-adaptive")]
366fn recall_tool_def() -> ToolDefinition {
367 ToolDefinition {
368 name: "recall_knowledge".to_string(),
369 description: "Search the agent's knowledge base for relevant information. Use this to recall facts, procedures, conventions, or patterns that may help with the current task. Use scope='conventions' with a directory to retrieve directory-scoped conventions.".to_string(),
370 parameters: serde_json::json!({
371 "type": "object",
372 "properties": {
373 "query": {
374 "type": "string",
375 "description": "The search query to find relevant knowledge (or language name when scope='conventions')"
376 },
377 "limit": {
378 "type": "integer",
379 "description": "Maximum number of results to return (default: 5)",
380 "default": 5
381 },
382 "directory": {
383 "type": "string",
384 "description": "Directory path for scoped convention retrieval. Walks up parent directories for convention inheritance."
385 },
386 "scope": {
387 "type": "string",
388 "description": "Set to 'conventions' to retrieve directory-scoped coding conventions instead of general knowledge.",
389 "enum": ["conventions"]
390 }
391 },
392 "required": ["query"]
393 }),
394 }
395}
396
397fn store_tool_def() -> ToolDefinition {
398 ToolDefinition {
399 name: "store_knowledge".to_string(),
400 description: "Store a new fact in the agent's knowledge base for future reference. Use this to remember important information learned during the conversation.".to_string(),
401 parameters: serde_json::json!({
402 "type": "object",
403 "properties": {
404 "subject": {
405 "type": "string",
406 "description": "The subject of the fact (e.g., 'Rust')"
407 },
408 "predicate": {
409 "type": "string",
410 "description": "The relationship (e.g., 'is_a')"
411 },
412 "object": {
413 "type": "string",
414 "description": "The object of the fact (e.g., 'systems programming language')"
415 },
416 "confidence": {
417 "type": "number",
418 "description": "Confidence level 0.0-1.0 (default: 0.8)",
419 "default": 0.8
420 }
421 },
422 "required": ["subject", "predicate", "object"]
423 }),
424 }
425}
426
427fn extract_search_terms(conversation: &Conversation) -> Vec<String> {
429 let messages = conversation.messages();
430 let mut terms = Vec::new();
431
432 for msg in messages.iter().rev().take(5) {
434 match msg.role {
435 MessageRole::User | MessageRole::Tool => {
436 let words: Vec<&str> = msg
438 .content
439 .split_whitespace()
440 .filter(|w| w.len() > 3)
441 .take(10)
442 .collect();
443 for word in words {
444 let cleaned = word.trim_matches(|c: char| !c.is_alphanumeric());
445 if !cleaned.is_empty() && !terms.contains(&cleaned.to_string()) {
446 terms.push(cleaned.to_string());
447 }
448 }
449 }
450 _ => {}
451 }
452 if terms.len() >= 15 {
454 break;
455 }
456 }
457
458 terms
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::reasoning::conversation::ConversationMessage;
465
466 #[test]
467 fn test_knowledge_config_default() {
468 let config = KnowledgeConfig::default();
469 assert_eq!(config.max_context_items, 5);
470 assert!((config.relevance_threshold - 0.3).abs() < f32::EPSILON);
471 assert!(config.auto_persist);
472 }
473
474 #[test]
475 fn test_extract_search_terms_from_user_message() {
476 let mut conv = Conversation::new();
477 conv.push(ConversationMessage::user(
478 "What is the weather forecast for tomorrow?",
479 ));
480
481 let terms = extract_search_terms(&conv);
482 assert!(!terms.is_empty());
483 assert!(terms.contains(&"weather".to_string()));
484 assert!(terms.contains(&"forecast".to_string()));
485 assert!(terms.contains(&"tomorrow".to_string()));
486 }
487
488 #[test]
489 fn test_extract_search_terms_skips_short_words() {
490 let mut conv = Conversation::new();
491 conv.push(ConversationMessage::user("I am at the big house"));
492
493 let terms = extract_search_terms(&conv);
494 assert!(terms.contains(&"house".to_string()));
496 assert!(!terms.iter().any(|t| t.len() <= 3));
497 }
498
499 #[test]
500 fn test_extract_search_terms_empty_conversation() {
501 let conv = Conversation::new();
502 let terms = extract_search_terms(&conv);
503 assert!(terms.is_empty());
504 }
505
506 #[test]
507 fn test_extract_search_terms_ignores_assistant() {
508 let mut conv = Conversation::new();
509 conv.push(ConversationMessage::assistant(
510 "Here is some information about databases",
511 ));
512
513 let terms = extract_search_terms(&conv);
514 assert!(terms.is_empty());
515 }
516
517 #[test]
518 fn test_tool_definitions() {
519 let recall = recall_tool_def();
520 assert_eq!(recall.name, "recall_knowledge");
521 assert!(recall.parameters["required"]
522 .as_array()
523 .unwrap()
524 .contains(&serde_json::json!("query")));
525
526 let store = store_tool_def();
527 assert_eq!(store.name, "store_knowledge");
528 assert!(store.parameters["required"]
529 .as_array()
530 .unwrap()
531 .contains(&serde_json::json!("subject")));
532 }
533
534 #[test]
535 fn test_is_knowledge_tool() {
536 assert!(KnowledgeBridge::is_knowledge_tool("recall_knowledge"));
537 assert!(KnowledgeBridge::is_knowledge_tool("store_knowledge"));
538 assert!(!KnowledgeBridge::is_knowledge_tool("web_search"));
539 assert!(!KnowledgeBridge::is_knowledge_tool(""));
540 }
541
542 #[cfg(feature = "orga-adaptive")]
543 #[test]
544 fn test_recall_tool_def_has_directory_and_scope() {
545 let def = recall_tool_def();
546 let props = &def.parameters["properties"];
547 assert!(props.get("directory").is_some());
548 assert!(props.get("scope").is_some());
549 assert!(def.parameters["required"]
551 .as_array()
552 .unwrap()
553 .contains(&serde_json::json!("query")));
554 }
555
556 #[cfg(feature = "orga-adaptive")]
557 #[test]
558 fn test_recall_tool_backward_compatible() {
559 let def = recall_tool_def();
560 let required = def.parameters["required"].as_array().unwrap();
561 assert!(!required.contains(&serde_json::json!("directory")));
563 assert!(!required.contains(&serde_json::json!("scope")));
564 }
565}