Italianhype commited on
Commit
1bea138
·
verified ·
1 Parent(s): 95e710d

Harden Blum autonomous learning persistence

Browse files
README.md CHANGED
@@ -191,6 +191,7 @@ The reasoning model APIs are backend-only:
191
  - `POST /model/capture/{ticker}`
192
  - `POST /model/capture-all`
193
  - `POST /model/evaluate-outcomes`
 
194
  - `GET /model/knowledge`
195
  - `GET /model/knowledge/{record_id}`
196
  - `GET /model/memory/search?q=...`
@@ -208,6 +209,8 @@ Training export uses JSONL and targets future Hugging Face training workflows fo
208
 
209
  The objective is not to predict stock prices. The objective is to learn how Blum reasons: explain, contextualize, compare, critique, calibrate confidence and improve future thesis quality.
210
 
 
 
211
  ## Chart Vision Technical Analyst
212
 
213
  Blum includes a dedicated technical chart intelligence module. It is designed to read financial chart images when a vision model is configured, but it never relies only on visual interpretation.
@@ -439,9 +442,11 @@ Optional self-learning cadence:
439
  ```bash
440
  export BLUM_ENABLE_LEARNING_LOOP=true
441
  export BLUM_LEARNING_LOOP_MINUTES=360
 
 
442
  ```
443
 
444
- The learning loop only updates database memory, confidence adjustments and reversible scoring-weight versions.
445
 
446
  ## Docker
447
 
@@ -459,6 +464,8 @@ docker run --rm -p 7860:7860 \
459
  blum-ai-financial-intelligence
460
  ```
461
 
 
 
462
  ## Hugging Face Spaces Deployment
463
 
464
  Use a Docker Space. Upload the repository with:
@@ -477,6 +484,7 @@ The UI exposes `/system/status` in the sidebar and dashboard. If the GUI looks u
477
 
478
  - `app_version` must show the latest deployed version.
479
  - `feature_set` must show the expected feature bundle.
 
480
  - `Financial Brain` shows `fallback mode` unless `BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true`.
481
  - Hugging Face serves the previous Docker image until the new build finishes successfully.
482
  - Existing Market Brain snapshots should be regenerated with `Run brain` after a deployment.
 
191
  - `POST /model/capture/{ticker}`
192
  - `POST /model/capture-all`
193
  - `POST /model/evaluate-outcomes`
194
+ - `POST /model/run-learning-cycle`
195
  - `GET /model/knowledge`
196
  - `GET /model/knowledge/{record_id}`
197
  - `GET /model/memory/search?q=...`
 
209
 
210
  The objective is not to predict stock prices. The objective is to learn how Blum reasons: explain, contextualize, compare, critique, calibrate confidence and improve future thesis quality.
211
 
212
+ The autonomous Blum Financial Model cycle is server-side and evidence-bound. When `BLUM_ENABLE_LEARNING_LOOP=true`, it runs on startup, during market refresh and on its own interval controlled by `BLUM_MODEL_CYCLE_MINUTES` and `BLUM_MODEL_CYCLE_LIMIT`. Each cycle captures recent signal reasoning, evaluates matured thesis outcomes, refreshes training examples and logs a `blum_model_autonomous_cycle` learning event. It updates database memory only; it does not self-modify source code and it does not execute trades.
213
+
214
  ## Chart Vision Technical Analyst
215
 
216
  Blum includes a dedicated technical chart intelligence module. It is designed to read financial chart images when a vision model is configured, but it never relies only on visual interpretation.
 
442
  ```bash
443
  export BLUM_ENABLE_LEARNING_LOOP=true
444
  export BLUM_LEARNING_LOOP_MINUTES=360
445
+ export BLUM_MODEL_CYCLE_MINUTES=5
446
+ export BLUM_MODEL_CYCLE_LIMIT=120
447
  ```
448
 
449
+ The learning loops only update database memory, confidence adjustments, proprietary reasoning examples and reversible scoring-weight versions.
450
 
451
  ## Docker
452
 
 
464
  blum-ai-financial-intelligence
465
  ```
466
 
467
+ For Hugging Face Docker demos without an external database, the startup script writes periodic embedded PostgreSQL backups to `/data/blum/embedded_postgres_blum.sql` and restores them on startup when the public schema is empty. This protects the learning memory only when Hugging Face persistent storage is enabled for the `/data` mount. The strict no-reset configuration is still an external `DATABASE_URL`.
468
+
469
  ## Hugging Face Spaces Deployment
470
 
471
  Use a Docker Space. Upload the repository with:
 
484
 
485
  - `app_version` must show the latest deployed version.
486
  - `feature_set` must show the expected feature bundle.
487
+ - `persistence.mode` must be `external_postgres` for strict no-reset durability, or `embedded_postgres` with a populated backup file plus persistent `/data` storage for demo durability.
488
  - `Financial Brain` shows `fallback mode` unless `BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true`.
489
  - Hugging Face serves the previous Docker image until the new build finishes successfully.
490
  - Existing Market Brain snapshots should be regenerated with `Run brain` after a deployment.
backend/app/api/routes.py CHANGED
@@ -71,6 +71,7 @@ from app.services.blum_financial_model import (
71
  narrative_memory,
72
  quality_overview,
73
  regime_memory,
 
74
  self_critique_for_record,
75
  semantic_reasoning_search,
76
  training_manifest,
@@ -132,7 +133,7 @@ def system_status(db: Session = Depends(get_db)) -> dict:
132
  return {
133
  "service": "blum-ai-financial-intelligence",
134
  "app_version": settings.app_version,
135
- "feature_set": "proprietary-blum-financial-model-v0.7.0",
136
  "environment": settings.environment,
137
  "generated_at": datetime.utcnow().isoformat(),
138
  "hugging_face": {
@@ -153,11 +154,14 @@ def system_status(db: Session = Depends(get_db)) -> dict:
153
  "accuracy_audit_minutes": settings.accuracy_audit_minutes,
154
  "learning_loop_enabled": settings.enable_learning_loop,
155
  "learning_loop_minutes": settings.learning_loop_minutes,
 
 
156
  "chart_vision_mode": settings.chart_vision_mode,
157
  "chart_vision_min_confidence": settings.chart_vision_min_confidence,
158
  "fundamentals_refresh_minutes": settings.fundamentals_refresh_minutes,
159
  "macro_refresh_minutes": settings.macro_refresh_minutes,
160
  },
 
161
  "active_models": {
162
  "finbert": settings.finbert_model,
163
  "embeddings": settings.embedding_model,
@@ -228,11 +232,33 @@ def system_status(db: Session = Depends(get_db)) -> dict:
228
  "Hugging Face serves the previous image until the Docker build finishes successfully.",
229
  "The finance-domain 7B model is disabled by default unless BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true.",
230
  "Existing snapshots must be regenerated with Run brain or full pipeline after a new deployment.",
231
- "Browser cache can keep old static Next.js chunks; hard refresh if app_version is not 0.7.0.",
232
  ],
233
  }
234
 
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  @router.get("/brain/status")
237
  def financial_brain_status(db: Session = Depends(get_db)) -> dict:
238
  return brain_status(db)
@@ -309,6 +335,11 @@ def blum_model_evaluate_outcomes(limit: int = Query(default=250, ge=1, le=2000),
309
  return evaluate_thesis_outcomes(db, limit=limit)
310
 
311
 
 
 
 
 
 
312
  @router.get("/model/knowledge")
313
  def blum_model_knowledge_records(
314
  ticker: str | None = Query(default=None),
 
71
  narrative_memory,
72
  quality_overview,
73
  regime_memory,
74
+ run_model_learning_cycle,
75
  self_critique_for_record,
76
  semantic_reasoning_search,
77
  training_manifest,
 
133
  return {
134
  "service": "blum-ai-financial-intelligence",
135
  "app_version": settings.app_version,
136
+ "feature_set": "persistent-autonomous-blum-financial-model-v0.7.1",
137
  "environment": settings.environment,
138
  "generated_at": datetime.utcnow().isoformat(),
139
  "hugging_face": {
 
154
  "accuracy_audit_minutes": settings.accuracy_audit_minutes,
155
  "learning_loop_enabled": settings.enable_learning_loop,
156
  "learning_loop_minutes": settings.learning_loop_minutes,
157
+ "blum_model_cycle_minutes": settings.blum_model_cycle_minutes,
158
+ "blum_model_cycle_limit": settings.blum_model_cycle_limit,
159
  "chart_vision_mode": settings.chart_vision_mode,
160
  "chart_vision_min_confidence": settings.chart_vision_min_confidence,
161
  "fundamentals_refresh_minutes": settings.fundamentals_refresh_minutes,
162
  "macro_refresh_minutes": settings.macro_refresh_minutes,
163
  },
164
+ "persistence": database_persistence_status(),
165
  "active_models": {
166
  "finbert": settings.finbert_model,
167
  "embeddings": settings.embedding_model,
 
232
  "Hugging Face serves the previous image until the Docker build finishes successfully.",
233
  "The finance-domain 7B model is disabled by default unless BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true.",
234
  "Existing snapshots must be regenerated with Run brain or full pipeline after a new deployment.",
235
+ "Browser cache can keep old static Next.js chunks; hard refresh if app_version is not 0.7.1.",
236
  ],
237
  }
238
 
239
 
240
+ def database_persistence_status() -> dict:
241
+ backup_file = os.getenv("BLUM_EMBEDDED_POSTGRES_BACKUP_FILE")
242
+ backup_exists = bool(backup_file and os.path.exists(backup_file))
243
+ backup_size = os.path.getsize(backup_file) if backup_exists and backup_file else 0
244
+ uses_external_database = bool(os.getenv("DATABASE_URL")) and not backup_file
245
+ mode = "external_postgres" if uses_external_database else "embedded_postgres"
246
+ return {
247
+ "mode": mode,
248
+ "external_database_configured": uses_external_database,
249
+ "embedded_backup_file": backup_file,
250
+ "embedded_backup_exists": backup_exists,
251
+ "embedded_backup_size_bytes": backup_size,
252
+ "embedded_backup_interval_seconds": int(os.getenv("BLUM_DB_BACKUP_SECONDS", "300")),
253
+ "persistent_dir": os.getenv("BLUM_PERSIST_DIR", "/data/blum"),
254
+ "strict_no_reset_mode": uses_external_database,
255
+ "durability_note": (
256
+ "External DATABASE_URL is the strict no-reset mode. Embedded PostgreSQL backup can recover learning state only "
257
+ "when Hugging Face persistent storage is enabled for the /data mount."
258
+ ),
259
+ }
260
+
261
+
262
  @router.get("/brain/status")
263
  def financial_brain_status(db: Session = Depends(get_db)) -> dict:
264
  return brain_status(db)
 
335
  return evaluate_thesis_outcomes(db, limit=limit)
336
 
337
 
338
+ @router.post("/model/run-learning-cycle")
339
+ def blum_model_run_learning_cycle(limit: int = Query(default=120, ge=1, le=2000), db: Session = Depends(get_db)) -> dict:
340
+ return run_model_learning_cycle(db, limit=limit)
341
+
342
+
343
  @router.get("/model/knowledge")
344
  def blum_model_knowledge_records(
345
  ticker: str | None = Query(default=None),
backend/app/core/config.py CHANGED
@@ -5,7 +5,7 @@ from pydantic_settings import BaseSettings
5
 
6
  class Settings(BaseSettings):
7
  app_name: str = "Blum AI Financial Intelligence"
8
- app_version: str = "0.7.0"
9
  environment: str = Field(default="demo", alias="ENVIRONMENT")
10
  database_url: str = Field(
11
  default="postgresql+psycopg2://postgres:postgres@127.0.0.1:5432/blum",
@@ -40,6 +40,8 @@ class Settings(BaseSettings):
40
  accuracy_audit_minutes: int = Field(default=240, alias="BLUM_ACCURACY_AUDIT_MINUTES")
41
  enable_learning_loop: bool = Field(default=True, alias="BLUM_ENABLE_LEARNING_LOOP")
42
  learning_loop_minutes: int = Field(default=360, alias="BLUM_LEARNING_LOOP_MINUTES")
 
 
43
  fundamentals_refresh_minutes: int = Field(default=720, alias="BLUM_FUNDAMENTALS_REFRESH_MINUTES")
44
  macro_refresh_minutes: int = Field(default=240, alias="BLUM_MACRO_REFRESH_MINUTES")
45
  stale_price_max_age_days: int = Field(default=7, alias="BLUM_STALE_PRICE_MAX_AGE_DAYS")
 
5
 
6
  class Settings(BaseSettings):
7
  app_name: str = "Blum AI Financial Intelligence"
8
+ app_version: str = "0.7.1"
9
  environment: str = Field(default="demo", alias="ENVIRONMENT")
10
  database_url: str = Field(
11
  default="postgresql+psycopg2://postgres:postgres@127.0.0.1:5432/blum",
 
40
  accuracy_audit_minutes: int = Field(default=240, alias="BLUM_ACCURACY_AUDIT_MINUTES")
41
  enable_learning_loop: bool = Field(default=True, alias="BLUM_ENABLE_LEARNING_LOOP")
42
  learning_loop_minutes: int = Field(default=360, alias="BLUM_LEARNING_LOOP_MINUTES")
43
+ blum_model_cycle_minutes: int = Field(default=5, alias="BLUM_MODEL_CYCLE_MINUTES")
44
+ blum_model_cycle_limit: int = Field(default=120, alias="BLUM_MODEL_CYCLE_LIMIT")
45
  fundamentals_refresh_minutes: int = Field(default=720, alias="BLUM_FUNDAMENTALS_REFRESH_MINUTES")
46
  macro_refresh_minutes: int = Field(default=240, alias="BLUM_MACRO_REFRESH_MINUTES")
47
  stale_price_max_age_days: int = Field(default=7, alias="BLUM_STALE_PRICE_MAX_AGE_DAYS")
backend/app/services/blum_financial_model.py CHANGED
@@ -72,6 +72,47 @@ def model_status(db: Session) -> dict:
72
  }
73
 
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  def capture_latest_asset_reasoning(db: Session, asset: Asset, source_type: str = "manual_capture") -> dict:
76
  signal = db.scalar(
77
  select(SignalSnapshot)
 
72
  }
73
 
74
 
75
+ def run_model_learning_cycle(db: Session, limit: int = 120) -> dict:
76
+ signals = db.scalars(select(SignalSnapshot).order_by(desc(SignalSnapshot.created_at)).limit(limit)).all()
77
+ skipped = 0
78
+ before_count = count(db, BlumKnowledgeRecord.id)
79
+ for signal in signals:
80
+ asset = signal.asset or db.get(Asset, signal.asset_id)
81
+ if asset is None:
82
+ skipped += 1
83
+ continue
84
+ capture_signal_reasoning(db, signal, asset)
85
+ db.flush()
86
+ captured = max(0, count(db, BlumKnowledgeRecord.id) - before_count)
87
+ outcome_result = evaluate_thesis_outcomes(db, limit=limit)
88
+ dataset_result = build_training_dataset(db, limit=limit, min_quality=55.0)
89
+ event = LearningEvent(
90
+ event_type="blum_model_autonomous_cycle",
91
+ severity="Info",
92
+ title="Blum Financial Model autonomous cycle completed",
93
+ description="Captured latest reasoning, evaluated matured thesis outcomes and refreshed proprietary training examples.",
94
+ payload={
95
+ "signals_seen": len(signals),
96
+ "knowledge_records_created": captured,
97
+ "signals_skipped": skipped,
98
+ "outcomes": outcome_result,
99
+ "dataset": dataset_result,
100
+ },
101
+ )
102
+ db.add(event)
103
+ db.commit()
104
+ return {
105
+ "status": "ok",
106
+ "signals_seen": len(signals),
107
+ "knowledge_records_created": captured,
108
+ "signals_skipped": skipped,
109
+ "outcomes": outcome_result,
110
+ "dataset": dataset_result,
111
+ "learning_event_id": event.id,
112
+ "disclaimer": DISCLAIMER,
113
+ }
114
+
115
+
116
  def capture_latest_asset_reasoning(db: Session, asset: Asset, source_type: str = "manual_capture") -> dict:
117
  signal = db.scalar(
118
  select(SignalSnapshot)
backend/app/services/realtime.py CHANGED
@@ -10,6 +10,7 @@ from app.core.config import get_settings
10
  from app.core.database import SessionLocal
11
  from app.ingestion.news_ingestor import NewsIngestor
12
  from app.services.accuracy import run_accuracy_audit
 
13
  from app.services.data_continuity import repair_data_gaps
14
  from app.services.etf import update_etf_trends
15
  from app.services.fundamentals import update_fundamentals
@@ -52,6 +53,7 @@ def start_realtime_services() -> None:
52
  _scheduler.add_job(run_ipo_refresh, "interval", minutes=settings.ipo_refresh_minutes, id="ipo_refresh", replace_existing=True, max_instances=1)
53
  if settings.enable_learning_loop:
54
  _scheduler.add_job(run_learning_cycle_job, "interval", minutes=settings.learning_loop_minutes, id="financial_brain_learning", replace_existing=True, max_instances=1)
 
55
  _scheduler.start()
56
  with _state_lock:
57
  _state["started"] = True
@@ -73,7 +75,8 @@ def run_startup_pipeline() -> None:
73
  def work(db):
74
  pipeline = PipelineService().run(db, limit=settings.startup_pipeline_limit, period=settings.historical_price_period)
75
  learning = run_learning_cycle(db, limit=settings.max_update_assets * 6) if settings.enable_learning_loop else {}
76
- return {"pipeline": pipeline, "financial_brain_learning": learning}
 
77
 
78
  _run_job("startup_pipeline", work)
79
 
@@ -88,7 +91,8 @@ def run_market_refresh() -> None:
88
  signals = SignalEngine().run(db, limit=settings.max_update_assets)
89
  etf = update_etf_trends(db)
90
  learning = run_learning_cycle(db, limit=settings.max_update_assets * 6) if settings.enable_learning_loop else {}
91
- return {"market_update": market, "signal_run": signals, "etf_update": etf, "financial_brain_learning": learning}
 
92
 
93
  _run_job("market_refresh", work)
94
 
@@ -120,6 +124,10 @@ def run_learning_cycle_job() -> None:
120
  _run_job("financial_brain_learning", lambda db: run_learning_cycle(db, limit=settings.max_update_assets * 6))
121
 
122
 
 
 
 
 
123
  def _run_job(job_name: str, work):
124
  with _state_lock:
125
  if _state["running"]:
 
10
  from app.core.database import SessionLocal
11
  from app.ingestion.news_ingestor import NewsIngestor
12
  from app.services.accuracy import run_accuracy_audit
13
+ from app.services.blum_financial_model import run_model_learning_cycle
14
  from app.services.data_continuity import repair_data_gaps
15
  from app.services.etf import update_etf_trends
16
  from app.services.fundamentals import update_fundamentals
 
53
  _scheduler.add_job(run_ipo_refresh, "interval", minutes=settings.ipo_refresh_minutes, id="ipo_refresh", replace_existing=True, max_instances=1)
54
  if settings.enable_learning_loop:
55
  _scheduler.add_job(run_learning_cycle_job, "interval", minutes=settings.learning_loop_minutes, id="financial_brain_learning", replace_existing=True, max_instances=1)
56
+ _scheduler.add_job(run_blum_model_cycle_job, "interval", minutes=settings.blum_model_cycle_minutes, id="blum_financial_model_cycle", replace_existing=True, max_instances=1)
57
  _scheduler.start()
58
  with _state_lock:
59
  _state["started"] = True
 
75
  def work(db):
76
  pipeline = PipelineService().run(db, limit=settings.startup_pipeline_limit, period=settings.historical_price_period)
77
  learning = run_learning_cycle(db, limit=settings.max_update_assets * 6) if settings.enable_learning_loop else {}
78
+ model_learning = run_model_learning_cycle(db, limit=settings.blum_model_cycle_limit) if settings.enable_learning_loop else {}
79
+ return {"pipeline": pipeline, "financial_brain_learning": learning, "blum_financial_model": model_learning}
80
 
81
  _run_job("startup_pipeline", work)
82
 
 
91
  signals = SignalEngine().run(db, limit=settings.max_update_assets)
92
  etf = update_etf_trends(db)
93
  learning = run_learning_cycle(db, limit=settings.max_update_assets * 6) if settings.enable_learning_loop else {}
94
+ model_learning = run_model_learning_cycle(db, limit=settings.blum_model_cycle_limit) if settings.enable_learning_loop else {}
95
+ return {"market_update": market, "signal_run": signals, "etf_update": etf, "financial_brain_learning": learning, "blum_financial_model": model_learning}
96
 
97
  _run_job("market_refresh", work)
98
 
 
124
  _run_job("financial_brain_learning", lambda db: run_learning_cycle(db, limit=settings.max_update_assets * 6))
125
 
126
 
127
+ def run_blum_model_cycle_job() -> None:
128
+ _run_job("blum_financial_model_cycle", lambda db: run_model_learning_cycle(db, limit=settings.blum_model_cycle_limit))
129
+
130
+
131
  def _run_job(job_name: str, work):
132
  with _state_lock:
133
  if _state["running"]:
frontend/lib/types.ts CHANGED
@@ -144,9 +144,24 @@ export type SystemStatus = {
144
  startup_accuracy_seed_enabled?: boolean;
145
  data_gap_repair_minutes?: number;
146
  accuracy_audit_minutes?: number;
 
 
 
 
147
  fundamentals_refresh_minutes?: number;
148
  macro_refresh_minutes?: number;
149
  };
 
 
 
 
 
 
 
 
 
 
 
150
  active_models: {
151
  finbert: string;
152
  embeddings: string;
 
144
  startup_accuracy_seed_enabled?: boolean;
145
  data_gap_repair_minutes?: number;
146
  accuracy_audit_minutes?: number;
147
+ learning_loop_enabled?: boolean;
148
+ learning_loop_minutes?: number;
149
+ blum_model_cycle_minutes?: number;
150
+ blum_model_cycle_limit?: number;
151
  fundamentals_refresh_minutes?: number;
152
  macro_refresh_minutes?: number;
153
  };
154
+ persistence?: {
155
+ mode: string;
156
+ external_database_configured: boolean;
157
+ embedded_backup_file?: string | null;
158
+ embedded_backup_exists: boolean;
159
+ embedded_backup_size_bytes: number;
160
+ embedded_backup_interval_seconds: number;
161
+ persistent_dir: string;
162
+ strict_no_reset_mode: boolean;
163
+ durability_note: string;
164
+ };
165
  active_models: {
166
  finbert: string;
167
  embeddings: string;
frontend/package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "blum-ai-financial-intelligence-frontend",
3
- "version": "0.7.0",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev -p 3000",
 
1
  {
2
  "name": "blum-ai-financial-intelligence-frontend",
3
+ "version": "0.7.1",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev -p 3000",
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "blum-ai-financial-intelligence",
3
- "version": "0.7.0",
4
  "private": true,
5
  "scripts": {
6
  "frontend:dev": "npm --prefix frontend run dev",
 
1
  {
2
  "name": "blum-ai-financial-intelligence",
3
+ "version": "0.7.1",
4
  "private": true,
5
  "scripts": {
6
  "frontend:dev": "npm --prefix frontend run dev",
scripts/start.sh CHANGED
@@ -2,13 +2,51 @@
2
  set -euo pipefail
3
 
4
  export PORT="${PORT:-7860}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  if [[ -z "${DATABASE_URL:-}" ]]; then
7
  echo "No DATABASE_URL provided. Starting embedded PostgreSQL for the Hugging Face Docker demo."
 
8
  service postgresql start
9
  su postgres -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='blum'\" | grep -q 1 || createdb blum"
10
  su postgres -c "psql -c \"ALTER USER postgres PASSWORD 'postgres';\""
 
 
 
 
 
 
 
 
 
 
11
  export DATABASE_URL="postgresql+psycopg2://postgres:postgres@127.0.0.1:5432/blum"
 
12
  else
13
  echo "Using external PostgreSQL DATABASE_URL."
14
  fi
 
2
  set -euo pipefail
3
 
4
  export PORT="${PORT:-7860}"
5
+ export BLUM_PERSIST_DIR="${BLUM_PERSIST_DIR:-/data/blum}"
6
+ export BLUM_DB_BACKUP_SECONDS="${BLUM_DB_BACKUP_SECONDS:-300}"
7
+
8
+ backup_embedded_postgres() {
9
+ if [[ -n "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE:-}" ]]; then
10
+ mkdir -p "$(dirname "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}")" || true
11
+ if su postgres -c "pg_dump blum" > "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}.tmp"; then
12
+ mv "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}.tmp" "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}"
13
+ echo "Embedded PostgreSQL backup updated at ${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}."
14
+ else
15
+ echo "Embedded PostgreSQL backup failed; keeping previous backup if present."
16
+ rm -f "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}.tmp"
17
+ fi
18
+ fi
19
+ }
20
+
21
+ start_backup_loop() {
22
+ if [[ -n "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE:-}" ]]; then
23
+ (
24
+ while true; do
25
+ sleep "${BLUM_DB_BACKUP_SECONDS}"
26
+ backup_embedded_postgres
27
+ done
28
+ ) &
29
+ fi
30
+ }
31
 
32
  if [[ -z "${DATABASE_URL:-}" ]]; then
33
  echo "No DATABASE_URL provided. Starting embedded PostgreSQL for the Hugging Face Docker demo."
34
+ mkdir -p "${BLUM_PERSIST_DIR}" || true
35
  service postgresql start
36
  su postgres -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='blum'\" | grep -q 1 || createdb blum"
37
  su postgres -c "psql -c \"ALTER USER postgres PASSWORD 'postgres';\""
38
+ export BLUM_EMBEDDED_POSTGRES_BACKUP_FILE="${BLUM_PERSIST_DIR}/embedded_postgres_blum.sql"
39
+ TABLE_COUNT="$(su postgres -c "psql -d blum -tAc \"SELECT count(*) FROM information_schema.tables WHERE table_schema='public';\"" | tr -d '[:space:]')"
40
+ if [[ "${TABLE_COUNT:-0}" == "0" && -s "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}" ]]; then
41
+ echo "Restoring embedded PostgreSQL backup from ${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}."
42
+ su postgres -c "psql -d blum" < "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}" || echo "Backup restore failed; continuing with migrations."
43
+ elif [[ ! -s "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}" ]]; then
44
+ echo "No embedded PostgreSQL backup found yet. A new backup will be written periodically."
45
+ else
46
+ echo "Embedded PostgreSQL already contains tables; skipping restore."
47
+ fi
48
  export DATABASE_URL="postgresql+psycopg2://postgres:postgres@127.0.0.1:5432/blum"
49
+ start_backup_loop
50
  else
51
  echo "Using external PostgreSQL DATABASE_URL."
52
  fi