Add FastAPI backend with agent system

This commit is contained in:
2026-03-21 10:13:29 +08:00
parent ed6bab59fe
commit 6ffa07adde
82 changed files with 11138 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
from app.models.base import Base
from app.models.user import User
from app.models.document import Document, DocumentChunk
from app.models.task import Task, TaskHistory
from app.models.forum import ForumPost, ForumReply
from app.models.agent import Agent, AgentMessage
from app.models.conversation import Conversation, Message
from app.models.knowledge_graph import KGNode, KGEdge
from app.models.memory import MemorySummary, UserMemory
from app.models.todo import DailyTodo, TodoSource
__all__ = [
"Base",
"User",
"Document",
"DocumentChunk",
"Task",
"TaskHistory",
"ForumPost",
"ForumReply",
"Agent",
"AgentMessage",
"Conversation",
"Message",
"KGNode",
"KGEdge",
"MemorySummary",
"UserMemory",
"DailyTodo",
"TodoSource",
]

View File

@@ -0,0 +1,28 @@
from sqlalchemy import Column, String, Text, Boolean, ForeignKey, Integer
from sqlalchemy.orm import relationship
from app.models.base import BaseModel
class Agent(BaseModel):
__tablename__ = "agents"
name = Column(String(100), nullable=False)
role = Column(String(100), nullable=False) # master, planner, executor, librarian, analyst
description = Column(Text, nullable=True)
system_prompt = Column(Text, nullable=False)
is_active = Column(Boolean, default=True)
is_default = Column(Boolean, default=False)
messages = relationship("AgentMessage", back_populates="agent", cascade="all, delete-orphan")
replies = relationship("ForumReply", back_populates="agent")
class AgentMessage(BaseModel):
__tablename__ = "agent_messages"
agent_id = Column(String(36), ForeignKey("agents.id"), nullable=False, index=True)
conversation_id = Column(String(36), ForeignKey("conversations.id"), nullable=False, index=True)
role = Column(String(20), nullable=False) # system, user, assistant
content = Column(Text, nullable=False)
agent = relationship("Agent", back_populates="messages")

View File

@@ -0,0 +1,12 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, DateTime
from app.database import Base
class BaseModel(Base):
__abstract__ = True
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)

View File

@@ -0,0 +1,26 @@
from sqlalchemy import Column, String, Text, Integer, ForeignKey, JSON
from sqlalchemy.orm import relationship
from app.models.base import BaseModel
class Conversation(BaseModel):
__tablename__ = "conversations"
user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
title = Column(String(500), nullable=True)
message_count = Column(Integer, default=0)
messages = relationship("Message", back_populates="conversation", cascade="all, delete-orphan")
class Message(BaseModel):
__tablename__ = "messages"
conversation_id = Column(String(36), ForeignKey("conversations.id"), nullable=False, index=True)
role = Column(String(20), nullable=False) # user, assistant, system
content = Column(Text, nullable=False)
model = Column(String(100), nullable=True)
tokens_used = Column(Integer, nullable=True)
attachments = Column(JSON, nullable=True) # 新增: [{file_id, filename, file_type, file_size}]
conversation = relationship("Conversation", back_populates="messages")

View File

@@ -0,0 +1,33 @@
from sqlalchemy import Column, String, Integer, Text, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from app.models.base import BaseModel
class Document(BaseModel):
__tablename__ = "documents"
user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
title = Column(String(500), nullable=False)
filename = Column(String(500), nullable=False)
file_type = Column(String(50), nullable=False) # pdf, md, txt, docx
file_size = Column(Integer, nullable=False)
file_path = Column(String(1000), nullable=False)
folder_id = Column(String(36), ForeignKey("folders.id"), nullable=True) # 新增
summary = Column(Text, nullable=True)
chunk_count = Column(Integer, default=0)
is_indexed = Column(Boolean, default=False)
chunks = relationship("DocumentChunk", back_populates="document", cascade="all, delete-orphan")
class DocumentChunk(BaseModel):
__tablename__ = "document_chunks"
document_id = Column(String(36), ForeignKey("documents.id"), nullable=False, index=True)
chunk_index = Column(Integer, nullable=False)
content = Column(Text, nullable=False)
metadata_ = Column(String(2000), nullable=True) # JSON 存储元数据
chroma_collection = Column(String(255), nullable=True)
chroma_id = Column(String(255), nullable=True)
document = relationship("Document", back_populates="chunks")

View File

@@ -0,0 +1,13 @@
from sqlalchemy import Column, String, ForeignKey, UniqueConstraint
from app.models.base import BaseModel
class Folder(BaseModel):
__tablename__ = "folders"
__table_args__ = (
UniqueConstraint('user_id', 'parent_id', 'name', name='uq_user_parent_name'),
)
user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
name = Column(String(255), nullable=False)
parent_id = Column(String(36), ForeignKey("folders.id"), nullable=True)

View File

@@ -0,0 +1,30 @@
from sqlalchemy import Column, String, Text, Integer, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from app.models.base import BaseModel
class ForumPost(BaseModel):
__tablename__ = "forum_posts"
user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
title = Column(String(500), nullable=False)
content = Column(Text, nullable=False)
category = Column(String(100), nullable=True) # instruction, discussion, question
is_executed = Column(Boolean, default=False)
execution_result = Column(Text, nullable=True)
reply_count = Column(Integer, default=0)
replies = relationship("ForumReply", back_populates="post", cascade="all, delete-orphan")
class ForumReply(BaseModel):
__tablename__ = "forum_replies"
post_id = Column(String(36), ForeignKey("forum_posts.id"), nullable=False, index=True)
user_id = Column(String(36), ForeignKey("users.id"), nullable=True)
agent_id = Column(String(36), ForeignKey("agents.id"), nullable=True)
content = Column(Text, nullable=False)
is_ai_reply = Column(Boolean, default=False)
post = relationship("ForumPost", back_populates="replies")
agent = relationship("Agent", back_populates="replies")

View File

@@ -0,0 +1,32 @@
from sqlalchemy import Column, String, Text, Integer, Float, ForeignKey, JSON
from sqlalchemy.orm import relationship
from app.models.base import BaseModel
class KGNode(BaseModel):
__tablename__ = "kg_nodes"
user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
name = Column(String(500), nullable=False)
entity_type = Column(String(100), nullable=False) # person, concept, task, document, chunk, tag
description = Column(Text, nullable=True)
properties_ = Column(JSON, nullable=True) # 额外属性
source_document_id = Column(String(36), ForeignKey("documents.id"), nullable=True)
importance = Column(Float, default=0.5) # 重要性 0-1
last_updated_by = Column(String(36), nullable=True) # 哪个 agent 更新过
outgoing_edges = relationship("KGEdge", foreign_keys="KGEdge.source_id", back_populates="source_node", cascade="all, delete-orphan")
incoming_edges = relationship("KGEdge", foreign_keys="KGEdge.target_id", back_populates="target_node", cascade="all, delete-orphan")
class KGEdge(BaseModel):
__tablename__ = "kg_edges"
source_id = Column(String(36), ForeignKey("kg_nodes.id"), nullable=False, index=True)
target_id = Column(String(36), ForeignKey("kg_nodes.id"), nullable=False, index=True)
relation_type = Column(String(100), nullable=False) # related_to, part_of, caused_by, depends_on, etc.
weight = Column(Float, default=0.5) # 关系强度 0-1
properties_ = Column(JSON, nullable=True)
source_node = relationship("KGNode", foreign_keys=[source_id], back_populates="outgoing_edges")
target_node = relationship("KGNode", foreign_keys=[target_id], back_populates="incoming_edges")

View File

@@ -0,0 +1,35 @@
from sqlalchemy import Column, String, Text, Integer, ForeignKey, Boolean, DateTime, Enum as SQLEnum
from datetime import datetime
from app.models.base import BaseModel
class MemorySummary(BaseModel):
"""
对话摘要 — 中期记忆
当一段对话超过阈值轮数时,自动生成摘要存入此表
"""
__tablename__ = "memory_summaries"
user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
conversation_id = Column(String(36), ForeignKey("conversations.id"), nullable=False, index=True)
summary_text = Column(Text, nullable=False) # 摘要内容
turn_count = Column(Integer, default=0) # 摘要时累计轮数
summary_at = Column(DateTime, default=datetime.utcnow, nullable=False)
class UserMemory(BaseModel):
"""
用户画像记忆 — 长期记忆
从对话中提取的用户事实、偏好、目标
"""
__tablename__ = "user_memories"
user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
memory_type = Column(String(50), nullable=False) # fact | preference | goal | habit | other
content = Column(Text, nullable=False) # 记忆内容
importance = Column(Integer, default=5) # 重要程度 1-10
is_recalled = Column(Boolean, default=False) # 是否在当前对话中被召回
recall_count = Column(Integer, default=0) # 被召回次数
source_conversation_id = Column(String(36), nullable=True) # 来源对话
extracted_at = Column(DateTime, default=datetime.utcnow, nullable=False)
last_recalled_at = Column(DateTime, nullable=True)

View File

@@ -0,0 +1,45 @@
from sqlalchemy import Column, String, Text, Integer, ForeignKey, DateTime, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
from enum import Enum as PyEnum
from app.models.base import BaseModel
class TaskStatus(str, PyEnum):
TODO = "todo"
IN_PROGRESS = "in_progress"
DONE = "done"
CANCELLED = "cancelled"
class TaskPriority(str, PyEnum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
URGENT = "urgent"
class Task(BaseModel):
__tablename__ = "tasks"
user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
title = Column(String(500), nullable=False)
description = Column(Text, nullable=True)
status = Column(Enum(TaskStatus), default=TaskStatus.TODO, nullable=False, index=True)
priority = Column(Enum(TaskPriority), default=TaskPriority.MEDIUM, nullable=False)
due_date = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
tags = Column(String(1000), nullable=True) # JSON 数组
history = relationship("TaskHistory", back_populates="task", cascade="all, delete-orphan")
class TaskHistory(BaseModel):
__tablename__ = "task_histories"
task_id = Column(String(36), ForeignKey("tasks.id"), nullable=False, index=True)
action = Column(String(100), nullable=False) # created, status_changed, updated, deleted
old_value = Column(Text, nullable=True)
new_value = Column(Text, nullable=True)
task = relationship("Task", back_populates="history")

View File

@@ -0,0 +1,8 @@
import pytest
from app.models.folder import Folder
def test_folder_model_creation():
folder = Folder(user_id="test-user", name="Test Folder")
assert folder.name == "Test Folder"
assert folder.parent_id is None

View File

@@ -0,0 +1,24 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Boolean, DateTime, Enum
from enum import Enum as PyEnum
from app.models.base import BaseModel
class TodoSource(str, PyEnum):
AI_KANBAN = "ai_kanban"
AI_CHAT = "ai_chat"
MANUAL = "manual"
class DailyTodo(BaseModel):
__tablename__ = "daily_todos"
user_id = Column(String(36), nullable=False, index=True)
title = Column(String(500), nullable=False)
is_completed = Column(Boolean, default=False, nullable=False)
source = Column(Enum(TodoSource), default=TodoSource.MANUAL, nullable=False)
source_detail = Column(String(500), nullable=True)
source_ref_id = Column(String(36), nullable=True)
todo_date = Column(String(10), nullable=False) # YYYY-MM-DD
completed_at = Column(DateTime, nullable=True)

View File

@@ -0,0 +1,15 @@
from sqlalchemy import Column, String, Boolean, JSON
from app.models.base import BaseModel
class User(BaseModel):
__tablename__ = "users"
email = Column(String(255), unique=True, nullable=False, index=True)
hashed_password = Column(String(255), nullable=False)
full_name = Column(String(255), nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
is_superuser = Column(Boolean, default=False, nullable=False)
# 用户级配置
llm_config = Column(JSON, nullable=True) # LLM 模型配置
scheduler_config = Column(JSON, nullable=True) # 定时任务配置