Upload signals_db.py with huggingface_hub
Browse files- signals_db.py +67 -12
signals_db.py
CHANGED
|
@@ -4,26 +4,43 @@ Stores one row per inference call and records the actual buyer outcome
|
|
| 4 |
when it becomes available — used as training labels on the next retrain.
|
| 5 |
"""
|
| 6 |
|
|
|
|
| 7 |
import sqlite3
|
|
|
|
| 8 |
from pathlib import Path
|
| 9 |
from typing import Optional
|
| 10 |
import pandas as pd
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
def get_connection(db_path: Path = DB_PATH) -> sqlite3.Connection:
|
| 16 |
-
"""Return a WAL-mode connection
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
conn.row_factory = sqlite3.Row
|
| 19 |
conn.execute("PRAGMA journal_mode=WAL")
|
| 20 |
conn.execute("PRAGMA foreign_keys=ON")
|
|
|
|
| 21 |
return conn
|
| 22 |
|
| 23 |
|
| 24 |
def init_db(db_path: Path = DB_PATH) -> None:
|
| 25 |
"""Create the prediction_log table and indexes. Idempotent."""
|
| 26 |
-
|
|
|
|
|
|
|
| 27 |
conn.executescript("""
|
| 28 |
CREATE TABLE IF NOT EXISTS prediction_log (
|
| 29 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -53,7 +70,7 @@ def log_prediction(
|
|
| 53 |
db_path: Path = DB_PATH,
|
| 54 |
) -> None:
|
| 55 |
"""Insert a new prediction entry. One row per prediction call."""
|
| 56 |
-
with get_connection(db_path) as conn:
|
| 57 |
conn.execute(
|
| 58 |
"""INSERT INTO prediction_log
|
| 59 |
(record_id, predicted_at, predicted_probability, predicted_label, predicted_roi_pct)
|
|
@@ -68,27 +85,65 @@ def record_outcome(
|
|
| 68 |
outcome_at: str,
|
| 69 |
db_path: Path = DB_PATH,
|
| 70 |
) -> int:
|
| 71 |
-
"""Update the
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
cursor = conn.execute(
|
| 75 |
"""UPDATE prediction_log
|
| 76 |
SET actual_outcome = ?, outcome_at = ?
|
| 77 |
-
WHERE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
(actual_outcome, outcome_at, record_id),
|
| 79 |
)
|
| 80 |
return cursor.rowcount
|
| 81 |
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
def get_prediction_log_df(db_path: Path = DB_PATH) -> pd.DataFrame:
|
| 84 |
"""Return the full prediction log as a DataFrame."""
|
| 85 |
-
with get_connection(db_path) as conn:
|
| 86 |
return pd.read_sql_query("SELECT * FROM prediction_log ORDER BY predicted_at DESC", conn)
|
| 87 |
|
| 88 |
|
| 89 |
def get_resolved_predictions(db_path: Path = DB_PATH) -> pd.DataFrame:
|
| 90 |
"""Return only predictions where the actual outcome is known."""
|
| 91 |
-
with get_connection(db_path) as conn:
|
| 92 |
return pd.read_sql_query(
|
| 93 |
"SELECT * FROM prediction_log WHERE actual_outcome IS NOT NULL ORDER BY outcome_at DESC",
|
| 94 |
conn,
|
|
@@ -97,7 +152,7 @@ def get_resolved_predictions(db_path: Path = DB_PATH) -> pd.DataFrame:
|
|
| 97 |
|
| 98 |
def db_summary(db_path: Path = DB_PATH) -> dict:
|
| 99 |
"""Return row count for prediction_log."""
|
| 100 |
-
with get_connection(db_path) as conn:
|
| 101 |
count = conn.execute("SELECT COUNT(*) FROM prediction_log").fetchone()[0]
|
| 102 |
return {"prediction_log": count}
|
| 103 |
|
|
|
|
| 4 |
when it becomes available — used as training labels on the next retrain.
|
| 5 |
"""
|
| 6 |
|
| 7 |
+
import os
|
| 8 |
import sqlite3
|
| 9 |
+
from contextlib import closing
|
| 10 |
from pathlib import Path
|
| 11 |
from typing import Optional
|
| 12 |
import pandas as pd
|
| 13 |
|
| 14 |
+
# Prefer a persistent-volume mount so the feedback log survives HF Space restarts.
|
| 15 |
+
# Override with SIGNALS_DB_PATH env var for local dev or alternative mount points.
|
| 16 |
+
DB_PATH = Path(os.environ.get("SIGNALS_DB_PATH", "/data/signals.db"))
|
| 17 |
|
| 18 |
|
| 19 |
def get_connection(db_path: Path = DB_PATH) -> sqlite3.Connection:
|
| 20 |
+
"""Return a WAL-mode connection.
|
| 21 |
+
|
| 22 |
+
check_same_thread=False: FastAPI runs sync handlers in a thread pool and
|
| 23 |
+
a daemon thread drives retraining — connections must be usable from any
|
| 24 |
+
thread that opens them. Each call site opens *and closes* its own
|
| 25 |
+
connection, so there is no shared-connection cross-thread hazard.
|
| 26 |
+
|
| 27 |
+
busy_timeout=5000: WAL mode handles read/write concurrency well, but a
|
| 28 |
+
brief write-lock collision (e.g. retrain thread + request thread writing
|
| 29 |
+
simultaneously) would otherwise raise OperationalError immediately.
|
| 30 |
+
"""
|
| 31 |
+
conn = sqlite3.connect(str(db_path), check_same_thread=False)
|
| 32 |
conn.row_factory = sqlite3.Row
|
| 33 |
conn.execute("PRAGMA journal_mode=WAL")
|
| 34 |
conn.execute("PRAGMA foreign_keys=ON")
|
| 35 |
+
conn.execute("PRAGMA busy_timeout=5000")
|
| 36 |
return conn
|
| 37 |
|
| 38 |
|
| 39 |
def init_db(db_path: Path = DB_PATH) -> None:
|
| 40 |
"""Create the prediction_log table and indexes. Idempotent."""
|
| 41 |
+
db_path = Path(db_path)
|
| 42 |
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
| 43 |
+
with closing(get_connection(db_path)) as conn, conn:
|
| 44 |
conn.executescript("""
|
| 45 |
CREATE TABLE IF NOT EXISTS prediction_log (
|
| 46 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
| 70 |
db_path: Path = DB_PATH,
|
| 71 |
) -> None:
|
| 72 |
"""Insert a new prediction entry. One row per prediction call."""
|
| 73 |
+
with closing(get_connection(db_path)) as conn, conn:
|
| 74 |
conn.execute(
|
| 75 |
"""INSERT INTO prediction_log
|
| 76 |
(record_id, predicted_at, predicted_probability, predicted_label, predicted_roi_pct)
|
|
|
|
| 85 |
outcome_at: str,
|
| 86 |
db_path: Path = DB_PATH,
|
| 87 |
) -> int:
|
| 88 |
+
"""Update the single most-recent unresolved prediction for record_id.
|
| 89 |
+
|
| 90 |
+
Returns 1 if a row was updated, 0 if no unresolved prediction existed.
|
| 91 |
+
Targets only the latest row so that a record scored multiple times before
|
| 92 |
+
the buyer responds does not get every prior prediction overwritten.
|
| 93 |
+
"""
|
| 94 |
+
with closing(get_connection(db_path)) as conn, conn:
|
| 95 |
cursor = conn.execute(
|
| 96 |
"""UPDATE prediction_log
|
| 97 |
SET actual_outcome = ?, outcome_at = ?
|
| 98 |
+
WHERE id = (
|
| 99 |
+
SELECT id FROM prediction_log
|
| 100 |
+
WHERE record_id = ? AND actual_outcome IS NULL
|
| 101 |
+
ORDER BY predicted_at DESC
|
| 102 |
+
LIMIT 1
|
| 103 |
+
)""",
|
| 104 |
(actual_outcome, outcome_at, record_id),
|
| 105 |
)
|
| 106 |
return cursor.rowcount
|
| 107 |
|
| 108 |
|
| 109 |
+
def record_outcomes_many(
|
| 110 |
+
outcomes: list[dict],
|
| 111 |
+
db_path: Path = DB_PATH,
|
| 112 |
+
) -> int:
|
| 113 |
+
"""Update the most-recent unresolved prediction for each record_id in one transaction.
|
| 114 |
+
|
| 115 |
+
outcomes: list of dicts with keys record_id, actual_outcome, outcome_at.
|
| 116 |
+
Returns total rows updated.
|
| 117 |
+
"""
|
| 118 |
+
if not outcomes:
|
| 119 |
+
return 0
|
| 120 |
+
with closing(get_connection(db_path)) as conn, conn:
|
| 121 |
+
total = 0
|
| 122 |
+
for o in outcomes:
|
| 123 |
+
cursor = conn.execute(
|
| 124 |
+
"""UPDATE prediction_log
|
| 125 |
+
SET actual_outcome = ?, outcome_at = ?
|
| 126 |
+
WHERE id = (
|
| 127 |
+
SELECT id FROM prediction_log
|
| 128 |
+
WHERE record_id = ? AND actual_outcome IS NULL
|
| 129 |
+
ORDER BY predicted_at DESC
|
| 130 |
+
LIMIT 1
|
| 131 |
+
)""",
|
| 132 |
+
(o["actual_outcome"], o["outcome_at"], o["record_id"]),
|
| 133 |
+
)
|
| 134 |
+
total += cursor.rowcount
|
| 135 |
+
return total
|
| 136 |
+
|
| 137 |
+
|
| 138 |
def get_prediction_log_df(db_path: Path = DB_PATH) -> pd.DataFrame:
|
| 139 |
"""Return the full prediction log as a DataFrame."""
|
| 140 |
+
with closing(get_connection(db_path)) as conn:
|
| 141 |
return pd.read_sql_query("SELECT * FROM prediction_log ORDER BY predicted_at DESC", conn)
|
| 142 |
|
| 143 |
|
| 144 |
def get_resolved_predictions(db_path: Path = DB_PATH) -> pd.DataFrame:
|
| 145 |
"""Return only predictions where the actual outcome is known."""
|
| 146 |
+
with closing(get_connection(db_path)) as conn:
|
| 147 |
return pd.read_sql_query(
|
| 148 |
"SELECT * FROM prediction_log WHERE actual_outcome IS NOT NULL ORDER BY outcome_at DESC",
|
| 149 |
conn,
|
|
|
|
| 152 |
|
| 153 |
def db_summary(db_path: Path = DB_PATH) -> dict:
|
| 154 |
"""Return row count for prediction_log."""
|
| 155 |
+
with closing(get_connection(db_path)) as conn:
|
| 156 |
count = conn.execute("SELECT COUNT(*) FROM prediction_log").fetchone()[0]
|
| 157 |
return {"prediction_log": count}
|
| 158 |
|