Skip to main content

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}