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 injected = systemprompt_extension::runtime_config::get_injected_extensions();
72
73 let registry = match extension_registry {
74 Some(r) => r,
75 None if injected.is_empty() => ExtensionRegistry::discover(),
76 None => ExtensionRegistry::discover_and_merge(injected)?,
77 };
78
79 registry.validate()?;
80
81 let extension_registry = Arc::new(registry);
82
83 let geoip_reader = Self::load_geoip_database(&config, show_startup_warnings);
84 let content_config = Self::load_content_config(&config);
85
86 #[allow(trivial_casts)]
87 let content_routing: Option<Arc<dyn ContentRouting>> =
88 content_config.clone().map(|c| c as Arc<dyn ContentRouting>);
89
90 let route_classifier = Arc::new(RouteClassifier::new(content_routing.clone()));
91
92 let analytics_service = Arc::new(AnalyticsService::new(
93 Arc::clone(&database),
94 geoip_reader.clone(),
95 content_routing,
96 ));
97
98 let fingerprint_repo = FingerprintRepository::new(&database).ok().map(Arc::new);
99
100 let user_service = UserService::new(&database).ok().map(Arc::new);
101
102 systemprompt_logging::init_logging(Arc::clone(&database));
103
104 Ok(Self {
105 config,
106 database,
107 api_registry,
108 extension_registry,
109 geoip_reader,
110 content_config,
111 route_classifier,
112 analytics_service,
113 fingerprint_repo,
114 user_service,
115 })
116 }
117
118 fn load_geoip_database(config: &Config, show_warnings: bool) -> Option<GeoIpReader> {
119 let Some(geoip_path) = &config.geoip_database_path else {
120 if show_warnings {
121 CliService::warning(
122 "GeoIP database not configured - geographic data will not be available",
123 );
124 CliService::info(" To enable geographic data:");
125 CliService::info(" 1. Download MaxMind GeoLite2-City database from: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data");
126 CliService::info(
127 " 2. Add paths.geoip_database to your profile pointing to the .mmdb file",
128 );
129 }
130 return None;
131 };
132
133 match maxminddb::Reader::open_readfile(geoip_path) {
134 Ok(reader) => Some(Arc::new(reader)),
135 Err(e) => {
136 if show_warnings {
137 CliService::warning(&format!(
138 "Could not load GeoIP database from {geoip_path}: {e}"
139 ));
140 CliService::info(
141 " Geographic data (country/region/city) will not be available.",
142 );
143 CliService::info(
144 " To fix: Ensure the path is correct and the file is a valid MaxMind \
145 .mmdb database",
146 );
147 }
148 None
149 },
150 }
151 }
152
153 fn load_content_config(config: &Config) -> Option<Arc<ContentConfigRaw>> {
154 let content_config_path = AppPaths::get()
155 .ok()?
156 .system()
157 .content_config()
158 .to_path_buf();
159
160 if !content_config_path.exists() {
161 CliService::warning(&format!(
162 "Content config not found at: {}",
163 content_config_path.display()
164 ));
165 CliService::info(" Landing page detection will not be available.");
166 return None;
167 }
168
169 let yaml_content = match std::fs::read_to_string(&content_config_path) {
170 Ok(c) => c,
171 Err(e) => {
172 CliService::warning(&format!(
173 "Could not read content config from {}: {}",
174 content_config_path.display(),
175 e
176 ));
177 CliService::info(" Landing page detection will not be available.");
178 return None;
179 },
180 };
181
182 match serde_yaml::from_str::<ContentConfigRaw>(&yaml_content) {
183 Ok(mut content_cfg) => {
184 let base_url = config.api_external_url.trim_end_matches('/');
185
186 content_cfg.metadata.structured_data.organization.url = base_url.to_string();
187
188 let logo = &content_cfg.metadata.structured_data.organization.logo;
189 if logo.starts_with('/') {
190 content_cfg.metadata.structured_data.organization.logo =
191 format!("{base_url}{logo}");
192 }
193
194 Some(Arc::new(content_cfg))
195 },
196 Err(e) => {
197 CliService::warning(&format!(
198 "Could not parse content config from {}: {}",
199 content_config_path.display(),
200 e
201 ));
202 CliService::info(" Landing page detection will not be available.");
203 None
204 },
205 }
206 }
207
208 pub fn config(&self) -> &Config {
209 &self.config
210 }
211
212 pub fn content_config(&self) -> Option<&ContentConfigRaw> {
213 self.content_config.as_ref().map(AsRef::as_ref)
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}