systemprompt_runtime/
builder.rs1use std::sync::Arc;
8
9use systemprompt_analytics::{AnalyticsService, FingerprintRepository};
10use systemprompt_config::ProfileBootstrap;
11use systemprompt_database::Database;
12use systemprompt_extension::ExtensionRegistry;
13use systemprompt_models::{AppPaths, Config, ContentConfigRaw, ContentRouting};
14use systemprompt_users::UserService;
15
16use crate::context::{AppContext, AppContextParts};
17use crate::error::{RuntimeError, RuntimeResult};
18use crate::registry::ModuleApiRegistry;
19
20#[derive(Debug, Default)]
21pub struct AppContextBuilder {
22 extension_registry: Option<ExtensionRegistry>,
23 show_startup_warnings: bool,
24}
25
26impl AppContextBuilder {
27 #[must_use]
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 #[must_use]
33 pub fn with_extensions(mut self, registry: ExtensionRegistry) -> Self {
34 self.extension_registry = Some(registry);
35 self
36 }
37
38 #[must_use]
39 pub const fn with_startup_warnings(mut self, show: bool) -> Self {
40 self.show_startup_warnings = show;
41 self
42 }
43
44 pub async fn build(self) -> RuntimeResult<AppContext> {
45 let profile = ProfileBootstrap::get()?;
46 let app_paths = Arc::new(AppPaths::from_profile(&profile.paths)?);
47 systemprompt_files::FilesConfig::init(&app_paths)?;
48 let config = Arc::new(Config::get()?.clone());
49
50 let database = Arc::new(
51 Database::from_config_with_write(
52 &config.database_type,
53 &config.database_url,
54 config.database_write_url.as_deref(),
55 )
56 .await?,
57 );
58
59 let authz_audit_pool = database.write_pool_arc().ok();
60 systemprompt_security::authz::install_from_governance_config(
61 profile.governance.as_ref(),
62 authz_audit_pool,
63 )
64 .map_err(|err| RuntimeError::Internal(format!("authz bootstrap: {err}")))?;
65
66 systemprompt_logging::init_logging(Arc::clone(&database));
67
68 if config.database_write_url.is_some() {
69 tracing::info!(
70 "Database read/write separation enabled: reads from replica, writes to primary"
71 );
72 }
73
74 let api_registry = Arc::new(ModuleApiRegistry::new());
75
76 let registry = self
77 .extension_registry
78 .unwrap_or_else(ExtensionRegistry::discover);
79 registry.validate()?;
80 let extension_registry = Arc::new(registry);
81
82 let geoip_reader = AppContext::load_geoip_database(&config, self.show_startup_warnings);
83 let content_config = AppContext::load_content_config(&config, &app_paths);
84 let content_routing = content_routing_from(content_config.as_ref());
85 let route_classifier = Arc::new(systemprompt_models::RouteClassifier::new(
86 content_routing.clone(),
87 ));
88 let analytics_service = Arc::new(AnalyticsService::new(
89 &database,
90 geoip_reader.clone(),
91 content_routing,
92 )?);
93
94 let fingerprint_repo = match FingerprintRepository::new(&database) {
95 Ok(repo) => Some(Arc::new(repo)),
96 Err(e) => {
97 tracing::warn!(error = %e, "Failed to initialize fingerprint repository");
98 None
99 },
100 };
101
102 let user_service = match UserService::new(&database) {
103 Ok(svc) => Some(Arc::new(svc)),
104 Err(e) => {
105 tracing::warn!(error = %e, "Failed to initialize user service");
106 None
107 },
108 };
109
110 Ok(AppContext::from_parts(AppContextParts {
111 config,
112 database,
113 api_registry,
114 extension_registry,
115 geoip_reader,
116 content_config,
117 route_classifier,
118 analytics_service,
119 fingerprint_repo,
120 user_service,
121 app_paths,
122 }))
123 }
124}
125
126fn content_routing_from(
127 content_config: Option<&Arc<ContentConfigRaw>>,
128) -> Option<Arc<dyn ContentRouting>> {
129 let concrete = Arc::clone(content_config?);
130 let routing: Arc<dyn ContentRouting> = concrete;
131 Some(routing)
132}