Skip to main content

systemprompt_runtime/
context.rs

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