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