use crate::chat::RawFrameRef;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq)]
pub struct UrlCitation {
pub start_index: usize,
pub end_index: usize,
pub url: String,
pub title: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GroundingToolCount {
pub tool: String,
pub count: u64,
pub search_query_count: Option<u64>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EnrichedEvent {
StepStart { index: u64, step_type: String },
StepStop { index: u64 },
SearchCall { queries: Vec<String> },
SearchResult {
is_error: bool,
search_suggestions: Option<String>,
},
UrlCitations(Vec<UrlCitation>),
GroundingCounts(Vec<GroundingToolCount>),
ServerTool { kind: String, payload: Value },
}
impl EnrichedEvent {
pub fn from_frame(frame: &RawFrameRef<'_>) -> Option<Self> {
match frame.event? {
"step.start" => {
let payload = frame.json()?;
Some(Self::StepStart {
index: payload.get("index").and_then(Value::as_u64).unwrap_or(0),
step_type: payload.pointer("/step/type").and_then(Value::as_str)?.to_string(),
})
}
"step.stop" => {
let payload = frame.json()?;
Some(Self::StepStop {
index: payload.get("index").and_then(Value::as_u64).unwrap_or(0),
})
}
"step.delta" => {
let payload = frame.json()?;
let delta = payload.get("delta")?;
Self::from_delta(delta)
}
"interaction.completed" => {
let payload = frame.json()?;
let counts = payload
.pointer("/interaction/usage/grounding_tool_count")
.and_then(Value::as_array)?;
let counts: Vec<GroundingToolCount> = counts
.iter()
.filter_map(|entry| {
Some(GroundingToolCount {
tool: entry.get("type").and_then(Value::as_str)?.to_string(),
count: entry.get("count").and_then(Value::as_u64).unwrap_or(0),
search_query_count: entry.get("search_query_count").and_then(Value::as_u64),
})
})
.collect();
(!counts.is_empty()).then_some(Self::GroundingCounts(counts))
}
_ => None,
}
}
fn from_delta(delta: &Value) -> Option<Self> {
let delta_type = delta.get("type").and_then(Value::as_str)?;
match delta_type {
"google_search_call" => {
let queries = delta
.pointer("/arguments/queries")
.and_then(Value::as_array)
.map(|queries| queries.iter().filter_map(Value::as_str).map(String::from).collect())
.unwrap_or_default();
Some(Self::SearchCall { queries })
}
"google_search_result" => Some(Self::SearchResult {
is_error: delta.get("is_error").and_then(Value::as_bool).unwrap_or(false),
search_suggestions: delta
.pointer("/result/0/search_suggestions")
.and_then(Value::as_str)
.map(String::from),
}),
"text_annotation_delta" => {
let citations: Vec<UrlCitation> = delta
.get("annotations")
.and_then(Value::as_array)?
.iter()
.filter(|annotation| annotation.get("type").and_then(Value::as_str) == Some("url_citation"))
.filter_map(|annotation| {
Some(UrlCitation {
start_index: annotation.get("start_index").and_then(Value::as_u64)? as usize,
end_index: annotation.get("end_index").and_then(Value::as_u64)? as usize,
url: annotation.get("url").and_then(Value::as_str)?.to_string(),
title: annotation.get("title").and_then(Value::as_str).map(String::from),
})
})
.collect();
(!citations.is_empty()).then_some(Self::UrlCitations(citations))
}
other if other.ends_with("_call") || other.ends_with("_result") => Some(Self::ServerTool {
kind: other.to_string(),
payload: delta.clone(),
}),
_ => None,
}
}
}
#[cfg(test)]
#[path = "ix_enriched_tests.rs"]
mod tests;