Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions agent/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,37 @@ def _row(r: dict) -> dict:
)
""")

# Append-only usage log for feature/command adoption reporting (see
# scripts/usage_report.py) — deliberately no event payload beyond a
# structural name/source, never message content or expense/income
# descriptions, since this is financial household data.
_run("""
CREATE TABLE IF NOT EXISTS usage_events (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
event_type TEXT NOT NULL,
event_name TEXT NOT NULL,
source TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
)
""")

_run("CREATE INDEX IF NOT EXISTS usage_events_name_idx ON usage_events (event_name)")

# Chat session history, externalized from the in-process dict it used to
# live in (agent/main.py's old _sessions) — that dict silently dropped every
# in-progress conversation on every redeploy/restart, since nothing survived
# process memory. One row per user; the whole message list is replaced on
# every save rather than appended in SQL, since the caller always has the
# full up-to-date list in hand already.
_run("""
CREATE TABLE IF NOT EXISTS chat_sessions (
user_id INTEGER PRIMARY KEY REFERENCES users(id),
messages JSONB NOT NULL DEFAULT '[]',
updated_at TIMESTAMPTZ DEFAULT NOW()
)
""")


def get_user_by_username(username: str) -> dict | None:
cur = _run("SELECT * FROM users WHERE username = %s", (username,))
Expand Down Expand Up @@ -898,3 +929,61 @@ def increment_api_call_count(user_id: int, date: str) -> None:
""",
(user_id, date),
)


def record_usage(user_id: int | None, event_type: str, event_name: str, source: str | None = None) -> None:
"""Best-effort usage logging — never let a logging failure break the
actual request it's attached to (an edit/delete/tool-call succeeding
is what matters; the analytics record is secondary)."""
try:
_run(
"INSERT INTO usage_events (user_id, event_type, event_name, source) VALUES (%s, %s, %s, %s)",
(user_id, event_type, event_name, source),
)
except Exception:
pass


def load_chat_session(user_id: int) -> list:
cur = _run("SELECT messages FROM chat_sessions WHERE user_id = %s", (user_id,))
row = cur.fetchone()
return row["messages"] if row else []


def save_chat_session(user_id: int, messages: list) -> None:
_run(
"""
INSERT INTO chat_sessions (user_id, messages, updated_at) VALUES (%s, %s, NOW())
ON CONFLICT (user_id) DO UPDATE SET messages = EXCLUDED.messages, updated_at = NOW()
""",
(user_id, psycopg2.extras.Json(messages)),
)


def clear_chat_session(user_id: int) -> None:
_run("DELETE FROM chat_sessions WHERE user_id = %s", (user_id,))


def get_usage_summary(since: str | None = None) -> list[dict]:
"""Event counts grouped by type/name/source, for scripts/usage_report.py."""
if since:
cur = _run(
"""
SELECT event_type, event_name, source, COUNT(*) AS count
FROM usage_events
WHERE created_at >= %s
GROUP BY event_type, event_name, source
ORDER BY count DESC
""",
(since,),
)
else:
cur = _run(
"""
SELECT event_type, event_name, source, COUNT(*) AS count
FROM usage_events
GROUP BY event_type, event_name, source
ORDER BY count DESC
"""
)
return [dict(r) for r in cur.fetchall()]
Loading