lc_core/structured_output/
extract.rs1use async_trait::async_trait;
5use serde::de::DeserializeOwned;
6use serde::Serialize;
7use serde_json::Value;
8
9use crate::language_models::{BaseChatModel, LLMResult};
10use crate::output_parsers::BaseOutputParser;
11use crate::output_parsers::JsonOutputParser;
12use lc_schema::Message;
13
14#[derive(Debug, Clone, thiserror::Error)]
16#[non_exhaustive]
17pub enum StructuredOutputError {
18 #[error("Schema error: {0}")]
20 SchemaError(String),
21
22 #[error("Parse error: {0}")]
24 ParseError(String),
25
26 #[error("Provider unsupported: {0}")]
28 ProviderUnsupported(String),
29
30 #[error("LLM error: {0}")]
32 LLMError(String),
33
34 #[error("Stream incomplete: {0}")]
36 StreamIncomplete(String),
37}
38
39#[async_trait]
44pub trait StructuredOutputExt: BaseChatModel {
45 async fn with_structured_output<T: DeserializeOwned + Serialize + Send + Sync + 'static>(
59 &self,
60 schema: Value,
61 prompt: &str,
62 ) -> Result<T, StructuredOutputError> {
63 with_structured_output(self, schema, prompt).await
64 }
65}
66
67impl<M: BaseChatModel> StructuredOutputExt for M {}
69
70pub async fn with_structured_output<T, M>(
94 llm: &M,
95 schema: Value,
96 prompt: &str,
97) -> Result<T, StructuredOutputError>
98where
99 T: DeserializeOwned + Serialize + Send + Sync + 'static,
100 M: BaseChatModel + ?Sized,
101{
102 if !schema.is_object() {
104 return Err(StructuredOutputError::SchemaError(format!(
105 "Schema must be a JSON object, got: {}",
106 schema
107 )));
108 }
109
110 let system_prompt = build_structured_system_prompt(&schema);
112
113 let messages = vec![Message::system(system_prompt), Message::human(prompt)];
114
115 let result: LLMResult = llm
117 .chat(messages, None)
118 .await
119 .map_err(|e| StructuredOutputError::LLMError(e.to_string()))?;
120
121 parse_structured_response::<T>(&result.content).await
123}
124
125pub(crate) fn build_structured_system_prompt(schema: &Value) -> String {
127 let schema_str = serde_json::to_string_pretty(schema).unwrap_or_else(|_| schema.to_string());
128
129 format!(
130 "You are a helpful assistant that responds exclusively in valid JSON format.\n\
131 \n\
132 You must respond with a JSON object that conforms to the following JSON Schema:\n\
133 ```json\n\
134 {schema_str}\n\
135 ```\n\
136 \n\
137 Important rules:\n\
138 1. Respond ONLY with valid JSON. Do not include any explanatory text before or after the JSON.\n\
139 2. The JSON must conform exactly to the schema above.\n\
140 3. All required fields must be present.\n\
141 4. Do not include fields that are not in the schema.\n\
142 5. If you cannot satisfy the schema, respond with the closest valid JSON you can produce."
143 )
144}
145
146pub(crate) async fn parse_structured_response<
151 T: DeserializeOwned + Serialize + Send + Sync + 'static,
152>(
153 content: &str,
154) -> Result<T, StructuredOutputError> {
155 let parser = JsonOutputParser::new();
156
157 let json_value: Value = parser.parse(content).await.map_err(|e| {
158 StructuredOutputError::ParseError(format!("Failed to parse LLM response as JSON: {}", e))
159 })?;
160
161 serde_json::from_value::<T>(json_value).map_err(|e| {
162 StructuredOutputError::ParseError(format!(
163 "Failed to deserialize JSON into target type: {}. Response was: {}",
164 e,
165 &content[..std::cmp::min(200, content.len())]
166 ))
167 })
168}