1use crate::serve::backends::{BackendSelector, PrivacyTier, ServingBackend};
4use crate::serve::circuit_breaker::{CircuitState, CostCircuitBreaker};
5use crate::serve::context::ContextManager;
6use crate::serve::router::SpilloverRouter;
7use crate::serve::templates::ChatTemplateEngine;
8use std::sync::Arc;
9use std::time::Instant;
10
11use super::audit::AuditLog;
12use super::auth::AuthStore;
13use super::batch::BatchStore;
14use super::config::BancoConfig;
15use super::conversations::ConversationStore;
16use super::eval::EvalStore;
17use super::events::EventBus;
18use super::experiment::ExperimentStore;
19use super::metrics::MetricsCollector;
20use super::model_slot::ModelSlot;
21use super::prompts::PromptStore;
22use super::rag::RagIndex;
23use super::recipes::RecipeStore;
24use super::storage::FileStore;
25use super::tools::ToolRegistry;
26use super::training::TrainingStore;
27use super::types::{HealthResponse, InferenceParams, ModelInfo, ModelsResponse, SystemResponse};
28use std::sync::RwLock;
29
30pub struct BancoStateInner {
36 pub backend_selector: BackendSelector,
37 pub router: SpilloverRouter,
38 pub circuit_breaker: CostCircuitBreaker,
39 pub context_manager: ContextManager,
40 pub template_engine: ChatTemplateEngine,
41 pub privacy_tier: PrivacyTier,
42 pub start_time: Instant,
43 pub conversations: Arc<ConversationStore>,
44 pub prompts: PromptStore,
45 pub auth: AuthStore,
46 pub model: ModelSlot,
47 pub inference_params: RwLock<InferenceParams>,
48 pub files: Arc<FileStore>,
49 pub recipes: Arc<RecipeStore>,
50 pub rag: RagIndex,
51 pub evals: Arc<EvalStore>,
52 pub training: Arc<TrainingStore>,
53 pub experiments: Arc<ExperimentStore>,
54 pub batches: Arc<BatchStore>,
55 pub tools: ToolRegistry,
56 pub metrics: MetricsCollector,
57 pub audit_log: AuditLog,
58 pub events: EventBus,
59}
60
61pub type BancoState = Arc<BancoStateInner>;
63
64impl BancoStateInner {
65 #[must_use]
67 pub fn with_defaults() -> BancoState {
68 Self::with_privacy(PrivacyTier::Standard)
69 }
70
71 #[must_use]
73 pub fn from_config() -> BancoState {
74 let config = BancoConfig::load();
75 let tier: PrivacyTier = config.server.privacy_tier.into();
76 let cb_config = crate::serve::circuit_breaker::CircuitBreakerConfig {
77 daily_budget_usd: config.budget.daily_limit_usd,
78 max_request_cost_usd: config.budget.max_request_usd,
79 ..Default::default()
80 };
81
82 let data_dir = BancoConfig::config_dir();
84 let conversations = match &data_dir {
85 Some(dir) => ConversationStore::with_data_dir(dir.join("conversations")),
86 None => ConversationStore::in_memory(),
87 };
88 let files = match &data_dir {
89 Some(dir) => FileStore::with_data_dir(dir.clone()),
90 None => FileStore::in_memory(),
91 };
92
93 let state = Arc::new(Self {
94 backend_selector: BackendSelector::new().with_privacy(tier),
95 router: SpilloverRouter::with_defaults(),
96 circuit_breaker: CostCircuitBreaker::new(cb_config),
97 context_manager: ContextManager::default(),
98 template_engine: ChatTemplateEngine::default(),
99 privacy_tier: tier,
100 start_time: Instant::now(),
101 conversations,
102 prompts: PromptStore::new(),
103 auth: AuthStore::local(),
104 model: ModelSlot::empty(),
105 inference_params: RwLock::new(InferenceParams::default()),
106 files,
107 recipes: RecipeStore::new(),
108 rag: RagIndex::new(),
109 evals: EvalStore::new(),
110 training: TrainingStore::new(),
111 experiments: match &data_dir {
112 Some(dir) => ExperimentStore::with_data_dir(dir.join("experiments")),
113 None => ExperimentStore::new(),
114 },
115 batches: BatchStore::new(),
116 tools: ToolRegistry::default(),
117 metrics: MetricsCollector::default(),
118 audit_log: match &data_dir {
119 Some(dir) => AuditLog::with_file(dir.join("audit.jsonl")),
120 None => AuditLog::new(),
121 },
122 events: EventBus::default(),
123 });
124
125 let loaded_files = state.files.list();
127 for file_info in &loaded_files {
128 if let Some(content) = state.files.read_content(&file_info.id) {
129 let text = String::from_utf8_lossy(&content);
130 state.rag.index_document(&file_info.id, &file_info.name, &text);
131 }
132 }
133 if !loaded_files.is_empty() {
134 eprintln!("[banco] Indexed {} files for RAG", loaded_files.len());
135 }
136
137 state
138 }
139
140 #[must_use]
142 pub fn with_privacy(tier: PrivacyTier) -> BancoState {
143 Arc::new(Self {
144 backend_selector: BackendSelector::new().with_privacy(tier),
145 router: SpilloverRouter::with_defaults(),
146 circuit_breaker: CostCircuitBreaker::with_defaults(),
147 context_manager: ContextManager::default(),
148 template_engine: ChatTemplateEngine::default(),
149 privacy_tier: tier,
150 start_time: Instant::now(),
151 conversations: ConversationStore::in_memory(),
152 prompts: PromptStore::new(),
153 auth: AuthStore::local(),
154 model: ModelSlot::empty(),
155 inference_params: RwLock::new(InferenceParams::default()),
156 files: FileStore::in_memory(),
157 recipes: RecipeStore::new(),
158 rag: RagIndex::new(),
159 evals: EvalStore::new(),
160 training: TrainingStore::new(),
161 experiments: ExperimentStore::new(),
162 batches: BatchStore::new(),
163 tools: ToolRegistry::default(),
164 metrics: MetricsCollector::default(),
165 audit_log: AuditLog::new(),
166 events: EventBus::default(),
167 })
168 }
169
170 #[must_use]
172 pub fn health_status(&self) -> HealthResponse {
173 let cb_state = match self.circuit_breaker.state() {
174 CircuitState::Closed => "closed",
175 CircuitState::Open => "open",
176 CircuitState::HalfOpen => "half_open",
177 };
178 HealthResponse {
179 status: "ok".to_string(),
180 circuit_breaker_state: cb_state.to_string(),
181 uptime_secs: self.start_time.elapsed().as_secs(),
182 }
183 }
184
185 #[must_use]
187 pub fn list_models(&self) -> ModelsResponse {
188 let backends = self.backend_selector.recommend();
189 let data = backends
190 .iter()
191 .map(|b| ModelInfo {
192 id: format!("{b:?}").to_lowercase(),
193 object: "model".to_string(),
194 owned_by: "batuta".to_string(),
195 local: b.is_local(),
196 })
197 .collect();
198 ModelsResponse { object: "list".to_string(), data }
199 }
200
201 #[must_use]
203 pub fn system_info(&self) -> SystemResponse {
204 let backends = self.backend_selector.recommend();
205 let rag_status = self.rag.status();
206 let tokenizer = if self.model.is_loaded() {
207 #[cfg(feature = "aprender")]
208 {
209 Some(if self.model.has_bpe_tokenizer() { "bpe" } else { "greedy" }.to_string())
210 }
211 #[cfg(not(feature = "aprender"))]
212 {
213 Some("greedy".to_string())
214 }
215 } else {
216 None
217 };
218 SystemResponse {
219 privacy_tier: format!("{:?}", self.privacy_tier),
220 backends: backends.iter().map(|b| format!("{b:?}")).collect(),
221 gpu_available: backends.contains(&ServingBackend::Realizar),
222 version: env!("CARGO_PKG_VERSION").to_string(),
223 telemetry: false,
224 model_loaded: self.model.is_loaded(),
225 model_id: self.model.info().map(|m| m.model_id),
226 hint: if self.model.is_loaded() {
227 None
228 } else {
229 Some(
230 "Load a model: POST /api/v1/models/load {\"model\": \"./model.gguf\"}"
231 .to_string(),
232 )
233 },
234 tokenizer,
235 endpoints: 82,
236 files: self.files.len(),
237 conversations: self.conversations.len(),
238 rag_indexed: rag_status.indexed,
239 rag_chunks: rag_status.chunk_count,
240 training_runs: self.training.list().len(),
241 audit_entries: self.audit_log.len(),
242 }
243 }
244}