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