Skip to content

Commit 3a88449

Browse files
authored
Create entities.py
1 parent 460afa0 commit 3a88449

1 file changed

Lines changed: 211 additions & 0 deletions

File tree

‎app/models/entities.py‎

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
"""SQLAlchemy ORM entity definitions (re-exported from the package __init__)."""
2+
3+
from __future__ import annotations
4+
5+
import uuid
6+
from datetime import UTC, datetime
7+
8+
from sqlalchemy import (
9+
JSON,
10+
Boolean,
11+
DateTime,
12+
ForeignKey,
13+
Index,
14+
Integer,
15+
String,
16+
Text,
17+
text,
18+
)
19+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
20+
21+
22+
def utcnow() -> datetime:
23+
return datetime.now(UTC)
24+
25+
26+
def new_id(prefix: str) -> str:
27+
return f"{prefix}_{uuid.uuid4().hex}"
28+
29+
30+
class Base(DeclarativeBase):
31+
pass
32+
33+
34+
class User(Base):
35+
__tablename__ = "users"
36+
37+
id: Mapped[str] = mapped_column(String(40), primary_key=True)
38+
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
39+
password_hash: Mapped[str] = mapped_column(String(255))
40+
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
41+
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
42+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
43+
44+
# Bumped to revoke every outstanding token for this user (logout,
45+
# password change). Tokens carry the version they were minted with
46+
# as a ``ver`` claim; a mismatch is rejected.
47+
token_version: Mapped[int] = mapped_column(Integer, default=0)
48+
49+
# Account / credits. plan is a tier name ("free", ...); phone is
50+
# optional (null = 未绑定). Points are split into subscription
51+
# points (plan_points) and top-up packs (pack_points); the total
52+
# shown to the user is their sum, so it is never stored.
53+
# points_expire_at is null when there is no expiry (无有效期).
54+
plan: Mapped[str] = mapped_column(String(32), default="free")
55+
phone: Mapped[str | None] = mapped_column(String(32), nullable=True)
56+
plan_points: Mapped[int] = mapped_column(Integer, default=0)
57+
pack_points: Mapped[int] = mapped_column(Integer, default=0)
58+
points_expire_at: Mapped[datetime | None] = mapped_column(
59+
DateTime(timezone=True), nullable=True
60+
)
61+
62+
conversations: Mapped[list[Conversation]] = relationship(back_populates="user")
63+
usage_events: Mapped[list[UsageEvent]] = relationship(back_populates="user")
64+
65+
66+
class Conversation(Base):
67+
__tablename__ = "conversations"
68+
69+
id: Mapped[str] = mapped_column(String(40), primary_key=True)
70+
user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), index=True)
71+
title: Mapped[str] = mapped_column(String(255), default="New conversation")
72+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
73+
updated_at: Mapped[datetime] = mapped_column(
74+
DateTime(timezone=True), default=utcnow, onupdate=utcnow
75+
)
76+
77+
user: Mapped[User] = relationship(back_populates="conversations")
78+
runs: Mapped[list[Run]] = relationship(
79+
back_populates="conversation",
80+
order_by="Run.created_at",
81+
cascade="all, delete-orphan",
82+
)
83+
84+
85+
class Run(Base):
86+
"""One harness exec: a single prompt through the agent.
87+
88+
``status``: queued -> running -> done | error | cancelled
89+
``events`` holds every harness JSON line (result last) so the
90+
conversation can be replayed; ``answer`` is the final text.
91+
"""
92+
93+
__tablename__ = "runs"
94+
95+
# At most one *running* run per conversation, enforced by the DB (a
96+
# partial unique index) so the guard survives the check-then-insert
97+
# race in start_run. Finished runs (done/error/cancelled) are not
98+
# indexed, so a conversation can start a new run once its last one
99+
# ends.
100+
__table_args__ = (
101+
Index(
102+
"uq_one_running_run_per_conversation",
103+
"conversation_id",
104+
unique=True,
105+
sqlite_where=text("status = 'running'"),
106+
postgresql_where=text("status = 'running'"),
107+
),
108+
)
109+
110+
id: Mapped[str] = mapped_column(String(40), primary_key=True)
111+
conversation_id: Mapped[str] = mapped_column(ForeignKey("conversations.id"), index=True)
112+
prompt: Mapped[str] = mapped_column(Text)
113+
status: Mapped[str] = mapped_column(String(16), default="queued", index=True)
114+
answer: Mapped[str] = mapped_column(Text, default="")
115+
events: Mapped[list] = mapped_column(JSON, default=list)
116+
error: Mapped[str] = mapped_column(Text, default="")
117+
harness_run_id: Mapped[str] = mapped_column(String(64), default="")
118+
exit_code: Mapped[int | None] = mapped_column(Integer, nullable=True)
119+
cancelled: Mapped[bool] = mapped_column(Boolean, default=False)
120+
duration_ms: Mapped[int] = mapped_column(Integer, default=0)
121+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
122+
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
123+
124+
conversation: Mapped[Conversation] = relationship(back_populates="runs")
125+
126+
127+
class UsageEvent(Base):
128+
"""Token ledger row, attributed per user/conversation/run."""
129+
130+
__tablename__ = "usage_events"
131+
132+
id: Mapped[str] = mapped_column(String(40), primary_key=True)
133+
user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), index=True)
134+
conversation_id: Mapped[str] = mapped_column(String(40), index=True)
135+
run_id: Mapped[str] = mapped_column(String(40), index=True)
136+
input_tokens: Mapped[int] = mapped_column(Integer, default=0)
137+
output_tokens: Mapped[int] = mapped_column(Integer, default=0)
138+
rounds: Mapped[int] = mapped_column(Integer, default=0)
139+
model: Mapped[str] = mapped_column(String(120), default="")
140+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
141+
142+
user: Mapped[User] = relationship(back_populates="usage_events")
143+
144+
145+
class Secret(Base):
146+
"""User secrets, Fernet-encrypted at rest; values never leave the
147+
API as plaintext (write-only from the client's perspective)."""
148+
149+
__tablename__ = "secrets"
150+
151+
id: Mapped[str] = mapped_column(String(40), primary_key=True)
152+
user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), index=True)
153+
name: Mapped[str] = mapped_column(String(120))
154+
value_encrypted: Mapped[str] = mapped_column(Text)
155+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
156+
157+
158+
class ConversationFile(Base):
159+
"""An uploaded file attached to a conversation.
160+
161+
Stored on disk inside the conversation's sandbox workspace so the
162+
agent can read it directly; ``path`` is the absolute location.
163+
"""
164+
165+
__tablename__ = "conversation_files"
166+
167+
id: Mapped[str] = mapped_column(String(40), primary_key=True)
168+
conversation_id: Mapped[str] = mapped_column(ForeignKey("conversations.id"), index=True)
169+
user_id: Mapped[str] = mapped_column(String(40), index=True)
170+
filename: Mapped[str] = mapped_column(String(255))
171+
stored_name: Mapped[str] = mapped_column(String(255))
172+
size: Mapped[int] = mapped_column(Integer, default=0)
173+
path: Mapped[str] = mapped_column(String(1024))
174+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
175+
176+
conversation: Mapped[Conversation] = relationship()
177+
178+
179+
class Sandbox(Base):
180+
"""Sandbox registry: one row per live (or recently live) sandbox.
181+
182+
``runner_id`` names the backend that owns it (``server`` today);
183+
``last_used_at`` drives the idle reaper.
184+
"""
185+
186+
__tablename__ = "sandboxes"
187+
188+
id: Mapped[str] = mapped_column(String(40), primary_key=True)
189+
runner_id: Mapped[str] = mapped_column(String(32), default="server")
190+
user_id: Mapped[str] = mapped_column(String(40), index=True)
191+
conversation_id: Mapped[str] = mapped_column(String(40), default="")
192+
spec: Mapped[dict] = mapped_column(JSON, default=dict)
193+
status: Mapped[str] = mapped_column(String(16), default="running", index=True)
194+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
195+
last_used_at: Mapped[datetime] = mapped_column(
196+
DateTime(timezone=True), default=utcnow, onupdate=utcnow
197+
)
198+
199+
200+
__all__ = [
201+
"Base",
202+
"Conversation",
203+
"ConversationFile",
204+
"Run",
205+
"Sandbox",
206+
"Secret",
207+
"UsageEvent",
208+
"User",
209+
"new_id",
210+
"utcnow",
211+
]

0 commit comments

Comments
 (0)