use std::collections::BTreeMap;
use dynamo_parsers::tool_calling::jail::{Annotated, JailedStream};
use dynamo_protocols::types::{
ChatChoiceStream, ChatCompletionMessageContent, ChatCompletionStreamResponseDelta,
CreateChatCompletionStreamResponse, FinishReason, Role,
};
use futures::StreamExt;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct Input {
family: String,
cases: BTreeMap<String, Vec<String>>,
}
#[derive(Serialize)]
struct DeltaEmit {
index: u32,
#[serde(skip_serializing_if = "is_false")]
id: bool,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
arguments: Option<String>,
}
#[derive(Serialize)]
struct ChunkEmit {
deltas: Vec<DeltaEmit>,
normal_text: String,
}
fn is_false(b: &bool) -> bool {
!*b
}
fn mock_chunk(
content: Option<String>,
finish: bool,
) -> Annotated<CreateChatCompletionStreamResponse> {
#[allow(deprecated)]
let choice = ChatChoiceStream {
index: 0,
delta: ChatCompletionStreamResponseDelta {
role: Some(Role::Assistant),
content: content.map(ChatCompletionMessageContent::Text),
tool_calls: None,
function_call: None,
refusal: None,
reasoning_content: None,
},
finish_reason: if finish {
Some(FinishReason::Stop)
} else {
None
},
logprobs: None,
};
let response = CreateChatCompletionStreamResponse {
id: "rec".to_string(),
choices: vec![choice],
created: 0,
model: "rec".to_string(),
system_fingerprint: None,
object: "chat.completion.chunk".to_string(),
usage: None,
service_tier: None,
};
Annotated {
data: Some(response),
id: None,
event: None,
comment: None,
error: None,
}
}
async fn record_case(family: &str, chunks: &[String]) -> Vec<ChunkEmit> {
let mut inputs: Vec<_> = chunks
.iter()
.map(|t| mock_chunk(Some(t.clone()), false))
.collect();
inputs.push(mock_chunk(None, true));
let jail = JailedStream::builder().tool_call_parser(family).build();
let out: Vec<Annotated<CreateChatCompletionStreamResponse>> =
jail.apply(futures::stream::iter(inputs)).collect().await;
let mut per_chunk = Vec::new();
for a in out {
let Some(resp) = a.data else { continue };
let Some(choice) = resp.choices.into_iter().next() else {
continue;
};
let normal_text = match choice.delta.content.as_ref() {
Some(ChatCompletionMessageContent::Text(t)) => t.clone(),
_ => String::new(),
};
let deltas = choice
.delta
.tool_calls
.unwrap_or_default()
.into_iter()
.map(|tc| DeltaEmit {
index: tc.index,
id: tc.id.is_some(),
name: tc.function.as_ref().and_then(|f| f.name.clone()),
arguments: tc.function.as_ref().and_then(|f| f.arguments.clone()),
})
.collect();
per_chunk.push(ChunkEmit {
deltas,
normal_text,
});
}
per_chunk
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let path = std::env::args()
.nth(1)
.ok_or_else(|| anyhow::anyhow!("usage: record_dynamo_jail_stream <input.json>"))?;
let input: Input = serde_json::from_str(&std::fs::read_to_string(&path)?)?;
let mut out: BTreeMap<String, Vec<ChunkEmit>> = BTreeMap::new();
for (cid, chunks) in &input.cases {
out.insert(cid.clone(), record_case(&input.family, chunks).await);
}
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}