diff --git a/chat/api/v1/ai_chat.py b/chat/api/v1/ai_chat.py index 6dc5e16..106810c 100644 --- a/chat/api/v1/ai_chat.py +++ b/chat/api/v1/ai_chat.py @@ -11,7 +11,6 @@ class ChatRequest(BaseModel): prompt: str - @router.post("/") def chat_api(data: ChatRequest, user=Depends(get_current_user)): # return {"msg": "pong"} diff --git a/chat/crud/ai_api_key.py b/chat/crud/ai_api_key.py new file mode 100644 index 0000000..1525d3d --- /dev/null +++ b/chat/crud/ai_api_key.py @@ -0,0 +1,18 @@ +from fastapi import HTTPException +from sqlalchemy.orm import Session + +from crud.base import CRUDBase +from models.ai import AIApiKey # SQLAlchemy模型 +from schemas.ai_api_key import AIApiKeyCreate, AIApiKeyUpdate + +# 继承通用CRUD基类,指定模型和Pydantic类型 +class CRUDApiKey(CRUDBase[AIApiKey, AIApiKeyCreate, AIApiKeyUpdate]): + # 如有特殊逻辑,可重写父类方法(如创建时验证平台唯一性) + def create(self, db: Session, *, obj_in: AIApiKeyCreate): + # 示例:验证平台+名称唯一 + if self.get_by(db, platform=obj_in.platform, name=obj_in.name): + raise HTTPException(status_code=400, detail="该平台下名称已存在") + return super().create(db, obj_in=obj_in) + +# 创建CRUD实例 +ai_api_key_crud = CRUDApiKey(AIApiKey) \ No newline at end of file diff --git a/chat/crud/base.py b/chat/crud/base.py new file mode 100644 index 0000000..b28212e --- /dev/null +++ b/chat/crud/base.py @@ -0,0 +1,90 @@ +from typing import Generic, TypeVar, List, Optional, Dict, Any +from fastapi import HTTPException +from sqlalchemy.orm import Session +from datetime import datetime + +# 定义泛型变量(分别对应:SQLAlchemy模型、创建Pydantic模型、更新Pydantic模型) +ModelType = TypeVar("ModelType") +CreateSchemaType = TypeVar("CreateSchemaType") +UpdateSchemaType = TypeVar("UpdateSchemaType") + + +class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): + def __init__(self, model: ModelType): + """ + 初始化CRUD类,需要传入SQLAlchemy模型 + :param model: SQLAlchemy模型类(如AIApiKey、AIModel等) + """ + self.model = model + + # 创建 + def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: + """创建一条记录""" + obj_in_data = obj_in.model_dump() # 解构Pydantic模型为字典 + + # 自动填充时间字段(如果模型有created_at/updated_at) + if hasattr(self.model, "created_at"): + obj_in_data["created_at"] = datetime.now() + if hasattr(self.model, "updated_at"): + obj_in_data["updated_at"] = datetime.now() + + db_obj = self.model(**obj_in_data) # 实例化模型 + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + # 按ID查询 + def get(self, db: Session, id: int) -> Optional[ModelType]: + """按ID查询单条记录""" + return db.query(self.model).filter(self.model.id == id).first() + + # 按条件查询单条记录 + def get_by(self, db: Session, **kwargs) -> Optional[ModelType]: + """按条件查询单条记录(如get_by(name="test"))""" + return db.query(self.model).filter_by(**kwargs).first() + + # 分页查询所有 + def get_multi( + self, db: Session, *, page: int = 0, limit: int = 100 + ) -> List[ModelType]: + """分页查询多条记录""" + return db.query(self.model).offset(page).limit(limit).all() + + # 更新 + def update( + self, + db: Session, + *, + db_obj: ModelType, + obj_in: UpdateSchemaType | Dict[str, Any] + ) -> ModelType: + """更新记录(支持Pydantic模型或字典)""" + if isinstance(obj_in, dict): + update_data = obj_in + else: + update_data = obj_in.model_dump(exclude_unset=True) # 只更新提供的字段 + + # 遍历更新字段 + for field in update_data: + if hasattr(db_obj, field): + setattr(db_obj, field, update_data[field]) + + # 自动更新updated_at(如果模型有该字段) + if hasattr(db_obj, "updated_at"): + db_obj.updated_at = datetime.now() + + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + # 删除 + def remove(self, db: Session, *, id: int) -> ModelType: + """删除记录""" + obj = db.query(self.model).get(id) + if not obj: + raise HTTPException(status_code=404, detail=f"{self.model.__name__}不存在") + db.delete(obj) + db.commit() + return obj \ No newline at end of file diff --git a/chat/main.py b/chat/main.py index 41989f4..d1dce16 100644 --- a/chat/main.py +++ b/chat/main.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from api.v1 import ai_chat from fastapi.middleware.cors import CORSMiddleware +from routers.ai_api_key import router as ai_api_key_router app = FastAPI() @@ -19,6 +20,7 @@ app.add_middleware( # 注册路由 app.include_router(ai_chat.router, prefix="/chat/api/v1", tags=["chat"]) +app.include_router(ai_api_key_router, tags=["chat"]) # 健康检查 @app.get("/ping") diff --git a/chat/models/ai.py b/chat/models/ai.py new file mode 100644 index 0000000..88b1f3b --- /dev/null +++ b/chat/models/ai.py @@ -0,0 +1,243 @@ +from sqlalchemy import ( + Column, Integer, String, Text, DateTime, Boolean, Float, ForeignKey +) +from sqlalchemy.orm import relationship, declarative_base +from models.user import DjangoUser # 确保导入 DjangoUser + +Base = declarative_base() + + +# 状态选择类(示例) +class CommonStatus: + DISABLED = 0 + ENABLED = 1 + + @staticmethod + def choices(): + return [(0, '禁用'), (1, '启用')] + + +# 平台选择类(示例) +class PlatformChoices: + OPENAI = 'openai' + ALIMNS = 'alimns' + + @staticmethod + def choices(): + return [('openai', 'OpenAI'), ('alimns', '阿里云MNS')] + + +# 消息类型选择类(示例) +class MessageType: + TEXT = 'text' + IMAGE = 'image' + + @staticmethod + def choices(): + return [('text', '文本'), ('image', '图片')] + + +# 基础模型类 +class CoreModel(Base): + __abstract__ = True + + id = Column(Integer, primary_key=True, autoincrement=True) + create_time = Column(DateTime) + update_time = Column(DateTime) + is_deleted = Column(Boolean, default=False) + + +# AI API 密钥表 +class AIApiKey(CoreModel): + __tablename__ = 'ai_api_key' + + name = Column(String(255), nullable=False) + platform = Column(String(100), nullable=False) + api_key = Column(String(255), nullable=False) + url = Column(String(255), nullable=True) + status = Column(Integer, default=CommonStatus.DISABLED) + + def __str__(self): + return self.name + + +# AI 模型表 +class AIModel(CoreModel): + __tablename__ = 'ai_model' + + name = Column(String(64), nullable=False) + sort = Column(Integer, default=0) + status = Column(Integer, default=CommonStatus.DISABLED) + key_id = Column(Integer, ForeignKey('ai_api_key.id'), nullable=False) + model_type = Column(String(32), nullable=True) + platform = Column(String(32), nullable=False) + model = Column(String(64), nullable=False) + temperature = Column(Float, nullable=True) + max_tokens = Column(Integer, nullable=True) + max_contexts = Column(Integer, nullable=True) + + key = relationship('AIApiKey', backref='models') + + def __str__(self): + return self.name + + +# AI 工具表 +# class Tool(CoreModel): +# __tablename__ = 'ai_tool' + +# name = Column(String(128), nullable=False) +# description = Column(String(256), nullable=True) +# status = Column(Integer, default=0) + +# def __str__(self): +# return self.name + + +# AI 知识库表 +# class Knowledge(CoreModel): +# __tablename__ = 'ai_knowledge' + +# name = Column(String(255), nullable=False) +# description = Column(Text, nullable=True) +# embedding_model_id = Column(Integer, ForeignKey('ai_model.id'), nullable=False) +# embedding_model = Column(String(32), nullable=False) +# top_k = Column(Integer, default=0) +# similarity_threshold = Column(Float, nullable=False) +# status = Column(Integer, default=CommonStatus.DISABLED) + +# embedding_model_rel = relationship('AIModel', backref='knowledges') +# documents = relationship('KnowledgeDocument', backref='knowledge', cascade='all, delete-orphan') +# segments = relationship('KnowledgeSegment', backref='knowledge', cascade='all, delete-orphan') +# roles = relationship('ChatRole', secondary='ai_chat_role_knowledge', backref='knowledges') + +# def __str__(self): +# return self.name + + +# AI 知识库文档表 +# class KnowledgeDocument(CoreModel): +# __tablename__ = 'ai_knowledge_document' + +# knowledge_id = Column(Integer, ForeignKey('ai_knowledge.id'), nullable=False) +# name = Column(String(255), nullable=False) +# url = Column(String(1024), nullable=False) +# content = Column(Text, nullable=False) +# content_length = Column(Integer, nullable=False) +# tokens = Column(Integer, nullable=False) +# segment_max_tokens = Column(Integer, nullable=False) +# retrieval_count = Column(Integer, default=0) +# status = Column(Integer, default=CommonStatus.DISABLED) + +# segments = relationship('KnowledgeSegment', backref='document', cascade='all, delete-orphan') + +# def __str__(self): +# return self.name + + +# AI 知识库分段表 +# class KnowledgeSegment(CoreModel): +# __tablename__ = 'ai_knowledge_segment' + +# knowledge_id = Column(Integer, ForeignKey('ai_knowledge.id'), nullable=False) +# document_id = Column(Integer, ForeignKey('ai_knowledge_document.id'), nullable=False) +# content = Column(Text, nullable=False) +# content_length = Column(Integer, nullable=False) +# tokens = Column(Integer, nullable=False) +# vector_id = Column(String(100), nullable=True) +# retrieval_count = Column(Integer, default=0) +# status = Column(Integer, default=CommonStatus.DISABLED) + +# def __str__(self): +# return f"Segment {self.id}" + + +# AI 聊天角色表 +# class ChatRole(CoreModel): +# __tablename__ = 'ai_chat_role' + +# name = Column(String(128), nullable=False) +# avatar = Column(String(256), nullable=False) +# description = Column(String(256), nullable=True) +# status = Column(Integer, default=CommonStatus.DISABLED) +# sort = Column(Integer, default=0) +# public_status = Column(Boolean, default=False) +# category = Column(String(32), nullable=True) +# model_id = Column(Integer, ForeignKey('ai_model.id'), nullable=False) +# system_message = Column(String(1024), nullable=True) +# user_id = Column( +# Integer, +# ForeignKey('system_users.id'), # 假设DjangoUser表名是system_users +# nullable=True # 允许为空(如匿名角色) +# ) +# user = relationship(DjangoUser, backref='chat_roles') # 正确:DjangoUser 已定义并导入 + +# model = relationship('AIModel', backref='chat_roles') +# tools = relationship('Tool', secondary='ai_chat_role_tool', backref='roles') +# # conversations = relationship('ChatConversation', backref='role', cascade='all, delete-orphan') +# # messages = relationship('ChatMessage', backref='role', cascade='all, delete-orphan') + +# def __str__(self): +# return self.name + + +# AI 聊天对话表 +# class ChatConversation(CoreModel): +# __tablename__ = 'ai_chat_conversation' + +# title = Column(String(256), nullable=False) +# pinned = Column(Boolean, default=False) +# pinned_time = Column(DateTime, nullable=True) +# # user_id = Column(Integer, ForeignKey('system_users.id'), nullable=True) +# role_id = Column(Integer, ForeignKey('ai_chat_role.id'), nullable=True) +# model_id = Column(Integer, ForeignKey('ai_model.id'), nullable=False) +# model = Column(String(32), nullable=False) +# system_message = Column(String(1024), nullable=True) +# temperature = Column(Float, nullable=False) +# max_tokens = Column(Integer, nullable=False) +# max_contexts = Column(Integer, nullable=False) +# # user = relationship(DjangoUser, backref='conversations') # 正确:DjangoUser 已定义并导入 + +# model_rel = relationship('AIModel', backref='conversations') +# messages = relationship('ChatMessage', backref='conversation', cascade='all, delete-orphan') + +# def __str__(self): +# return self.title + + +# AI 聊天消息表 +# class ChatMessage(CoreModel): +# __tablename__ = 'ai_chat_message' + +# conversation_id = Column(Integer, nullable=False) +# # user_id = Column(Integer, ForeignKey('system_users.id'), nullable=True) +# role_id = Column(Integer, ForeignKey('ai_chat_role.id'), nullable=True) +# model = Column(String(32), nullable=False) +# model_id = Column(Integer, ForeignKey('ai_model.id'), nullable=False) +# type = Column(String(16), nullable=False) +# reply_id = Column(Integer, nullable=True) +# content = Column(String(2048), nullable=False) +# use_context = Column(Boolean, default=False) +# segment_ids = Column(String(2048), nullable=True) + +# # user = relationship(DjangoUser, backref='messages') # 正确:DjangoUser 已定义并导入 +# model_rel = relationship('AIModel', backref='messages') + +# def __str__(self): +# return self.content[:30] + + +# # 聊天角色与知识库的关联表 +# class ChatRoleKnowledge(Base): +# __tablename__ = 'ai_chat_role_knowledge' + +# chat_role_id = Column(Integer, ForeignKey('ai_chat_role.id'), primary_key=True) +# knowledge_id = Column(Integer, ForeignKey('ai_knowledge.id'), primary_key=True) + + +# # 聊天角色与工具的关联表 +# class ChatRoleTool(Base): +# __tablename__ = 'ai_chat_role_tool' + +# chat_role_id = Column(Integer, ForeignKey('ai_chat_role.id'), primary_key=True) +# tool_id = Column(Integer, ForeignKey('ai_tool.id'), primary_key=True) \ No newline at end of file diff --git a/chat/models/user.py b/chat/models/user.py index ffcfaf3..cc089cc 100644 --- a/chat/models/user.py +++ b/chat/models/user.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, DateTime +from sqlalchemy import Column, Integer, String, DateTime, Boolean from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() @@ -9,8 +9,15 @@ class AuthToken(Base): user_id = Column(Integer, nullable=False) created = Column(DateTime) + class DjangoUser(Base): __tablename__ = 'system_users' id = Column(Integer, primary_key=True) username = Column(String(150), nullable=False) - email = Column(String(254)) \ No newline at end of file + email = Column(String(254)) + password = Column(String(128)) + is_active = Column(Boolean, default=True) + is_staff = Column(Boolean, default=False) + is_superuser = Column(Boolean, default=False) + last_login = Column(DateTime) + date_joined = Column(DateTime) \ No newline at end of file diff --git a/chat/routers/ai_api_key.py b/chat/routers/ai_api_key.py new file mode 100644 index 0000000..35b33cc --- /dev/null +++ b/chat/routers/ai_api_key.py @@ -0,0 +1,13 @@ +from schemas.ai_api_key import AIApiKeyCreate, AIApiKeyUpdate, AIApiKeyRead +from crud.ai_api_key import ai_api_key_crud +from routers.base import GenericRouter + +# 继承通用路由基类,传入参数即可生成所有CRUD接口 +router = GenericRouter( + crud=ai_api_key_crud, + create_schema=AIApiKeyCreate, + update_schema=AIApiKeyUpdate, + read_schema=AIApiKeyRead, + prefix="/chat/api/ai-api-keys", + tags=["AI API密钥"] +) \ No newline at end of file diff --git a/chat/routers/base.py b/chat/routers/base.py new file mode 100644 index 0000000..e63acc5 --- /dev/null +++ b/chat/routers/base.py @@ -0,0 +1,101 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import Generic, TypeVar, List + +from db.session import get_db +from schemas.base import ReadSchemaType # 通用的响应模型基类 +from crud.base import CRUDBase + +# 泛型变量(对应:CRUD类、创建模型、更新模型、响应模型) +CRUDType = TypeVar("CRUDType") +CreateSchemaType = TypeVar("CreateSchemaType") +UpdateSchemaType = TypeVar("UpdateSchemaType") +ReadSchemaType = TypeVar("ReadSchemaType") + + +class GenericRouter( + APIRouter, + Generic[CRUDType, CreateSchemaType, UpdateSchemaType, ReadSchemaType] +): + def __init__( + self, + crud: CRUDType, + create_schema: CreateSchemaType, + update_schema: UpdateSchemaType, + read_schema: ReadSchemaType, + prefix: str, + tags: List[str], + **kwargs + ): + """ + 初始化通用路由 + :param crud: CRUD实例(如CRUDApiKey) + :param create_schema: 创建Pydantic模型 + :param update_schema: 更新Pydantic模型 + :param read_schema: 响应Pydantic模型 + :param prefix: 路由前缀(如"/api/ai-api-keys") + :param tags: 文档标签 + """ + super().__init__(prefix=prefix, tags=tags,** kwargs) + self.crud = crud + self.create_schema = create_schema + self.update_schema = update_schema + self.read_schema = read_schema + + # 注册通用路由 + self.add_api_route( + "/", + self.create, + methods=["POST"], + response_model=read_schema, + status_code=201 + ) + self.add_api_route( + "/", + self.get_multi, + methods=["GET"], + response_model=List[read_schema] + ) + self.add_api_route( + "/{id}/", + self.get, + methods=["GET"], + response_model=read_schema + ) + self.add_api_route( + "/{id}/", + self.update, + methods=["PUT"], + response_model=read_schema + ) + self.add_api_route( + "/{id}/", + self.remove, + methods=["DELETE"] + ) + + # 创建 + def create(self, obj_in: CreateSchemaType, db: Session = Depends(get_db)): + return self.crud.create(db=db, obj_in=obj_in) + + # 按ID查询 + def get(self, id: int, db: Session = Depends(get_db)): + obj = self.crud.get(db=db, id=id) + if not obj: + raise HTTPException(status_code=404, detail=f"记录不存在") + return obj + + # 分页查询 + def get_multi(self, page: int = 0, limit: int = 10, db: Session = Depends(get_db)): + return self.crud.get_multi(db=db, page=page, limit=limit) + + # 更新 + def update(self, id: int, obj_in: UpdateSchemaType, db: Session = Depends(get_db)): + obj = self.crud.get(db=db, id=id) + if not obj: + raise HTTPException(status_code=404, detail=f"记录不存在") + return self.crud.update(db=db, db_obj=obj, obj_in=obj_in) + + # 删除 + def remove(self, id: int, db: Session = Depends(get_db)): + return self.crud.remove(db=db, id=id) \ No newline at end of file diff --git a/chat/schemas/ai_api_key.py b/chat/schemas/ai_api_key.py new file mode 100644 index 0000000..eba96ba --- /dev/null +++ b/chat/schemas/ai_api_key.py @@ -0,0 +1,33 @@ +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime + +# 基础模型(共享字段) +class AIApiKeyBase(BaseModel): + name: str = Field(..., max_length=255, description="密钥名称") + platform: str = Field(..., max_length=100, description="平台(如openai)") + api_key: str = Field(..., max_length=255, description="API密钥") + url: Optional[str] = Field(None, max_length=255, description="自定义API地址") + status: int = Field(0, description="状态(0=禁用,1=启用)") + +# 创建请求模型(无需ID和时间字段) +class AIApiKeyCreate(AIApiKeyBase): + pass + +# 更新请求模型(所有字段可选) +class AIApiKeyUpdate(BaseModel): + name: Optional[str] = Field(None, max_length=255) + platform: Optional[str] = Field(None, max_length=100) + api_key: Optional[str] = Field(None, max_length=255) + url: Optional[str] = Field(None, max_length=255) + status: Optional[int] = None + +# 响应模型(包含数据库自动生成的字段) +class AIApiKeyRead(AIApiKeyBase): + id: int + created_at: Optional[datetime] + updated_at: Optional[datetime] + + # 支持ORM模型直接转换为响应 + class Config: + from_attributes = True # Pydantic v2用from_attributes,v1用orm_mode \ No newline at end of file diff --git a/chat/schemas/base.py b/chat/schemas/base.py new file mode 100644 index 0000000..ad50867 --- /dev/null +++ b/chat/schemas/base.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel +from datetime import datetime +from typing import Optional + +class ReadSchemaType(BaseModel): + """ + 所有响应模型的基类,包含公共字段和ORM转换配置 + """ + id: int + created_at: Optional[datetime] = None # 数据创建时间(可选,部分模型可能没有) + updated_at: Optional[datetime] = None # 数据更新时间(可选) + + class Config: + """ + 配置Pydantic模型如何处理ORM对象: + - from_attributes=True:支持直接从SQLAlchemy ORM模型转换(Pydantic v2) + - 若使用Pydantic v1,需替换为 orm_mode=True + """ + from_attributes = True \ No newline at end of file diff --git a/chat/services/chat_service.py b/chat/services/chat_service.py index 3c16094..5031c0d 100644 --- a/chat/services/chat_service.py +++ b/chat/services/chat_service.py @@ -12,4 +12,4 @@ class ChatService: # 简单调用LLM return self.llm(prompt) -chat_service = ChatService() \ No newline at end of file +chat_service = ChatService() \ No newline at end of file diff --git a/web/apps/web-antd/src/locales/langs/en-US/ai.json b/web/apps/web-antd/src/locales/langs/en-US/ai.json index 2505bb1..d347132 100644 --- a/web/apps/web-antd/src/locales/langs/en-US/ai.json +++ b/web/apps/web-antd/src/locales/langs/en-US/ai.json @@ -15,5 +15,9 @@ "knowledge": { "title": "KNOWLEDGE Management", "name": "KNOWLEDGE Management" + }, + "chat": { + "title": "AI CHAT", + "name": "AI CHAT" } } diff --git a/web/apps/web-antd/src/locales/langs/zh-CN/ai.json b/web/apps/web-antd/src/locales/langs/zh-CN/ai.json index 47116d7..224e451 100644 --- a/web/apps/web-antd/src/locales/langs/zh-CN/ai.json +++ b/web/apps/web-antd/src/locales/langs/zh-CN/ai.json @@ -15,5 +15,9 @@ "knowledge": { "title": "知识库管理", "name": "知识库管理" + }, + "chat": { + "title": "AI对话", + "name": "AI对话" } } diff --git a/web/apps/web-antd/src/views/ai/chat/index.vue b/web/apps/web-antd/src/views/ai/chat/index.vue new file mode 100644 index 0000000..f4181e5 --- /dev/null +++ b/web/apps/web-antd/src/views/ai/chat/index.vue @@ -0,0 +1,11 @@ + + + + +