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