use crabllm_core::{
ChatCompletionChunk, FunctionCall, Message, Role, ToolCall, ToolCallDelta, ToolType,
};
use std::collections::BTreeMap;
fn empty_tool_call() -> ToolCall {
ToolCall {
index: None,
id: String::new(),
kind: ToolType::Function,
function: FunctionCall::default(),
}
}
pub struct MessageBuilder {
role: Role,
content: String,
reasoning: String,
calls: BTreeMap<u32, ToolCall>,
}
impl MessageBuilder {
pub fn new(role: Role) -> Self {
Self {
role,
content: String::new(),
reasoning: String::new(),
calls: BTreeMap::new(),
}
}
pub fn accept(&mut self, chunk: &ChatCompletionChunk) -> bool {
let Some(choice) = chunk.choices.first() else {
return false;
};
let delta = &choice.delta;
let mut has_content = false;
if let Some(text) = delta.content.as_deref()
&& !text.is_empty()
{
self.content.push_str(text);
has_content = true;
}
if let Some(reason) = delta.reasoning_content.as_deref()
&& !reason.is_empty()
{
self.reasoning.push_str(reason);
}
if let Some(calls) = delta.tool_calls.as_deref() {
for call in calls {
self.merge_tool_call(call);
}
}
has_content
}
fn merge_tool_call(&mut self, delta: &ToolCallDelta) {
let entry = self
.calls
.entry(delta.index)
.or_insert_with(empty_tool_call);
entry.index = Some(delta.index);
if let Some(id) = &delta.id
&& !id.is_empty()
{
entry.id = id.clone();
}
if let Some(kind) = delta.kind {
entry.kind = kind;
}
if let Some(function) = &delta.function {
if let Some(name) = &function.name
&& !name.is_empty()
{
entry.function.name = name.clone();
}
if let Some(args) = &function.arguments {
entry.function.arguments.push_str(args);
}
}
}
pub fn peek_tool_calls(&self) -> Vec<ToolCall> {
self.calls
.values()
.filter(|c| !c.function.name.is_empty())
.cloned()
.collect()
}
pub fn build(self) -> Message {
let tool_calls: Vec<ToolCall> = self
.calls
.into_values()
.filter(|c| !c.id.is_empty() && !c.function.name.is_empty())
.collect();
let has_tool_calls = !tool_calls.is_empty();
let content = if self.content.is_empty() && has_tool_calls && self.role == Role::Assistant {
Some(serde_json::Value::Null)
} else {
Some(serde_json::Value::String(self.content))
};
let reasoning_content = if self.reasoning.is_empty() {
None
} else {
Some(self.reasoning)
};
Message {
role: self.role,
content,
tool_calls: if has_tool_calls {
Some(tool_calls)
} else {
None
},
tool_call_id: None,
name: None,
reasoning_content,
extra: Default::default(),
}
}
}