70 lines
3 KiB
Rust
70 lines
3 KiB
Rust
|
|
use crate::history::{annotated_transcript_from_history, partial_findings_from_history};
|
||
|
|
use crate::models::WRITER_MODEL;
|
||
|
|
use crate::progress::{SUMMARIZE_EMOJI, Spinner};
|
||
|
|
use rig::client::AgentClientExt;
|
||
|
|
use rig::completion::{Message, Prompt};
|
||
|
|
use rig::providers::ollama;
|
||
|
|
|
||
|
|
/// Agentic alternative to the plain programmatic extraction in `history`:
|
||
|
|
/// hands the annotated transcript to a fresh model call and asks it to
|
||
|
|
/// reconstruct the same footnote-style findings dump the researcher would
|
||
|
|
/// have written itself, had it not run out of turns. This can dedupe
|
||
|
|
/// repeated URLs and restore correct `[n]` citation numbering in a way
|
||
|
|
/// string concatenation can't — but it's a model call like any other, so on
|
||
|
|
/// an empty transcript or a failure it falls back to
|
||
|
|
/// `partial_findings_from_history` rather than letting a second turn-budget
|
||
|
|
/// problem take down the one recovery path that's supposed to be
|
||
|
|
/// bulletproof.
|
||
|
|
#[tracing::instrument(skip(client, chat_history), fields(gen_ai.agent.name = "history-summarizer"))]
|
||
|
|
pub(crate) async fn summarize_partial_history(
|
||
|
|
client: &ollama::Client,
|
||
|
|
topic: &str,
|
||
|
|
chat_history: &[Message],
|
||
|
|
show_progress: bool,
|
||
|
|
) -> String {
|
||
|
|
let transcript = annotated_transcript_from_history(chat_history);
|
||
|
|
|
||
|
|
if transcript.is_empty() {
|
||
|
|
tracing::warn!("no usable transcript to summarize; skipping summarizer pass");
|
||
|
|
return partial_findings_from_history(chat_history);
|
||
|
|
}
|
||
|
|
|
||
|
|
let summarizer = client
|
||
|
|
.agent(WRITER_MODEL)
|
||
|
|
.name("history-summarizer")
|
||
|
|
.preamble(
|
||
|
|
"You are reconstructing research notes from a research session that was cut off \
|
||
|
|
before the researcher could write its own summary. You'll be given a raw transcript \
|
||
|
|
of tool calls (searches run, pages fetched), their results, and any interim comments \
|
||
|
|
the researcher made. Turn this into a footnote-style findings dump: write each fact \
|
||
|
|
the transcript actually supports, followed by a bracketed number like [1], then end \
|
||
|
|
with a 'Sources' section mapping each number to its exact URL, one per line. Reuse \
|
||
|
|
the same number for a URL that appears more than once. Do not invent facts beyond \
|
||
|
|
what the transcript shows, and note explicitly where it looks thin or cuts off \
|
||
|
|
mid-investigation.",
|
||
|
|
)
|
||
|
|
.build();
|
||
|
|
|
||
|
|
let spinner = Spinner::start(
|
||
|
|
show_progress,
|
||
|
|
format!("{SUMMARIZE_EMOJI} Reconstructing partial findings..."),
|
||
|
|
);
|
||
|
|
let result = summarizer
|
||
|
|
.prompt(format!(
|
||
|
|
"Topic: {topic}\n\nPartial research transcript:\n{transcript}"
|
||
|
|
))
|
||
|
|
.await;
|
||
|
|
drop(spinner);
|
||
|
|
|
||
|
|
match result {
|
||
|
|
Ok(findings) => {
|
||
|
|
tracing::info!(findings = %findings, "summarizer reconstructed partial findings");
|
||
|
|
findings
|
||
|
|
}
|
||
|
|
Err(error) => {
|
||
|
|
tracing::warn!(%error, "summarizer agent failed; falling back to programmatic extraction");
|
||
|
|
partial_findings_from_history(chat_history)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|