Skip to main content

lc_core/runnables/
config.rs

1// src/core/runnables/config.rs
2//! Runnable execution configuration.
3
4use serde_json::Value;
5use std::collections::HashMap;
6use std::sync::Arc;
7use uuid::Uuid;
8
9use lc_callbacks::CallbackManager;
10
11use super::cancellation::CancellationToken;
12
13/// Runnable execution configuration.
14#[derive(Debug, Clone, Default)]
15pub struct RunnableConfig {
16    /// Tags for filtering and tracking.
17    pub tags: Vec<String>,
18
19    /// Metadata - custom data (JSON serializable).
20    pub metadata: HashMap<String, Value>,
21
22    /// Max concurrency for batch operations.
23    pub max_concurrency: Option<usize>,
24
25    /// Run ID for tracking.
26    pub run_id: Option<Uuid>,
27
28    /// Run name for debugging.
29    pub run_name: Option<String>,
30
31    /// Callback manager for tracing and monitoring.
32    pub callbacks: Option<Arc<CallbackManager>>,
33
34    /// Cancellation token for aborting long-running operations.
35    pub cancellation_token: Option<CancellationToken>,
36
37    /// Sampling temperature override for the current call.
38    ///
39    /// When set, takes precedence over the model's own configured
40    /// temperature. This is how wrapper layers (e.g. `LLMClient`) make
41    /// `with_temperature` effective through a trait object (providers Q2).
42    pub temperature: Option<f32>,
43
44    /// Max-tokens override for the current call.
45    ///
46    /// When set, takes precedence over the model's own configured max
47    /// tokens (providers Q2).
48    pub max_tokens: Option<usize>,
49
50    /// Configurable values — the Rust counterpart of Python LCEL's
51    /// `config["configurable"]`.
52    ///
53    /// Runtime selection keys live here: `configurable_alternatives` reads
54    /// its `which` key from this map, and `RunnableWithMessageHistory`
55    /// (session mode) reads `session_id` from it.
56    pub configurable: HashMap<String, Value>,
57}
58
59impl RunnableConfig {
60    /// Creates an empty configuration.
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Adds a tag.
66    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
67        self.tags.push(tag.into());
68        self
69    }
70
71    /// Adds metadata.
72    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
73        self.metadata.insert(key.into(), value);
74        self
75    }
76
77    /// Sets max concurrency.
78    pub fn with_max_concurrency(mut self, max: usize) -> Self {
79        self.max_concurrency = Some(max);
80        self
81    }
82
83    /// Sets run ID.
84    pub fn with_run_id(mut self, id: Uuid) -> Self {
85        self.run_id = Some(id);
86        self
87    }
88
89    /// Sets run name.
90    pub fn with_run_name(mut self, name: impl Into<String>) -> Self {
91        self.run_name = Some(name.into());
92        self
93    }
94
95    /// Sets callback manager.
96    pub fn with_callbacks(mut self, callbacks: Arc<CallbackManager>) -> Self {
97        self.callbacks = Some(callbacks);
98        self
99    }
100
101    /// Sets cancellation token.
102    pub fn with_cancellation_token(mut self, token: CancellationToken) -> Self {
103        self.cancellation_token = Some(token);
104        self
105    }
106
107    /// Sets a sampling temperature override for the call.
108    pub fn with_temperature(mut self, temperature: f32) -> Self {
109        self.temperature = Some(temperature);
110        self
111    }
112
113    /// Sets a max-tokens override for the call.
114    pub fn with_max_tokens(mut self, max_tokens: usize) -> Self {
115        self.max_tokens = Some(max_tokens);
116        self
117    }
118
119    /// Sets a configurable value — the Rust counterpart of Python LCEL's
120    /// `config["configurable"][key] = value`.
121    ///
122    /// # Example
123    ///
124    /// ```rust,ignore
125    /// // 给 session 记忆选槽 / 给 configurable_alternatives 路由
126    /// let config = RunnableConfig::new().with_configurable("session_id", json!("s1"));
127    /// ```
128    pub fn with_configurable(mut self, key: impl Into<String>, value: Value) -> Self {
129        self.configurable.insert(key.into(), value);
130        self
131    }
132
133    /// Reads a configurable value by key.
134    pub fn configurable_value(&self, key: &str) -> Option<&Value> {
135        self.configurable.get(key)
136    }
137
138    /// Checks if cancellation has been requested.
139    pub fn is_cancelled(&self) -> bool {
140        self.cancellation_token
141            .as_ref()
142            .is_some_and(|t| t.is_cancelled())
143    }
144
145    /// Merges two configurations (later overrides earlier).
146    pub fn merge(mut self, other: RunnableConfig) -> Self {
147        // Merge tags as an order-preserving union (dedup without sorting).
148        for tag in other.tags {
149            if !self.tags.iter().any(|existing| existing == &tag) {
150                self.tags.push(tag);
151            }
152        }
153
154        // Merge metadata (override)
155        self.metadata.extend(other.metadata);
156
157        // Merge configurable values (override)
158        self.configurable.extend(other.configurable);
159
160        // Override other fields
161        if other.max_concurrency.is_some() {
162            self.max_concurrency = other.max_concurrency;
163        }
164        if other.run_id.is_some() {
165            self.run_id = other.run_id;
166        }
167        if other.run_name.is_some() {
168            self.run_name = other.run_name;
169        }
170        if other.cancellation_token.is_some() {
171            self.cancellation_token = other.cancellation_token;
172        }
173        if other.temperature.is_some() {
174            self.temperature = other.temperature;
175        }
176        if other.max_tokens.is_some() {
177            self.max_tokens = other.max_tokens;
178        }
179
180        // Merge callbacks: append `other`'s handlers instead of replacing the
181        // whole manager (Q8), so observers configured at different layers of
182        // the pipeline all keep firing.
183        if let (Some(self_cb), Some(other_cb)) = (&self.callbacks, &other.callbacks) {
184            self.callbacks = Some(Arc::new(self_cb.merge_with(other_cb)));
185        } else if other.callbacks.is_some() {
186            self.callbacks = other.callbacks;
187        }
188
189        self
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use serde_json::json;
197
198    #[test]
199    fn configurable_roundtrip() {
200        let cfg = RunnableConfig::new().with_configurable("session_id", json!("s1"));
201        assert_eq!(cfg.configurable_value("session_id"), Some(&json!("s1")));
202        assert_eq!(cfg.configurable_value("missing"), None);
203    }
204
205    #[test]
206    fn configurable_merge_overrides() {
207        let base = RunnableConfig::new().with_configurable("which", json!("a"));
208        let other = RunnableConfig::new().with_configurable("which", json!("b"));
209        let merged = base.merge(other);
210        assert_eq!(merged.configurable_value("which"), Some(&json!("b")));
211        // 不存在的键不进 merge 结果
212        assert_eq!(merged.configurable_value("none"), None);
213    }
214}