feat(rag): add attach-only Qdrant provider, attach wizard and sandbox wiring
Adds QdrantProvider as a read-only driver for pre-existing remote Qdrant collections, an interactive '.rag attach' wizard, and the sandbox credential and domain-whitelisting wiring that lets an attached RAG work inside a sandbox. Attach-only by design: rebuild_indexes bails for both the attached and the unattached case rather than silently succeeding. Coyote never writes to Qdrant in this change. Vectors are never hydrated back from Qdrant. Cosine collections L2-normalize stored vectors on write, so reading them back returns unit-length copies of the originals; the YAML vector copy is authoritative and the serialization guard stays scoped to the duckdb driver alone. Collections keyed by string or UUID point IDs are rejected at attach time. The read path parses point ids as u64 inside a filter_map, so such a collection would otherwise yield zero results with no error. The three response-shape parsers are pure functions over an already-parsed JSON body, unit-tested against captured fixtures, with the async wrappers delegating to them rather than duplicating the logic. '.rag' now splits its first argument, so '.rag attach <name>' no longer tries to load a RAG literally named 'attach <name>'.
This commit is contained in:
+22
-4
@@ -53,7 +53,7 @@ pub const DEFAULT_CONTINUATION_PROMPT: &str = indoc! {"
|
||||
4. Continue with the next pending item now. Call tools immediately."
|
||||
};
|
||||
|
||||
static REPL_COMMANDS: LazyLock<[ReplCommand; 59]> = LazyLock::new(|| {
|
||||
static REPL_COMMANDS: LazyLock<[ReplCommand; 60]> = LazyLock::new(|| {
|
||||
[
|
||||
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
|
||||
ReplCommand::new(".info", "Show system info", AssertState::pass()),
|
||||
@@ -217,6 +217,11 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 59]> = LazyLock::new(|| {
|
||||
"Initialize or access RAG",
|
||||
AssertState::False(StateFlags::AGENT),
|
||||
),
|
||||
ReplCommand::new(
|
||||
".rag attach",
|
||||
"Attach to a pre-existing external RAG (Qdrant)",
|
||||
AssertState::False(StateFlags::AGENT),
|
||||
),
|
||||
ReplCommand::new(
|
||||
".edit rag-docs",
|
||||
"Add or remove documents from an existing RAG",
|
||||
@@ -885,7 +890,20 @@ pub async fn run_repl_command(
|
||||
task::spawn_blocking(move || config::run_self_update(version, false)).await??;
|
||||
}
|
||||
".rag" => {
|
||||
ctx.use_rag(args, abort_signal.clone()).await?;
|
||||
// `split_first_arg` rather than `starts_with("attach ")`: the latter
|
||||
// misses a bare `.rag attach`, which would silently create a RAG
|
||||
// literally named "attach".
|
||||
match split_first_arg(args) {
|
||||
Some(("attach", rest)) => match rest {
|
||||
Some(name) if !name.trim().is_empty() => {
|
||||
ctx.attach_rag(name.trim()).await?;
|
||||
}
|
||||
_ => println!("Usage: .rag attach <name>"),
|
||||
},
|
||||
_ => {
|
||||
ctx.use_rag(args, abort_signal.clone()).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
".agent" => match split_first_arg(args) {
|
||||
Some((agent_name, args)) => {
|
||||
@@ -1711,8 +1729,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repl_commands_has_59_entries() {
|
||||
assert_eq!(REPL_COMMANDS.len(), 59);
|
||||
fn repl_commands_has_60_entries() {
|
||||
assert_eq!(REPL_COMMANDS.len(), 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user