Refactor IPC and DTO structures; remove unused code and streamline message handling

- Removed unused structs and methods from `response.rs`, `usage.rs`, and `client.rs`.
- Simplified `Connection` handling in `conn.rs` to only support Unix sockets.
- Updated `IpcServer` to exclusively use Unix sockets and removed TCP handling.
- Cleaned up `editlog.rs` by removing loading and recent entry methods.
- Refactored `memory.rs` to eliminate unused functions related to lesson promotion and retrospective creation.
- Enhanced `search.rs` to support multiple search providers and improved error handling.
- Updated chat view logic to simplify message display and improve user experience.
- Removed deprecated modules and constants from various files to streamline the codebase.
This commit is contained in:
asepharyana
2026-07-11 23:45:13 +07:00
parent 93d1bbb7c1
commit fcef85a327
51 changed files with 1431 additions and 1246 deletions
+172
View File
@@ -0,0 +1,172 @@
pub mod turn;
pub mod tools;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StreamEvent {
Token(String),
Reasoning(String),
ToolCallDelta {
index: usize,
id: Option<String>,
name: Option<String>,
arguments_delta: String,
},
Usage {
prompt_tokens: u64,
completion_tokens: u64,
total_tokens: u64,
},
Done,
Error(String),
}
pub struct SseParser {
buffer: String,
event_type: Option<String>,
data_lines: Vec<String>,
}
impl SseParser {
pub fn new() -> Self {
SseParser {
buffer: String::new(),
event_type: None,
data_lines: Vec::new(),
}
}
pub fn feed(&mut self, chunk: &str) -> Vec<StreamEvent> {
self.buffer.push_str(chunk);
let mut events = Vec::new();
while let Some(line_end) = self.buffer.find('\n') {
let line = self.buffer[..line_end].trim_end_matches('\r').to_string();
self.buffer = self.buffer[line_end + 1..].to_string();
if line.is_empty() {
if let Some(event) = self.flush_event() {
events.push(event);
}
} else if let Some(ty) = line.strip_prefix("event: ") {
self.event_type = Some(ty.trim().to_string());
} else if let Some(data) = line.strip_prefix("data: ") {
self.data_lines.push(data.to_string());
} else if line.starts_with("data:") {
self.data_lines.push(String::new());
}
}
events
}
fn flush_event(&mut self) -> Option<StreamEvent> {
let data = self.data_lines.join("\n");
self.data_lines.clear();
let event_type = self.event_type.take().unwrap_or_default();
if data.is_empty() || data == "[DONE]" {
if data == "[DONE]" {
return Some(StreamEvent::Done);
}
return None;
}
let value: Value = serde_json::from_str(&data).ok()?;
match event_type.as_str() {
"message.stop" => Some(StreamEvent::Done),
"message.start" => None,
"message.delta" | "" => {
let delta = value.get("delta").or_else(|| value.get("choices"))?;
if let Some(choices) = delta.as_array() {
let choice = choices.first()?;
let delta = choice.get("delta")?;
if let Some(content) = delta.get("content").and_then(|c| c.as_str()) {
return Some(StreamEvent::Token(content.to_string()));
}
if let Some(reasoning) = delta.get("reasoning_content").and_then(|r| r.as_str()) {
return Some(StreamEvent::Reasoning(reasoning.to_string()));
}
if let Some(tool_calls) = delta.get("tool_calls").and_then(|tc| tc.as_array()) {
for tc in tool_calls {
let index = tc.get("index").and_then(|i| i.as_u64()).unwrap_or(0) as usize;
let id = tc.get("id").and_then(|i| i.as_str()).map(|s| s.to_string());
let name = tc.get("function")
.and_then(|f| f.get("name"))
.and_then(|n| n.as_str())
.map(|s| s.to_string());
let args_delta = tc.get("function")
.and_then(|f| f.get("arguments"))
.and_then(|a| a.as_str())
.unwrap_or("")
.to_string();
return Some(StreamEvent::ToolCallDelta {
index,
id,
name,
arguments_delta: args_delta,
});
}
}
let finish = choice.get("finish_reason");
if let Some(reason) = finish.and_then(|r| r.as_str()) {
if reason == "stop" || reason == "tool_calls" {
return Some(StreamEvent::Done);
}
}
}
if let Some(content) = delta.get("content").and_then(|c| c.as_str()) {
return Some(StreamEvent::Token(content.to_string()));
}
None
}
_ => None,
}
}
pub fn reset(&mut self) {
self.buffer.clear();
self.event_type = None;
self.data_lines.clear();
}
}
pub fn parse_stream_chunk(data: &str) -> Option<StreamEvent> {
let value: Value = serde_json::from_str(data).ok()?;
if value == Value::Null {
return None;
}
let choices = value.get("choices")?.as_array()?;
let choice = choices.first()?;
let delta = choice.get("delta")?;
if let Some(content) = delta.get("content").and_then(|c| c.as_str()) {
return Some(StreamEvent::Token(content.to_string()));
}
if let Some(reasoning) = delta.get("reasoning_content").and_then(|r| r.as_str()) {
return Some(StreamEvent::Reasoning(reasoning.to_string()));
}
if let Some(finish) = choice.get("finish_reason").and_then(|r| r.as_str()) {
if finish == "stop" || finish == "tool_calls" {
return Some(StreamEvent::Done);
}
}
if let Some(tool_calls) = delta.get("tool_calls").and_then(|tc| tc.as_array()) {
if let Some(tc) = tool_calls.first() {
let index = tc.get("index").and_then(|i| i.as_u64()).unwrap_or(0) as usize;
let id = tc.get("id").and_then(|i| i.as_str()).map(|s| s.to_string());
let name = tc.get("function")
.and_then(|f| f.get("name"))
.and_then(|n| n.as_str())
.map(|s| s.to_string());
let args = tc.get("function")
.and_then(|f| f.get("arguments"))
.and_then(|a| a.as_str())
.unwrap_or("")
.to_string();
return Some(StreamEvent::ToolCallDelta {
index,
id,
name,
arguments_delta: args,
});
}
}
None
}
+73
View File
@@ -0,0 +1,73 @@
use super::turn::ParsedToolCall;
use serde_json::{json, Value};
pub struct ToolCallAccumulator {
calls: Vec<ParsedToolCall>,
}
impl ToolCallAccumulator {
pub fn new() -> Self {
ToolCallAccumulator { calls: Vec::new() }
}
pub fn add_delta(
&mut self,
index: usize,
id: Option<&str>,
name: Option<&str>,
arguments_delta: &str,
) {
while self.calls.len() <= index {
self.calls.push(ParsedToolCall {
id: String::new(),
name: String::new(),
arguments: String::new(),
is_complete: false,
});
}
let tc = &mut self.calls[index];
if let Some(new_id) = id {
if !new_id.is_empty() {
tc.id = new_id.to_string();
}
}
if let Some(new_name) = name {
if !new_name.is_empty() {
tc.name = new_name.to_string();
}
}
tc.arguments.push_str(arguments_delta);
}
pub fn calls(&self) -> &[ParsedToolCall] {
&self.calls
}
pub fn is_complete(&self) -> bool {
!self.calls.is_empty() && self.calls.iter().all(|tc| !tc.name.is_empty() && !tc.arguments.is_empty())
}
pub fn reset(&mut self) {
self.calls.clear();
}
pub fn pending_args(&self) -> Vec<Value> {
self.calls
.iter()
.filter(|tc| !tc.name.is_empty())
.map(|tc| {
json!({
"tool_call_id": tc.id,
"name": tc.name,
"arguments": tc.arguments,
})
})
.collect()
}
}
impl Default for ToolCallAccumulator {
fn default() -> Self {
Self::new()
}
}
+131
View File
@@ -0,0 +1,131 @@
use super::StreamEvent;
use crate::dto::chat::message::ChatMessage;
use crate::dto::chat::tool::{ToolCall, ToolFunction};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamedTurn {
pub messages: Vec<ChatMessage>,
pub tool_calls: Vec<ParsedToolCall>,
pub is_complete: bool,
pub accumulated_content: String,
pub accumulated_reasoning: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParsedToolCall {
pub id: String,
pub name: String,
pub arguments: String,
pub is_complete: bool,
}
impl ParsedToolCall {
pub fn try_parse(&self) -> Option<Value> {
serde_json::from_str(&self.arguments).ok()
}
}
impl StreamedTurn {
pub fn new() -> Self {
StreamedTurn {
messages: Vec::new(),
tool_calls: Vec::new(),
is_complete: false,
accumulated_content: String::new(),
accumulated_reasoning: String::new(),
}
}
pub fn apply_event(&mut self, event: &StreamEvent) {
match event {
StreamEvent::Token(token) => {
self.accumulated_content.push_str(token);
}
StreamEvent::Reasoning(reasoning) => {
self.accumulated_reasoning.push_str(reasoning);
}
StreamEvent::ToolCallDelta {
index,
id,
name,
arguments_delta,
} => {
while self.tool_calls.len() <= *index {
self.tool_calls.push(ParsedToolCall {
id: String::new(),
name: String::new(),
arguments: String::new(),
is_complete: false,
});
}
let tc = &mut self.tool_calls[*index];
if let Some(new_id) = id {
if !new_id.is_empty() {
tc.id = new_id.clone();
}
}
if let Some(new_name) = name {
if !new_name.is_empty() {
tc.name = new_name.clone();
}
}
tc.arguments.push_str(arguments_delta);
}
StreamEvent::Done => {
self.is_complete = true;
}
_ => {}
}
}
pub fn build_assistant_message(&self) -> ChatMessage {
let mut msg = if self.tool_calls.is_empty() {
ChatMessage::assistant(None)
} else {
let tool_dtos: Vec<ToolCall> = self.tool_calls
.iter()
.filter(|tc| !tc.name.is_empty())
.map(|tc| {
let args_value: serde_json::Value = serde_json::from_str(&tc.arguments)
.unwrap_or(serde_json::Value::String(tc.arguments.clone()));
ToolCall {
id: tc.id.clone(),
type_: "function".to_string(),
function: ToolFunction {
name: tc.name.clone(),
arguments: args_value,
},
}
})
.collect();
let mut msg = ChatMessage::assistant(None);
if !tool_dtos.is_empty() {
msg.tool_calls = Some(tool_dtos);
}
msg
};
let content = if self.accumulated_content.is_empty() {
None
} else {
Some(self.accumulated_content.clone())
};
msg.content = content;
msg
}
pub fn has_tool_calls(&self) -> bool {
self.tool_calls.iter().any(|tc| !tc.name.is_empty())
}
pub fn content(&self) -> &str {
&self.accumulated_content
}
}
impl Default for StreamedTurn {
fn default() -> Self {
Self::new()
}
}