68 lines
2.7 KiB
Rust
68 lines
2.7 KiB
Rust
//! `ask` tool — wraps `kebab_app::ask_with_config` (single-shot).
|
||
//! Input: { query, mode? }. Output: answer.v1 JSON.
|
||
//!
|
||
//! `Answer` (kebab-core) does NOT carry a `schema_version` field; we tag
|
||
//! it inline here, matching the pattern from `search.rs`.
|
||
|
||
use rmcp::model::CallToolResult;
|
||
use schemars::JsonSchema;
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
use crate::error::{to_tool_error, to_tool_success};
|
||
use crate::state::KebabAppState;
|
||
|
||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||
pub struct AskInput {
|
||
/// The user question.
|
||
pub query: String,
|
||
/// Optional retrieval mode override ("lexical" / "vector" / "hybrid"). Default "hybrid".
|
||
pub mode: Option<String>,
|
||
/// p9-fb-41: opt the ask into the multi-hop pipeline. Default `false`.
|
||
/// When `true`, the query is decomposed into sub-questions, each
|
||
/// retrieved independently, then synthesized over the merged
|
||
/// chunk pool. Cost trade-off: 2–5× LLM calls vs. single-pass.
|
||
/// Use for compound questions / cross-doc reasoning / prereq
|
||
/// chains; keep `false` for simple fact lookups. The full
|
||
/// per-hop trace (`decompose` / `decide` / `synthesize`) is
|
||
/// exposed on `Answer.hops`.
|
||
pub multi_hop: Option<bool>,
|
||
}
|
||
|
||
pub fn handle(state: &KebabAppState, input: AskInput) -> CallToolResult {
|
||
let mode = match input.mode.as_deref() {
|
||
Some("lexical") => kebab_core::SearchMode::Lexical,
|
||
Some("vector") => kebab_core::SearchMode::Vector,
|
||
_ => kebab_core::SearchMode::Hybrid, // default + "hybrid" + unknown
|
||
};
|
||
let opts = kebab_app::AskOpts {
|
||
k: 10,
|
||
explain: false,
|
||
mode,
|
||
temperature: None,
|
||
seed: None,
|
||
stream_sink: None,
|
||
multi_hop: input.multi_hop.unwrap_or(false),
|
||
};
|
||
let cfg_clone = (*state.config).clone();
|
||
let result = kebab_app::ask_with_config(cfg_clone, &input.query, opts);
|
||
match result {
|
||
Ok(answer) => {
|
||
// `Answer` does not carry `schema_version`; tag inline (idempotent
|
||
// via entry().or_insert_with in case a future version adds it).
|
||
let mut v = match serde_json::to_value(&answer) {
|
||
Ok(v) => v,
|
||
Err(e) => return to_tool_error(&anyhow::anyhow!("answer serialize failed: {e}")),
|
||
};
|
||
if let serde_json::Value::Object(ref mut map) = v {
|
||
map.entry("schema_version".to_string())
|
||
.or_insert_with(|| serde_json::Value::String("answer.v1".to_string()));
|
||
}
|
||
match serde_json::to_string(&v) {
|
||
Ok(json) => to_tool_success(json),
|
||
Err(e) => to_tool_error(&anyhow::anyhow!(e)),
|
||
}
|
||
}
|
||
Err(e) => to_tool_error(&e),
|
||
}
|
||
}
|