Skip to main content

systemprompt_runtime/
builder.rs

1//! Builder that assembles an [`AppContext`] from profile + config state.
2//!
3//! The builder owns the bootstrap order: profile -> paths -> files ->
4//! database -> logging -> extensions -> ancillary services. Failures at
5//! any step propagate as [`RuntimeError`](crate::error::RuntimeError).
6
7use 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}