use super::super::focus::Focus;
use super::super::inference::{Inference, InferenceStatus};
use crate::ai::stream::spawn_chat_stream;
use crate::graph_rag::ask::{AskSession, AskStep};
use crate::graph_rag::oracle::StoreOracle;
pub(in crate::tui::app) struct GraphWalk {
session: AskSession,
question: String,
synthesizing: bool,
}
impl GraphWalk {
pub(in crate::tui::app) fn transcript(&self) -> &[String] {
self.session.transcript()
}
pub(in crate::tui::app) fn synthesizing(&self) -> bool {
self.synthesizing
}
}
impl super::App {
pub(in crate::tui::app) fn graph_walk_active(&self) -> bool {
self.graph_walk.is_some()
}
pub(in crate::tui::app) fn graph_walk(&self) -> Option<&GraphWalk> {
self.graph_walk.as_ref()
}
pub(in crate::tui::app) fn start_graph_walk(&mut self) {
let question = self.ai_input.as_str().trim().to_string();
if question.is_empty() {
self.status = "graph walk: type a question in the AI prompt first".into();
return;
}
if self.graph_walk.is_some() {
self.status = "graph walk: one already running (Esc to stop it)".into();
return;
}
if self.ai.resolve_provider(&self.cfg.llm, None).is_err() {
self.status = "graph walk: no LLM provider configured".into();
return;
}
let max_steps = self.cfg.graph.ask_max_steps.max(1);
let width = self.cfg.graph.ask_search_width.max(1);
let session = AskSession::new(question.clone(), max_steps, width);
let prompt = session.next_prompt();
self.graph_walk = Some(GraphWalk { session, question, synthesizing: false });
self.ai_input.clear();
self.change_focus(Focus::Ai);
self.status = format!("graph walk · turn 1/{max_steps} · Esc to stop");
self.kick_graph_walk_turn(prompt, self.graph_walk_explore_system());
}
pub(in crate::tui::app) fn advance_graph_walk(&mut self) {
let reply = self
.inference
.as_ref()
.map(|i| i.response.clone())
.unwrap_or_default();
let Some(mut walk) = self.graph_walk.take() else {
return;
};
let step = {
let oracle = StoreOracle { store: &self.store, hierarchy: &self.hierarchy };
walk.session.on_reply(&reply, &oracle)
};
match step {
AskStep::Continue => {
let (k, n) = walk.session.turn();
let prompt = walk.session.next_prompt();
self.status = format!("graph walk · turn {k}/{n} · Esc to stop");
self.graph_walk = Some(walk);
self.kick_graph_walk_turn(prompt, self.graph_walk_explore_system());
}
AskStep::Answer(_) | AskStep::Synthesize => {
let prompt = walk.session.synthesize_prompt();
walk.synthesizing = true;
self.pending_chat_user_msg = Some(walk.question.clone());
self.status = "graph walk · writing the grounded answer · Esc to stop".into();
let system = self.graph_walk_answer_system();
self.graph_walk = Some(walk);
self.kick_graph_walk_turn(prompt, system);
}
}
}
pub(in crate::tui::app) fn cancel_graph_walk(&mut self) {
self.graph_walk = None;
self.inference = None;
self.pending_chat_user_msg = None;
self.status = "graph walk cancelled".into();
}
fn kick_graph_walk_turn(&mut self, user_prompt: String, system: String) {
let (model, _env) = match self.ai.resolve_provider(&self.cfg.llm, None) {
Ok(pair) => pair,
Err(e) => {
self.status = format!("graph walk: {e}");
self.graph_walk = None;
return;
}
};
let rx = spawn_chat_stream(
self.ai.client.clone(),
model.to_string(),
Some(system),
Vec::new(),
user_prompt,
"graph_rag",
);
self.inference = Some(Inference {
provider: self.ai.default_provider.clone(),
model: model.to_string(),
response: String::new(),
status: InferenceStatus::Streaming,
rx,
started_at: std::time::Instant::now(),
});
}
fn graph_walk_explore_system(&self) -> String {
let iso = crate::ai::prompts::iso_from_long(&self.cfg.language);
crate::graph_rag::ask::system_prompt(iso).to_string()
}
fn graph_walk_answer_system(&self) -> String {
let iso = crate::ai::prompts::iso_from_long(&self.cfg.language);
crate::graph_rag::system_prompt(iso).to_string()
}
}