448 lines
19 KiB
Python
448 lines
19 KiB
Python
"""SQLAlchemy ORM models for all persistent tables.
|
|
|
|
Only auth, billing, scout config, and memory data live here.
|
|
User content (notes, tasks, etc.) lives exclusively on the client.
|
|
|
|
Table inventory:
|
|
users — account credentials + tier
|
|
refresh_tokens — hashed refresh token store
|
|
subscriptions — Stripe subscription records
|
|
local_scout_configs — per-device batch scout configs
|
|
cloud_scout_configs — OAuth-backed cloud scout configs
|
|
scout_run_logs — execution history for all scouts
|
|
memory_core — per-user persistent key/value preferences (encrypted)
|
|
memory_associative — per-user semantic memory with embeddings (encrypted)
|
|
memory_episodic — per-user session summaries (encrypted)
|
|
memory_proactive — per-user behavioral patterns (encrypted)
|
|
memory_relations — per-user entity/relation graph (Mem0g-light, Phase 3)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
|
|
from pgvector.sqlalchemy import Vector
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
DateTime,
|
|
Enum,
|
|
Float,
|
|
ForeignKey,
|
|
Integer,
|
|
JSON,
|
|
LargeBinary,
|
|
String,
|
|
Text,
|
|
Uuid,
|
|
func,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.db import Base
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _uuid() -> str:
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
def _now() -> datetime:
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
# ── Enum types ────────────────────────────────────────────────────────────
|
|
|
|
TierEnum = Enum("free", "pro", "power", "team", name="billing_tier")
|
|
AgentTypeEnum = Enum("local", "cloud", name="agent_type")
|
|
AgentStatusEnum = Enum("running", "success", "error", "partial", name="agent_run_status")
|
|
CloudProviderEnum = Enum("gmail", "teams", "outlook", name="cloud_provider")
|
|
|
|
|
|
# ── Models ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
|
name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
surname: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
password_hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
avatar_url: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
tier: Mapped[str] = mapped_column(TierEnum, nullable=False, default="free")
|
|
stripe_customer_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
# Per-user Fernet key (base64-urlsafe, 44 chars). Generated on registration.
|
|
# Used to encrypt/decrypt all memory rows for this user.
|
|
encryption_key: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
onboarding_completed_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True, default=None
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
refresh_tokens: Mapped[list[RefreshToken]] = relationship(
|
|
back_populates="user", cascade="all, delete-orphan"
|
|
)
|
|
subscription: Mapped[Subscription | None] = relationship(
|
|
back_populates="user", uselist=False, cascade="all, delete-orphan"
|
|
)
|
|
oauth_accounts: Mapped[list[OAuthAccount]] = relationship(
|
|
back_populates="user", cascade="all, delete-orphan"
|
|
)
|
|
|
|
|
|
class RefreshToken(Base):
|
|
__tablename__ = "refresh_tokens"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
token_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
|
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
user: Mapped[User] = relationship(back_populates="refresh_tokens")
|
|
|
|
|
|
class OAuthAccount(Base):
|
|
__tablename__ = "oauth_accounts"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
provider: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
provider_user_id: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
provider_email: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
user: Mapped[User] = relationship(back_populates="oauth_accounts")
|
|
|
|
|
|
class Subscription(Base):
|
|
__tablename__ = "subscriptions"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False, unique=True, index=True
|
|
)
|
|
stripe_subscription_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
|
|
tier: Mapped[str] = mapped_column(TierEnum, nullable=False, default="free")
|
|
status: Mapped[str] = mapped_column(String(50), nullable=False, default="free")
|
|
current_period_end: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
user: Mapped[User] = relationship(back_populates="subscription")
|
|
|
|
|
|
class LocalScoutConfig(Base):
|
|
__tablename__ = "local_scout_configs"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
device_id: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
directory_paths: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
|
|
data_types: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
|
|
prompt_template: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
|
scout_config: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
file_extensions: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
|
|
schedule_cron: Mapped[str] = mapped_column(String(100), nullable=False, default="0 */6 * * *")
|
|
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
last_run_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
run_logs: Mapped[list["ScoutRunLog"]] = relationship(
|
|
back_populates="local_scout",
|
|
primaryjoin="and_(ScoutRunLog.scout_id == LocalScoutConfig.id, ScoutRunLog.scout_type == 'local')",
|
|
foreign_keys="ScoutRunLog.scout_id",
|
|
cascade="all, delete-orphan",
|
|
overlaps="run_logs,cloud_scout",
|
|
)
|
|
|
|
|
|
class CloudScoutConfig(Base):
|
|
__tablename__ = "cloud_scout_configs"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
provider: Mapped[str] = mapped_column(CloudProviderEnum, nullable=False)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
data_types: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
|
|
prompt_template: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
|
oauth_token_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
filter_config: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
schedule_cron: Mapped[str] = mapped_column(String(100), nullable=False, default="0 */6 * * *")
|
|
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
last_run_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
run_logs: Mapped[list["ScoutRunLog"]] = relationship(
|
|
back_populates="cloud_scout",
|
|
primaryjoin="and_(ScoutRunLog.scout_id == CloudScoutConfig.id, ScoutRunLog.scout_type == 'cloud')",
|
|
foreign_keys="ScoutRunLog.scout_id",
|
|
cascade="all, delete-orphan",
|
|
overlaps="run_logs,local_scout",
|
|
)
|
|
|
|
|
|
class ScoutRunLog(Base):
|
|
__tablename__ = "scout_run_logs"
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), primary_key=True, default=_uuid
|
|
)
|
|
# Plain string — not a FK because it references either local_scout_configs or cloud_scout_configs
|
|
# depending on scout_type. Query by (scout_id, scout_type) to locate the source config.
|
|
scout_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
|
scout_type: Mapped[str] = mapped_column(AgentTypeEnum, nullable=False)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
status: Mapped[str] = mapped_column(AgentStatusEnum, nullable=False, default="running")
|
|
items_processed: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
items_created: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
tokens_used: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
|
errors: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
|
started_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
local_scout: Mapped["LocalScoutConfig | None"] = relationship(
|
|
back_populates="run_logs",
|
|
primaryjoin="and_(ScoutRunLog.scout_id == LocalScoutConfig.id, ScoutRunLog.scout_type == 'local')",
|
|
foreign_keys="ScoutRunLog.scout_id",
|
|
overlaps="run_logs,cloud_scout",
|
|
)
|
|
cloud_scout: Mapped["CloudScoutConfig | None"] = relationship(
|
|
back_populates="run_logs",
|
|
primaryjoin="and_(ScoutRunLog.scout_id == CloudScoutConfig.id, ScoutRunLog.scout_type == 'cloud')",
|
|
foreign_keys="ScoutRunLog.scout_id",
|
|
overlaps="run_logs,local_scout",
|
|
)
|
|
|
|
|
|
class MonthlyTokenUsage(Base):
|
|
__tablename__ = "monthly_token_usage"
|
|
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), primary_key=True
|
|
)
|
|
year_month: Mapped[str] = mapped_column(String(7), primary_key=True) # 'YYYY-MM'
|
|
feature: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
tokens_used: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
|
|
|
|
|
# ── Memory models ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
class MemoryCore(Base):
|
|
"""Per-user persistent key/value preferences, encrypted at rest.
|
|
|
|
Examples: preferred_language, timezone, work_style.
|
|
Decrypted in-memory only using User.encryption_key.
|
|
"""
|
|
|
|
__tablename__ = "memory_core"
|
|
|
|
id: Mapped[str] = mapped_column(Uuid(as_uuid=False), primary_key=True, default=_uuid)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False, index=True,
|
|
)
|
|
key: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
value_encrypted: Mapped[str] = mapped_column(Text, nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
|
|
class MemoryAssociative(Base):
|
|
"""Per-user semantic memory: encrypted content + pgvector embedding for similarity search.
|
|
|
|
Production: ``embedding`` column is ``vector(1536)`` via pgvector.
|
|
Tests (SQLite): stored as JSON list.
|
|
"""
|
|
|
|
__tablename__ = "memory_associative"
|
|
|
|
id: Mapped[str] = mapped_column(Uuid(as_uuid=False), primary_key=True, default=_uuid)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False, index=True,
|
|
)
|
|
content_encrypted: Mapped[str] = mapped_column(Text, nullable=False)
|
|
# vector(1536) via pgvector; SQLite tests use NULL embeddings so no dialect issue.
|
|
embedding: Mapped[list | None] = mapped_column(Vector(1536), nullable=True)
|
|
entity_type: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
entity_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
|
|
class MemoryEpisodic(Base):
|
|
"""Per-user session summaries, encrypted at rest.
|
|
|
|
One row per session interaction; used to recall recent conversations.
|
|
"""
|
|
|
|
__tablename__ = "memory_episodic"
|
|
|
|
id: Mapped[str] = mapped_column(Uuid(as_uuid=False), primary_key=True, default=_uuid)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False, index=True,
|
|
)
|
|
summary_encrypted: Mapped[str] = mapped_column(Text, nullable=False)
|
|
session_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
|
|
class MemoryProactive(Base):
|
|
"""Per-user inferred behavioral patterns, encrypted at rest.
|
|
|
|
Confidence in [0.0, 1.0]; only patterns above threshold are injected.
|
|
Source: 'inferred' (from episodes) or 'explicit' (user-stated).
|
|
"""
|
|
|
|
__tablename__ = "memory_proactive"
|
|
|
|
id: Mapped[str] = mapped_column(Uuid(as_uuid=False), primary_key=True, default=_uuid)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False, index=True,
|
|
)
|
|
pattern_encrypted: Mapped[str] = mapped_column(Text, nullable=False)
|
|
confidence: Mapped[float] = mapped_column(Float, nullable=False, default=0.5)
|
|
source: Mapped[str] = mapped_column(String(50), nullable=False, default="inferred")
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
|
|
class ExtractionQueue(Base):
|
|
"""Batch extraction queue for Free-tier users (Phase 2).
|
|
|
|
Pro/Power/Team users get realtime asyncio.create_task() extraction.
|
|
Free users get a queue row here; a daily cron (Phase 5) drains it.
|
|
"""
|
|
|
|
__tablename__ = "extraction_queue"
|
|
|
|
id: Mapped[str] = mapped_column(Uuid(as_uuid=False), primary_key=True, default=_uuid)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False, index=True,
|
|
)
|
|
episode_id: Mapped[str | None] = mapped_column(
|
|
Uuid(as_uuid=False), nullable=True,
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
|
|
|
|
class MemoryRelation(Base):
|
|
"""Per-user entity/relation graph row (Mem0g-light, Phase 3).
|
|
|
|
subject_label/object_label are plaintext entity identifiers (not user content).
|
|
notes_encrypted is optional Fernet-encrypted per-user commentary.
|
|
confidence in [0.0, 1.0] — decays 5 % per 30 days since last_confirmed_at.
|
|
"""
|
|
|
|
__tablename__ = "memory_relations"
|
|
|
|
id: Mapped[str] = mapped_column(Uuid(as_uuid=False), primary_key=True, default=_uuid)
|
|
user_id: Mapped[str] = mapped_column(
|
|
Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False, index=True,
|
|
)
|
|
subject_label: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
subject_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
predicate: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
object_label: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
object_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
confidence: Mapped[float] = mapped_column(Float, nullable=False, default=0.7)
|
|
source_episode_id: Mapped[str | None] = mapped_column(
|
|
Uuid(as_uuid=False),
|
|
ForeignKey("memory_episodic.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
)
|
|
notes_encrypted: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
|
)
|
|
last_confirmed_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True
|
|
)
|
|
|
|
|
|
class Plugin(Base):
|
|
"""Plugin marketplace catalog entry."""
|
|
|
|
__tablename__ = "plugins"
|
|
|
|
id: Mapped[str] = mapped_column(String(255), primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
description: Mapped[str] = mapped_column(Text, nullable=False)
|
|
version: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
author_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
category: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
price_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
permissions: Mapped[str] = mapped_column(Text, nullable=False, default="[]")
|
|
status: Mapped[str] = mapped_column(String(50), nullable=False, default="pending")
|
|
s3_package_key: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
install_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
avg_rating: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|