-
Notifications
You must be signed in to change notification settings - Fork 360
feat: temporal filtering for channel_recall + self-channel recall #284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| Recall conversation transcript from another channel. Use without a channel argument to list all available channels. Use with a channel name or ID to retrieve recent messages from that channel's conversation history. | ||
| Recall conversation transcript from any channel, including the current one. Use without a channel argument to list all available channels. Use with a channel name or ID to retrieve messages from that channel's conversation history. Supports temporal filtering with `before` and `after` (RFC 3339 timestamps) to query specific time ranges, and `oldest_first` to retrieve the earliest messages instead of the most recent. For example, to find the first messages ever sent in a channel, use `oldest_first: true` with a small limit. This queries the full persisted message history in the database, not just the current in-memory context window. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -172,24 +172,52 @@ impl ConversationLogger { | |||||||||||||||||||||||||
| Ok(messages) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /// Load recent messages from any channel (not just the current one). | ||||||||||||||||||||||||||
| /// Load messages from any channel (not just the current one). | ||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||
| /// Supports optional temporal filtering via `before` and `after` (RFC 3339 strings) | ||||||||||||||||||||||||||
| /// and ordering via `oldest_first`. When `oldest_first` is true, returns the earliest | ||||||||||||||||||||||||||
| /// matching messages instead of the most recent. | ||||||||||||||||||||||||||
| pub async fn load_channel_transcript( | ||||||||||||||||||||||||||
| &self, | ||||||||||||||||||||||||||
| channel_id: &str, | ||||||||||||||||||||||||||
| limit: i64, | ||||||||||||||||||||||||||
| before: Option<&str>, | ||||||||||||||||||||||||||
| after: Option<&str>, | ||||||||||||||||||||||||||
| oldest_first: bool, | ||||||||||||||||||||||||||
| ) -> crate::error::Result<Vec<ConversationMessage>> { | ||||||||||||||||||||||||||
| let rows = sqlx::query( | ||||||||||||||||||||||||||
| let mut sql = String::from( | ||||||||||||||||||||||||||
| "SELECT id, channel_id, role, sender_name, sender_id, content, metadata, created_at \ | ||||||||||||||||||||||||||
| FROM conversation_messages \ | ||||||||||||||||||||||||||
| WHERE channel_id = ? \ | ||||||||||||||||||||||||||
| ORDER BY created_at DESC \ | ||||||||||||||||||||||||||
| LIMIT ?", | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| .bind(channel_id) | ||||||||||||||||||||||||||
| .bind(limit) | ||||||||||||||||||||||||||
| .fetch_all(&self.pool) | ||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||
| .map_err(|e| anyhow::anyhow!(e))?; | ||||||||||||||||||||||||||
| WHERE channel_id = ?", | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if before.is_some() { | ||||||||||||||||||||||||||
| sql.push_str(" AND created_at < ?"); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if after.is_some() { | ||||||||||||||||||||||||||
| sql.push_str(" AND created_at > ?"); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+194
to
+199
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, check if the file exists and get line count
wc -l src/conversation/history.rsRepository: spacedriveapp/spacebot Length of output: 97 🏁 Script executed: # Read the relevant section (lines 180-220 for context)
sed -n '180,220p' src/conversation/history.rsRepository: spacedriveapp/spacebot Length of output: 1309 🏁 Script executed: # Check the function signature and parameter documentation
sed -n '170,200p' src/conversation/history.rsRepository: spacedriveapp/spacebot Length of output: 1081 🏁 Script executed: # Look for the referenced timeline query at lines 413-415
sed -n '410,420p' src/conversation/history.rsRepository: spacedriveapp/spacebot Length of output: 377 🏁 Script executed: # Search for how before/after are documented or typed
rg -n "before|after" src/conversation/history.rs | head -20Repository: spacedriveapp/spacebot Length of output: 810 🏁 Script executed: # Run the verification script to demonstrate SQLite comparison behavior
python3 << 'PY'
import sqlite3
conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE t(created_at TEXT)")
cur.executemany("INSERT INTO t(created_at) VALUES (?)", [
("2026-01-15 00:30:00",),
("2026-01-15 23:59:59",),
])
bound = "2026-01-15T00:00:00Z"
print(f"Testing comparison with bound value: {bound}")
print()
plain = cur.execute(
"SELECT created_at FROM t WHERE created_at < ? ORDER BY created_at",
(bound,)
).fetchall()
normalized = cur.execute(
"SELECT created_at FROM t WHERE datetime(created_at) < datetime(?) ORDER BY created_at",
(bound,)
).fetchall()
print("Plain text compare (created_at < ?):", plain)
print("datetime() compare (datetime(created_at) < datetime(?)):", normalized)
print()
print("Analysis:")
print(f" 'T' (ASCII {ord('T')}) vs ' ' (ASCII {ord(' ')}) in string comparison")
print(f" 2026-01-15T00:00:00Z > 2026-01-15 (as strings): {bound > '2026-01-15'}")
PYRepository: spacedriveapp/spacebot Length of output: 396 🏁 Script executed: # Verify the table schema to confirm created_at is stored as TEXT
rg -n "conversation_messages|CREATE TABLE" src/conversation/history.rs | head -10Repository: spacedriveapp/spacebot Length of output: 553 🏁 Script executed: # Check if there are other places where before/after are used to see the broader pattern
rg -n "load_channel_transcript|RFC 3339" src/conversation/history.rsRepository: spacedriveapp/spacebot Length of output: 204 Use SQLite datetime normalization in temporal filters. Lines 194-195 and 198 compare 🛠️ Proposed fix if before.is_some() {
- sql.push_str(" AND created_at < ?");
+ sql.push_str(" AND datetime(created_at) < datetime(?)");
}
if after.is_some() {
- sql.push_str(" AND created_at > ?");
+ sql.push_str(" AND datetime(created_at) > datetime(?)");
}A reference implementation with the correct pattern already exists in the same file (lines 413–415). 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if oldest_first { | ||||||||||||||||||||||||||
| sql.push_str(" ORDER BY created_at ASC"); | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| sql.push_str(" ORDER BY created_at DESC"); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| sql.push_str(" LIMIT ?"); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let mut query = sqlx::query(&sql).bind(channel_id); | ||||||||||||||||||||||||||
| if let Some(before) = before { | ||||||||||||||||||||||||||
| query = query.bind(before); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if let Some(after) = after { | ||||||||||||||||||||||||||
| query = query.bind(after); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| query = query.bind(limit); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let rows = query | ||||||||||||||||||||||||||
| .fetch_all(&self.pool) | ||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||
| .map_err(|e| anyhow::anyhow!(e))?; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let mut messages: Vec<ConversationMessage> = rows | ||||||||||||||||||||||||||
| .into_iter() | ||||||||||||||||||||||||||
|
|
@@ -207,7 +235,10 @@ impl ConversationLogger { | |||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| .collect(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| messages.reverse(); | ||||||||||||||||||||||||||
| // When fetching newest-first, reverse to chronological for the caller | ||||||||||||||||||||||||||
| if !oldest_first { | ||||||||||||||||||||||||||
| messages.reverse(); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Ok(messages) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,4 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| //! Cross-channel transcript recall tool for branches. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| //! Channel transcript recall tool for branches. Queries any channel including the current one. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::conversation::channels::ChannelStore; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::conversation::history::ConversationLogger; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Maximum messages to return in a single recall. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_TRANSCRIPT_MESSAGES: i64 = 100; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Tool for recalling conversation transcript from other channels. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Tool for recalling conversation transcript from any channel. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[derive(Debug, Clone)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub struct ChannelRecallTool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| conversation_logger: ConversationLogger, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -43,6 +43,16 @@ pub struct ChannelRecallArgs { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Maximum number of messages to return (default 50, max 100). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[serde(default = "default_message_limit")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub limit: i64, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Only return messages sent before this timestamp (RFC 3339, e.g. "2026-01-15T00:00:00Z"). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[serde(default)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub before: Option<String>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Only return messages sent after this timestamp (RFC 3339, e.g. "2026-01-15T00:00:00Z"). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[serde(default)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub after: Option<String>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// When true, returns the oldest matching messages first instead of the most recent. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Useful for finding the earliest messages in a channel. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[serde(default)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub oldest_first: bool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn default_message_limit() -> i64 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -107,6 +117,19 @@ impl Tool for ChannelRecallTool { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "maximum": 100, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "default": 50, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "description": "Maximum number of messages to retrieve (1-100)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "before": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "type": "string", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "description": "Only return messages before this timestamp (RFC 3339, e.g. \"2026-01-15T00:00:00Z\")" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "after": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "type": "string", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "description": "Only return messages after this timestamp (RFC 3339, e.g. \"2026-01-15T00:00:00Z\")" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "oldest_first": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "type": "boolean", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "default": false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "description": "Return oldest messages first instead of most recent. Use this to find the earliest messages in a channel." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -139,7 +162,13 @@ impl Tool for ChannelRecallTool { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Load transcript | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let messages = self | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .conversation_logger | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .load_channel_transcript(&channel.id, limit) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .load_channel_transcript( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| &channel.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| limit, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args.before.as_deref(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args.after.as_deref(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args.oldest_first, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
162
to
+171
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth validating
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map_err(|e| ChannelRecallError(format!("Failed to load transcript: {e}")))?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor correctness gotcha:
created_atis coming from SQLiteCURRENT_TIMESTAMP(YYYY-MM-DD HH:MM:SS), so comparing it directly to RFC3339 input via< ?/> ?can behave oddly (esp. same-day comparisons because of the space vsT). Usingdatetime(?)keeps RFC3339 working without changing storage format.