doubleo7/src/summarizer.rs

70 lines
3 KiB
Rust
Raw Normal View History

Split core.rs into one file per concern core.rs had grown into a 612-line grab-bag mixing six unrelated concerns: CLI arg parsing, logging setup, top-level orchestration, the researcher agent phase, chat-history reconstruction utilities, the summarizer agent phase, and the writer agent phase — while review.rs, tools.rs, stream.rs, and progress.rs already correctly isolated their own concerns. This splits core.rs to match that existing pattern instead of being the one file that doesn't follow it: - cli.rs — Cli struct + DEFAULT_TOPIC - observability.rs — initialize_observability - models.rs — RESEARCHER_MODEL / WRITER_MODEL (previously duplicated across call sites, now a single source of truth) - history.rs — pure chat-history parsing/reconstruction helpers (partial_findings_from_history, annotated_transcript_from_history, and their private helpers), plus their unit tests. Also dedupes MAX_TOOL_RESULT_CHARS, which was previously defined twice. - researcher.rs — gather_findings + GatheredFindings (the tool-calling research phase) - summarizer.rs — summarize_partial_history (the max-turns recovery agent) - writer.rs — write_report - research.rs — the top-level research() orchestration loop main.rs now only does argument parsing, logging setup, and the top-level call — no orchestration logic of its own. Unit tests stay co-located with the code they test per Rust convention (not pulled into separate files) rather than under "prefer new files" — that applies to production code organization here. No behavior changes; cargo test/clippy/fmt all clean.
2026-08-18 10:59:03 +00:00
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)
}
}
}