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}