Skip to main content

systemprompt_runtime/
context.rs

1use crate::builder::AppContextBuilder;
2use crate::registry::ModuleApiRegistry;
3use anyhow::Result;
4use std::sync::Arc;
5use systemprompt_analytics::{AnalyticsService, FingerprintRepository, GeoIpReader};
6use systemprompt_database::DbPool;
7use systemprompt_extension::{Extension, ExtensionContext, ExtensionRegistry};
8use systemprompt_logging::CliService;
9use systemprompt_models::{AppPaths, Config, ContentConfigRaw, ContentRouting, RouteClassifier};
10use systemprompt_traits::{
11    AnalyticsProvider, AppContext as AppContextTrait, ConfigProvider, DatabaseHandle,
12    FingerprintProvider, UserProvider,
13};
14use systemprompt_users::UserService;
15
16#[derive(Clone)]
17pub struct AppContext {
18    config: Arc<Config>,
19    database: DbPool,
20    api_registry: Arc<ModuleApiRegistry>,
21    extension_registry: Arc<ExtensionRegistry>,
22    geoip_reader: Option<GeoIpReader>,
23    content_config: Option<Arc<ContentConfigRaw>>,
24    route_classifier: Arc<RouteClassifier>,
25    analytics_service: Arc<AnalyticsService>,
26    fingerprint_repo: Option<Arc<FingerprintRepository>>,
27    user_service: Option<Arc<UserService>>,
28}
29
30impl std::fmt::Debug for AppContext {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.debug_struct("AppContext")
33            .field("config", &"Config")
34            .field("database", &"DbPool")
35            .field("api_registry", &"ModuleApiRegistry")
36            .field("extension_registry", &self.extension_registry)
37            .field("geoip_reader", &self.geoip_reader.is_some())
38            .field("content_config", &self.content_config.is_some())
39            .field("route_classifier", &"RouteClassifier")
40            .field("analytics_service", &"AnalyticsService")
41            .field("fingerprint_repo", &self.fingerprint_repo.is_some())
42            .field("user_service", &self.user_service.is_some())
43            .finish()
44    }
45}
46
47#[derive(Debug)]
48pub struct AppContextParts {
49    pub config: Arc<Config>,
50    pub database: DbPool,
51    pub api_registry: Arc<ModuleApiRegistry>,
52    pub extension_registry: Arc<ExtensionRegistry>,
53    pub geoip_reader: Option<GeoIpReader>,
54    pub content_config: Option<Arc<ContentConfigRaw>>,
55    pub route_classifier: Arc<RouteClassifier>,
56    pub analytics_service: Arc<AnalyticsService>,
57    pub fingerprint_repo: Option<Arc<FingerprintRepository>>,
58    pub user_service: Option<Arc<UserService>>,
59}
60
61impl AppContext {
62    pub async fn new() -> Result<Self> {
63        Self::builder().build().await
64    }
65
66    #[must_use]
67    pub fn builder() -> AppContextBuilder {
68        AppContextBuilder::new()
69    }
70
71    pub fn from_parts(parts: AppContextParts) -> Self {
72        Self {
73            config: parts.config,
74            database: parts.database,
75            api_registry: parts.api_registry,
76            extension_registry: parts.extension_registry,
77            geoip_reader: parts.geoip_reader,
78            content_config: parts.content_config,
79            route_classifier: parts.route_classifier,
80            analytics_service: parts.analytics_service,
81            fingerprint_repo: parts.fingerprint_repo,
82            user_service: parts.user_service,
83        }
84    }
85
86    #[cfg(feature = "geolocation")]
87    pub fn load_geoip_database(config: &Config, show_warnings: bool) -> Option<GeoIpReader> {
88        let Some(geoip_path) = &config.geoip_database_path else {
89            if show_warnings {
90                CliService::warning(
91                    "GeoIP database not configured - geographic data will not be available",
92                );
93                CliService::info("  To enable geographic data:");
94                CliService::info("  1. Download MaxMind GeoLite2-City database from: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data");
95                CliService::info(
96                    "  2. Add paths.geoip_database to your profile pointing to the .mmdb file",
97                );
98            }
99            return None;
100        };
101
102        match maxminddb::Reader::open_readfile(geoip_path) {
103            Ok(reader) => Some(Arc::new(reader)),
104            Err(e) => {
105                if show_warnings {
106                    CliService::warning(&format!(
107                        "Could not load GeoIP database from {geoip_path}: {e}"
108                    ));
109                    CliService::info(
110                        "  Geographic data (country/region/city) will not be available.",
111                    );
112                    CliService::info(
113                        "  To fix: Ensure the path is correct and the file is a valid MaxMind \
114                         .mmdb database",
115                    );
116                }
117                None
118            },
119        }
120    }
121
122    #[cfg(not(feature = "geolocation"))]
123    pub fn load_geoip_database(_config: &Config, _show_warnings: bool) -> Option<GeoIpReader> {
124        None
125    }
126
127    pub fn load_content_config(config: &Config) -> Option<Arc<ContentConfigRaw>> {
128        let content_config_path = AppPaths::get()
129            .ok()?
130            .system()
131            .content_config()
132            .to_path_buf();
133
134        if !content_config_path.exists() {
135            CliService::warning(&format!(
136                "Content config not found at: {}",
137                content_config_path.display()
138            ));
139            CliService::info("  Landing page detection will not be available.");
140            return None;
141        }
142
143        let yaml_content = match std::fs::read_to_string(&content_config_path) {
144            Ok(c) => c,
145            Err(e) => {
146                CliService::warning(&format!(
147                    "Could not read content config from {}: {}",
148                    content_config_path.display(),
149                    e
150                ));
151                CliService::info("  Landing page detection will not be available.");
152                return None;
153            },
154        };
155
156        match serde_yaml::from_str::<ContentConfigRaw>(&yaml_content) {
157            Ok(mut content_cfg) => {
158                let base_url = config.api_external_url.trim_end_matches('/');
159
160                content_cfg.metadata.structured_data.organization.url = base_url.to_string();
161
162                let logo = &content_cfg.metadata.structured_data.organization.logo;
163                if logo.starts_with('/') {
164                    content_cfg.metadata.structured_data.organization.logo =
165                        format!("{base_url}{logo}");
166                }
167
168                Some(Arc::new(content_cfg))
169            },
170            Err(e) => {
171                CliService::warning(&format!(
172                    "Could not parse content config from {}: {}",
173                    content_config_path.display(),
174                    e
175                ));
176                CliService::info("  Landing page detection will not be available.");
177                None
178            },
179        }
180    }
181
182    pub fn config(&self) -> &Config {
183        &self.config
184    }
185
186    pub fn content_config(&self) -> Option<&ContentConfigRaw> {
187        self.content_config.as_ref().map(AsRef::as_ref)
188    }
189
190    #[allow(trivial_casts)]
191    pub fn content_routing(&self) -> Option<Arc<dyn ContentRouting>> {
192        self.content_config
193            .clone()
194            .map(|c| c as Arc<dyn ContentRouting>)
195    }
196
197    pub const fn db_pool(&self) -> &DbPool {
198        &self.database
199    }
200
201    pub const fn database(&self) -> &DbPool {
202        &self.database
203    }
204
205    pub fn api_registry(&self) -> &ModuleApiRegistry {
206        &self.api_registry
207    }
208
209    pub fn extension_registry(&self) -> &ExtensionRegistry {
210        &self.extension_registry
211    }
212
213    pub fn server_address(&self) -> String {
214        format!("{}:{}", self.config.host, self.config.port)
215    }
216
217    pub fn get_provided_audiences() -> Vec<String> {
218        vec!["a2a".to_string(), "api".to_string(), "mcp".to_string()]
219    }
220
221    pub fn get_valid_audiences(_module_name: &str) -> Vec<String> {
222        Self::get_provided_audiences()
223    }
224
225    pub fn get_server_audiences(_server_name: &str, _port: u16) -> Vec<String> {
226        Self::get_provided_audiences()
227    }
228
229    pub const fn geoip_reader(&self) -> Option<&GeoIpReader> {
230        self.geoip_reader.as_ref()
231    }
232
233    pub const fn analytics_service(&self) -> &Arc<AnalyticsService> {
234        &self.analytics_service
235    }
236
237    pub const fn route_classifier(&self) -> &Arc<RouteClassifier> {
238        &self.route_classifier
239    }
240}
241
242#[allow(trivial_casts)]
243impl AppContextTrait for AppContext {
244    fn config(&self) -> Arc<dyn ConfigProvider> {
245        Arc::clone(&self.config) as _
246    }
247
248    fn database_handle(&self) -> Arc<dyn DatabaseHandle> {
249        Arc::clone(&self.database) as _
250    }
251
252    fn analytics_provider(&self) -> Option<Arc<dyn AnalyticsProvider>> {
253        Some(Arc::clone(&self.analytics_service) as _)
254    }
255
256    fn fingerprint_provider(&self) -> Option<Arc<dyn FingerprintProvider>> {
257        let repo = self.fingerprint_repo.as_ref()?;
258        Some(Arc::clone(repo) as _)
259    }
260
261    fn user_provider(&self) -> Option<Arc<dyn UserProvider>> {
262        let service = self.user_service.as_ref()?;
263        Some(Arc::clone(service) as _)
264    }
265}
266
267#[allow(trivial_casts)]
268impl ExtensionContext for AppContext {
269    fn config(&self) -> Arc<dyn ConfigProvider> {
270        Arc::clone(&self.config) as _
271    }
272
273    fn database(&self) -> Arc<dyn DatabaseHandle> {
274        Arc::clone(&self.database) as _
275    }
276
277    fn get_extension(&self, id: &str) -> Option<Arc<dyn Extension>> {
278        self.extension_registry.get(id).cloned()
279    }
280}