lc_core/structured_output/streaming.rs
1// src/core/structured_output/streaming.rs
2//! Streaming structured output support for incremental LLM response parsing.
3
4use async_trait::async_trait;
5use futures_util::Stream;
6use futures_util::StreamExt;
7use serde::de::DeserializeOwned;
8use serde::Serialize;
9use serde_json::Value;
10use std::pin::Pin;
11
12use crate::language_models::BaseChatModel;
13use lc_schema::Message;
14
15use super::extract::{build_structured_system_prompt, StructuredOutputError};
16use super::parser::{PartialJsonError, PartialJsonParser};
17
18/// Trait that extends `BaseChatModel` with streaming structured output capabilities.
19///
20/// Provides a streaming variant of `with_structured_output` that yields partial
21/// `T` values as the model generates tokens. The target type `T` should derive
22/// `#[serde(default)]` or use `Option` fields so that partial JSON can be
23/// deserialized with missing fields filled by defaults.
24#[async_trait]
25pub trait StreamingStructuredOutputExt: BaseChatModel {
26 /// Stream structured output from a chat model.
27 ///
28 /// Returns a stream of `T` values. Each item represents the best partial
29 /// result that could be parsed from the tokens received so far. The final
30 /// item in the stream is the complete result.
31 ///
32 /// # Arguments
33 ///
34 /// * `schema` - A JSON Schema describing the expected output shape.
35 /// * `prompt` - The user prompt to send to the LLM.
36 ///
37 /// # Returns
38 ///
39 /// A stream of `Result<T, StructuredOutputError>` items.
40 async fn stream_structured_output<T>(
41 &self,
42 schema: Value,
43 prompt: &str,
44 ) -> Result<
45 Pin<Box<dyn Stream<Item = Result<T, StructuredOutputError>> + Send>>,
46 StructuredOutputError,
47 >
48 where
49 T: DeserializeOwned + Serialize + Clone + PartialEq + Unpin + Send + Sync + 'static,
50 {
51 stream_structured_output(self, schema, prompt).await
52 }
53}
54
55/// Blanket implementation: every `BaseChatModel` automatically gets
56/// `StreamingStructuredOutputExt`.
57impl<M: BaseChatModel> StreamingStructuredOutputExt for M {}
58
59/// Stream structured output from a chat model.
60///
61/// Returns a stream of partial `T` values as the model generates tokens.
62/// The implementation:
63/// 1. Calls `stream_chat` with a prompt that asks for JSON matching the schema
64/// 2. Accumulates tokens through the `PartialJsonParser`
65/// 3. At each successful parse, yields a `T` value (partial fields filled by
66/// serde defaults, rest default)
67/// 4. On stream end, yields the final complete `T`
68///
69/// # Arguments
70///
71/// * `llm` - Any type implementing `BaseChatModel`.
72/// * `schema` - A JSON Schema describing the expected output.
73/// * `prompt` - The user prompt to send to the LLM.
74///
75/// # Returns
76///
77/// A `Result` containing a stream of `Result<T, StructuredOutputError>` items,
78/// or a `StructuredOutputError` if the stream could not be set up.
79///
80/// # Type requirements
81///
82/// The target type `T` should use `#[serde(default)]` or `Option` fields so
83/// that partial JSON can be deserialized with missing fields filled by defaults.
84/// If `T` does not support default deserialization, partial results will fail
85/// and only the final complete result will be yielded.
86///
87/// # Example
88///
89/// ```ignore
90/// use serde::{Deserialize, Serialize};
91/// use langchainrust::core::structured_output::stream_structured_output;
92/// use futures_util::StreamExt;
93///
94/// #[derive(Debug, Deserialize, Serialize, Clone)]
95/// #[serde(default)]
96/// struct Person {
97/// name: String,
98/// age: u32,
99/// }
100///
101/// impl Default for Person {
102/// fn default() -> Self {
103/// Self { name: String::new(), age: 0 }
104/// }
105/// }
106///
107/// let mut stream = stream_structured_output::<Person, _>(
108/// &llm, schema, "Tell me about Alice"
109/// ).await?;
110/// while let Some(result) = stream.next().await {
111/// let person = result?;
112/// println!("Partial: name={}, age={}", person.name, person.age);
113/// }
114/// ```
115pub async fn stream_structured_output<T, M>(
116 llm: &M,
117 schema: Value,
118 prompt: &str,
119) -> Result<
120 Pin<Box<dyn Stream<Item = Result<T, StructuredOutputError>> + Send>>,
121 StructuredOutputError,
122>
123where
124 T: DeserializeOwned + Serialize + Clone + PartialEq + Unpin + Send + Sync + 'static,
125 M: BaseChatModel + ?Sized,
126{
127 // Validate the schema is an object
128 if !schema.is_object() {
129 return Err(StructuredOutputError::SchemaError(format!(
130 "Schema must be a JSON object, got: {}",
131 schema
132 )));
133 }
134
135 // Build the system prompt with schema and format instructions
136 let system_prompt = build_structured_system_prompt(&schema);
137
138 let messages = vec![Message::system(system_prompt), Message::human(prompt)];
139
140 // Start the stream. We need to erase the model's error type by mapping
141 // it to StructuredOutputError before passing to the stream processor.
142 let token_stream = llm
143 .stream_chat(messages, None)
144 .await
145 .map_err(|e| StructuredOutputError::LLMError(e.to_string()))?;
146
147 // Map the inner stream's error type from M::Error to StructuredOutputError
148 let mapped_stream =
149 token_stream.map(|item| item.map_err(|e| StructuredOutputError::LLMError(e.to_string())));
150
151 // Box the mapped stream so it has a concrete type for StructuredStreamProcessor
152 let boxed: Pin<Box<dyn Stream<Item = Result<String, StructuredOutputError>> + Send>> =
153 Box::pin(mapped_stream);
154
155 let output_stream = StructuredStreamProcessor::<T>::new(boxed);
156
157 Ok(Box::pin(output_stream))
158}
159
160/// Stream processor that accumulates LLM tokens through a `PartialJsonParser`
161/// and yields partial `T` values.
162///
163/// This is implemented as a manual `Stream` rather than using `stream::unfold`
164/// because we need to maintain mutable state (the `PartialJsonParser`) across
165/// stream polls.
166struct StructuredStreamProcessor<T> {
167 inner: Pin<Box<dyn Stream<Item = Result<String, StructuredOutputError>> + Send>>,
168 parser: PartialJsonParser,
169 last_value: Option<T>,
170 done: bool,
171}
172
173// Safety: StructuredStreamProcessor is Unpin when T is Unpin because all fields
174// are Unpin: Pin<Box<dyn ..>> is Unpin, PartialJsonParser is Unpin,
175// Option<T> is Unpin when T is Unpin, and bool is Unpin.
176impl<T: Unpin> Unpin for StructuredStreamProcessor<T> {}
177
178impl<T> StructuredStreamProcessor<T>
179where
180 T: DeserializeOwned + Serialize + Clone + PartialEq + Unpin + Send + Sync + 'static,
181{
182 fn new(
183 inner: Pin<Box<dyn Stream<Item = Result<String, StructuredOutputError>> + Send>>,
184 ) -> Self {
185 Self {
186 inner,
187 parser: PartialJsonParser::new(),
188 last_value: None,
189 done: false,
190 }
191 }
192}
193
194impl<T> Stream for StructuredStreamProcessor<T>
195where
196 T: DeserializeOwned + Serialize + Clone + PartialEq + Send + Sync + Unpin + 'static,
197{
198 type Item = Result<T, StructuredOutputError>;
199
200 fn poll_next(
201 self: Pin<&mut Self>,
202 cx: &mut std::task::Context<'_>,
203 ) -> std::task::Poll<Option<Self::Item>> {
204 // Safety: StructuredStreamProcessor is Unpin because all its fields are Unpin.
205 // Pin<Box<dyn Stream + Send>> is Unpin, PartialJsonParser is Unpin,
206 // Option<T> is Unpin, and bool is Unpin.
207 let this = self.get_mut();
208
209 if this.done {
210 return std::task::Poll::Ready(None);
211 }
212
213 loop {
214 match this.inner.as_mut().poll_next(cx) {
215 std::task::Poll::Ready(Some(Ok(token))) => {
216 match this.parser.push_and_parse(&token) {
217 Ok(json_value) => match serde_json::from_value::<T>(json_value) {
218 Ok(value) => {
219 this.last_value = Some(value.clone());
220 return std::task::Poll::Ready(Some(Ok(value)));
221 }
222 Err(_) => {
223 // Partial JSON parsed but cannot deserialize into T yet.
224 // Continue accumulating tokens.
225 continue;
226 }
227 },
228 Err(PartialJsonError::Incomplete(_)) => {
229 // Not enough JSON yet, continue accumulating
230 continue;
231 }
232 Err(PartialJsonError::Invalid(msg)) => {
233 // The accumulated buffer is invalid JSON even after repair.
234 // This can happen with garbage tokens; skip and continue.
235 // We don't terminate the stream for a single bad parse.
236 log::warn!("结构化输出流出现非法 JSON 片段(已跳过,继续累积): {msg}");
237 continue;
238 }
239 }
240 }
241 std::task::Poll::Ready(Some(Err(e))) => {
242 // Error from the underlying token stream
243 return std::task::Poll::Ready(Some(Err(e)));
244 }
245 std::task::Poll::Ready(None) => {
246 // Stream ended. Try to finalize the parser.
247 this.done = true;
248
249 // Take ownership of the parser to finalize it
250 let parser = std::mem::take(&mut this.parser);
251
252 match parser.finalize() {
253 Ok(json_value) => match serde_json::from_value::<T>(json_value) {
254 Ok(value) => {
255 // Only yield if this is a new value different from last
256 let is_new = this.last_value.as_ref() != Some(&value);
257 if is_new {
258 return std::task::Poll::Ready(Some(Ok(value)));
259 }
260 return std::task::Poll::Ready(None);
261 }
262 Err(e) => {
263 return std::task::Poll::Ready(Some(Err(
264 StructuredOutputError::ParseError(format!(
265 "Failed to deserialize final JSON into target type: {}",
266 e
267 )),
268 )));
269 }
270 },
271 Err(PartialJsonError::Invalid(msg)) => {
272 // If we had a last_value, the stream was still "successful"
273 // in yielding partial results, but the final buffer is invalid.
274 // This can happen if the LLM appended non-JSON text.
275 if this.last_value.is_some() {
276 log::warn!("结构化输出流结束时缓冲非法(已返回此前部分结果): {msg}");
277 return std::task::Poll::Ready(None);
278 }
279 return std::task::Poll::Ready(Some(Err(
280 StructuredOutputError::StreamIncomplete(msg),
281 )));
282 }
283 Err(PartialJsonError::Incomplete(msg)) => {
284 if this.last_value.is_some() {
285 log::warn!("结构化输出流结束时缓冲仍不完整(已返回此前部分结果): {msg}");
286 return std::task::Poll::Ready(None);
287 }
288 return std::task::Poll::Ready(Some(Err(
289 StructuredOutputError::StreamIncomplete(msg),
290 )));
291 }
292 }
293 }
294 std::task::Poll::Pending => {
295 return std::task::Poll::Pending;
296 }
297 }
298 }
299 }
300}
301
302/// Attempt to deserialize a `serde_json::Value` into `T`, returning `None`
303/// if deserialization fails (e.g., missing required fields).
304#[allow(dead_code)]
305pub(crate) fn try_deserialize_partial<T: DeserializeOwned>(value: Value) -> Option<T> {
306 serde_json::from_value::<T>(value).ok()
307}