Skip to content

Commit 8aed0e4

Browse files
authored
feat: implement agent-scoped knowledge base and context-specific search (#2647)
1 parent 3f26b1f commit 8aed0e4

File tree

12 files changed

+442
-224
lines changed

12 files changed

+442
-224
lines changed

crates/chat-cli/src/cli/chat/cli/knowledge.rs

Lines changed: 109 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use crossterm::style::{
88
};
99
use eyre::Result;
1010
use semantic_search_client::{
11-
KnowledgeContext,
1211
OperationStatus,
1312
SystemStatus,
1413
};
@@ -103,6 +102,11 @@ impl KnowledgeSubcommand {
103102
}
104103
}
105104

105+
/// Get the current agent from the session
106+
fn get_agent(session: &ChatSession) -> Option<&crate::cli::Agent> {
107+
session.conversation.agents.get_active()
108+
}
109+
106110
async fn execute_operation(&self, os: &Os, session: &mut ChatSession) -> OperationResult {
107111
match self {
108112
KnowledgeSubcommand::Show => {
@@ -116,127 +120,105 @@ impl KnowledgeSubcommand {
116120
include,
117121
exclude,
118122
index_type,
119-
} => Self::handle_add(os, path, include, exclude, index_type).await,
120-
KnowledgeSubcommand::Remove { path } => Self::handle_remove(os, path).await,
121-
KnowledgeSubcommand::Update { path } => Self::handle_update(os, path).await,
123+
} => Self::handle_add(os, session, path, include, exclude, index_type).await,
124+
KnowledgeSubcommand::Remove { path } => Self::handle_remove(os, session, path).await,
125+
KnowledgeSubcommand::Update { path } => Self::handle_update(os, session, path).await,
122126
KnowledgeSubcommand::Clear => Self::handle_clear(os, session).await,
123-
KnowledgeSubcommand::Status => Self::handle_status(os).await,
124-
KnowledgeSubcommand::Cancel { operation_id } => Self::handle_cancel(os, operation_id.as_deref()).await,
127+
KnowledgeSubcommand::Status => Self::handle_status(os, session).await,
128+
KnowledgeSubcommand::Cancel { operation_id } => {
129+
Self::handle_cancel(os, session, operation_id.as_deref()).await
130+
},
125131
}
126132
}
127133

128134
async fn handle_show(os: &Os, session: &mut ChatSession) -> Result<(), std::io::Error> {
129-
match KnowledgeStore::get_async_instance_with_os(os).await {
130-
Ok(store) => {
131-
let store = store.lock().await;
132-
let entries = store.get_all().await.unwrap_or_else(|e| {
133-
let _ = queue!(
134-
session.stderr,
135-
style::SetForegroundColor(Color::Red),
136-
style::Print(&format!("Error getting knowledge base entries: {}\n", e)),
137-
style::ResetColor
138-
);
139-
Vec::new()
140-
});
141-
let _ = Self::format_knowledge_entries(session, &entries);
142-
},
143-
Err(e) => {
144-
queue!(
145-
session.stderr,
146-
style::SetForegroundColor(Color::Red),
147-
style::Print(&format!("Error accessing knowledge base: {}\n", e)),
148-
style::SetForegroundColor(Color::Reset)
149-
)?;
150-
},
151-
}
152-
Ok(())
153-
}
135+
let agent_name = Self::get_agent(session).map(|a| a.name.clone());
154136

155-
fn format_knowledge_entries(
156-
session: &mut ChatSession,
157-
knowledge_entries: &[KnowledgeContext],
158-
) -> Result<(), std::io::Error> {
159-
if knowledge_entries.is_empty() {
160-
queue!(
161-
session.stderr,
162-
style::Print("\nNo knowledge base entries found.\n"),
163-
style::Print("💡 Tip: If indexing is in progress, entries may not appear until indexing completes.\n"),
164-
style::Print(" Use 'knowledge status' to check active operations.\n\n")
165-
)?;
166-
} else {
137+
// Show agent-specific knowledge
138+
if let Some(agent) = agent_name {
167139
queue!(
168140
session.stderr,
169-
style::Print("\n📚 Knowledge Base Entries:\n"),
170-
style::Print(format!("{}\n", "━".repeat(80)))
141+
style::SetAttribute(crossterm::style::Attribute::Bold),
142+
style::SetForegroundColor(Color::Magenta),
143+
style::Print(format!("👤 Agent ({}):\n", agent)),
144+
style::SetAttribute(crossterm::style::Attribute::Reset),
171145
)?;
172146

173-
for entry in knowledge_entries {
174-
Self::format_single_entry(session, &entry)?;
175-
queue!(session.stderr, style::Print(format!("{}\n", "━".repeat(80))))?;
147+
match KnowledgeStore::get_async_instance(os, Self::get_agent(session)).await {
148+
Ok(store) => {
149+
let store = store.lock().await;
150+
let contexts = store.get_all().await.unwrap_or_default();
151+
152+
if contexts.is_empty() {
153+
queue!(
154+
session.stderr,
155+
style::SetForegroundColor(Color::DarkGrey),
156+
style::Print(" <none>\n\n"),
157+
style::SetForegroundColor(Color::Reset)
158+
)?;
159+
} else {
160+
Self::format_knowledge_entries_with_indent(session, &contexts, " ")?;
161+
}
162+
},
163+
Err(_) => {
164+
queue!(
165+
session.stderr,
166+
style::SetForegroundColor(Color::DarkGrey),
167+
style::Print(" <none>\n\n"),
168+
style::SetForegroundColor(Color::Reset)
169+
)?;
170+
},
176171
}
177-
// Add final newline to match original formatting exactly
178-
queue!(session.stderr, style::Print("\n"))?;
179172
}
173+
180174
Ok(())
181175
}
182176

183-
fn format_single_entry(session: &mut ChatSession, entry: &&KnowledgeContext) -> Result<(), std::io::Error> {
184-
queue!(
185-
session.stderr,
186-
style::SetAttribute(style::Attribute::Bold),
187-
style::SetForegroundColor(Color::Cyan),
188-
style::Print(format!("📂 {}: ", entry.id)),
189-
style::SetForegroundColor(Color::Green),
190-
style::Print(&entry.name),
191-
style::SetAttribute(style::Attribute::Reset),
192-
style::Print("\n")
193-
)?;
194-
195-
queue!(
196-
session.stderr,
197-
style::Print(format!(" Description: {}\n", entry.description)),
198-
style::Print(format!(
199-
" Created: {}\n",
200-
entry.created_at.format("%Y-%m-%d %H:%M:%S")
201-
)),
202-
style::Print(format!(
203-
" Updated: {}\n",
204-
entry.updated_at.format("%Y-%m-%d %H:%M:%S")
205-
))
206-
)?;
207-
208-
if let Some(path) = &entry.source_path {
209-
queue!(session.stderr, style::Print(format!(" Source: {}\n", path)))?;
210-
}
211-
212-
queue!(
213-
session.stderr,
214-
style::Print(" Items: "),
215-
style::SetForegroundColor(Color::Yellow),
216-
style::Print(entry.item_count.to_string()),
217-
style::SetForegroundColor(Color::Reset),
218-
style::Print(" | Index Type: "),
219-
style::SetForegroundColor(Color::Magenta),
220-
style::Print(entry.embedding_type.description().to_string()),
221-
style::SetForegroundColor(Color::Reset),
222-
style::Print(" | Persistent: ")
223-
)?;
224-
225-
if entry.persistent {
177+
fn format_knowledge_entries_with_indent(
178+
session: &mut ChatSession,
179+
contexts: &[semantic_search_client::KnowledgeContext],
180+
indent: &str,
181+
) -> Result<(), std::io::Error> {
182+
for ctx in contexts {
183+
// Main entry line with name and ID
226184
queue!(
227185
session.stderr,
186+
style::Print(format!("{}📂 ", indent)),
187+
style::SetAttribute(style::Attribute::Bold),
188+
style::SetForegroundColor(Color::Grey),
189+
style::Print(&ctx.name),
228190
style::SetForegroundColor(Color::Green),
229-
style::Print("Yes"),
191+
style::Print(format!(" ({})", &ctx.id[..8])),
192+
style::SetAttribute(style::Attribute::Reset),
230193
style::SetForegroundColor(Color::Reset),
231194
style::Print("\n")
232195
)?;
233-
} else {
196+
197+
// Description line with original description
234198
queue!(
235199
session.stderr,
236-
style::SetForegroundColor(Color::Yellow),
237-
style::Print("No"),
200+
style::Print(format!("{} ", indent)),
201+
style::SetForegroundColor(Color::Grey),
202+
style::Print(format!("{}\n", ctx.description)),
203+
style::SetForegroundColor(Color::Reset)
204+
)?;
205+
206+
// Stats line with improved colors
207+
queue!(
208+
session.stderr,
209+
style::Print(format!("{} ", indent)),
210+
style::SetForegroundColor(Color::Green),
211+
style::Print(format!("{} items", ctx.item_count)),
212+
style::SetForegroundColor(Color::DarkGrey),
213+
style::Print(" • "),
214+
style::SetForegroundColor(Color::Blue),
215+
style::Print(ctx.embedding_type.description()),
216+
style::SetForegroundColor(Color::DarkGrey),
217+
style::Print(" • "),
218+
style::SetForegroundColor(Color::DarkGrey),
219+
style::Print(format!("{}", ctx.updated_at.format("%m/%d %H:%M"))),
238220
style::SetForegroundColor(Color::Reset),
239-
style::Print("\n")
221+
style::Print("\n\n")
240222
)?;
241223
}
242224
Ok(())
@@ -254,14 +236,17 @@ impl KnowledgeSubcommand {
254236

255237
async fn handle_add(
256238
os: &Os,
239+
session: &mut ChatSession,
257240
path: &str,
258241
include_patterns: &[String],
259242
exclude_patterns: &[String],
260243
index_type: &Option<String>,
261244
) -> OperationResult {
262245
match Self::validate_and_sanitize_path(os, path) {
263246
Ok(sanitized_path) => {
264-
let async_knowledge_store = match KnowledgeStore::get_async_instance_with_os(os).await {
247+
let agent = Self::get_agent(session);
248+
249+
let async_knowledge_store = match KnowledgeStore::get_async_instance(os, agent).await {
265250
Ok(store) => store,
266251
Err(e) => return OperationResult::Error(format!("Error accessing knowledge base: {}", e)),
267252
};
@@ -307,30 +292,40 @@ impl KnowledgeSubcommand {
307292
}
308293

309294
/// Handle remove operation
310-
async fn handle_remove(os: &Os, path: &str) -> OperationResult {
295+
async fn handle_remove(os: &Os, session: &ChatSession, path: &str) -> OperationResult {
311296
let sanitized_path = sanitize_path_tool_arg(os, path);
297+
let agent = Self::get_agent(session);
312298

313-
let async_knowledge_store = match KnowledgeStore::get_async_instance_with_os(os).await {
299+
let async_knowledge_store = match KnowledgeStore::get_async_instance(os, agent).await {
314300
Ok(store) => store,
315301
Err(e) => return OperationResult::Error(format!("Error accessing knowledge base: {}", e)),
316302
};
317303
let mut store = async_knowledge_store.lock().await;
318304

305+
let scope_desc = "agent";
306+
319307
// Try path first, then name
320308
if store.remove_by_path(&sanitized_path.to_string_lossy()).await.is_ok() {
321-
OperationResult::Success(format!("Removed knowledge base entry with path '{}'", path))
309+
OperationResult::Success(format!(
310+
"Removed {} knowledge base entry with path '{}'",
311+
scope_desc, path
312+
))
322313
} else if store.remove_by_name(path).await.is_ok() {
323-
OperationResult::Success(format!("Removed knowledge base entry with name '{}'", path))
314+
OperationResult::Success(format!(
315+
"Removed {} knowledge base entry with name '{}'",
316+
scope_desc, path
317+
))
324318
} else {
325-
OperationResult::Warning(format!("Entry not found in knowledge base: {}", path))
319+
OperationResult::Warning(format!("Entry not found in {} knowledge base: {}", scope_desc, path))
326320
}
327321
}
328322

329323
/// Handle update operation
330-
async fn handle_update(os: &Os, path: &str) -> OperationResult {
324+
async fn handle_update(os: &Os, session: &ChatSession, path: &str) -> OperationResult {
331325
match Self::validate_and_sanitize_path(os, path) {
332326
Ok(sanitized_path) => {
333-
let async_knowledge_store = match KnowledgeStore::get_async_instance_with_os(os).await {
327+
let agent = Self::get_agent(session);
328+
let async_knowledge_store = match KnowledgeStore::get_async_instance(os, agent).await {
334329
Ok(store) => store,
335330
Err(e) => {
336331
return OperationResult::Error(format!("Error accessing knowledge base directory: {}", e));
@@ -368,7 +363,8 @@ impl KnowledgeSubcommand {
368363
return OperationResult::Info("Clear operation cancelled".to_string());
369364
}
370365

371-
let async_knowledge_store = match KnowledgeStore::get_async_instance_with_os(os).await {
366+
let agent = Self::get_agent(session);
367+
let async_knowledge_store = match KnowledgeStore::get_async_instance(os, agent).await {
372368
Ok(store) => store,
373369
Err(e) => return OperationResult::Error(format!("Error accessing knowledge base directory: {}", e)),
374370
};
@@ -401,8 +397,9 @@ impl KnowledgeSubcommand {
401397
}
402398

403399
/// Handle status operation
404-
async fn handle_status(os: &Os) -> OperationResult {
405-
let async_knowledge_store = match KnowledgeStore::get_async_instance_with_os(os).await {
400+
async fn handle_status(os: &Os, session: &ChatSession) -> OperationResult {
401+
let agent = Self::get_agent(session);
402+
let async_knowledge_store = match KnowledgeStore::get_async_instance(os, agent).await {
406403
Ok(store) => store,
407404
Err(e) => return OperationResult::Error(format!("Error accessing knowledge base directory: {}", e)),
408405
};
@@ -512,8 +509,9 @@ impl KnowledgeSubcommand {
512509
}
513510

514511
/// Handle cancel operation
515-
async fn handle_cancel(os: &Os, operation_id: Option<&str>) -> OperationResult {
516-
let async_knowledge_store = match KnowledgeStore::get_async_instance_with_os(os).await {
512+
async fn handle_cancel(os: &Os, session: &ChatSession, operation_id: Option<&str>) -> OperationResult {
513+
let agent = Self::get_agent(session);
514+
let async_knowledge_store = match KnowledgeStore::get_async_instance(os, agent).await {
517515
Ok(store) => store,
518516
Err(e) => return OperationResult::Error(format!("Error accessing knowledge base directory: {}", e)),
519517
};

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1950,7 +1950,12 @@ impl ChatSession {
19501950

19511951
let invoke_result = tool
19521952
.tool
1953-
.invoke(os, &mut self.stdout, &mut self.conversation.file_line_tracker)
1953+
.invoke(
1954+
os,
1955+
&mut self.stdout,
1956+
&mut self.conversation.file_line_tracker,
1957+
self.conversation.agents.get_active(),
1958+
)
19541959
.await;
19551960

19561961
if self.spinner.is_some() {

crates/chat-cli/src/cli/chat/tools/knowledge.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,9 +312,13 @@ impl Knowledge {
312312
Ok(())
313313
}
314314

315-
pub async fn invoke(&self, os: &Os, _updates: &mut impl Write) -> Result<InvokeOutput> {
316-
// Get the async knowledge store singleton with OS-aware directory
317-
let async_knowledge_store = KnowledgeStore::get_async_instance_with_os(os)
315+
pub async fn invoke(
316+
&self,
317+
os: &Os,
318+
_updates: &mut impl Write,
319+
agent: Option<&crate::cli::Agent>,
320+
) -> Result<InvokeOutput> {
321+
let async_knowledge_store = KnowledgeStore::get_async_instance(os, agent)
318322
.await
319323
.map_err(|e| eyre::eyre!("Failed to access knowledge base: {}", e))?;
320324
let mut store = async_knowledge_store.lock().await;

crates/chat-cli/src/cli/chat/tools/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ impl Tool {
120120
os: &Os,
121121
stdout: &mut impl Write,
122122
line_tracker: &mut HashMap<String, FileLineTracker>,
123+
agent: Option<&crate::cli::agent::Agent>,
123124
) -> Result<InvokeOutput> {
124125
match self {
125126
Tool::FsRead(fs_read) => fs_read.invoke(os, stdout).await,
@@ -128,7 +129,7 @@ impl Tool {
128129
Tool::UseAws(use_aws) => use_aws.invoke(os, stdout).await,
129130
Tool::Custom(custom_tool) => custom_tool.invoke(os, stdout).await,
130131
Tool::GhIssue(gh_issue) => gh_issue.invoke(os, stdout).await,
131-
Tool::Knowledge(knowledge) => knowledge.invoke(os, stdout).await,
132+
Tool::Knowledge(knowledge) => knowledge.invoke(os, stdout, agent).await,
132133
Tool::Thinking(think) => think.invoke(stdout).await,
133134
}
134135
}

crates/chat-cli/src/cli/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ use std::io::{
1616
use std::process::ExitCode;
1717

1818
use agent::AgentArgs;
19+
pub use agent::{
20+
Agent,
21+
DEFAULT_AGENT_NAME,
22+
};
1923
use anstream::println;
2024
pub use chat::ConversationState;
2125
use clap::{

0 commit comments

Comments
 (0)