systemprompt_runtime/
context.rs1use 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}