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,2 @@
# Schemas package - import directly from submodules
# e.g.: from app.schemas.auth import UserCreate

View File

@@ -0,0 +1,55 @@
from pydantic import BaseModel
class AgentCreate(BaseModel):
name: str
role: str
description: str | None = None
system_prompt: str
class AgentOut(BaseModel):
id: str
name: str
role: str
description: str | None
is_active: bool
is_default: bool
model_config = {"from_attributes": True}
class AgentMessageOut(BaseModel):
id: str
agent_id: str
conversation_id: str
role: str
content: str
model_config = {"from_attributes": True}
class AgentStats(BaseModel):
agent_id: str
call_count: int
current_task: str | None
status: str # active | idle | disabled
class AgentConfigUpdate(BaseModel):
name: str | None = None
description: str | None = None
system_prompt: str | None = None
enabled: bool | None = None
class AgentConfigOut(BaseModel):
id: str
name: str
role: str
description: str | None
system_prompt: str
enabled: bool
is_active: bool
model_config = {"from_attributes": True}

View File

@@ -0,0 +1,26 @@
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
email: EmailStr
password: str
full_name: str | None = None
class UserOut(BaseModel):
id: str
email: str
full_name: str | None
is_active: bool
is_superuser: bool
model_config = {"from_attributes": True}
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
class TokenPayload(BaseModel):
sub: str

View File

@@ -0,0 +1,45 @@
from pydantic import BaseModel
from datetime import datetime
class MessageCreate(BaseModel):
content: str
class MessageOut(BaseModel):
id: str
role: str
content: str
model: str | None
tokens_used: int | None
created_at: datetime
model_config = {"from_attributes": True}
class ConversationCreate(BaseModel):
title: str | None = None
class ConversationOut(BaseModel):
id: str
title: str | None
message_count: int
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
class ChatRequest(BaseModel):
message: str
conversation_id: str | None = None
agent_id: str | None = None
file_ids: list[str] = [] # 新增
class ChatResponse(BaseModel):
conversation_id: str
message_id: str
content: str
agent_name: str

View File

@@ -0,0 +1,40 @@
from pydantic import BaseModel
from datetime import datetime
class DocumentOut(BaseModel):
id: str
title: str
filename: str
file_type: str
file_size: int
summary: str | None
chunk_count: int
is_indexed: bool
created_at: datetime
model_config = {"from_attributes": True}
class DocumentChunkOut(BaseModel):
id: str
chunk_index: int
content: str
metadata_: str | None
model_config = {"from_attributes": True}
class SearchRequest(BaseModel):
query: str
top_k: int = 5
user_id: str
class SearchResult(BaseModel):
chunk_id: str
document_id: str
document_title: str
content: str
score: float
metadata_: str | None

View File

@@ -0,0 +1,39 @@
from pydantic import BaseModel, Field, field_validator
from typing import Optional, List
from datetime import datetime
class FolderCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
parent_id: Optional[str] = None
@field_validator('name')
@classmethod
def validate_name(cls, v):
forbidden = '/\\*?:'
for c in forbidden:
if c in v:
raise ValueError(f'Folder name cannot contain: {forbidden}')
return v
class FolderUpdate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
class FolderOut(BaseModel):
id: str
name: str
parent_id: Optional[str]
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
class FolderTreeOut(BaseModel):
id: str
name: str
parent_id: Optional[str]
children: List["FolderTreeOut"] = []
model_config = {"from_attributes": True}
# 递归模型需要 forward ref
FolderTreeOut.model_rebuild()

View File

@@ -0,0 +1,37 @@
from pydantic import BaseModel
class ForumPostCreate(BaseModel):
title: str
content: str
category: str | None = "discussion"
class ForumPostOut(BaseModel):
id: str
user_id: str
title: str
content: str
category: str | None
is_executed: bool
execution_result: str | None
reply_count: int
created_at: str
model_config = {"from_attributes": True}
class ForumReplyCreate(BaseModel):
content: str
class ForumReplyOut(BaseModel):
id: str
post_id: str
user_id: str | None
agent_id: str | None
content: str
is_ai_reply: bool
created_at: str
model_config = {"from_attributes": True}

View File

@@ -0,0 +1,66 @@
from pydantic import BaseModel, Field
from typing import Literal
class TagProperties(BaseModel):
tag_path: str = Field(..., description="完整标签路径,如 '编程语言/Python/异步'")
short_name: str = Field(..., description="显示名称,如 '异步'")
level: int = Field(..., ge=1, description="层级深度1为顶级")
parent_path: str | None = Field(None, description="父路径,如 '编程语言/Python'")
description: str | None = Field(None, description="AI生成的标签描述")
color: str | None = Field(None, description="标签颜色,如 '#FF5733'")
class KGNodeOut(BaseModel):
id: str
name: str
entity_type: str
description: str | None
properties_: dict | None
importance: float
created_at: str
# 新增:如果是 tag 节点,返回 tag 属性
tag_properties: TagProperties | None = None
model_config = {"from_attributes": True}
def model_post_init(self, __context):
if self.entity_type == "tag" and self.properties_:
self.tag_properties = TagProperties(**self.properties_)
class KGEdgeOut(BaseModel):
id: str
source_id: str
target_id: str
relation_type: str
weight: float
properties_: dict | None
model_config = {"from_attributes": True}
class GraphOut(BaseModel):
nodes: list[KGNodeOut]
edges: list[KGEdgeOut]
class KGBuildRequest(BaseModel):
user_id: str
document_ids: list[str] | None = None # None = 全量重建
class TagExtractRequest(BaseModel):
content: str = Field(..., min_length=10)
user_id: str
class TagExtractResponse(BaseModel):
tags: list[TagProperties]
tag_count: int
class RelatedContentRequest(BaseModel):
tag_ids: list[str]
user_id: str
limit: int = 10

View File

@@ -0,0 +1,58 @@
from pydantic import BaseModel, Field
from typing import Optional, Literal, List
from app.schemas.auth import UserOut
# LLM Provider 类型
LLMProviderType = Literal["openai", "claude", "ollama", "deepseek", "custom"]
LLMType = Literal["chat", "vlm", "embedding", "rerank"]
# 单个模型配置
class LLMModelConfig(BaseModel):
name: str = "" # 模型名称/别名,用于标识
provider: LLMProviderType = "openai"
model: str = ""
base_url: str = ""
api_key: str = ""
enabled: bool = True # 是否启用
# LLM 配置输入 - 每种类型支持多个模型
class LLMConfigIn(BaseModel):
chat: Optional[List[LLMModelConfig]] = []
vlm: Optional[List[LLMModelConfig]] = []
embedding: Optional[List[LLMModelConfig]] = []
rerank: Optional[List[LLMModelConfig]] = []
# 定时任务配置
class SchedulerConfigIn(BaseModel):
daily_plan_time: Optional[str] = "08:00"
forum_scan_interval_minutes: Optional[int] = 30
todo_ai_generate_time: Optional[str] = "08:00"
enabled: Optional[bool] = True
# 用户资料更新
class ProfileUpdateIn(BaseModel):
full_name: Optional[str] = Field(None, min_length=2, max_length=50)
password: Optional[str] = Field(None, min_length=8)
current_password: Optional[str] = None
# 完整设置输出
class SettingsOut(BaseModel):
profile: UserOut
llm_config: Optional[dict] = None
scheduler_config: Optional[dict] = None
model_config = {"from_attributes": True}
# 测试 LLM 连接请求
class LLMTestIn(BaseModel):
type: LLMType
provider: LLMProviderType
model: str
base_url: str
api_key: str

View File

@@ -0,0 +1,82 @@
from pydantic import BaseModel
from typing import Optional
# ===== System Health =====
class SystemHealth(BaseModel):
uptime_seconds: int
cpu_percent: float
memory_used_mb: float
memory_total_mb: float
memory_percent: float
disk_used_gb: float
disk_total_gb: float
disk_percent: float
active_users_24h: int
# ===== Daily Stats Base =====
class DailyStatItem(BaseModel):
date: str
count: int
class DailyTokenStatItem(BaseModel):
date: str
input_tokens: int
output_tokens: int
# ===== Conversation Stats =====
class ConversationStats(BaseModel):
daily_conversations: list[DailyStatItem]
daily_messages: list[DailyStatItem]
daily_input_tokens: list[DailyTokenStatItem]
daily_output_tokens: list[DailyTokenStatItem]
totals: dict
# ===== Knowledge Stats =====
class KnowledgeStats(BaseModel):
daily_new_tags: list[DailyStatItem]
daily_documents: list[DailyStatItem]
daily_knowledge_queries: list[DailyStatItem]
daily_tag_relations: list[DailyStatItem]
totals: dict
# ===== Kanban Stats =====
class KanbanStats(BaseModel):
daily_new_tasks: list[DailyStatItem]
daily_completed_tasks: list[DailyStatItem]
daily_completion_rate: list[DailyStatItem]
current_pending_tasks: int
totals: dict
# ===== Community Stats =====
class CommunityStats(BaseModel):
daily_posts: list[DailyStatItem]
daily_replies: list[DailyStatItem]
daily_ai_executions: list[DailyStatItem]
daily_agent_calls: list[DailyStatItem]
totals: dict
# ===== Personal Insights =====
class HourlyActivity(BaseModel):
hour: int
count: int
class TagUsage(BaseModel):
tag_path: str
usage_count: int
class PersonalInsights(BaseModel):
hourly_activity: list[HourlyActivity]
top_tags: list[TagUsage]
token_trend_percent: float
this_month_tokens: int
last_month_tokens: int

View File

@@ -0,0 +1,39 @@
from pydantic import BaseModel
from datetime import datetime
from app.models.task import TaskStatus, TaskPriority
class TaskCreate(BaseModel):
title: str
description: str | None = None
priority: TaskPriority = TaskPriority.MEDIUM
due_date: datetime | None = None
tags: list[str] | None = None
class TaskUpdate(BaseModel):
title: str | None = None
description: str | None = None
status: TaskStatus | None = None
priority: TaskPriority | None = None
due_date: datetime | None = None
tags: list[str] | None = None
class TaskOut(BaseModel):
id: str
title: str
description: str | None
status: TaskStatus
priority: TaskPriority
due_date: datetime | None
completed_at: datetime | None
tags: str | None
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
class DailyPlanRequest(BaseModel):
user_id: str

View File

@@ -0,0 +1,40 @@
from pydantic import BaseModel
from datetime import datetime
from app.models.todo import TodoSource
class TodoCreate(BaseModel):
title: str
class TodoUpdate(BaseModel):
title: str | None = None
is_completed: bool | None = None
class TodoOut(BaseModel):
id: str
title: str
is_completed: bool
source: TodoSource
source_detail: str | None
todo_date: str
completed_at: datetime | None
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
class TodoListOut(BaseModel):
items: list[TodoOut]
total: int
page: int
page_size: int
class TodoSummaryOut(BaseModel):
date: str
total: int
completed: int
pending: int