huginn sources: CLI door for SearchSources
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1c227f5907
commit
d77747023e
3 changed files with 95 additions and 2 deletions
|
|
@ -31,7 +31,7 @@ use cultnet_rs::{PROJECTION_DOCUMENT, Selection};
|
|||
use huginn_mind::eureka_pipeline::{Line, PipelineDocument, PipelineRef, Short, Slug, pipeline_key};
|
||||
use huginn_mind::{
|
||||
BATCH_MAX, Faculty, HuginnMindRequest, HuginnMindResponse, MindRefusal, PipelineAdmissionBatch, PipelineAdmissionOutcome,
|
||||
PipelinePageItems, PipelineProvenance, PipelineSelectionPage, SemanticQuery,
|
||||
PipelinePageItems, PipelineProvenance, PipelineSelectionPage, SemanticQuery, SourceQuery,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
|
|
@ -83,6 +83,20 @@ enum Command {
|
|||
#[arg(long)]
|
||||
documents: bool,
|
||||
},
|
||||
/// Search the public forge repos' code and docs by meaning.
|
||||
Sources {
|
||||
/// What to find, in words.
|
||||
text: String,
|
||||
/// Look in this repo alone (its forge name).
|
||||
#[arg(long)]
|
||||
repo: Option<String>,
|
||||
/// How many chunks (default 10, at most 50).
|
||||
#[arg(long)]
|
||||
top_k: Option<u32>,
|
||||
/// Print the answer as JSON.
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
#[command(flatten)]
|
||||
Recipe(Recipe),
|
||||
/// Admit a batch of documents, whole or not at all, and read it back.
|
||||
|
|
@ -168,6 +182,7 @@ fn run(settings: &Settings, command: Command) -> Result<u8, Trouble> {
|
|||
let semantic = near.map(|text| SemanticQuery { text: Line(text), top_k: top_k.unwrap_or(DEFAULT_TOP_K) });
|
||||
query(settings, selection, semantic, None, documents)
|
||||
}
|
||||
Command::Sources { text, repo, top_k, json } => sources(settings, text, repo, top_k, json),
|
||||
Command::Recipe(recipe) => {
|
||||
let ask = recipe.ask()?;
|
||||
query(settings, door::selection_value(&ask.selection)?, ask.semantic, ask.page.cursor, ask.page.documents)
|
||||
|
|
@ -249,6 +264,22 @@ fn query(
|
|||
}
|
||||
}
|
||||
|
||||
fn sources(settings: &Settings, text: String, repo: Option<String>, top_k: Option<u32>, json: bool) -> Result<u8, Trouble> {
|
||||
let query = SourceQuery { text: Line(text), top_k: top_k.unwrap_or(DEFAULT_TOP_K), repo: repo.map(Short) };
|
||||
match settings.ask(|instance| HuginnMindRequest::SearchSources { instance: instance.clone(), query })? {
|
||||
HuginnMindResponse::Sources(found) => {
|
||||
if json {
|
||||
say(serde_json::to_string_pretty(&found).map_err(|error| Trouble::Internal(error.to_string()))?);
|
||||
} else {
|
||||
say(render::source_hits(&found));
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
HuginnMindResponse::Refused(refusal) => Ok(refused(&refusal)),
|
||||
other => Err(unexpected(&other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn show(page: &PipelineSelectionPage) {
|
||||
match &page.items {
|
||||
PipelinePageItems::Headers(headers) => {
|
||||
|
|
@ -392,6 +423,7 @@ fn unexpected(answer: &HuginnMindResponse) -> Trouble {
|
|||
HuginnMindResponse::Query(_) => "query",
|
||||
HuginnMindResponse::Persona(_) => "a persona",
|
||||
HuginnMindResponse::PersonaPut(_) => "a persona put",
|
||||
HuginnMindResponse::Sources(_) => "source hits",
|
||||
HuginnMindResponse::Refused(_) => "a refusal",
|
||||
HuginnMindResponse::Deferred(_) => "a deferral",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use std::fmt::Write;
|
||||
|
||||
use huginn_mind::{IndexStatus, PersonaStatus, PipelineDocumentSummary, PipelineDocumentView, PipelineStatus, PipelineStatusSummary};
|
||||
use huginn_mind::{SourceHits, IndexStatus, PersonaStatus, PipelineDocumentSummary, PipelineDocumentView, PipelineStatus, PipelineStatusSummary};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
|
|
@ -96,6 +96,32 @@ pub fn header(item: &PipelineDocumentSummary) -> String {
|
|||
line.chars().take(HEADER_MAX_CHARS).collect()
|
||||
}
|
||||
|
||||
/// Source hits as the CLI prints them: per hit a citation line,
|
||||
/// `repo:path:start-end @commit7 score`, then the chunk's text, and a blank line
|
||||
/// between hits. Nothing for no hits.
|
||||
pub fn source_hits(found: &SourceHits) -> String {
|
||||
found
|
||||
.hits
|
||||
.iter()
|
||||
.map(|hit| {
|
||||
format!(
|
||||
"{}:{}:{}-{} @{} {:.3}
|
||||
{}
|
||||
",
|
||||
hit.repo,
|
||||
hit.path,
|
||||
hit.line_start,
|
||||
hit.line_end,
|
||||
&hit.commit[..hit.commit.len().min(7)],
|
||||
hit.score,
|
||||
hit.text
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("
|
||||
")
|
||||
}
|
||||
|
||||
/// A fact as one token: a string, a number, a flag, a reference's id, or a
|
||||
/// list of those joined by commas. Anything deeper is not a header fact.
|
||||
fn scalar(value: &Value) -> Option<String> {
|
||||
|
|
@ -121,6 +147,35 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
|
||||
/// The citation line is what an agent copies: repo, path, the inclusive
|
||||
/// line range and the first seven of the commit.
|
||||
#[test]
|
||||
fn a_source_hit_prints_as_a_citation_then_its_text() {
|
||||
let hit = |path: &str, text: &str| huginn_mind::SourceHit {
|
||||
repo: "Huginn".into(),
|
||||
path: path.into(),
|
||||
commit: "0123456789abcdef".into(),
|
||||
line_start: 3,
|
||||
line_end: 9,
|
||||
score: 0.87654,
|
||||
text: text.into(),
|
||||
};
|
||||
let found = SourceHits {
|
||||
indexed_under: huginn_mind::SourceModel { name: "m".into(), digest: "d".into() },
|
||||
hits: vec![hit("a.rs", "one"), hit("b/c.rs", "two")],
|
||||
};
|
||||
assert_eq!(source_hits(&found), "Huginn:a.rs:3-9 @0123456 0.877
|
||||
one
|
||||
|
||||
Huginn:b/c.rs:3-9 @0123456 0.877
|
||||
two
|
||||
");
|
||||
let empty = SourceHits { indexed_under: found.indexed_under.clone(), hits: vec![] };
|
||||
assert_eq!(source_hits(&empty), "");
|
||||
let json = serde_json::to_string(&found).unwrap();
|
||||
assert_eq!(serde_json::from_str::<SourceHits>(&json).unwrap(), found, "--json round-trips");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fact_is_one_token_or_nothing() {
|
||||
assert_eq!(scalar(&json!("")), None);
|
||||
|
|
|
|||
|
|
@ -92,6 +92,12 @@ impl<S: MindStore> IndexSink<S> for Inert {
|
|||
fn search(&mut self, _text: &str, _top_k: u32, _among: Vec<String>) -> Result<SearchTicket, String> {
|
||||
Err("this index cannot search".into())
|
||||
}
|
||||
fn search_sources(&mut self, _query: &huginn_mind::SourceQuery) -> Result<SearchTicket, String> {
|
||||
Err("this index cannot search".into())
|
||||
}
|
||||
fn sources_searched(&mut self) -> Vec<(SearchTicket, Result<huginn_mind::SourceHits, String>)> {
|
||||
Vec::new()
|
||||
}
|
||||
fn search_deadline(&self) -> Duration {
|
||||
Duration::ZERO
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue