use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use serde_json::{json, Map, Value};
use crate::content::GeneratedContent;
use crate::error::FMError;
use crate::generation::GenerationOptions;
use crate::prompt::{
Instructions, ResponseFormat, Segment, StructuredSegment, TextSegment, ToolDefinition,
};
static NEXT_SYNTHETIC_ID: AtomicU64 = AtomicU64::new(1);
fn synthetic_id(prefix: &str) -> String {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let counter = NEXT_SYNTHETIC_ID.fetch_add(1, Ordering::Relaxed);
format!("{prefix}-{millis}-{counter}")
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Transcript {
entries: Vec<Entry>,
}
impl Transcript {
#[must_use]
pub const fn new() -> Self {
Self {
entries: Vec::new(),
}
}
#[must_use]
pub fn from_entries(entries: Vec<Entry>) -> Self {
Self { entries }
}
#[must_use]
pub fn entries(&self) -> &[Entry] {
&self.entries
}
pub fn iter(&self) -> impl Iterator<Item = &Entry> {
self.entries.iter()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn push(&mut self, entry: Entry) {
self.entries.push(entry);
}
pub fn from_json_str(json: &str) -> Result<Self, FMError> {
let root: Value = serde_json::from_str(json)
.map_err(|error| FMError::DecodingFailure(error.to_string()))?;
let entries = root
.get("transcript")
.and_then(|transcript| transcript.get("entries"))
.and_then(Value::as_array)
.ok_or_else(|| {
FMError::DecodingFailure("transcript JSON is missing transcript.entries".into())
})?;
let entries = entries
.iter()
.map(Entry::from_json_value)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { entries })
}
pub fn to_json_string(&self) -> Result<String, FMError> {
serde_json::to_string(&json!({
"version": 1,
"type": "FoundationModels.Transcript",
"transcript": {
"entries": self.entries.iter().map(Entry::to_json_value).collect::<Result<Vec<_>, _>>()?
}
}))
.map_err(|error| FMError::InvalidArgument(format!("failed to encode transcript JSON: {error}")))
}
}
impl From<Vec<Entry>> for Transcript {
fn from(entries: Vec<Entry>) -> Self {
Self::from_entries(entries)
}
}
impl<'a> IntoIterator for &'a Transcript {
type Item = &'a Entry;
type IntoIter = std::slice::Iter<'a, Entry>;
fn into_iter(self) -> Self::IntoIter {
self.entries.iter()
}
}
impl IntoIterator for Transcript {
type Item = Entry;
type IntoIter = std::vec::IntoIter<Entry>;
fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Entry {
Instructions(TranscriptInstructions),
Prompt(TranscriptPrompt),
ToolCalls(ToolCalls),
ToolOutput(ToolOutput),
Response(TranscriptResponse),
}
impl Entry {
#[must_use]
pub fn id(&self) -> Option<&str> {
match self {
Self::Instructions(entry) => entry.id.as_deref(),
Self::Prompt(entry) => entry.id.as_deref(),
Self::ToolCalls(entry) => entry.id.as_deref(),
Self::ToolOutput(entry) => Some(entry.id.as_str()),
Self::Response(entry) => entry.id.as_deref(),
}
}
fn from_json_value(value: &Value) -> Result<Self, FMError> {
let role = value
.get("role")
.and_then(Value::as_str)
.ok_or_else(|| FMError::DecodingFailure("transcript entry is missing role".into()))?;
match role {
"instructions" => Ok(Self::Instructions(TranscriptInstructions::from_json_value(
value,
)?)),
"user" => Ok(Self::Prompt(TranscriptPrompt::from_json_value(value)?)),
"tool" => Ok(Self::ToolOutput(ToolOutput::from_json_value(value)?)),
"response" if value.get("toolCalls").is_some() => {
Ok(Self::ToolCalls(ToolCalls::from_json_value(value)?))
}
"response" => Ok(Self::Response(TranscriptResponse::from_json_value(value)?)),
other => Err(FMError::DecodingFailure(format!(
"unsupported transcript role `{other}`"
))),
}
}
fn to_json_value(&self) -> Result<Value, FMError> {
match self {
Self::Instructions(entry) => entry.to_json_value(),
Self::Prompt(entry) => entry.to_json_value(),
Self::ToolCalls(entry) => entry.to_json_value(),
Self::ToolOutput(entry) => entry.to_json_value(),
Self::Response(entry) => entry.to_json_value(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TranscriptInstructions {
pub id: Option<String>,
pub instructions: Instructions,
pub tool_definitions: Vec<ToolDefinition>,
}
impl TranscriptInstructions {
#[must_use]
pub fn new(instructions: Instructions) -> Self {
Self {
id: None,
instructions,
tool_definitions: Vec::new(),
}
}
fn from_json_value(value: &Value) -> Result<Self, FMError> {
Ok(Self {
id: value
.get("id")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
instructions: Instructions::from(parse_segments(value.get("contents"))?),
tool_definitions: parse_tool_definitions(value.get("tools"))?,
})
}
fn to_json_value(&self) -> Result<Value, FMError> {
let mut object = Map::new();
object.insert("role".into(), Value::String("instructions".into()));
object.insert(
"id".into(),
Value::String(
self.id
.clone()
.unwrap_or_else(|| synthetic_id("instructions")),
),
);
object.insert(
"contents".into(),
segments_to_json(self.instructions.segments())?,
);
if !self.tool_definitions.is_empty() {
object.insert(
"tools".into(),
Value::Array(
self.tool_definitions
.iter()
.map(ToolDefinition::to_transcript_json_value)
.collect(),
),
);
}
Ok(Value::Object(object))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TranscriptPrompt {
pub id: Option<String>,
pub prompt: crate::prompt::Prompt,
pub options: GenerationOptions,
pub response_format: Option<ResponseFormat>,
}
impl TranscriptPrompt {
#[must_use]
pub fn new(prompt: crate::prompt::Prompt) -> Self {
Self {
id: None,
prompt,
options: GenerationOptions::new(),
response_format: None,
}
}
fn from_json_value(value: &Value) -> Result<Self, FMError> {
Ok(Self {
id: value
.get("id")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
prompt: crate::prompt::Prompt::from(parse_segments(value.get("contents"))?),
options: GenerationOptions::from_transcript_json_value(value.get("options")),
response_format: value
.get("responseFormat")
.map(ResponseFormat::from_transcript_json_value)
.transpose()?,
})
}
fn to_json_value(&self) -> Result<Value, FMError> {
let mut object = Map::new();
object.insert("role".into(), Value::String("user".into()));
object.insert(
"id".into(),
Value::String(self.id.clone().unwrap_or_else(|| synthetic_id("prompt"))),
);
object.insert("contents".into(), segments_to_json(self.prompt.segments())?);
object.insert("options".into(), self.options.to_transcript_json_value());
if let Some(response_format) = &self.response_format {
object.insert(
"responseFormat".into(),
response_format.to_transcript_json_value(),
);
}
Ok(Value::Object(object))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ToolCalls {
pub id: Option<String>,
pub calls: Vec<ToolCall>,
}
impl<'a> IntoIterator for &'a ToolCalls {
type Item = &'a ToolCall;
type IntoIter = std::slice::Iter<'a, ToolCall>;
fn into_iter(self) -> Self::IntoIter {
self.calls.iter()
}
}
impl IntoIterator for ToolCalls {
type Item = ToolCall;
type IntoIter = std::vec::IntoIter<ToolCall>;
fn into_iter(self) -> Self::IntoIter {
self.calls.into_iter()
}
}
impl ToolCalls {
#[must_use]
pub fn new(calls: Vec<ToolCall>) -> Self {
Self { id: None, calls }
}
#[must_use]
pub fn calls(&self) -> &[ToolCall] {
&self.calls
}
pub fn iter(&self) -> impl Iterator<Item = &ToolCall> {
self.calls.iter()
}
#[must_use]
pub fn len(&self) -> usize {
self.calls.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.calls.is_empty()
}
fn from_json_value(value: &Value) -> Result<Self, FMError> {
Ok(Self {
id: value
.get("id")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
calls: value
.get("toolCalls")
.and_then(Value::as_array)
.map_or(&[] as &[Value], Vec::as_slice)
.iter()
.map(ToolCall::from_json_value)
.collect::<Result<Vec<_>, _>>()?,
})
}
fn to_json_value(&self) -> Result<Value, FMError> {
Ok(json!({
"role": "response",
"id": self.id.clone().unwrap_or_else(|| synthetic_id("tool-calls")),
"toolCalls": self.calls.iter().map(ToolCall::to_json_value).collect::<Result<Vec<_>, _>>()?,
}))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ToolCall {
pub id: String,
pub tool_name: String,
pub arguments: GeneratedContent,
}
impl ToolCall {
#[must_use]
pub fn new(
id: impl Into<String>,
tool_name: impl Into<String>,
arguments: GeneratedContent,
) -> Self {
Self {
id: id.into(),
tool_name: tool_name.into(),
arguments,
}
}
fn from_json_value(value: &Value) -> Result<Self, FMError> {
let arguments = value
.get("arguments")
.and_then(Value::as_str)
.ok_or_else(|| FMError::DecodingFailure("tool call is missing arguments".into()))?;
Ok(Self {
id: value
.get("id")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
tool_name: value
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
arguments: GeneratedContent::from_json_str(arguments)?,
})
}
fn to_json_value(&self) -> Result<Value, FMError> {
Ok(json!({
"id": self.id,
"name": self.tool_name,
"arguments": self.arguments.json_string()?,
}))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ToolOutput {
pub id: String,
pub tool_name: String,
pub tool_call_id: Option<String>,
pub segments: Vec<Segment>,
}
impl ToolOutput {
#[must_use]
pub fn new(
id: impl Into<String>,
tool_name: impl Into<String>,
segments: Vec<Segment>,
) -> Self {
let id = id.into();
Self {
id: id.clone(),
tool_name: tool_name.into(),
tool_call_id: Some(id),
segments,
}
}
fn from_json_value(value: &Value) -> Result<Self, FMError> {
Ok(Self {
id: value
.get("id")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
tool_name: value
.get("toolName")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
tool_call_id: value
.get("toolCallID")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
segments: parse_segments(value.get("contents"))?,
})
}
fn to_json_value(&self) -> Result<Value, FMError> {
Ok(json!({
"role": "tool",
"id": self.id,
"toolCallID": self.tool_call_id.clone().unwrap_or_else(|| self.id.clone()),
"toolName": self.tool_name,
"contents": segments_to_json(&self.segments)?,
}))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TranscriptResponse {
pub id: Option<String>,
pub asset_ids: Vec<String>,
pub segments: Vec<Segment>,
}
impl TranscriptResponse {
#[must_use]
pub fn new(segments: Vec<Segment>) -> Self {
Self {
id: None,
asset_ids: Vec::new(),
segments,
}
}
fn from_json_value(value: &Value) -> Result<Self, FMError> {
Ok(Self {
id: value
.get("id")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
asset_ids: value
.get("assets")
.and_then(Value::as_array)
.map(|assets| {
assets
.iter()
.filter_map(Value::as_str)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
})
.unwrap_or_default(),
segments: parse_segments(value.get("contents"))?,
})
}
fn to_json_value(&self) -> Result<Value, FMError> {
Ok(json!({
"role": "response",
"id": self.id.clone().unwrap_or_else(|| synthetic_id("response")),
"assets": self.asset_ids,
"contents": segments_to_json(&self.segments)?,
}))
}
}
fn parse_segments(value: Option<&Value>) -> Result<Vec<Segment>, FMError> {
value
.and_then(Value::as_array)
.map_or(&[] as &[Value], Vec::as_slice)
.iter()
.map(|segment| {
let segment_type = segment
.get("type")
.and_then(Value::as_str)
.ok_or_else(|| FMError::DecodingFailure("segment is missing type".into()))?;
match segment_type {
"text" => Ok(Segment::Text(TextSegment {
id: segment
.get("id")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
text: segment
.get("text")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
})),
"structure" => {
let structure = segment.get("structure").ok_or_else(|| {
FMError::DecodingFailure("structured segment is missing structure".into())
})?;
let content = structure.get("content").ok_or_else(|| {
FMError::DecodingFailure("structured segment is missing content".into())
})?;
Ok(Segment::Structure(StructuredSegment {
id: segment
.get("id")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
source: structure
.get("source")
.and_then(Value::as_str)
.unwrap_or("GeneratedContent")
.to_string(),
content: GeneratedContent::from_json_str(
&serde_json::to_string(content).map_err(|error| {
FMError::InvalidArgument(format!(
"structured segment content is not valid JSON: {error}"
))
})?,
)?,
}))
}
other => Err(FMError::DecodingFailure(format!(
"unsupported segment type `{other}`"
))),
}
})
.collect()
}
fn segments_to_json(segments: &[Segment]) -> Result<Value, FMError> {
Ok(Value::Array(
segments
.iter()
.map(|segment| match segment {
Segment::Text(TextSegment { id, text }) => Ok(json!({
"type": "text",
"id": id.clone().unwrap_or_else(|| synthetic_id("segment-text")),
"text": text,
})),
Segment::Structure(StructuredSegment {
id,
source,
content,
}) => {
let content_value: Value = serde_json::from_str(&content.json_string()?)
.map_err(|error| {
FMError::InvalidArgument(format!(
"structured segment content is not valid JSON: {error}"
))
})?;
Ok(json!({
"type": "structure",
"id": id.clone().unwrap_or_else(|| synthetic_id("segment-structure")),
"structure": {
"source": source,
"content": content_value,
}
}))
}
})
.collect::<Result<Vec<_>, _>>()?,
))
}
fn parse_tool_definitions(value: Option<&Value>) -> Result<Vec<ToolDefinition>, FMError> {
value
.and_then(Value::as_array)
.map_or(&[] as &[Value], Vec::as_slice)
.iter()
.map(|tool| {
let function = tool.get("function").ok_or_else(|| {
FMError::DecodingFailure("tool definition is missing function body".into())
})?;
let parameters = function.get("parameters").ok_or_else(|| {
FMError::DecodingFailure("tool definition is missing parameters".into())
})?;
Ok(ToolDefinition::new(
function
.get("name")
.and_then(Value::as_str)
.unwrap_or_default(),
function
.get("description")
.and_then(Value::as_str)
.unwrap_or_default(),
crate::schema::GenerationSchema::from_json_schema_unchecked(
serde_json::to_string(parameters).map_err(|error| {
FMError::InvalidArgument(format!(
"tool parameters are not valid JSON: {error}"
))
})?,
),
))
})
.collect()
}