ForcePilot/src/storage/db/models.py

349 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import datetime as dt
from sqlalchemy import JSON, Column, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from src.utils.datetime_utils import coerce_datetime, utc_isoformat, utc_now
Base = declarative_base()
## Removed legacy RDBMS knowledge models (KnowledgeDatabase/KnowledgeFile/KnowledgeNode)
class Conversation(Base):
"""Conversation table - new storage system"""
__tablename__ = "conversations"
id = Column(Integer, primary_key=True, autoincrement=True, comment="Primary key")
thread_id = Column(String(64), unique=True, index=True, nullable=False, comment="Thread ID (UUID)")
user_id = Column(String(64), index=True, nullable=False, comment="User ID")
agent_id = Column(String(64), index=True, nullable=False, comment="Agent ID")
title = Column(String(255), nullable=True, comment="Conversation title")
status = Column(String(20), default="active", comment="Status: active/archived/deleted")
created_at = Column(DateTime, default=utc_now, comment="Creation time")
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, comment="Update time")
extra_metadata = Column(JSON, nullable=True, comment="Additional metadata")
# Relationships
messages = relationship("Message", back_populates="conversation", cascade="all, delete-orphan")
stats = relationship(
"ConversationStats", back_populates="conversation", uselist=False, cascade="all, delete-orphan"
)
def to_dict(self):
def format_utc_datetime(dt_value):
if dt_value is None:
return None
if dt_value.tzinfo is None:
dt_value = dt_value.replace(tzinfo=dt.UTC)
return utc_isoformat(dt_value)
return {
"id": self.id,
"thread_id": self.thread_id,
"user_id": self.user_id,
"agent_id": self.agent_id,
"title": self.title,
"status": self.status,
"created_at": format_utc_datetime(self.created_at),
"updated_at": format_utc_datetime(self.updated_at),
"metadata": self.extra_metadata or {},
}
class Message(Base):
"""Message table - stores conversation messages"""
__tablename__ = "messages"
id = Column(Integer, primary_key=True, autoincrement=True, comment="Primary key")
conversation_id = Column(
Integer, ForeignKey("conversations.id"), nullable=False, index=True, comment="Conversation ID"
)
role = Column(String(20), nullable=False, comment="Message role: user/assistant/system/tool")
content = Column(Text, nullable=False, comment="Message content")
message_type = Column(String(30), default="text", comment="Message type: text/tool_call/tool_result")
created_at = Column(DateTime, default=utc_now, comment="Creation time")
token_count = Column(Integer, nullable=True, comment="Token count (optional)")
extra_metadata = Column(JSON, nullable=True, comment="Additional metadata (complete message dump)")
# Relationships
conversation = relationship("Conversation", back_populates="messages")
tool_calls = relationship("ToolCall", back_populates="message", cascade="all, delete-orphan")
def to_dict(self):
def format_utc_datetime(dt_value):
if dt_value is None:
return None
if dt_value.tzinfo is None:
dt_value = dt_value.replace(tzinfo=dt.UTC)
return utc_isoformat(dt_value)
return {
"id": self.id,
"conversation_id": self.conversation_id,
"role": self.role,
"content": self.content,
"message_type": self.message_type,
"created_at": format_utc_datetime(self.created_at),
"token_count": self.token_count,
"metadata": self.extra_metadata or {},
"tool_calls": [tc.to_dict() for tc in self.tool_calls] if self.tool_calls else [],
}
def to_simple_dict(self):
return {
"role": self.role,
"content": self.content,
}
class ToolCall(Base):
"""ToolCall table - stores tool invocations"""
__tablename__ = "tool_calls"
id = Column(Integer, primary_key=True, autoincrement=True, comment="Primary key")
message_id = Column(Integer, ForeignKey("messages.id"), nullable=False, index=True, comment="Message ID")
langgraph_tool_call_id = Column(
String(100), nullable=True, index=True, comment="LangGraph tool_call_id for matching"
)
tool_name = Column(String(100), nullable=False, comment="Tool name")
tool_input = Column(JSON, nullable=True, comment="Tool input parameters")
tool_output = Column(Text, nullable=True, comment="Tool execution result")
status = Column(String(20), default="pending", comment="Status: pending/success/error")
error_message = Column(Text, nullable=True, comment="Error message if failed")
created_at = Column(DateTime, default=utc_now, comment="Creation time")
# Relationships
message = relationship("Message", back_populates="tool_calls")
def to_dict(self):
def format_utc_datetime(dt_value):
if dt_value is None:
return None
if dt_value.tzinfo is None:
dt_value = dt_value.replace(tzinfo=dt.UTC)
return utc_isoformat(dt_value)
return {
"id": self.id,
"message_id": self.message_id,
"langgraph_tool_call_id": self.langgraph_tool_call_id,
"tool_name": self.tool_name,
"tool_input": self.tool_input or {},
"tool_output": self.tool_output,
"status": self.status,
"error_message": self.error_message,
"created_at": format_utc_datetime(self.created_at),
}
class ConversationStats(Base):
"""ConversationStats table - stores conversation statistics"""
__tablename__ = "conversation_stats"
id = Column(Integer, primary_key=True, autoincrement=True, comment="Primary key")
conversation_id = Column(
Integer, ForeignKey("conversations.id"), unique=True, nullable=False, comment="Conversation ID"
)
message_count = Column(Integer, default=0, comment="Total message count")
total_tokens = Column(Integer, default=0, comment="Total tokens used")
model_used = Column(String(100), nullable=True, comment="Model used")
user_feedback = Column(JSON, nullable=True, comment="User feedback")
created_at = Column(DateTime, default=utc_now, comment="Creation time")
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, comment="Update time")
# Relationships
conversation = relationship("Conversation", back_populates="stats")
def to_dict(self):
def format_utc_datetime(dt_value):
if dt_value is None:
return None
if dt_value.tzinfo is None:
dt_value = dt_value.replace(tzinfo=dt.UTC)
return utc_isoformat(dt_value)
return {
"id": self.id,
"conversation_id": self.conversation_id,
"message_count": self.message_count,
"total_tokens": self.total_tokens,
"model_used": self.model_used,
"user_feedback": self.user_feedback or {},
"created_at": format_utc_datetime(self.created_at),
"updated_at": format_utc_datetime(self.updated_at),
}
class User(Base):
"""用户模型"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(String, nullable=False, unique=True, index=True) # 显示名称2-20字符
user_id = Column(String, nullable=False, unique=True, index=True) # 登录ID根据用户名生成
phone_number = Column(String, nullable=True, unique=True, index=True) # 手机号,可选登录方式
avatar = Column(String, nullable=True) # 头像URL
password_hash = Column(String, nullable=False)
role = Column(String, nullable=False, default="user") # 角色: superadmin, admin, user
created_at = Column(DateTime, default=utc_now)
last_login = Column(DateTime, nullable=True)
# 登录失败限制相关字段
login_failed_count = Column(Integer, nullable=False, default=0) # 登录失败次数
last_failed_login = Column(DateTime, nullable=True) # 最后一次登录失败时间
login_locked_until = Column(DateTime, nullable=True) # 锁定到什么时候
# 软删除相关字段
is_deleted = Column(Integer, nullable=False, default=0, index=True) # 是否已删除0=否1=是
deleted_at = Column(DateTime, nullable=True) # 删除时间
# 关联操作日志
operation_logs = relationship("OperationLog", back_populates="user", cascade="all, delete-orphan")
def to_dict(self, include_password=False):
# SQLite 存储 naive datetime需要标记为 UTC 后再转换
def format_utc_datetime(dt_value):
if dt_value is None:
return None
# 如果是 naive datetime假设它是 UTC因为代码中使用 utc_now() 存储)
if dt_value.tzinfo is None:
dt_value = dt_value.replace(tzinfo=dt.UTC)
return utc_isoformat(dt_value)
result = {
"id": self.id,
"username": self.username,
"user_id": self.user_id,
"phone_number": self.phone_number,
"avatar": self.avatar,
"role": self.role,
"created_at": format_utc_datetime(self.created_at),
"last_login": format_utc_datetime(self.last_login),
"login_failed_count": self.login_failed_count,
"last_failed_login": format_utc_datetime(self.last_failed_login),
"login_locked_until": format_utc_datetime(self.login_locked_until),
"is_deleted": self.is_deleted,
"deleted_at": format_utc_datetime(self.deleted_at),
}
if include_password:
result["password_hash"] = self.password_hash
return result
def is_login_locked(self):
"""检查用户是否处于登录锁定状态"""
lock_deadline = coerce_datetime(self.login_locked_until)
if lock_deadline is None:
return False
return utc_now() < lock_deadline
def get_remaining_lock_time(self):
"""获取剩余锁定时间(秒)"""
lock_deadline = coerce_datetime(self.login_locked_until)
if lock_deadline is None:
return 0
remaining = int((lock_deadline - utc_now()).total_seconds())
return max(0, remaining)
def calculate_lock_duration(self):
"""根据失败次数计算锁定时长(秒)"""
if self.login_failed_count < 10:
return 0
# 从第10次失败开始等待时间从1秒开始每次翻倍
wait_seconds = 2 ** (self.login_failed_count - 10)
# 最大锁定时间365天
max_seconds = 365 * 24 * 60 * 60
return min(wait_seconds, max_seconds)
def increment_failed_login(self):
"""增加登录失败次数并设置锁定时间"""
self.login_failed_count += 1
self.last_failed_login = utc_now()
lock_duration = self.calculate_lock_duration()
if lock_duration > 0:
self.login_locked_until = utc_now() + dt.timedelta(seconds=lock_duration)
def reset_failed_login(self):
"""重置登录失败相关字段"""
self.login_failed_count = 0
self.last_failed_login = None
self.login_locked_until = None
class OperationLog(Base):
"""操作日志模型"""
__tablename__ = "operation_logs"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
operation = Column(String, nullable=False)
details = Column(Text, nullable=True)
ip_address = Column(String, nullable=True)
timestamp = Column(DateTime, default=utc_now)
# 关联用户
user = relationship("User", back_populates="operation_logs")
def to_dict(self):
def format_utc_datetime(dt_value):
if dt_value is None:
return None
if dt_value.tzinfo is None:
dt_value = dt_value.replace(tzinfo=dt.UTC)
return utc_isoformat(dt_value)
return {
"id": self.id,
"user_id": self.user_id,
"operation": self.operation,
"details": self.details,
"ip_address": self.ip_address,
"timestamp": format_utc_datetime(self.timestamp),
}
class MessageFeedback(Base):
"""Message feedback table - stores user feedback on AI responses"""
__tablename__ = "message_feedbacks"
id = Column(Integer, primary_key=True, autoincrement=True, comment="Primary key")
message_id = Column(
Integer, ForeignKey("messages.id"), nullable=False, index=True, comment="Message ID being rated"
)
user_id = Column(String(64), nullable=False, index=True, comment="User ID who provided feedback")
rating = Column(String(10), nullable=False, comment="Feedback rating: like or dislike")
reason = Column(Text, nullable=True, comment="Optional reason for dislike feedback")
created_at = Column(DateTime, default=utc_now, comment="Feedback creation time")
# Relationships
message = relationship("Message", backref="feedbacks")
def to_dict(self):
def format_utc_datetime(dt_value):
if dt_value is None:
return None
if dt_value.tzinfo is None:
dt_value = dt_value.replace(tzinfo=dt.UTC)
return utc_isoformat(dt_value)
return {
"id": self.id,
"message_id": self.message_id,
"user_id": self.user_id,
"rating": self.rating,
"reason": self.reason,
"created_at": format_utc_datetime(self.created_at),
}