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