feat(tui): implement streaming support for LLM responses and update transcript handling
This commit is contained in:
@@ -158,6 +158,20 @@ pub fn apply_action(state: &mut crate::state::AppStateRest, action: Action) {
|
|||||||
zesdex_infrastructure::TurnEvent::Error(msg) => {
|
zesdex_infrastructure::TurnEvent::Error(msg) => {
|
||||||
state.toast_error(msg);
|
state.toast_error(msg);
|
||||||
}
|
}
|
||||||
|
zesdex_infrastructure::TurnEvent::StreamStart => {
|
||||||
|
state.push_transcript(crate::state::ChatMessageDisplay::new(
|
||||||
|
zesdex_domain::core::Role::Assistant,
|
||||||
|
String::new(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
zesdex_infrastructure::TurnEvent::StreamToken(text) => {
|
||||||
|
state.append_to_last_transcript(&text);
|
||||||
|
}
|
||||||
|
zesdex_infrastructure::TurnEvent::StreamDone(_msg) => {
|
||||||
|
// The final message is already accumulated in the transcript.
|
||||||
|
// We might want to trigger a save or something here, but for UI, we just mark dirty.
|
||||||
|
state.dirty = true;
|
||||||
|
}
|
||||||
zesdex_infrastructure::TurnEvent::Compacted(msgs) => {
|
zesdex_infrastructure::TurnEvent::Compacted(msgs) => {
|
||||||
if let Some(ref mut rt) = state.session_runtime {
|
if let Some(ref mut rt) = state.session_runtime {
|
||||||
rt.messages = msgs;
|
rt.messages = msgs;
|
||||||
|
|||||||
@@ -1008,6 +1008,17 @@ impl AppStateRest {
|
|||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Append text to the last assistant message in the transcript, if one exists.
|
||||||
|
pub fn append_to_last_transcript(&mut self, text: &str) {
|
||||||
|
if let Some(msg) = self.transcript_cache.messages.back_mut() {
|
||||||
|
if msg.role == zesdex_domain::core::Role::Assistant {
|
||||||
|
msg.content.push_str(text);
|
||||||
|
self.transcript_cache.dirty = true;
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark the app state as dirty, triggering a TUI re-render.
|
/// Mark the app state as dirty, triggering a TUI re-render.
|
||||||
pub fn mark_dirty(&mut self) {
|
pub fn mark_dirty(&mut self) {
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
|
|||||||
@@ -128,13 +128,27 @@ fn run_turn(
|
|||||||
|
|
||||||
debug!("agent turn iteration {iteration}");
|
debug!("agent turn iteration {iteration}");
|
||||||
|
|
||||||
// Blocking LLM call (reqwest::blocking::Client is sync)
|
// Stream the LLM response
|
||||||
let result = client.chat_with_tools_non_streaming(
|
push_event(turn_events, TurnEvent::StreamStart);
|
||||||
|
|
||||||
|
let result = client.chat_with_tools_streaming(
|
||||||
messages,
|
messages,
|
||||||
Some(defs.clone()),
|
Some(defs.clone()),
|
||||||
Some(4096),
|
|
||||||
Some(0.7),
|
Some(0.7),
|
||||||
None, // no atomic abort flag for the sync API
|
Some(4096),
|
||||||
|
|event| {
|
||||||
|
if abort.load(Ordering::SeqCst) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match event {
|
||||||
|
zesdex_domain::core::StreamEvent::Token(s) | zesdex_domain::core::StreamEvent::Reasoning(s) => {
|
||||||
|
push_event(turn_events, TurnEvent::StreamToken(s.clone()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
},
|
||||||
|
Some(&abort),
|
||||||
);
|
);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
@@ -142,6 +156,8 @@ fn run_turn(
|
|||||||
let content = assistant_msg.content.clone().unwrap_or_default();
|
let content = assistant_msg.content.clone().unwrap_or_default();
|
||||||
let tool_calls = assistant_msg.tool_calls.clone().unwrap_or_default();
|
let tool_calls = assistant_msg.tool_calls.clone().unwrap_or_default();
|
||||||
|
|
||||||
|
push_event(turn_events, TurnEvent::StreamDone(assistant_msg.clone()));
|
||||||
|
|
||||||
if let Some((tokens_in, tokens_out)) = usage {
|
if let Some((tokens_in, tokens_out)) = usage {
|
||||||
push_event(turn_events, TurnEvent::Usage {
|
push_event(turn_events, TurnEvent::Usage {
|
||||||
tokens_in,
|
tokens_in,
|
||||||
@@ -149,10 +165,6 @@ fn run_turn(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if !content.is_empty() {
|
|
||||||
push_event(turn_events, TurnEvent::AssistantMessage(assistant_msg.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if tool_calls.is_empty() {
|
if tool_calls.is_empty() {
|
||||||
messages.push(ChatMessage::assistant(Some(content)));
|
messages.push(ChatMessage::assistant(Some(content)));
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user