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::{
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}