Add FastAPI backend with agent system
This commit is contained in:
31
backend/app/models/__init__.py
Normal file
31
backend/app/models/__init__.py
Normal 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",
|
||||
]
|
||||
28
backend/app/models/agent.py
Normal file
28
backend/app/models/agent.py
Normal 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")
|
||||
12
backend/app/models/base.py
Normal file
12
backend/app/models/base.py
Normal 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)
|
||||
26
backend/app/models/conversation.py
Normal file
26
backend/app/models/conversation.py
Normal 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")
|
||||
33
backend/app/models/document.py
Normal file
33
backend/app/models/document.py
Normal 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")
|
||||
13
backend/app/models/folder.py
Normal file
13
backend/app/models/folder.py
Normal 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)
|
||||
30
backend/app/models/forum.py
Normal file
30
backend/app/models/forum.py
Normal 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")
|
||||
32
backend/app/models/knowledge_graph.py
Normal file
32
backend/app/models/knowledge_graph.py
Normal 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")
|
||||
35
backend/app/models/memory.py
Normal file
35
backend/app/models/memory.py
Normal 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)
|
||||
45
backend/app/models/task.py
Normal file
45
backend/app/models/task.py
Normal 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")
|
||||
8
backend/app/models/test_folder.py
Normal file
8
backend/app/models/test_folder.py
Normal 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
|
||||
24
backend/app/models/todo.py
Normal file
24
backend/app/models/todo.py
Normal 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)
|
||||
15
backend/app/models/user.py
Normal file
15
backend/app/models/user.py
Normal 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) # 定时任务配置
|
||||
Reference in New Issue
Block a user