Embellish progress spinner with per-activity emojis #6
4 changed files with 52 additions and 7 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::progress::Spinner;
|
use crate::progress::{REJECTED_EMOJI, REPORT_EMOJI, RESEARCH_EMOJI, Spinner};
|
||||||
use crate::review::{self, Review};
|
use crate::review::{self, Review};
|
||||||
use crate::stream::write_text_stream;
|
use crate::stream::write_text_stream;
|
||||||
use crate::tools::{FetchPage, SearchWeb};
|
use crate::tools::{FetchPage, SearchWeb};
|
||||||
|
|
@ -73,6 +73,10 @@ pub(crate) async fn research(topic: &str, show_progress: bool) -> anyhow::Result
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if show_progress {
|
||||||
|
eprintln!("{REJECTED_EMOJI} Findings rejected — revising for round {}...", round + 1);
|
||||||
|
}
|
||||||
|
|
||||||
feedback = Some(review);
|
feedback = Some(review);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,7 +135,7 @@ async fn gather_findings(
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let spinner = Spinner::start(show_progress, "Researching...");
|
let spinner = Spinner::start(show_progress, format!("{RESEARCH_EMOJI} Researching..."));
|
||||||
let findings = researcher
|
let findings = researcher
|
||||||
.runner(task)
|
.runner(task)
|
||||||
.max_turns(MAX_RESEARCH_TURNS)
|
.max_turns(MAX_RESEARCH_TURNS)
|
||||||
|
|
@ -169,7 +173,7 @@ async fn write_report(
|
||||||
|
|
||||||
// Drop the spinner before streaming starts: report text is about to print
|
// Drop the spinner before streaming starts: report text is about to print
|
||||||
// to the same terminal line, so the two must not race over stdout.
|
// to the same terminal line, so the two must not race over stdout.
|
||||||
let spinner = Spinner::start(show_progress, "Writing report...");
|
let spinner = Spinner::start(show_progress, format!("{REPORT_EMOJI} Writing report..."));
|
||||||
let response_stream = writer
|
let response_stream = writer
|
||||||
.stream_prompt(format!("Topic: {topic}\n\nResearch notes:\n{findings}"))
|
.stream_prompt(format!("Topic: {topic}\n\nResearch notes:\n{findings}"))
|
||||||
.await;
|
.await;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,28 @@
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Emoji shown on the spinner line for whichever activity is currently
|
||||||
|
/// running, so the different phases (and the tool calls within the
|
||||||
|
/// research phase) stay visually distinct at a glance.
|
||||||
|
pub(crate) const RESEARCH_EMOJI: &str = "🔎";
|
||||||
|
pub(crate) const SEARCH_EMOJI: &str = "🌐";
|
||||||
|
pub(crate) const FETCH_EMOJI: &str = "📄";
|
||||||
|
pub(crate) const REVIEW_EMOJI: &str = "🧐";
|
||||||
|
pub(crate) const REJECTED_EMOJI: &str = "❌";
|
||||||
|
pub(crate) const REPORT_EMOJI: &str = "✍️";
|
||||||
|
|
||||||
|
/// The spinner currently on screen, if any — set by `Spinner::start` and
|
||||||
|
/// cleared on drop. Tool implementations don't otherwise have a handle to
|
||||||
|
/// the active spinner (they're plain functions invoked by the model, not
|
||||||
|
/// passed one down through the tool-calling loop), so `set_activity` lets
|
||||||
|
/// them reach it here instead to reflect what they're doing — e.g. which
|
||||||
|
/// page they're fetching — on the same line.
|
||||||
|
static ACTIVE: OnceLock<Mutex<Option<indicatif::ProgressBar>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn active() -> &'static Mutex<Option<indicatif::ProgressBar>> {
|
||||||
|
ACTIVE.get_or_init(|| Mutex::new(None))
|
||||||
|
}
|
||||||
|
|
||||||
/// A terminal spinner for a research phase, shown only when logging is off —
|
/// A terminal spinner for a research phase, shown only when logging is off —
|
||||||
/// with logging on, the trace output already tells the user something is
|
/// with logging on, the trace output already tells the user something is
|
||||||
/// happening, and interleaving both would just be noisy. Clearing on drop
|
/// happening, and interleaving both would just be noisy. Clearing on drop
|
||||||
|
|
@ -7,7 +30,7 @@ use std::time::Duration;
|
||||||
pub(crate) struct Spinner(Option<indicatif::ProgressBar>);
|
pub(crate) struct Spinner(Option<indicatif::ProgressBar>);
|
||||||
|
|
||||||
impl Spinner {
|
impl Spinner {
|
||||||
pub(crate) fn start(enabled: bool, message: &'static str) -> Self {
|
pub(crate) fn start(enabled: bool, message: impl Into<String>) -> Self {
|
||||||
if !enabled {
|
if !enabled {
|
||||||
return Self(None);
|
return Self(None);
|
||||||
}
|
}
|
||||||
|
|
@ -18,7 +41,9 @@ impl Spinner {
|
||||||
indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}")
|
indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}")
|
||||||
.expect("static template is valid"),
|
.expect("static template is valid"),
|
||||||
);
|
);
|
||||||
bar.set_message(message);
|
bar.set_message(message.into());
|
||||||
|
|
||||||
|
*active().lock().expect("spinner mutex poisoned") = Some(bar.clone());
|
||||||
|
|
||||||
Self(Some(bar))
|
Self(Some(bar))
|
||||||
}
|
}
|
||||||
|
|
@ -28,6 +53,17 @@ impl Drop for Spinner {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(bar) = &self.0 {
|
if let Some(bar) = &self.0 {
|
||||||
bar.finish_and_clear();
|
bar.finish_and_clear();
|
||||||
|
*active().lock().expect("spinner mutex poisoned") = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the message of whatever spinner is currently running, if any.
|
||||||
|
/// A no-op when progress display is off (no spinner was ever started, so
|
||||||
|
/// `active()` stays empty) or between phases (the previous `Spinner` has
|
||||||
|
/// already dropped and cleared it).
|
||||||
|
pub(crate) fn set_activity(message: impl Into<String>) {
|
||||||
|
if let Some(bar) = active().lock().expect("spinner mutex poisoned").as_ref() {
|
||||||
|
bar.set_message(message.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use rig::providers::ollama;
|
||||||
use rig::schemars::JsonSchema;
|
use rig::schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::progress::Spinner;
|
use crate::progress::{REVIEW_EMOJI, Spinner};
|
||||||
|
|
||||||
/// Judging whether a conclusion actually follows from its cited sources is a
|
/// Judging whether a conclusion actually follows from its cited sources is a
|
||||||
/// bounded, single-shot classification task, not multi-step reasoning — so
|
/// bounded, single-shot classification task, not multi-step reasoning — so
|
||||||
|
|
@ -50,7 +50,7 @@ pub(crate) async fn review_findings(
|
||||||
.retries(2)
|
.retries(2)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let spinner = Spinner::start(show_progress, "Reviewing findings...");
|
let spinner = Spinner::start(show_progress, format!("{REVIEW_EMOJI} Reviewing findings..."));
|
||||||
let review = reviewer
|
let review = reviewer
|
||||||
.extract(format!("Topic: {topic}\n\nResearch findings to review:\n{findings}"))
|
.extract(format!("Topic: {topic}\n\nResearch findings to review:\n{findings}"))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::progress::{self, FETCH_EMOJI, SEARCH_EMOJI};
|
||||||
use rig::tool::ToolExecutionError;
|
use rig::tool::ToolExecutionError;
|
||||||
use scraper::{Html, Selector};
|
use scraper::{Html, Selector};
|
||||||
|
|
||||||
|
|
@ -12,6 +13,8 @@ pub(crate) async fn search_web(
|
||||||
/// The search query
|
/// The search query
|
||||||
query: String,
|
query: String,
|
||||||
) -> Result<String, ToolExecutionError> {
|
) -> Result<String, ToolExecutionError> {
|
||||||
|
progress::set_activity(format!("{SEARCH_EMOJI} Searching: {query}"));
|
||||||
|
|
||||||
let response = reqwest::Client::new()
|
let response = reqwest::Client::new()
|
||||||
.get("https://html.duckduckgo.com/html/")
|
.get("https://html.duckduckgo.com/html/")
|
||||||
.query(&[("q", query.as_str())])
|
.query(&[("q", query.as_str())])
|
||||||
|
|
@ -42,6 +45,8 @@ pub(crate) async fn fetch_page(
|
||||||
/// The URL to fetch
|
/// The URL to fetch
|
||||||
url: String,
|
url: String,
|
||||||
) -> Result<String, ToolExecutionError> {
|
) -> Result<String, ToolExecutionError> {
|
||||||
|
progress::set_activity(format!("{FETCH_EMOJI} Fetching: {url}"));
|
||||||
|
|
||||||
let response = reqwest::Client::new()
|
let response = reqwest::Client::new()
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.header("User-Agent", "Mozilla/5.0 (research-agent)")
|
.header("User-Agent", "Mozilla/5.0 (research-agent)")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue