use async_trait::async_trait;
use futures_util::Stream;
use futures_util::StreamExt;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::Value;
use std::pin::Pin;
use crate::core::language_models::{BaseChatModel, LLMResult};
use crate::core::output_parsers::BaseOutputParser;
use crate::core::output_parsers::JsonOutputParser;
use crate::schema::Message;
#[derive(Debug, Clone, thiserror::Error)]
pub enum StructuredOutputError {
#[error("Schema error: {0}")]
SchemaError(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Provider unsupported: {0}")]
ProviderUnsupported(String),
#[error("LLM error: {0}")]
LLMError(String),
#[error("Stream incomplete: {0}")]
StreamIncomplete(String),
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum PartialJsonError {
#[error("Incomplete JSON: {0}")]
Incomplete(String),
#[error("Invalid JSON: {0}")]
Invalid(String),
}
pub struct PartialJsonParser {
buffer: String,
depth: usize,
in_string: bool,
escape_next: bool,
}
impl PartialJsonParser {
pub fn new() -> Self {
Self {
buffer: String::new(),
depth: 0,
in_string: false,
escape_next: false,
}
}
pub fn push_and_parse(&mut self, token: &str) -> Result<Value, PartialJsonError> {
for ch in token.chars() {
if self.escape_next {
self.escape_next = false;
continue;
}
if ch == '\\' && self.in_string {
self.escape_next = true;
continue;
}
if ch == '"' {
self.in_string = !self.in_string;
continue;
}
if !self.in_string {
match ch {
'{' | '[' => self.depth += 1,
'}' | ']' => {
if self.depth > 0 {
self.depth -= 1;
}
}
_ => {}
}
}
}
if token.is_char_boundary(0) {
self.buffer.push_str(token);
} else {
let mut pos = 0;
while pos < token.len() && !token.is_char_boundary(pos) {
pos += 1;
}
self.buffer.push_str(&token[pos..]);
}
if let Ok(value) = serde_json::from_str::<Value>(&self.buffer) {
return Ok(value);
}
if self.depth > 0
|| self.buffer.trim().starts_with('{')
|| self.buffer.trim().starts_with('[')
{
let repaired = Self::repair_partial_json(&self.buffer);
if let Ok(value) = serde_json::from_str::<Value>(&repaired) {
return Ok(value);
}
}
Err(PartialJsonError::Incomplete(format!(
"Buffer has {} chars, depth={}",
self.buffer.len(),
self.depth
)))
}
pub fn finalize(self) -> Result<Value, PartialJsonError> {
if let Ok(value) = serde_json::from_str::<Value>(&self.buffer) {
return Ok(value);
}
let repaired = Self::repair_partial_json(&self.buffer);
serde_json::from_str::<Value>(&repaired).map_err(|e| {
PartialJsonError::Invalid(format!(
"Failed to parse final buffer ({} chars): {}. Buffer: {}",
self.buffer.len(),
e,
&self.buffer[..std::cmp::min(200, self.buffer.len())]
))
})
}
pub fn buffer(&self) -> &str {
&self.buffer
}
pub fn is_in_string(&self) -> bool {
self.in_string
}
pub fn depth(&self) -> usize {
self.depth
}
fn repair_partial_json(text: &str) -> String {
let mut repaired = text.trim().to_string();
let mut in_string = false;
let mut escape_next = false;
let mut open_braces = 0usize;
let mut close_braces = 0usize;
let mut open_brackets = 0usize;
let mut close_brackets = 0usize;
let mut unescaped_quote_count = 0usize;
for ch in repaired.chars() {
if escape_next {
escape_next = false;
continue;
}
if ch == '\\' && in_string {
escape_next = true;
continue;
}
if ch == '"' {
unescaped_quote_count += 1;
in_string = !in_string;
continue;
}
if !in_string {
match ch {
'{' => open_braces += 1,
'}' => close_braces += 1,
'[' => open_brackets += 1,
']' => close_brackets += 1,
_ => {}
}
}
}
if unescaped_quote_count % 2 != 0 {
repaired.push('"');
}
for _ in close_braces..open_braces {
repaired.push('}');
}
for _ in close_brackets..open_brackets {
repaired.push(']');
}
repaired = Self::remove_trailing_commas(&repaired);
repaired
}
fn remove_trailing_commas(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == ',' && i + 1 < chars.len() {
let next_non_ws = chars[i + 1..].iter().find(|c| !c.is_whitespace());
if next_non_ws == Some(&'}') || next_non_ws == Some(&']') {
i += 1;
continue;
}
}
result.push(chars[i]);
i += 1;
}
result
}
}
impl Default for PartialJsonParser {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
pub trait StructuredOutputExt: BaseChatModel {
async fn with_structured_output<T: DeserializeOwned + Serialize + Send + Sync + 'static>(
&self,
schema: Value,
prompt: &str,
) -> Result<T, StructuredOutputError> {
with_structured_output(self, schema, prompt).await
}
}
impl<M: BaseChatModel> StructuredOutputExt for M {}
pub async fn with_structured_output<T, M>(
llm: &M,
schema: Value,
prompt: &str,
) -> Result<T, StructuredOutputError>
where
T: DeserializeOwned + Serialize + Send + Sync + 'static,
M: BaseChatModel + ?Sized,
{
if !schema.is_object() {
return Err(StructuredOutputError::SchemaError(format!(
"Schema must be a JSON object, got: {}",
schema
)));
}
let system_prompt = build_structured_system_prompt(&schema);
let messages = vec![Message::system(system_prompt), Message::human(prompt)];
let result: LLMResult = llm
.chat(messages, None)
.await
.map_err(|e| StructuredOutputError::LLMError(e.to_string()))?;
parse_structured_response::<T>(&result.content).await
}
fn build_structured_system_prompt(schema: &Value) -> String {
let schema_str = serde_json::to_string_pretty(schema).unwrap_or_else(|_| schema.to_string());
format!(
"You are a helpful assistant that responds exclusively in valid JSON format.\n\
\n\
You must respond with a JSON object that conforms to the following JSON Schema:\n\
```json\n\
{schema_str}\n\
```\n\
\n\
Important rules:\n\
1. Respond ONLY with valid JSON. Do not include any explanatory text before or after the JSON.\n\
2. The JSON must conform exactly to the schema above.\n\
3. All required fields must be present.\n\
4. Do not include fields that are not in the schema.\n\
5. If you cannot satisfy the schema, respond with the closest valid JSON you can produce."
)
}
async fn parse_structured_response<T: DeserializeOwned + Serialize + Send + Sync + 'static>(
content: &str,
) -> Result<T, StructuredOutputError> {
let parser = JsonOutputParser::new();
let json_value: Value = parser.parse(content).await.map_err(|e| {
StructuredOutputError::ParseError(format!("Failed to parse LLM response as JSON: {}", e))
})?;
serde_json::from_value::<T>(json_value).map_err(|e| {
StructuredOutputError::ParseError(format!(
"Failed to deserialize JSON into target type: {}. Response was: {}",
e,
&content[..std::cmp::min(200, content.len())]
))
})
}
#[async_trait]
pub trait StreamingStructuredOutputExt: BaseChatModel {
async fn stream_structured_output<T>(
&self,
schema: Value,
prompt: &str,
) -> Result<
Pin<Box<dyn Stream<Item = Result<T, StructuredOutputError>> + Send>>,
StructuredOutputError,
>
where
T: DeserializeOwned + Serialize + Clone + PartialEq + Unpin + Send + Sync + 'static,
{
stream_structured_output(self, schema, prompt).await
}
}
impl<M: BaseChatModel> StreamingStructuredOutputExt for M {}
pub async fn stream_structured_output<T, M>(
llm: &M,
schema: Value,
prompt: &str,
) -> Result<
Pin<Box<dyn Stream<Item = Result<T, StructuredOutputError>> + Send>>,
StructuredOutputError,
>
where
T: DeserializeOwned + Serialize + Clone + PartialEq + Unpin + Send + Sync + 'static,
M: BaseChatModel + ?Sized,
{
if !schema.is_object() {
return Err(StructuredOutputError::SchemaError(format!(
"Schema must be a JSON object, got: {}",
schema
)));
}
let system_prompt = build_structured_system_prompt(&schema);
let messages = vec![Message::system(system_prompt), Message::human(prompt)];
let token_stream = llm
.stream_chat(messages, None)
.await
.map_err(|e| StructuredOutputError::LLMError(e.to_string()))?;
let mapped_stream =
token_stream.map(|item| item.map_err(|e| StructuredOutputError::LLMError(e.to_string())));
let boxed: Pin<Box<dyn Stream<Item = Result<String, StructuredOutputError>> + Send>> =
Box::pin(mapped_stream);
let output_stream = StructuredStreamProcessor::<T>::new(boxed);
Ok(Box::pin(output_stream))
}
struct StructuredStreamProcessor<T> {
inner: Pin<Box<dyn Stream<Item = Result<String, StructuredOutputError>> + Send>>,
parser: PartialJsonParser,
last_value: Option<T>,
done: bool,
}
impl<T: Unpin> Unpin for StructuredStreamProcessor<T> {}
impl<T> StructuredStreamProcessor<T>
where
T: DeserializeOwned + Serialize + Clone + PartialEq + Unpin + Send + Sync + 'static,
{
fn new(
inner: Pin<Box<dyn Stream<Item = Result<String, StructuredOutputError>> + Send>>,
) -> Self {
Self {
inner,
parser: PartialJsonParser::new(),
last_value: None,
done: false,
}
}
}
impl<T> Stream for StructuredStreamProcessor<T>
where
T: DeserializeOwned + Serialize + Clone + PartialEq + Send + Sync + Unpin + 'static,
{
type Item = Result<T, StructuredOutputError>;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let this = self.get_mut();
if this.done {
return std::task::Poll::Ready(None);
}
loop {
match this.inner.as_mut().poll_next(cx) {
std::task::Poll::Ready(Some(Ok(token))) => {
match this.parser.push_and_parse(&token) {
Ok(json_value) => match serde_json::from_value::<T>(json_value) {
Ok(value) => {
this.last_value = Some(value.clone());
return std::task::Poll::Ready(Some(Ok(value)));
}
Err(_) => {
continue;
}
},
Err(PartialJsonError::Incomplete(_)) => {
continue;
}
Err(PartialJsonError::Invalid(_msg)) => {
continue;
}
}
}
std::task::Poll::Ready(Some(Err(e))) => {
return std::task::Poll::Ready(Some(Err(e)));
}
std::task::Poll::Ready(None) => {
this.done = true;
let parser = std::mem::take(&mut this.parser);
match parser.finalize() {
Ok(json_value) => match serde_json::from_value::<T>(json_value) {
Ok(value) => {
let is_new = this.last_value.as_ref() != Some(&value);
if is_new {
return std::task::Poll::Ready(Some(Ok(value)));
}
return std::task::Poll::Ready(None);
}
Err(e) => {
return std::task::Poll::Ready(Some(Err(
StructuredOutputError::ParseError(format!(
"Failed to deserialize final JSON into target type: {}",
e
)),
)));
}
},
Err(PartialJsonError::Invalid(msg)) => {
if this.last_value.is_some() {
return std::task::Poll::Ready(None);
}
return std::task::Poll::Ready(Some(Err(
StructuredOutputError::StreamIncomplete(msg),
)));
}
Err(PartialJsonError::Incomplete(msg)) => {
if this.last_value.is_some() {
return std::task::Poll::Ready(None);
}
return std::task::Poll::Ready(Some(Err(
StructuredOutputError::StreamIncomplete(msg),
)));
}
}
}
std::task::Poll::Pending => {
return std::task::Poll::Pending;
}
}
}
}
}
#[allow(dead_code)]
fn try_deserialize_partial<T: DeserializeOwned>(value: Value) -> Option<T> {
serde_json::from_value::<T>(value).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use futures_util::Stream;
use std::pin::Pin;
use crate::core::language_models::{BaseLanguageModel, LLMResult};
use crate::core::runnables::Runnable;
use crate::RunnableConfig;
struct MockChatModel {
response: String,
}
impl MockChatModel {
fn new(response: impl Into<String>) -> Self {
Self {
response: response.into(),
}
}
}
#[async_trait]
impl Runnable<Vec<Message>, LLMResult> for MockChatModel {
type Error = MockError;
async fn invoke(
&self,
_input: Vec<Message>,
_config: Option<RunnableConfig>,
) -> Result<LLMResult, Self::Error> {
Ok(LLMResult {
content: self.response.clone(),
model: "mock".to_string(),
token_usage: None,
tool_calls: None,
thinking_content: None,
})
}
}
#[async_trait]
impl BaseLanguageModel<Vec<Message>, LLMResult> for MockChatModel {
fn model_name(&self) -> &str {
"mock"
}
fn get_num_tokens(&self, text: &str) -> usize {
text.len() / 4
}
fn temperature(&self) -> Option<f32> {
None
}
fn max_tokens(&self) -> Option<usize> {
None
}
fn with_temperature(self, _temp: f32) -> Self
where
Self: Sized,
{
self
}
fn with_max_tokens(self, _max: usize) -> Self
where
Self: Sized,
{
self
}
}
#[async_trait]
impl BaseChatModel for MockChatModel {
async fn chat(
&self,
_messages: Vec<Message>,
_config: Option<RunnableConfig>,
) -> Result<LLMResult, Self::Error> {
Ok(LLMResult {
content: self.response.clone(),
model: "mock".to_string(),
token_usage: None,
tool_calls: None,
thinking_content: None,
})
}
async fn stream_chat(
&self,
_messages: Vec<Message>,
_config: Option<RunnableConfig>,
) -> Result<Pin<Box<dyn Stream<Item = Result<String, Self::Error>> + Send>>, Self::Error>
{
let content = self.response.clone();
let stream = futures_util::stream::once(async move { Ok(content) });
Ok(Box::pin(stream))
}
}
#[derive(Debug)]
struct MockError(String);
impl std::fmt::Display for MockError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "MockError: {}", self.0)
}
}
impl std::error::Error for MockError {}
#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq)]
struct Person {
name: String,
age: u32,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq)]
struct Country {
name: String,
capital: String,
population: u64,
}
#[tokio::test]
async fn test_with_structured_output_parses_valid_json() {
let llm = MockChatModel::new(r#"{"name": "Alice", "age": 30}"#);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
});
let result: Person = with_structured_output(&llm, schema, "Tell me about Alice")
.await
.unwrap();
assert_eq!(
result,
Person {
name: "Alice".to_string(),
age: 30
}
);
}
#[tokio::test]
async fn test_with_structured_output_parses_json_in_markdown_block() {
let llm = MockChatModel::new(
"Here is the result:\n```json\n{\"name\": \"Bob\", \"age\": 25}\n```\n",
);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
});
let result: Person = with_structured_output(&llm, schema, "Tell me about Bob")
.await
.unwrap();
assert_eq!(
result,
Person {
name: "Bob".to_string(),
age: 25
}
);
}
#[tokio::test]
async fn test_with_structured_output_rejects_invalid_schema() {
let llm = MockChatModel::new("{}");
let schema = serde_json::json!([1, 2, 3]);
let result = with_structured_output::<Person, _>(&llm, schema, "test").await;
assert!(result.is_err());
match result.unwrap_err() {
StructuredOutputError::SchemaError(msg) => {
assert!(msg.contains("must be a JSON object"));
}
other => panic!("Expected SchemaError, got: {:?}", other),
}
}
#[tokio::test]
async fn test_with_structured_output_parse_error_on_invalid_json() {
let llm = MockChatModel::new("This is not JSON at all!");
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let result = with_structured_output::<Person, _>(&llm, schema, "test").await;
assert!(result.is_err());
match result.unwrap_err() {
StructuredOutputError::ParseError(msg) => {
assert!(msg.contains("Failed to parse LLM response as JSON"));
}
other => panic!("Expected ParseError, got: {:?}", other),
}
}
#[tokio::test]
async fn test_with_structured_output_type_mismatch() {
let llm = MockChatModel::new(r#"{"name": "Alice", "age": "thirty"}"#);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
});
let result = with_structured_output::<Person, _>(&llm, schema, "test").await;
assert!(result.is_err());
match result.unwrap_err() {
StructuredOutputError::ParseError(msg) => {
assert!(msg.contains("Failed to deserialize JSON into target type"));
}
other => panic!("Expected ParseError, got: {:?}", other),
}
}
#[tokio::test]
async fn test_with_structured_output_complex_type() {
let llm =
MockChatModel::new(r#"{"name": "France", "capital": "Paris", "population": 67000000}"#);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"capital": {"type": "string"},
"population": {"type": "integer"}
},
"required": ["name", "capital", "population"]
});
let result: Country = with_structured_output(&llm, schema, "Tell me about France")
.await
.unwrap();
assert_eq!(
result,
Country {
name: "France".to_string(),
capital: "Paris".to_string(),
population: 67000000
}
);
}
#[tokio::test]
async fn test_structured_output_ext_trait() {
let llm = MockChatModel::new(r#"{"name": "Charlie", "age": 40}"#);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
});
let result: Person = llm
.with_structured_output(schema, "Tell me about Charlie")
.await
.unwrap();
assert_eq!(
result,
Person {
name: "Charlie".to_string(),
age: 40
}
);
}
#[test]
fn test_build_structured_system_prompt() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let prompt = build_structured_system_prompt(&schema);
assert!(prompt.contains("JSON"));
assert!(prompt.contains("Schema"));
assert!(prompt.contains("required fields"));
assert!(prompt.contains("\"type\": \"object\""));
}
#[tokio::test]
async fn test_parse_structured_response_valid() {
let content = r#"{"name": "Test", "age": 99}"#;
let result: Person = parse_structured_response(content).await.unwrap();
assert_eq!(result.name, "Test");
assert_eq!(result.age, 99);
}
#[tokio::test]
async fn test_parse_structured_response_with_markdown() {
let content = "```json\n{\"name\": \"Test\", \"age\": 1}\n```";
let result: Person = parse_structured_response(content).await.unwrap();
assert_eq!(result.name, "Test");
assert_eq!(result.age, 1);
}
#[test]
fn test_structured_output_error_display() {
let err = StructuredOutputError::SchemaError("bad schema".to_string());
assert_eq!(format!("{}", err), "Schema error: bad schema");
let err = StructuredOutputError::ParseError("bad parse".to_string());
assert_eq!(format!("{}", err), "Parse error: bad parse");
let err = StructuredOutputError::ProviderUnsupported("no func call".to_string());
assert_eq!(format!("{}", err), "Provider unsupported: no func call");
let err = StructuredOutputError::LLMError("timeout".to_string());
assert_eq!(format!("{}", err), "LLM error: timeout");
let err = StructuredOutputError::StreamIncomplete("partial".to_string());
assert_eq!(format!("{}", err), "Stream incomplete: partial");
}
#[test]
fn test_partial_json_parser_empty() {
let parser = PartialJsonParser::new();
assert!(parser.buffer().is_empty());
assert_eq!(parser.depth(), 0);
assert!(!parser.is_in_string());
}
#[test]
fn test_partial_json_parser_complete_json_single_push() {
let mut parser = PartialJsonParser::new();
let result = parser.push_and_parse(r#"{"name": "Alice", "age": 30}"#);
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value["name"], "Alice");
assert_eq!(value["age"], 30);
}
#[test]
fn test_partial_json_parser_incremental_object() {
let mut parser = PartialJsonParser::new();
parser.push_and_parse("{").ok(); parser.push_and_parse(r#""name""#).ok();
parser.push_and_parse(":").ok();
parser.push_and_parse(r#" "Alice""#).ok();
let result = parser.push_and_parse("}");
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value["name"], "Alice");
}
#[test]
fn test_partial_json_parser_incremental_with_comma() {
let mut parser = PartialJsonParser::new();
parser.push_and_parse(r#"{"name": "Alice","#).ok();
let result = parser.push_and_parse(r#" "age": 30}"#);
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value["name"], "Alice");
assert_eq!(value["age"], 30);
}
#[test]
fn test_partial_json_parser_partial_object_repair() {
let mut parser = PartialJsonParser::new();
let result = parser.push_and_parse(r#"{"name": "Alice""#);
if let Ok(value) = result {
assert_eq!(value["name"], "Alice");
}
}
#[test]
fn test_partial_json_parser_trailing_comma_repair() {
let mut parser = PartialJsonParser::new();
let result = parser.push_and_parse(r#"{"name": "Alice","#);
if let Ok(value) = result {
assert_eq!(value["name"], "Alice");
}
}
#[test]
fn test_partial_json_parser_nested_object() {
let mut parser = PartialJsonParser::new();
parser.push_and_parse(r#"{"person": {"#).ok(); parser.push_and_parse(r#""name": "Bob""#).ok();
let result = parser.push_and_parse(r#"}, "active": true}"#);
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value["person"]["name"], "Bob");
assert_eq!(value["active"], true);
}
#[test]
fn test_partial_json_parser_array() {
let mut parser = PartialJsonParser::new();
parser.push_and_parse("[1,").ok();
let result = parser.push_and_parse(" 2, 3]");
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value[0], 1);
assert_eq!(value[2], 3);
}
#[test]
fn test_partial_json_parser_string_with_escapes() {
let mut parser = PartialJsonParser::new();
let result = parser.push_and_parse(r#"{"text": "hello \"world\""}"#);
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value["text"], r#"hello "world""#);
}
#[test]
fn test_partial_json_parser_finalize_complete() {
let mut parser = PartialJsonParser::new();
parser.push_and_parse(r#"{"name": "Eve"}"#).ok();
let result = parser.finalize();
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value["name"], "Eve");
}
#[test]
fn test_partial_json_parser_finalize_incomplete() {
let mut parser = PartialJsonParser::new();
parser.push_and_parse(r#"{"name": "#).ok();
let result = parser.finalize();
match result {
Ok(value) => {
assert!(value.is_object());
}
Err(PartialJsonError::Invalid(_)) => {
}
Err(PartialJsonError::Incomplete(_)) => {
}
}
}
#[test]
fn test_partial_json_parser_default_impl() {
let parser = PartialJsonParser::default();
assert!(parser.buffer().is_empty());
}
#[test]
fn test_partial_json_error_display() {
let err = PartialJsonError::Incomplete("not enough".to_string());
assert!(format!("{}", err).contains("Incomplete JSON"));
let err = PartialJsonError::Invalid("bad json".to_string());
assert!(format!("{}", err).contains("Invalid JSON"));
}
#[test]
fn test_repair_partial_json_closes_braces() {
let repaired = PartialJsonParser::repair_partial_json(r#"{"a": 1"#);
assert!(repaired.ends_with('}'));
let parsed: serde_json::Value = serde_json::from_str(&repaired).unwrap();
assert_eq!(parsed["a"], 1);
}
#[test]
fn test_repair_partial_json_closes_brackets() {
let repaired = PartialJsonParser::repair_partial_json("[1, 2");
assert!(repaired.ends_with(']'));
let parsed: serde_json::Value = serde_json::from_str(&repaired).unwrap();
assert_eq!(parsed[0], 1);
assert_eq!(parsed[1], 2);
}
#[test]
fn test_repair_partial_json_unclosed_string() {
let repaired = PartialJsonParser::repair_partial_json(r#"{"name": "Ali"#);
let parsed: serde_json::Value = serde_json::from_str(&repaired).unwrap();
assert_eq!(parsed["name"], "Ali");
}
#[test]
fn test_remove_trailing_commas() {
let result = PartialJsonParser::remove_trailing_commas(r#"{"a": 1,}"#);
assert_eq!(result, r#"{"a": 1}"#);
let result = PartialJsonParser::remove_trailing_commas(r#"{"a": [1, 2,]}"#);
assert_eq!(result, r#"{"a": [1, 2]}"#);
let result = PartialJsonParser::remove_trailing_commas(r#"{"a": 1, "b": 2}"#);
assert_eq!(result, r#"{"a": 1, "b": 2}"#);
}
struct StreamingMockChatModel {
tokens: Vec<String>,
}
impl StreamingMockChatModel {
fn from_tokens(tokens: Vec<&str>) -> Self {
Self {
tokens: tokens.into_iter().map(|s| s.to_string()).collect(),
}
}
fn from_response_char_chunks(response: &str) -> Self {
let tokens: Vec<String> = response.chars().map(|c| c.to_string()).collect();
Self { tokens }
}
fn from_response_token_chunks(response: &str, chunk_size: usize) -> Self {
let mut tokens = Vec::new();
let chars: Vec<char> = response.chars().collect();
let mut i = 0;
while i < chars.len() {
let end = std::cmp::min(i + chunk_size, chars.len());
tokens.push(chars[i..end].iter().collect());
i = end;
}
Self { tokens }
}
}
#[async_trait]
impl Runnable<Vec<Message>, LLMResult> for StreamingMockChatModel {
type Error = MockError;
async fn invoke(
&self,
_input: Vec<Message>,
_config: Option<RunnableConfig>,
) -> Result<LLMResult, Self::Error> {
let content = self.tokens.join("");
Ok(LLMResult {
content,
model: "streaming-mock".to_string(),
token_usage: None,
tool_calls: None,
thinking_content: None,
})
}
}
#[async_trait]
impl BaseLanguageModel<Vec<Message>, LLMResult> for StreamingMockChatModel {
fn model_name(&self) -> &str {
"streaming-mock"
}
fn get_num_tokens(&self, text: &str) -> usize {
text.len() / 4
}
fn temperature(&self) -> Option<f32> {
None
}
fn max_tokens(&self) -> Option<usize> {
None
}
fn with_temperature(self, _temp: f32) -> Self
where
Self: Sized,
{
self
}
fn with_max_tokens(self, _max: usize) -> Self
where
Self: Sized,
{
self
}
}
#[async_trait]
impl BaseChatModel for StreamingMockChatModel {
async fn chat(
&self,
_messages: Vec<Message>,
_config: Option<RunnableConfig>,
) -> Result<LLMResult, Self::Error> {
let content = self.tokens.join("");
Ok(LLMResult {
content,
model: "streaming-mock".to_string(),
token_usage: None,
tool_calls: None,
thinking_content: None,
})
}
async fn stream_chat(
&self,
_messages: Vec<Message>,
_config: Option<RunnableConfig>,
) -> Result<Pin<Box<dyn Stream<Item = Result<String, Self::Error>> + Send>>, Self::Error>
{
let tokens = self.tokens.clone();
let stream = futures_util::stream::iter(tokens.into_iter().map(|t| Ok(t)));
Ok(Box::pin(stream))
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
#[serde(default)]
struct PartialPerson {
name: String,
age: u32,
}
impl Default for PartialPerson {
fn default() -> Self {
Self {
name: String::new(),
age: 0,
}
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
#[serde(default)]
struct PartialCountry {
name: String,
capital: String,
population: u64,
}
impl Default for PartialCountry {
fn default() -> Self {
Self {
name: String::new(),
capital: String::new(),
population: 0,
}
}
}
#[tokio::test]
async fn test_stream_structured_output_single_chunk() {
let llm = StreamingMockChatModel::from_tokens(vec![r#"{"name": "Alice", "age": 30}"#]);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
});
let mut stream =
stream_structured_output::<PartialPerson, _>(&llm, schema, "Tell me about Alice")
.await
.unwrap();
let results: Vec<Result<PartialPerson, StructuredOutputError>> =
stream.as_mut().collect().await;
assert!(!results.is_empty());
let final_result = results.into_iter().last().unwrap().unwrap();
assert_eq!(
final_result,
PartialPerson {
name: "Alice".to_string(),
age: 30
}
);
}
#[tokio::test]
async fn test_stream_structured_output_incremental_tokens() {
let llm =
StreamingMockChatModel::from_response_token_chunks(r#"{"name": "Bob", "age": 25}"#, 5);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
});
let mut stream =
stream_structured_output::<PartialPerson, _>(&llm, schema, "Tell me about Bob")
.await
.unwrap();
let results: Vec<Result<PartialPerson, StructuredOutputError>> =
stream.as_mut().collect().await;
assert!(!results.is_empty());
let final_result = results.into_iter().last().unwrap().unwrap();
assert_eq!(
final_result,
PartialPerson {
name: "Bob".to_string(),
age: 25
}
);
}
#[tokio::test]
async fn test_stream_structured_output_char_by_char() {
let llm =
StreamingMockChatModel::from_response_char_chunks(r#"{"name": "Charlie", "age": 40}"#);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
});
let mut stream =
stream_structured_output::<PartialPerson, _>(&llm, schema, "Tell me about Charlie")
.await
.unwrap();
let results: Vec<Result<PartialPerson, StructuredOutputError>> =
stream.as_mut().collect().await;
assert!(!results.is_empty());
let final_result = results.into_iter().last().unwrap().unwrap();
assert_eq!(
final_result,
PartialPerson {
name: "Charlie".to_string(),
age: 40
}
);
}
#[tokio::test]
async fn test_stream_structured_output_partial_values_evolve() {
let llm =
StreamingMockChatModel::from_tokens(vec![r#"{"name": "Diana","#, r#" "age": 28}"#]);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
});
let mut stream =
stream_structured_output::<PartialPerson, _>(&llm, schema, "Tell me about Diana")
.await
.unwrap();
let results: Vec<Result<PartialPerson, StructuredOutputError>> =
stream.as_mut().collect().await;
assert!(
results.len() >= 2,
"Expected at least 2 results, got {}",
results.len()
);
let final_result = results.into_iter().last().unwrap().unwrap();
assert_eq!(
final_result,
PartialPerson {
name: "Diana".to_string(),
age: 28
}
);
}
#[tokio::test]
async fn test_stream_structured_output_rejects_invalid_schema() {
let llm = StreamingMockChatModel::from_tokens(vec!["{}"]);
let schema = serde_json::json!([1, 2, 3]);
let result = stream_structured_output::<PartialPerson, _>(&llm, schema, "test").await;
assert!(result.is_err());
match result.err().unwrap() {
StructuredOutputError::SchemaError(msg) => {
assert!(msg.contains("must be a JSON object"));
}
other => panic!("Expected SchemaError, got: {:?}", other),
}
}
#[tokio::test]
async fn test_stream_structured_output_complex_type() {
let llm = StreamingMockChatModel::from_tokens(vec![
r#"{"name": "France","#,
r#" "capital": "Paris","#,
r#" "population": 67000000}"#,
]);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"capital": {"type": "string"},
"population": {"type": "integer"}
},
"required": ["name", "capital", "population"]
});
let mut stream =
stream_structured_output::<PartialCountry, _>(&llm, schema, "Tell me about France")
.await
.unwrap();
let results: Vec<Result<PartialCountry, StructuredOutputError>> =
stream.as_mut().collect().await;
assert!(!results.is_empty());
let final_result = results.into_iter().last().unwrap().unwrap();
assert_eq!(
final_result,
PartialCountry {
name: "France".to_string(),
capital: "Paris".to_string(),
population: 67000000
}
);
}
#[tokio::test]
async fn test_stream_structured_output_via_trait() {
let llm = StreamingMockChatModel::from_tokens(vec![r#"{"name": "Eve", "age": 22}"#]);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
});
let mut stream = llm
.stream_structured_output::<PartialPerson>(schema, "Tell me about Eve")
.await
.unwrap();
let results: Vec<Result<PartialPerson, StructuredOutputError>> =
stream.as_mut().collect().await;
assert!(!results.is_empty());
let final_result = results.into_iter().last().unwrap().unwrap();
assert_eq!(
final_result,
PartialPerson {
name: "Eve".to_string(),
age: 22
}
);
}
#[tokio::test]
async fn test_stream_equals_non_stream_result() {
let response = r#"{"name": "Frank", "age": 35}"#;
let non_stream_llm = MockChatModel::new(response);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
});
let non_stream_result: Person =
with_structured_output(&non_stream_llm, schema.clone(), "Tell me about Frank")
.await
.unwrap();
let stream_llm = StreamingMockChatModel::from_response_char_chunks(response);
let mut stream = stream_structured_output::<PartialPerson, _>(
&stream_llm,
schema,
"Tell me about Frank",
)
.await
.unwrap();
let results: Vec<Result<PartialPerson, StructuredOutputError>> =
stream.as_mut().collect().await;
let stream_result = results.into_iter().last().unwrap().unwrap();
assert_eq!(non_stream_result.name, stream_result.name);
assert_eq!(non_stream_result.age, stream_result.age);
}
#[tokio::test]
async fn test_stream_structured_output_empty_stream() {
let llm = StreamingMockChatModel::from_tokens(vec![]);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
}
});
let mut stream = stream_structured_output::<PartialPerson, _>(&llm, schema, "test")
.await
.unwrap();
let results: Vec<Result<PartialPerson, StructuredOutputError>> =
stream.as_mut().collect().await;
if let Some(result) = results.into_iter().last() {
match result {
Err(StructuredOutputError::StreamIncomplete(_)) => {
}
Err(StructuredOutputError::ParseError(_)) => {
}
other => {
panic!("Expected StreamIncomplete or ParseError, got: {:?}", other);
}
}
}
}
#[tokio::test]
async fn test_stream_structured_output_non_json_stream() {
let llm = StreamingMockChatModel::from_tokens(vec!["This is not JSON at all!"]);
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let mut stream = stream_structured_output::<PartialPerson, _>(&llm, schema, "test")
.await
.unwrap();
let results: Vec<Result<PartialPerson, StructuredOutputError>> =
stream.as_mut().collect().await;
if let Some(result) = results.into_iter().last() {
assert!(
result.is_err(),
"Expected error for non-JSON output, got: {:?}",
result
);
}
}
#[test]
fn test_try_deserialize_partial_success() {
let value = serde_json::json!({"name": "Test", "age": 99});
let result = try_deserialize_partial::<PartialPerson>(value);
assert!(result.is_some());
let person = result.unwrap();
assert_eq!(person.name, "Test");
assert_eq!(person.age, 99);
}
#[test]
fn test_try_deserialize_partial_missing_field() {
let value = serde_json::json!({"name": "Test"});
let result = try_deserialize_partial::<PartialPerson>(value);
assert!(result.is_some());
let person = result.unwrap();
assert_eq!(person.name, "Test");
assert_eq!(person.age, 0); }
#[test]
fn test_try_deserialize_partial_wrong_type() {
let value = serde_json::json!({"name": "Test", "age": "not a number"});
let result = try_deserialize_partial::<PartialPerson>(value);
assert!(result.is_none());
}
}