Deep research agentic loop with rig AgentRunner + Gemma models #2
3 changed files with 35 additions and 14 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1979,6 +1979,7 @@ name = "doubleo7"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
"reqwest 0.13.4",
|
"reqwest 0.13.4",
|
||||||
"rig",
|
"rig",
|
||||||
"schemars 1.2.2",
|
"schemars 1.2.2",
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,4 @@ tokio = { version = "1.53.1", features = ["full"] }
|
||||||
toml = "1.1.4+spec-1.1.0"
|
toml = "1.1.4+spec-1.1.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||||
|
chrono = "0.4.45"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
use crate::deep_research::review::{self, Review};
|
||||||
|
use crate::deep_research::tools::{FetchPage, SearchWeb};
|
||||||
use rig::client::{AgentClientExt, Nothing};
|
use rig::client::{AgentClientExt, Nothing};
|
||||||
use rig::completion::Prompt;
|
use rig::completion::Prompt;
|
||||||
use rig::providers::ollama;
|
use rig::providers::ollama;
|
||||||
use crate::deep_research::review::{self, Review};
|
|
||||||
use crate::deep_research::tools::{FetchPage, SearchWeb};
|
|
||||||
|
|
||||||
/// The tool-calling research loop needs to reliably decide what to search
|
/// The tool-calling research loop needs to reliably decide what to search
|
||||||
/// for, when a page is worth fetching, and when it has enough evidence —
|
/// for, when a page is worth fetching, and when it has enough evidence —
|
||||||
|
|
@ -37,7 +37,9 @@ fn initialize_observability() {
|
||||||
pub(crate) async fn start() -> anyhow::Result<()> {
|
pub(crate) async fn start() -> anyhow::Result<()> {
|
||||||
initialize_observability();
|
initialize_observability();
|
||||||
|
|
||||||
let topic = std::env::args().nth(1).unwrap_or_else(|| DEFAULT_TOPIC.to_string());
|
let topic = std::env::args()
|
||||||
|
.nth(1)
|
||||||
|
.unwrap_or_else(|| DEFAULT_TOPIC.to_string());
|
||||||
|
|
||||||
let report = research(&topic).await?;
|
let report = research(&topic).await?;
|
||||||
|
|
||||||
|
|
@ -84,17 +86,25 @@ async fn gather_findings(
|
||||||
feedback: Option<&Review>,
|
feedback: Option<&Review>,
|
||||||
round: usize,
|
round: usize,
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
|
let current_date = chrono::offset::Local::now().to_string();
|
||||||
|
|
||||||
let researcher = client
|
let researcher = client
|
||||||
.agent(RESEARCHER_MODEL)
|
.agent(RESEARCHER_MODEL)
|
||||||
.name("researcher")
|
.name("researcher")
|
||||||
.preamble(
|
.preamble(format!(
|
||||||
"You are a meticulous research assistant. Use the search_web and fetch_page tools to \
|
"You are a meticulous research assistant. Use the search_web and fetch_page tools to \
|
||||||
investigate the user's topic: run several searches with varied phrasing, fetch the \
|
investigate the user's topic: run several searches with varied phrasing, fetch the \
|
||||||
most promising pages, and cross-check claims across at least two sources before \
|
most promising pages, and cross-check claims across at least two sources before \
|
||||||
trusting them. Once you are confident you have enough evidence, stop calling tools \
|
trusting them. Ensure sources are up to date: the current date is {current_date}. \
|
||||||
and reply with a plain-text dump of every fact you gathered, the source URL it came \
|
Once you are confident you have enough evidence, stop calling tools and reply with a \
|
||||||
from, and any open questions or contradictions between sources. This is raw research \
|
plain-text dump of every fact you gathered, using footnote-style citations: write each \
|
||||||
material for a writer, not a final report, so favor completeness over polish.",
|
fact followed by a bracketed number like [1], then at the end of your reply list a \
|
||||||
|
'Sources' section mapping each number to the exact URL it came from, one per line, e.g. \
|
||||||
|
'[1] https://example.com/page'. Reuse the same number when multiple facts come from the \
|
||||||
|
same URL — do not give one URL two different numbers. Also call out any open questions \
|
||||||
|
or contradictions between sources, citing the footnotes involved. This is raw research \
|
||||||
|
material for a writer, not a final report, so favor completeness over polish.")
|
||||||
|
.as_str(),
|
||||||
)
|
)
|
||||||
.tool(SearchWeb)
|
.tool(SearchWeb)
|
||||||
.tool(FetchPage)
|
.tool(FetchPage)
|
||||||
|
|
@ -107,7 +117,8 @@ async fn gather_findings(
|
||||||
You already ran a research pass on this topic. A reviewer checked it against its \
|
You already ran a research pass on this topic. A reviewer checked it against its \
|
||||||
cited sources and found it insufficient. Do more research to address the reviewer's \
|
cited sources and found it insufficient. Do more research to address the reviewer's \
|
||||||
feedback, then produce an updated findings dump: carry forward what's solid, and \
|
feedback, then produce an updated findings dump: carry forward what's solid, and \
|
||||||
add, correct, or better-source whatever the gaps call for.\n\n\
|
add, correct, or better-source whatever the gaps call for. Gaps include out-of-date,\
|
||||||
|
irrelevant, or clearly wrong information. The date is {current_date}.\n\n\
|
||||||
Solid findings from the last pass — keep and build on these:\n{}\n\n\
|
Solid findings from the last pass — keep and build on these:\n{}\n\n\
|
||||||
Gaps the reviewer found — conclusions not actually backed by their source, sources \
|
Gaps the reviewer found — conclusions not actually backed by their source, sources \
|
||||||
that don't line up with the conclusion drawn from them, or parts of the topic still \
|
that don't line up with the conclusion drawn from them, or parts of the topic still \
|
||||||
|
|
@ -129,15 +140,23 @@ async fn gather_findings(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(client, findings), fields(gen_ai.agent.name = "writer"))]
|
#[tracing::instrument(skip(client, findings), fields(gen_ai.agent.name = "writer"))]
|
||||||
async fn write_report(client: &ollama::Client, topic: &str, findings: &str) -> anyhow::Result<String> {
|
async fn write_report(
|
||||||
|
client: &ollama::Client,
|
||||||
|
topic: &str,
|
||||||
|
findings: &str,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
let writer = client
|
let writer = client
|
||||||
.agent(WRITER_MODEL)
|
.agent(WRITER_MODEL)
|
||||||
.name("writer")
|
.name("writer")
|
||||||
.preamble(
|
.preamble(
|
||||||
"You turn raw research notes into a clear, well-organized report for the reader. \
|
"You turn raw research notes into a clear, well-organized report for the reader. The \
|
||||||
Structure the report with headings, cite source URLs inline next to the claims they \
|
notes use footnote-style citations — a bracketed number like [1] after a fact, with a \
|
||||||
support, and call out any open questions or contradictions the research turned up. Do \
|
Sources section mapping numbers to URLs. Preserve this scheme in your report: keep the \
|
||||||
not invent facts beyond what the notes provide.",
|
same [n] markers next to the claims they support (renumbering only if you drop unused \
|
||||||
|
sources), and end the report with a 'Sources' section listing every footnote number \
|
||||||
|
still in use next to its exact URL. Structure the body with headings, and call out any \
|
||||||
|
open questions or contradictions the research turned up. Do not invent facts beyond \
|
||||||
|
what the notes provide.",
|
||||||
)
|
)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue