ferrin_core/stream_text/
builder.rs1use futures_util::FutureExt;
4use std::fmt;
5use std::future::Future;
6use std::future::IntoFuture;
7use std::panic::AssertUnwindSafe;
8use std::panic::catch_unwind;
9use std::sync::Arc;
10use std::sync::Mutex;
11
12use ferrin_spec::BoxFuture;
13use ferrin_spec::LanguageModelRef;
14
15use super::StreamErrorInfo;
16use super::StreamEvent;
17use super::pipeline;
18use super::result::StreamTextResult;
19use super::transforms::StreamTransform;
20use crate::error::Error;
21use crate::generate_text::config::CallConfig;
22use crate::hooks::HookFn;
23use crate::output::NoOutput;
24use crate::output::Output;
25use crate::output::OutputHandler;
26use crate::telemetry::AbortEvent;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30#[non_exhaustive]
31pub enum ErrorDecision {
32 #[default]
34 Continue,
35 Retry,
38}
39
40pub trait OnErrorFn: Send + Sync + 'static {
45 fn call(&self, error: StreamErrorInfo) -> BoxFuture<'static, ErrorDecision>;
47}
48
49impl<F, Fut> OnErrorFn for F
50where
51 F: Fn(StreamErrorInfo) -> Fut + Send + Sync + 'static,
52 Fut: Future<Output = ErrorDecision> + Send + 'static,
53{
54 fn call(&self, error: StreamErrorInfo) -> BoxFuture<'static, ErrorDecision> {
55 Box::pin(self(error))
56 }
57}
58
59#[derive(Default)]
61pub(crate) struct StreamConfig {
62 pub(crate) transforms: Vec<Arc<dyn StreamTransform>>,
63 pub(crate) include_raw_chunks: bool,
64 pub(crate) stream_retries: Option<u32>,
65 pub(crate) on_error: Option<Arc<dyn OnErrorFn>>,
66 handled_errors: Mutex<Vec<StreamErrorInfo>>,
67}
68
69impl StreamConfig {
70 pub(crate) fn mark_error_handled(&self, error: StreamErrorInfo) {
71 self.handled_errors
72 .lock()
73 .unwrap_or_else(std::sync::PoisonError::into_inner)
74 .push(error);
75 }
76
77 pub(crate) fn take_error_handled(&self, error: &StreamErrorInfo) -> bool {
78 let mut handled = self
79 .handled_errors
80 .lock()
81 .unwrap_or_else(std::sync::PoisonError::into_inner);
82 if let Some(index) = handled.iter().position(|item| item == error) {
83 handled.remove(index);
84 true
85 } else {
86 false
87 }
88 }
89
90 pub(crate) async fn error_decision(&self, error: StreamErrorInfo) -> ErrorDecision {
91 let Some(callback) = &self.on_error else {
92 return ErrorDecision::Continue;
93 };
94 let Ok(future) = catch_unwind(AssertUnwindSafe(|| callback.call(error))) else {
95 return ErrorDecision::Continue;
96 };
97 AssertUnwindSafe(future)
98 .catch_unwind()
99 .await
100 .unwrap_or(ErrorDecision::Continue)
101 }
102}
103
104impl fmt::Debug for StreamConfig {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 f.debug_struct("StreamConfig")
107 .field("transforms", &self.transforms.len())
108 .field("include_raw_chunks", &self.include_raw_chunks)
109 .field("stream_retries", &self.stream_retries)
110 .field("on_error", &self.on_error.is_some())
111 .finish()
112 }
113}
114
115#[must_use]
120pub fn stream_text(model: impl Into<LanguageModelRef>) -> StreamText<()> {
121 StreamText {
122 config: CallConfig::new(model.into()),
123 output: Arc::new(NoOutput),
124 stream: StreamConfig {
125 transforms: Vec::new(),
126 include_raw_chunks: false,
127 stream_retries: None,
128 on_error: None,
129 handled_errors: Mutex::new(Vec::new()),
130 },
131 }
132}
133
134pub struct StreamText<O> {
136 pub(crate) config: CallConfig,
137 pub(crate) output: Arc<dyn OutputHandler<O>>,
138 pub(crate) stream: StreamConfig,
139}
140
141crate::builder::impl_call_builder!(StreamText);
142
143impl<O> StreamText<O> {
144 pub fn output<T>(self, output: Output<T>) -> StreamText<T> {
146 StreamText {
147 config: self.config,
148 output: output.handler(),
149 stream: self.stream,
150 }
151 }
152
153 #[must_use]
156 pub fn transform(mut self, transform: impl StreamTransform + 'static) -> Self {
157 self.stream.transforms.push(Arc::new(transform));
158 self
159 }
160
161 #[must_use]
163 pub fn include_raw_chunks(mut self) -> Self {
164 self.stream.include_raw_chunks = true;
165 self
166 }
167
168 #[must_use]
173 pub fn stream_retries(mut self, retries: u32) -> Self {
174 self.stream.stream_retries = Some(retries);
175 self
176 }
177
178 #[must_use]
184 pub fn on_chunk(mut self, f: impl HookFn<StreamEvent>) -> Self {
185 self.config.hooks.on_chunk.push(Arc::new(f));
186 self
187 }
188
189 #[must_use]
191 pub fn on_abort(mut self, f: impl HookFn<AbortEvent>) -> Self {
192 self.config.hooks.on_abort.push(Arc::new(f));
193 self
194 }
195
196 #[must_use]
201 pub fn on_error(mut self, f: impl OnErrorFn) -> Self {
202 self.stream.on_error = Some(Arc::new(f));
203 self
204 }
205}
206
207impl<O> fmt::Debug for StreamText<O> {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 f.debug_struct("StreamText")
210 .field("config", &self.config)
211 .field("stream", &self.stream)
212 .finish_non_exhaustive()
213 }
214}
215
216impl<O: Send + 'static> IntoFuture for StreamText<O> {
217 type Output = Result<StreamTextResult<O>, Error>;
218 type IntoFuture = BoxFuture<'static, Self::Output>;
219
220 fn into_future(self) -> Self::IntoFuture {
221 Box::pin(async move {
222 self.output.validate_configuration()?;
223 pipeline::start(self.config, self.output, self.stream).await
224 })
225 }
226}