Skip to content

Commit b301bd8

Browse files
added support for custom sessions
1 parent 7ff200c commit b301bd8

File tree

9 files changed

+331
-14
lines changed

9 files changed

+331
-14
lines changed

Cargo.lock

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

kb_core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ ignore = "0.4.23"
2828
sha2 = "0.10.9"
2929
hex = "0.4.3"
3030
futures = "0.3.31"
31+
chrono = "0.4.41"

kb_core/src/cli/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod config;
22
pub mod index;
33
pub mod query;
4+
pub mod session;

kb_core/src/cli/commands/query.rs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,51 @@ use crate::llm;
44
use crate::utils;
55
use reqwest::Client;
66
use std::path::Path;
7-
use crate::state::{QueryState, hash_query_context};
7+
use crate::state::{QueryState, SessionManager, hash_query_context};
88
use crate::config;
99

1010
pub async fn handle_query(
1111
client: &Client,
1212
query: &str,
1313
top_k: usize,
1414
format: &str,
15+
session_id: Option<String>,
1516
) -> anyhow::Result<()> {
1617
let config_dir = config::get_config_dir()?;
1718
let mut cache = QueryState::load(&config_dir)?;
19+
let mut session_manager = SessionManager::load(&config_dir)?;
20+
21+
// Handle session management
22+
if let Some(id) = session_id {
23+
if id == "new" {
24+
let new_id = session_manager.create_session();
25+
println!("🆕 Created new session: {}", new_id);
26+
} else {
27+
// Pass a reference to set_active_session
28+
session_manager.set_active_session(&id)?;
29+
println!("🔄 Switched to session: {}", id);
30+
}
31+
} else if session_manager.active_session.is_none() {
32+
// Create a default session if none exists
33+
let new_id = session_manager.create_session();
34+
println!("🆕 Created default session: {}", new_id);
35+
}
36+
1837

1938
// Embed the query
2039
let query_embedding = embedding::get_embedding(client, query).await?;
2140

2241
// 🔍 Try similarity cache
2342
if let Some(similar) = cache.find_similar(&query_embedding, 0.93) {
2443
println!("💡 Cached Answer:\n\n{}", utils::render_markdown_highlighted(&similar));
44+
45+
// Add to session history even if cached
46+
if let Some(session) = session_manager.get_active_session_mut() {
47+
session.queries.push(query.to_string());
48+
session.responses.push(similar);
49+
session_manager.save(&config_dir)?;
50+
}
51+
2552
return Ok(());
2653
}
2754

@@ -97,14 +124,33 @@ pub async fn handle_query(
97124
.collect();
98125

99126
let context_hash = hash_query_context(query, &context_chunks);
100-
let raw_answer = llm::get_llm_response(client, query, &context_chunks).await?;
127+
128+
// Pass session manager to get_llm_response
129+
let raw_answer = llm::get_llm_response(
130+
client,
131+
query,
132+
&context_chunks,
133+
Some(&session_manager)
134+
).await?;
135+
101136
let rendered = utils::render_markdown_highlighted(&raw_answer);
102137

103138
// 🧠 Cache the answer with the current query embedding
104139
cache.insert_answer(query.to_string(), context_hash, query_embedding.clone(), raw_answer.clone());
105140
cache.save(&config_dir)?;
106141

142+
// Add to session history
143+
session_manager.add_interaction(query.to_string(), raw_answer)?;
144+
session_manager.save(&config_dir)?;
145+
107146
println!("💡 Answer:\n\n{}", rendered);
147+
148+
if let Some(session) = session_manager.get_active_session() {
149+
println!("\n📝 Session: {} (Q&A: {})",
150+
&session.id[..8],
151+
session.queries.len()
152+
);
153+
}
108154
}
109155
_ => {
110156
for r in &results {
@@ -119,3 +165,4 @@ pub async fn handle_query(
119165

120166
Ok(())
121167
}
168+

kb_core/src/cli/commands/session.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use crate::{config, state::SessionManager};
2+
3+
4+
pub fn handle_sessions(list: bool, clear: bool, switch: Option<String>) -> anyhow::Result<()> {
5+
let config_dir = config::get_config_dir()?;
6+
let mut session_manager = SessionManager::load(&config_dir)?;
7+
8+
if clear {
9+
if let Some(active_id) = session_manager.active_session.clone() {
10+
session_manager.sessions.remove(&active_id);
11+
session_manager.active_session = None;
12+
session_manager.save(&config_dir)?;
13+
println!("🧹 Cleared session: {}", active_id);
14+
} else {
15+
println!("⚠️ No active session to clear");
16+
}
17+
return Ok(());
18+
}
19+
20+
if let Some(id) = switch {
21+
session_manager.set_active_session(&id)?;
22+
println!("🔄 Switched to session: {}", id);
23+
session_manager.save(&config_dir)?;
24+
return Ok(());
25+
}
26+
27+
if list || session_manager.sessions.is_empty() {
28+
println!("📋 Available Sessions:");
29+
for (id, session) in session_manager.list_sessions() {
30+
let active = if Some(id) == session_manager.active_session.as_ref() {
31+
"* "
32+
} else {
33+
" "
34+
};
35+
36+
let time = chrono::DateTime::<chrono::Utc>::from_timestamp(
37+
session.last_updated as i64, 0
38+
).unwrap_or_default().format("%Y-%m-%d %H:%M");
39+
40+
println!("{}{} - {} Q&A pairs, last updated: {}",
41+
active,
42+
&id[..8],
43+
session.queries.len(),
44+
time
45+
);
46+
}
47+
48+
if session_manager.sessions.is_empty() {
49+
println!(" No sessions found. Create one with 'kb query --session new'");
50+
}
51+
}
52+
53+
Ok(())
54+
}

kb_core/src/cli/mod.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,24 @@ pub enum Cli {
1919
/// Number of results to return
2020
#[arg(short, long, default_value_t = 5)]
2121
top_k: usize,
22-
/// Output format (pretty, json, markdown)
22+
/// Output format (pretty, json, markdown, smart)
2323
#[arg(short, long, default_value = "smart")]
2424
format: String,
25+
/// Session ID or 'new' to create a new session
26+
#[arg(long)]
27+
session: Option<String>,
28+
},
29+
/// Manage sessions for conversation history
30+
Sessions {
31+
/// List all available sessions
32+
#[arg(short, long, default_value_t = false)]
33+
list: bool,
34+
/// Clear the current session
35+
#[arg(short, long, default_value_t = false)]
36+
clear: bool,
37+
/// Switch to a specific session by ID
38+
#[arg(short, long)]
39+
switch: Option<String>,
2540
},
2641
/// Configure the application
2742
Config {
@@ -33,3 +48,4 @@ pub enum Cli {
3348
show: bool,
3449
},
3550
}
51+

kb_core/src/llm/mod.rs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use crate::config;
22
use crate::embedding;
3+
use crate::state::SessionManager;
34
use crate::state::{QueryState, hash_query_context};
45
use reqwest::Client;
56

67
pub async fn get_llm_response(
78
client: &Client,
89
prompt: &str,
910
context_chunks: &[String],
11+
session_manager: Option<&SessionManager>,
1012
) -> anyhow::Result<String> {
1113
let api_key = config::get_openai_api_key()?;
1214
let cfg = config::load_config()?;
@@ -21,27 +23,56 @@ pub async fn get_llm_response(
2123
if let Some(similar) = state.find_similar(&embedding, 0.93) {
2224
return Ok(similar);
2325
}
26+
2427
// Check for cached similar answer
2528
if let Some(cached) = state.get_cached_answer(prompt, &context_hash) {
2629
return Ok(cached);
2730
}
2831

29-
// Prepare full prompt
32+
// Prepare full prompt with session context if available
3033
let full_context = context_chunks.join("\n\n---\n\n");
31-
let full_prompt = format!(
32-
"You are an expert personal and code assistant.\n\
33-
Use the following code snippets to answer the question. \
34+
35+
let mut messages = vec![
36+
serde_json::json!({
37+
"role": "system",
38+
"content": "You are an expert personal and code assistant."
39+
}),
40+
];
41+
42+
// Add session context if available
43+
if let Some(manager) = session_manager {
44+
if let Some(session) = manager.get_active_session() {
45+
// Add previous interactions as context
46+
for (q, r) in session.queries.iter().zip(session.responses.iter()) {
47+
messages.push(serde_json::json!({
48+
"role": "user",
49+
"content": q
50+
}));
51+
52+
messages.push(serde_json::json!({
53+
"role": "assistant",
54+
"content": r
55+
}));
56+
}
57+
}
58+
}
59+
60+
// Add current query with context
61+
let user_content = format!(
62+
"Use the following code snippets to answer the question. \
3463
Format your response in Markdown and include code where necessary.\n\n\
35-
Question:\n{}\n\nContext:\n{}\n\nAnswer:",
64+
Question:\n{}\n\nContext:\n{}",
3665
prompt, full_context
3766
);
3867

68+
messages.push(serde_json::json!({
69+
"role": "user",
70+
"content": user_content
71+
}));
72+
3973
let body = serde_json::json!({
4074
"model": cfg.openai_completion_model,
41-
"messages": [
42-
{ "role": "system", "content": "You are an expert personal and code assistant." },
43-
{ "role": "user", "content": full_prompt }
44-
],
75+
"messages": messages,
4576
"temperature": 0.4
4677
});
4778

@@ -65,3 +96,4 @@ pub async fn get_llm_response(
6596

6697
Ok(answer)
6798
}
99+

0 commit comments

Comments
 (0)