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::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        systemprompt_logging::init_logging(Arc::clone(&database));
60
61        if config.database_write_url.is_some() {
62            tracing::info!(
63                "Database read/write separation enabled: reads from replica, writes to primary"
64            );
65        }
66
67        let api_registry = Arc::new(ModuleApiRegistry::new());
68
69        let registry = self
70            .extension_registry
71            .unwrap_or_else(ExtensionRegistry::discover);
72        registry.validate()?;
73        let extension_registry = Arc::new(registry);
74
75        let geoip_reader = AppContext::load_geoip_database(&config, self.show_startup_warnings);
76        let content_config = AppContext::load_content_config(&config, &app_paths);
77        let content_routing = content_routing_from(content_config.as_ref());
78        let route_classifier = Arc::new(systemprompt_models::RouteClassifier::new(
79            content_routing.clone(),
80        ));
81        let analytics_service = Arc::new(AnalyticsService::new(
82            &database,
83            geoip_reader.clone(),
84            content_routing,
85        )?);
86
87        let fingerprint_repo = match FingerprintRepository::new(&database) {
88            Ok(repo) => Some(Arc::new(repo)),
89            Err(e) => {
90                tracing::warn!(error = %e, "Failed to initialize fingerprint repository");
91                None
92            },
93        };
94
95        let user_service = match UserService::new(&database) {
96            Ok(svc) => Some(Arc::new(svc)),
97            Err(e) => {
98                tracing::warn!(error = %e, "Failed to initialize user service");
99                None
100            },
101        };
102
103        Ok(AppContext::from_parts(AppContextParts {
104            config,
105            database,
106            api_registry,
107            extension_registry,
108            geoip_reader,
109            content_config,
110            route_classifier,
111            analytics_service,
112            fingerprint_repo,
113            user_service,
114            app_paths,
115        }))
116    }
117}
118
119fn content_routing_from(
120    content_config: Option<&Arc<ContentConfigRaw>>,
121) -> Option<Arc<dyn ContentRouting>> {
122    let concrete = Arc::clone(content_config?);
123    let routing: Arc<dyn ContentRouting> = concrete;
124    Some(routing)
125}