use crate::types::{ContentPart, GenerateResponse, Message, ResponseContent, Role, Tool};
use std::collections::HashMap;
use tracing::Span;
use tracing_opentelemetry::OpenTelemetrySpanExt;
#[derive(Debug, Clone)]
pub struct ToolCallInfo {
pub id: String,
pub name: String,
pub arguments: String,
}
pub fn record_input_messages(messages: &[Message]) {
let span = Span::current();
let messages_json: Vec<serde_json::Value> =
messages.iter().map(message_to_otel_format).collect();
let json_str = serde_json::to_string(&messages_json).unwrap_or_default();
span.record("gen_ai.input.messages", json_str.as_str());
}
pub fn record_response_content(response: &GenerateResponse, finish_reason: &str) {
let span = Span::current();
let mut parts: Vec<serde_json::Value> = Vec::new();
let text_content: String = response
.content
.iter()
.filter_map(|c| match c {
ResponseContent::Text { text } => Some(text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
if !text_content.is_empty() {
parts.push(serde_json::json!({
"type": "text",
"content": text_content,
}));
}
for content in &response.content {
if let ResponseContent::ToolCall(tc) = content {
parts.push(serde_json::json!({
"type": "tool_call",
"id": tc.id,
"name": tc.name,
"arguments": serde_json::from_str::<serde_json::Value>(&tc.arguments.to_string())
.unwrap_or(tc.arguments.clone()),
}));
}
}
let output_message = serde_json::json!({
"role": "assistant",
"parts": parts,
"finish_reason": finish_reason,
});
let output_messages = vec![output_message];
let json_str = serde_json::to_string(&output_messages).unwrap_or_default();
span.record("gen_ai.output.messages", json_str.as_str());
}
pub fn record_streamed_response(
text_content: &str,
tool_calls: &[ToolCallInfo],
finish_reason: &str,
) {
let span = Span::current();
let mut parts: Vec<serde_json::Value> = Vec::new();
if !text_content.is_empty() {
parts.push(serde_json::json!({
"type": "text",
"content": text_content,
}));
}
for tc in tool_calls {
parts.push(serde_json::json!({
"type": "tool_call",
"id": tc.id,
"name": tc.name,
"arguments": serde_json::from_str::<serde_json::Value>(&tc.arguments)
.unwrap_or(serde_json::Value::String(tc.arguments.clone())),
}));
}
let output_message = serde_json::json!({
"role": "assistant",
"parts": parts,
"finish_reason": finish_reason,
});
let output_messages = vec![output_message];
let json_str = serde_json::to_string(&output_messages).unwrap_or_default();
span.record("gen_ai.output.messages", json_str.as_str());
}
fn message_to_otel_format(message: &Message) -> serde_json::Value {
let role = role_to_string(&message.role);
let mut parts: Vec<serde_json::Value> = Vec::new();
for part in message.parts() {
match part {
ContentPart::Text { text, .. } => {
parts.push(serde_json::json!({
"type": "text",
"content": text,
}));
}
ContentPart::ToolCall {
id,
name,
arguments,
..
} => {
parts.push(serde_json::json!({
"type": "tool_call",
"id": id,
"name": name,
"arguments": serde_json::from_str::<serde_json::Value>(&arguments.to_string())
.unwrap_or(arguments.clone()),
}));
}
ContentPart::ToolResult {
tool_call_id,
content,
..
} => {
parts.push(serde_json::json!({
"type": "tool_call_response",
"id": tool_call_id,
"result": content,
}));
}
ContentPart::Image { .. } => {
parts.push(serde_json::json!({
"type": "image",
"content": "[image omitted]",
}));
}
}
}
serde_json::json!({
"role": role,
"parts": parts,
})
}
fn role_to_string(role: &Role) -> &'static str {
match role {
Role::System => "system",
Role::User => "user",
Role::Assistant => "assistant",
Role::Tool => "tool",
}
}
pub fn record_telemetry_metadata(metadata: &HashMap<String, String>) {
let span = Span::current();
for (key, value) in metadata {
span.set_attribute(key.clone(), value.clone());
}
}
pub fn record_tool_definitions(tools: &[Tool]) {
let span = Span::current();
let tools_json: Vec<serde_json::Value> = tools.iter().map(tool_to_otel_format).collect();
let json_str = serde_json::to_string(&tools_json).unwrap_or_default();
span.record("gen_ai.tool.definitions", json_str.as_str());
}
fn tool_to_otel_format(tool: &Tool) -> serde_json::Value {
serde_json::json!({
"type": tool.tool_type,
"name": tool.function.name,
"description": tool.function.description,
"parameters": tool.function.parameters,
})
}