use std::collections::BTreeMap;
use serde_json::{Map, Value, json};
use super::{InteractionsDecoder, InteractionsEvent, SseEvent};
use crate::json_utils::Lenient;
use crate::operation::completion::merge;
use crate::wire::document::Reassemble;
use crate::wire::{Decoder, WireEvent, WireFrame};
#[derive(Default)]
pub struct Interaction {
classifier: InteractionsDecoder,
document: Map<String, Value>,
steps: BTreeMap<usize, Value>,
arguments: BTreeMap<usize, String>,
whole: Option<Map<String, Value>>,
}
impl Interaction {
fn fields(&mut self, interaction: Map<String, Value>) {
for (key, value) in interaction {
if key == "steps" && value.as_array().is_none_or(Vec::is_empty) {
continue;
}
if !value.is_null() || !self.document.contains_key(&key) {
self.document.insert(key, value);
}
}
}
fn step(&mut self, index: usize, delta: &str) -> &mut Value {
self.steps.entry(index).or_insert_with(|| {
let kind = match delta {
"text" | "text_annotation_delta" | "image" | "audio" | "document" | "video" => {
"model_output"
}
"thought_summary" | "thought_signature" => "thought",
"arguments_delta" => "function_call",
other => other,
};
json!({ "type": kind })
})
}
fn delta(&mut self, index: usize, mut delta: Map<String, Value>) {
let kind = delta
.get("type")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
let step = self.step(index, &kind);
let own = step.str("type").unwrap_or_default().to_owned();
match (own.as_str(), kind.as_str()) {
("model_output", _) => {
let Some(content) = list(step, "content") else {
return;
};
match content.last_mut() {
Some(item)
if matches!(kind.as_str(), "text" | "text_annotation_delta")
&& item.str("type") == Some("text") =>
{
delta.shift_remove("type");
merge(item, &delta);
}
_ => content.push(Value::Object(delta)),
}
}
("thought", "thought_summary") => {
let content = delta.get("content").cloned().unwrap_or_default();
let Some(summary) = list(step, "summary") else {
return;
};
match (summary.last_mut(), content.as_object()) {
(Some(item), Some(fields))
if item.str("type") == Some("text")
&& content.str("type") == Some("text") =>
{
merge(item, fields);
}
_ => summary.push(content),
}
}
("function_call", "arguments_delta") => {
let fragment = match delta.get("arguments") {
Some(Value::String(fragment)) => fragment.clone(),
Some(Value::Null) | None => String::new(),
Some(other) => other.to_string(),
};
self.arguments.entry(index).or_default().push_str(&fragment);
}
(own, kind) if own == kind => {
if let Some(step) = step.as_object_mut() {
step.extend(delta);
}
}
_ => merge(step, &delta),
}
}
fn stop(&mut self, index: usize) {
let Some(arguments) = self.arguments.remove(&index) else {
return;
};
if let (false, Ok(arguments), Some(Value::Object(step))) = (
arguments.is_empty(),
crate::json_utils::parse_tool_arguments(&arguments),
self.steps.get_mut(&index),
) {
step.insert("arguments".to_owned(), arguments);
}
}
}
fn list<'a>(step: &'a mut Value, key: &str) -> Option<&'a mut Vec<Value>> {
let slot = step
.as_object_mut()?
.entry(key)
.or_insert_with(|| Value::Array(Vec::new()));
if !slot.is_array() {
*slot = Value::Array(Vec::new());
}
slot.as_array_mut()
}
impl crate::wire::document::Serves<crate::operation::Completion> for Interaction {}
impl Reassemble<WireFrame> for Interaction {
fn absorb(&mut self, frame: &WireFrame) {
let WireEvent::Known(event) = self.classifier.classify(frame.clone()) else {
return;
};
let SseEvent { event_type, fields } = match event {
InteractionsEvent::Whole(interaction) => {
self.whole = Some(interaction);
return;
}
InteractionsEvent::Sse(event) => event,
};
let index = fields
.get("index")
.and_then(Value::as_u64)
.and_then(|index| usize::try_from(index).ok());
let mut fields = fields;
match (event_type.as_str(), index) {
("interaction.created" | "interaction.completed", _) => {
if let Some(Value::Object(interaction)) = fields.shift_remove("interaction") {
self.fields(interaction);
}
}
("interaction.status_update", _) => {
if let Some(status) = fields.shift_remove("status") {
self.fields(Map::from_iter([("status".to_owned(), status)]));
}
}
("step.start", Some(index)) => {
if let Some(step @ Value::Object(_)) = fields.shift_remove("step") {
self.steps.insert(index, step);
}
}
("step.delta", Some(index)) => {
if let Some(Value::Object(delta)) = fields.shift_remove("delta") {
self.delta(index, delta);
}
}
("step.stop", Some(index)) => self.stop(index),
_ => {}
}
}
fn finish(mut self) -> Value {
if let Some(whole) = self.whole {
return Value::Object(whole);
}
if !self.steps.is_empty() && !self.document.contains_key("steps") {
let steps = std::mem::take(&mut self.steps).into_values().collect();
self.document
.insert("steps".to_owned(), Value::Array(steps));
}
if self.document.is_empty() {
Value::Null
} else {
Value::Object(self.document)
}
}
}
#[cfg(test)]
mod tests;