use std::sync::Arc;
use std::time::SystemTime;
use serde::Deserialize;
use crate::context::manager::ContextManager as KnowledgeContextManager;
use crate::context::types::*;
use crate::reasoning::conversation::{Conversation, MessageRole};
use crate::reasoning::inference::ToolDefinition;
use crate::types::AgentId;
#[derive(Debug, Clone)]
pub struct KnowledgeConfig {
pub max_context_items: usize,
pub relevance_threshold: f32,
pub auto_persist: bool,
}
impl Default for KnowledgeConfig {
fn default() -> Self {
Self {
max_context_items: 5,
relevance_threshold: 0.3,
auto_persist: true,
}
}
}
pub struct KnowledgeBridge {
context_manager: Arc<dyn KnowledgeContextManager>,
config: KnowledgeConfig,
}
impl KnowledgeBridge {
pub fn new(context_manager: Arc<dyn KnowledgeContextManager>, config: KnowledgeConfig) -> Self {
Self {
context_manager,
config,
}
}
pub async fn inject_context(
&self,
agent_id: &AgentId,
conversation: &mut Conversation,
) -> Result<usize, ContextError> {
let search_terms = extract_search_terms(conversation);
if search_terms.is_empty() {
return Ok(0);
}
let query = ContextQuery {
query_type: QueryType::Hybrid,
search_terms: search_terms.clone(),
time_range: None,
memory_types: vec![],
relevance_threshold: self.config.relevance_threshold,
max_results: self.config.max_context_items,
include_embeddings: false,
};
let context_items = self.context_manager.query_context(*agent_id, query).await?;
let search_query = search_terms.join(" ");
let knowledge_items = self
.context_manager
.search_knowledge(*agent_id, &search_query, self.config.max_context_items)
.await?;
let mut lines = Vec::new();
for item in &context_items {
lines.push(format!(
"- [memory, relevance={:.2}] {}",
item.relevance_score, item.content
));
}
for item in &knowledge_items {
lines.push(format!(
"- [knowledge/{:?}, confidence={:.2}] {}",
item.knowledge_type, item.confidence, item.content
));
}
let total_items = context_items.len() + knowledge_items.len();
if !lines.is_empty() {
let context_text = format!(
"The following relevant knowledge and context was retrieved for this conversation:\n{}",
lines.join("\n")
);
conversation.inject_knowledge_context(context_text);
}
Ok(total_items)
}
pub async fn persist_learnings(
&self,
agent_id: &AgentId,
conversation: &Conversation,
) -> Result<(), ContextError> {
let assistant_messages: Vec<&str> = conversation
.messages()
.iter()
.filter(|m| m.role == MessageRole::Assistant && !m.content.is_empty())
.map(|m| m.content.as_str())
.collect();
if assistant_messages.is_empty() {
return Ok(());
}
let summary = if assistant_messages.len() == 1 {
assistant_messages[0].to_string()
} else {
let combined = assistant_messages.join("\n---\n");
if combined.len() > 2000 {
format!("{}...", &combined[..2000])
} else {
combined
}
};
let memory_update = MemoryUpdate {
operation: UpdateOperation::Add,
target: MemoryTarget::Working("last_conversation_summary".to_string()),
data: serde_json::Value::String(summary),
};
self.context_manager
.update_memory(*agent_id, vec![memory_update])
.await
}
pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
vec![recall_tool_def(), store_tool_def()]
}
pub async fn handle_tool_call(
&self,
agent_id: &AgentId,
tool_name: &str,
arguments: &str,
) -> Result<String, String> {
match tool_name {
"recall_knowledge" => self.handle_recall(agent_id, arguments).await,
"store_knowledge" => self.handle_store(agent_id, arguments).await,
_ => Err(format!("Unknown knowledge tool: {}", tool_name)),
}
}
pub fn is_knowledge_tool(tool_name: &str) -> bool {
matches!(tool_name, "recall_knowledge" | "store_knowledge")
}
async fn handle_recall(&self, agent_id: &AgentId, arguments: &str) -> Result<String, String> {
#[derive(Deserialize)]
struct RecallArgs {
query: String,
#[serde(default = "default_limit")]
limit: usize,
#[cfg(feature = "orga-adaptive")]
#[serde(default)]
directory: Option<String>,
#[cfg(feature = "orga-adaptive")]
#[serde(default)]
scope: Option<String>,
}
fn default_limit() -> usize {
5
}
let args: RecallArgs =
serde_json::from_str(arguments).map_err(|e| format!("Invalid arguments: {}", e))?;
#[cfg(feature = "orga-adaptive")]
{
if let (Some(ref dir), Some(ref scope)) = (&args.directory, &args.scope) {
if scope == "conventions" {
return self
.retrieve_scoped_conventions(agent_id, &args.query, dir, args.limit)
.await;
}
}
}
let items = self
.context_manager
.search_knowledge(*agent_id, &args.query, args.limit)
.await
.map_err(|e| format!("Knowledge search failed: {}", e))?;
if items.is_empty() {
return Ok("No relevant knowledge found.".to_string());
}
let mut lines = Vec::new();
for item in &items {
lines.push(format!(
"- [{:?}, confidence={:.2}] {}",
item.knowledge_type, item.confidence, item.content
));
}
Ok(lines.join("\n"))
}
#[cfg(feature = "orga-adaptive")]
async fn retrieve_scoped_conventions(
&self,
agent_id: &AgentId,
language: &str,
directory: &str,
limit: usize,
) -> Result<String, String> {
let mut all_items = Vec::new();
let mut seen_content = std::collections::HashSet::new();
let mut current_dir = std::path::PathBuf::from(directory);
loop {
let dir_query = format!("{} conventions {}", language, current_dir.display());
if let Ok(items) = self
.context_manager
.search_knowledge(*agent_id, &dir_query, limit)
.await
{
for item in items {
if seen_content.insert(item.content.clone()) {
all_items.push(item);
}
}
}
if !current_dir.pop() {
break;
}
}
let lang_query = format!("{} conventions", language);
if let Ok(items) = self
.context_manager
.search_knowledge(*agent_id, &lang_query, limit)
.await
{
for item in items {
if seen_content.insert(item.content.clone()) {
all_items.push(item);
}
}
}
all_items.truncate(limit);
if all_items.is_empty() {
return Ok("No relevant conventions found.".to_string());
}
let mut lines = Vec::new();
for item in &all_items {
lines.push(format!(
"- [{:?}, confidence={:.2}] {}",
item.knowledge_type, item.confidence, item.content
));
}
Ok(lines.join("\n"))
}
async fn handle_store(&self, agent_id: &AgentId, arguments: &str) -> Result<String, String> {
#[derive(Deserialize)]
struct StoreArgs {
subject: String,
predicate: String,
object: String,
#[serde(default = "default_confidence")]
confidence: f32,
}
fn default_confidence() -> f32 {
0.8
}
let args: StoreArgs =
serde_json::from_str(arguments).map_err(|e| format!("Invalid arguments: {}", e))?;
let fact = KnowledgeFact {
id: KnowledgeId::new(),
subject: args.subject.clone(),
predicate: args.predicate.clone(),
object: args.object.clone(),
confidence: args.confidence,
source: KnowledgeSource::Experience,
created_at: SystemTime::now(),
verified: false,
};
let knowledge_id = self
.context_manager
.add_knowledge(*agent_id, Knowledge::Fact(fact))
.await
.map_err(|e| format!("Failed to store knowledge: {}", e))?;
Ok(format!(
"Stored fact: {} {} {} (id: {})",
args.subject, args.predicate, args.object, knowledge_id.0
))
}
}
#[cfg(not(feature = "orga-adaptive"))]
fn recall_tool_def() -> ToolDefinition {
ToolDefinition {
name: "recall_knowledge".to_string(),
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(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to find relevant knowledge"
},
"limit": {
"type": "integer",
"description": "Maximum number of results to return (default: 5)",
"default": 5
}
},
"required": ["query"]
}),
}
}
#[cfg(feature = "orga-adaptive")]
fn recall_tool_def() -> ToolDefinition {
ToolDefinition {
name: "recall_knowledge".to_string(),
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(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to find relevant knowledge (or language name when scope='conventions')"
},
"limit": {
"type": "integer",
"description": "Maximum number of results to return (default: 5)",
"default": 5
},
"directory": {
"type": "string",
"description": "Directory path for scoped convention retrieval. Walks up parent directories for convention inheritance."
},
"scope": {
"type": "string",
"description": "Set to 'conventions' to retrieve directory-scoped coding conventions instead of general knowledge.",
"enum": ["conventions"]
}
},
"required": ["query"]
}),
}
}
fn store_tool_def() -> ToolDefinition {
ToolDefinition {
name: "store_knowledge".to_string(),
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(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"subject": {
"type": "string",
"description": "The subject of the fact (e.g., 'Rust')"
},
"predicate": {
"type": "string",
"description": "The relationship (e.g., 'is_a')"
},
"object": {
"type": "string",
"description": "The object of the fact (e.g., 'systems programming language')"
},
"confidence": {
"type": "number",
"description": "Confidence level 0.0-1.0 (default: 0.8)",
"default": 0.8
}
},
"required": ["subject", "predicate", "object"]
}),
}
}
fn extract_search_terms(conversation: &Conversation) -> Vec<String> {
let messages = conversation.messages();
let mut terms = Vec::new();
for msg in messages.iter().rev().take(5) {
match msg.role {
MessageRole::User | MessageRole::Tool => {
let words: Vec<&str> = msg
.content
.split_whitespace()
.filter(|w| w.len() > 3)
.take(10)
.collect();
for word in words {
let cleaned = word.trim_matches(|c: char| !c.is_alphanumeric());
if !cleaned.is_empty() && !terms.contains(&cleaned.to_string()) {
terms.push(cleaned.to_string());
}
}
}
_ => {}
}
if terms.len() >= 15 {
break;
}
}
terms
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reasoning::conversation::ConversationMessage;
#[test]
fn test_knowledge_config_default() {
let config = KnowledgeConfig::default();
assert_eq!(config.max_context_items, 5);
assert!((config.relevance_threshold - 0.3).abs() < f32::EPSILON);
assert!(config.auto_persist);
}
#[test]
fn test_extract_search_terms_from_user_message() {
let mut conv = Conversation::new();
conv.push(ConversationMessage::user(
"What is the weather forecast for tomorrow?",
));
let terms = extract_search_terms(&conv);
assert!(!terms.is_empty());
assert!(terms.contains(&"weather".to_string()));
assert!(terms.contains(&"forecast".to_string()));
assert!(terms.contains(&"tomorrow".to_string()));
}
#[test]
fn test_extract_search_terms_skips_short_words() {
let mut conv = Conversation::new();
conv.push(ConversationMessage::user("I am at the big house"));
let terms = extract_search_terms(&conv);
assert!(terms.contains(&"house".to_string()));
assert!(!terms.iter().any(|t| t.len() <= 3));
}
#[test]
fn test_extract_search_terms_empty_conversation() {
let conv = Conversation::new();
let terms = extract_search_terms(&conv);
assert!(terms.is_empty());
}
#[test]
fn test_extract_search_terms_ignores_assistant() {
let mut conv = Conversation::new();
conv.push(ConversationMessage::assistant(
"Here is some information about databases",
));
let terms = extract_search_terms(&conv);
assert!(terms.is_empty());
}
#[test]
fn test_tool_definitions() {
let recall = recall_tool_def();
assert_eq!(recall.name, "recall_knowledge");
assert!(recall.parameters["required"]
.as_array()
.unwrap()
.contains(&serde_json::json!("query")));
let store = store_tool_def();
assert_eq!(store.name, "store_knowledge");
assert!(store.parameters["required"]
.as_array()
.unwrap()
.contains(&serde_json::json!("subject")));
}
#[test]
fn test_is_knowledge_tool() {
assert!(KnowledgeBridge::is_knowledge_tool("recall_knowledge"));
assert!(KnowledgeBridge::is_knowledge_tool("store_knowledge"));
assert!(!KnowledgeBridge::is_knowledge_tool("web_search"));
assert!(!KnowledgeBridge::is_knowledge_tool(""));
}
#[cfg(feature = "orga-adaptive")]
#[test]
fn test_recall_tool_def_has_directory_and_scope() {
let def = recall_tool_def();
let props = &def.parameters["properties"];
assert!(props.get("directory").is_some());
assert!(props.get("scope").is_some());
assert!(def.parameters["required"]
.as_array()
.unwrap()
.contains(&serde_json::json!("query")));
}
#[cfg(feature = "orga-adaptive")]
#[test]
fn test_recall_tool_backward_compatible() {
let def = recall_tool_def();
let required = def.parameters["required"].as_array().unwrap();
assert!(!required.contains(&serde_json::json!("directory")));
assert!(!required.contains(&serde_json::json!("scope")));
}
}