use async_trait::async_trait;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::Value;
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),
}
#[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
}
pub(crate) 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."
)
}
pub(crate) 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())]
))
})
}