diff --git a/apps/common/oidc.py b/apps/common/oidc.py index b5e2bb62b2d4da5abac72f0ddc7b72ad6acc77de..46c7ad93d207ee43bc0ac23f48e5f45f60d17d0f 100644 --- a/apps/common/oidc.py +++ b/apps/common/oidc.py @@ -7,11 +7,10 @@ from typing import Any from apps.common.postgres import postgres from apps.constants import OIDC_ACCESS_TOKEN_EXPIRE_TIME, OIDC_REFRESH_TOKEN_EXPIRE_TIME -from apps.models.session import Session, SessionType +from apps.models import Session, SessionType from .config import config -from .oidc_provider.authhub import AuthhubOIDCProvider -from .oidc_provider.openeuler import OpenEulerOIDCProvider +from .oidc_provider import AuthhubOIDCProvider, OpenEulerOIDCProvider logger = logging.getLogger(__name__) diff --git a/apps/common/oidc_provider/__init__.py b/apps/common/oidc_provider/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..28375f5c7bff99fc2da3521c772de374a8cef488 --- /dev/null +++ b/apps/common/oidc_provider/__init__.py @@ -0,0 +1,11 @@ +"""OIDC Provider""" + +from .authhub import AuthhubOIDCProvider +from .base import OIDCProviderBase +from .openeuler import OpenEulerOIDCProvider + +__all__ = [ + "AuthhubOIDCProvider", + "OIDCProviderBase", + "OpenEulerOIDCProvider", +] diff --git a/apps/dependency/user.py b/apps/dependency/user.py index 44a963b2a8872821e769e6d5cea8a7e93fbe235c..38d38095e6b491b3ee92d57fd96f2cdbef86faf4 100644 --- a/apps/dependency/user.py +++ b/apps/dependency/user.py @@ -8,9 +8,7 @@ from starlette.exceptions import HTTPException from starlette.requests import HTTPConnection from apps.common.config import config -from apps.services.personal_token import PersonalTokenManager -from apps.services.session import SessionManager -from apps.services.user import UserManager +from apps.services import PersonalTokenManager, SessionManager, UserManager logger = logging.getLogger(__name__) diff --git a/apps/llm/__init__.py b/apps/llm/__init__.py index e28ad31d70fd0bbc692694ff57f16014fd7580d5..bafa67237d2241b394e82ab91a680308f0681d83 100644 --- a/apps/llm/__init__.py +++ b/apps/llm/__init__.py @@ -1,2 +1,15 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """模型调用模块""" + +from .embedding import Embedding +from .function import FunctionLLM, JsonGenerator +from .reasoning import ReasoningLLM +from .token import TokenCalculator + +__all__ = [ + "Embedding", + "FunctionLLM", + "JsonGenerator", + "ReasoningLLM", + "TokenCalculator", +] diff --git a/apps/llm/embedding.py b/apps/llm/embedding.py index ec8215e6a8064ca3978352ce2731f7a0a0136446..679683f7e2a69644be2b33d5a7f010f6a50cefad 100644 --- a/apps/llm/embedding.py +++ b/apps/llm/embedding.py @@ -9,8 +9,7 @@ from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import declarative_base from apps.common.postgres import postgres -from apps.models.llm import LLMData -from apps.schemas.llm import LLMProvider +from apps.models import LLMData, LLMProvider from .providers import BaseProvider, OpenAIProvider, TEIProvider diff --git a/apps/llm/function.py b/apps/llm/function.py index 07497e8aef37b553412a950731146c99462d03bf..6725c46cfb0d9311c480ca90f63be7f9a2fb077e 100644 --- a/apps/llm/function.py +++ b/apps/llm/function.py @@ -12,8 +12,7 @@ from jinja2.sandbox import SandboxedEnvironment from jsonschema import Draft7Validator from apps.constants import JSON_GEN_MAX_TRIAL -from apps.models.llm import LLMData -from apps.schemas.llm import LLMProvider +from apps.models import LLMData, LLMProvider from .prompt import JSON_GEN_BASIC from .providers import ( diff --git a/apps/llm/providers/base.py b/apps/llm/providers/base.py index 0f76879543eae9009806b43f28fad49a60822797..207fa0391c102b41a0c38d70b40a418454359f90 100644 --- a/apps/llm/providers/base.py +++ b/apps/llm/providers/base.py @@ -6,7 +6,7 @@ from abc import abstractmethod from collections.abc import AsyncGenerator from apps.constants import LLM_TIMEOUT -from apps.models.llm import LLMData +from apps.models import LLMData from apps.schemas.llm import LLMChunk, LLMFunctions _logger = logging.getLogger(__name__) diff --git a/apps/llm/providers/ollama.py b/apps/llm/providers/ollama.py index e54ac59cb558aebbc54eebd201063df37f5a9633..7d0578a7a654079bb5efab204cdf5baf5ce3736a 100644 --- a/apps/llm/providers/ollama.py +++ b/apps/llm/providers/ollama.py @@ -7,8 +7,9 @@ from typing import Any from ollama import AsyncClient, ChatResponse from typing_extensions import override -from apps.llm.token import TokenCalculator -from apps.schemas.llm import LLMChunk, LLMFunctions, LLMType +from apps.llm import TokenCalculator +from apps.models import LLMType +from apps.schemas.llm import LLMChunk, LLMFunctions from .base import BaseProvider diff --git a/apps/llm/providers/openai.py b/apps/llm/providers/openai.py index 44250ffab3e122a83ec67c7c584f7b4ec48b8d29..d13e1d0b8dcc32cbe7f38ee6dfe15027ebcfb9bc 100644 --- a/apps/llm/providers/openai.py +++ b/apps/llm/providers/openai.py @@ -15,8 +15,9 @@ from openai.types.chat import ( ) from typing_extensions import override -from apps.llm.token import TokenCalculator -from apps.schemas.llm import LLMChunk, LLMFunctions, LLMType +from apps.llm import TokenCalculator +from apps.models import LLMType +from apps.schemas.llm import LLMChunk, LLMFunctions from .base import BaseProvider diff --git a/apps/llm/providers/tei.py b/apps/llm/providers/tei.py index 08e8b870b7276614fa7feb8f510062bccf39738d..0e5e9476cdd3b64428cda19e4c7c12da561d2b8d 100644 --- a/apps/llm/providers/tei.py +++ b/apps/llm/providers/tei.py @@ -6,7 +6,7 @@ import logging import httpx from typing_extensions import override -from apps.schemas.llm import LLMType +from apps.models import LLMType from .base import BaseProvider diff --git a/apps/llm/reasoning.py b/apps/llm/reasoning.py index c54d63636018f5d1d763702826bf4a9c8c1a1520..efa8d23b0df1d00d5d18cfa8429a0e70903479d1 100644 --- a/apps/llm/reasoning.py +++ b/apps/llm/reasoning.py @@ -4,8 +4,8 @@ import logging from collections.abc import AsyncGenerator -from apps.models.llm import LLMData -from apps.schemas.llm import LLMChunk, LLMProvider +from apps.models import LLMData, LLMProvider +from apps.schemas.llm import LLMChunk from .providers import ( BaseProvider, diff --git a/apps/main.py b/apps/main.py index e416fd32c93227791eff3b7ae92db3e1931b3263..eca90cf91ac06cd59feedc49e08b7562f28f1bdc 100644 --- a/apps/main.py +++ b/apps/main.py @@ -53,26 +53,28 @@ app.add_middleware( allow_headers=["*"], ) # 关联API路由 -app.include_router(conversation.router) -app.include_router(auth.router) app.include_router(appcenter.router) -app.include_router(service.router) -app.include_router(comment.router) -app.include_router(record.router) -app.include_router(health.router) -app.include_router(chat.router) +app.include_router(auth.admin_router) +app.include_router(auth.router) app.include_router(blacklist.admin_router) -app.include_router(blacklist.user_router) +app.include_router(blacklist.router) +app.include_router(chat.router) +app.include_router(comment.router) +app.include_router(conversation.router) app.include_router(document.router) +app.include_router(flow.router) +app.include_router(health.router) app.include_router(knowledge.router) app.include_router(llm.router) app.include_router(llm.admin_router) app.include_router(mcp_service.router) -app.include_router(flow.router) -app.include_router(user.router) +app.include_router(mcp_service.admin_router) app.include_router(parameter.router) +app.include_router(record.router) +app.include_router(service.router) +app.include_router(service.admin_router) app.include_router(tag.admin_router) -app.include_router(tag.admin_router) +app.include_router(user.router) # logger配置 LOGGER_FORMAT = "%(funcName)s() - %(message)s" diff --git a/apps/models/__init__.py b/apps/models/__init__.py index 9b0215ed4b820af0d31aadc01f50e16b90d32277..b67b00ee7887eb97642be0dc302e974452025525 100644 --- a/apps/models/__init__.py +++ b/apps/models/__init__.py @@ -1,5 +1,68 @@ """SQLAlchemy 数据库表结构""" +from .app import App, AppACL, AppHashes, AppMCP, AppType, PermissionType from .base import Base +from .blacklist import Blacklist +from .comment import Comment, CommentType +from .conversation import ConvDocAssociated, Conversation, ConversationDocument +from .document import Document +from .flow import Flow +from .llm import LLMData, LLMProvider, LLMType +from .mcp import MCPActivated, MCPInfo, MCPInstallStatus, MCPTools, MCPType +from .node import NodeInfo +from .record import FootNoteType, Record, RecordFootNote, RecordMetadata +from .service import Service, ServiceACL, ServiceHashes +from .session import Session, SessionActivity, SessionType +from .tag import Tag +from .task import ExecutorCheckpoint, ExecutorHistory, ExecutorStatus, LanguageType, StepStatus, Task, TaskRuntime +from .user import User, UserAppUsage, UserFavorite, UserFavoriteType, UserTag -__all__ = ["Base"] +__all__ = [ + "App", + "AppACL", + "AppHashes", + "AppMCP", + "AppType", + "Base", + "Blacklist", + "Comment", + "CommentType", + "ConvDocAssociated", + "Conversation", + "ConversationDocument", + "Document", + "ExecutorCheckpoint", + "ExecutorHistory", + "ExecutorStatus", + "Flow", + "FootNoteType", + "LLMData", + "LLMProvider", + "LLMType", + "LanguageType", + "MCPActivated", + "MCPInfo", + "MCPInstallStatus", + "MCPTools", + "MCPType", + "NodeInfo", + "PermissionType", + "Record", + "RecordFootNote", + "RecordMetadata", + "Service", + "ServiceACL", + "ServiceHashes", + "Session", + "SessionActivity", + "SessionType", + "StepStatus", + "Tag", + "Task", + "TaskRuntime", + "User", + "UserAppUsage", + "UserFavorite", + "UserFavoriteType", + "UserTag", +] diff --git a/apps/models/app.py b/apps/models/app.py index 27400fcb3fe6966b2195880890b15f66fb53324f..00f8e9f4fbb98f8736f2c45248497efdfa89c805 100644 --- a/apps/models/app.py +++ b/apps/models/app.py @@ -2,16 +2,30 @@ import uuid from datetime import UTC, datetime +from enum import Enum as PyEnum from sqlalchemy import BigInteger, Boolean, DateTime, Enum, ForeignKey, Index, String from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column -from apps.schemas.enum_var import AppType, PermissionType - from .base import Base +class AppType(str, PyEnum): + """应用中心应用类型""" + + FLOW = "flow" + AGENT = "agent" + + +class PermissionType(str, PyEnum): + """权限类型""" + + PROTECTED = "protected" + PUBLIC = "public" + PRIVATE = "private" + + class App(Base): """应用""" @@ -72,3 +86,33 @@ class AppHashes(Base): """文件路径""" hash: Mapped[str] = mapped_column(String(255), nullable=False) """哈希值""" + + +class AppMCP(Base): + """App使用MCP情况""" + + __tablename__ = "framework_app_mcp" + + appId: Mapped[uuid.UUID] = mapped_column( # noqa: N815 + UUID(as_uuid=True), + ForeignKey("framework_app.id"), + primary_key=True, + nullable=False, + ) + """关联的应用ID""" + + mcpId: Mapped[str] = mapped_column( # noqa: N815 + String(255), + ForeignKey("framework_mcp.id"), + primary_key=True, + nullable=False, + ) + """关联的MCP服务ID""" + + createdAt: Mapped[datetime] = mapped_column( # noqa: N815 + DateTime(timezone=True), + default_factory=lambda: datetime.now(tz=UTC), + nullable=False, + index=True, + ) + """创建时间""" diff --git a/apps/models/comment.py b/apps/models/comment.py index a0a1ad2257f062fa4061087fe5b11f9233f95826..000c24970cc48a0f66d1b3450d37fcbaf2bb4636 100644 --- a/apps/models/comment.py +++ b/apps/models/comment.py @@ -2,16 +2,23 @@ import uuid from datetime import UTC, datetime +from enum import Enum as PyEnum from sqlalchemy import ARRAY, BigInteger, DateTime, Enum, ForeignKey, String from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column -from apps.schemas.enum_var import CommentType - from .base import Base +class CommentType(str, PyEnum): + """点赞点踩类型""" + + LIKE = "liked" + DISLIKE = "disliked" + NONE = "none" + + class Comment(Base): """评论""" diff --git a/apps/models/llm.py b/apps/models/llm.py index e8fbb5f94a27ba4b9105be08db686bfcc9da1904..6249f5b377f334df01eab92224ab30a02ab1718e 100644 --- a/apps/models/llm.py +++ b/apps/models/llm.py @@ -1,17 +1,42 @@ """大模型信息 数据库表""" from datetime import UTC, datetime +from enum import Enum as PyEnum from typing import Any from sqlalchemy import ARRAY, DateTime, Enum, Float, Integer, String from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column -from apps.schemas.llm import LLMProvider, LLMType - from .base import Base +class LLMType(str, PyEnum): + """大模型类型""" + + CHAT = "chat" + """模型支持Chat""" + FUNCTION = "function" + """模型支持Function Call""" + EMBEDDING = "embedding" + """模型支持Embedding""" + VISION = "vision" + """模型支持图片理解""" + THINKING = "thinking" + """模型支持思考推理""" + + +class LLMProvider(str, PyEnum): + """Function Call后端""" + + OLLAMA = "ollama" + """Ollama""" + OPENAI = "openai" + """OpenAI""" + TEI = "tei" + """TEI""" + + class LLMData(Base): """大模型信息""" diff --git a/apps/models/service.py b/apps/models/service.py index 6a8080e2163fdbbc005a07283bb8d411db5700dc..712018692e6242f89bd427884908452e3d92c75d 100644 --- a/apps/models/service.py +++ b/apps/models/service.py @@ -7,8 +7,7 @@ from sqlalchemy import BigInteger, DateTime, Enum, ForeignKey, String from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column -from apps.schemas.enum_var import PermissionType - +from .app import PermissionType from .base import Base diff --git a/apps/models/task.py b/apps/models/task.py index 694fab066fead5ea973835a4bddafdeac71d9d0a..e672c0d9db1eaaac6cd38033125177568480ea78 100644 --- a/apps/models/task.py +++ b/apps/models/task.py @@ -2,17 +2,48 @@ import uuid from datetime import UTC, datetime +from enum import Enum as PyEnum from typing import Any from sqlalchemy import DateTime, Enum, Float, ForeignKey, String, Text from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import Mapped, mapped_column -from apps.schemas.enum_var import ExecutorStatus, LanguageType, StepStatus - from .base import Base +class ExecutorStatus(str, PyEnum): + """执行器状态""" + + UNKNOWN = "unknown" + INIT = "init" + WAITING = "waiting" + RUNNING = "running" + SUCCESS = "success" + ERROR = "error" + CANCELLED = "cancelled" + + +class LanguageType(str, PyEnum): + """语言""" + + CHINESE = "zh" + ENGLISH = "en" + + +class StepStatus(str, PyEnum): + """步骤状态""" + + UNKNOWN = "unknown" + INIT = "init" + WAITING = "waiting" + RUNNING = "running" + SUCCESS = "success" + ERROR = "error" + PARAM = "param" + CANCELLED = "cancelled" + + class Task(Base): """任务""" diff --git a/apps/routers/appcenter.py b/apps/routers/appcenter.py index 30e1612a7e5b7080c6db603ee5afa49efc3ea18c..5e644977294464c81d652b9209f613ae4b27612c 100644 --- a/apps/routers/appcenter.py +++ b/apps/routers/appcenter.py @@ -10,6 +10,7 @@ from fastapi.responses import JSONResponse from apps.dependency.user import verify_personal_token, verify_session from apps.exceptions import InstancePermissionError +from apps.models import AppType from apps.schemas.appcenter import ( AppFlowInfo, AppMcpServiceInfo, @@ -26,10 +27,9 @@ from apps.schemas.appcenter import ( GetAppPropertyRsp, GetRecentAppListRsp, ) -from apps.schemas.enum_var import AppFilterType, AppType +from apps.schemas.enum_var import AppFilterType from apps.schemas.response_data import ResponseData -from apps.services.appcenter import AppCenterManager -from apps.services.mcp_service import MCPServiceManager +from apps.services import AppCenterManager, MCPServiceManager logger = logging.getLogger(__name__) router = APIRouter( diff --git a/apps/routers/auth.py b/apps/routers/auth.py index 30e7df2f0e24cbcecbd37a3cda31a6acac45b45b..5300f1994a6c437570cd2321d3744613af3682a5 100644 --- a/apps/routers/auth.py +++ b/apps/routers/auth.py @@ -2,6 +2,7 @@ """FastAPI 用户认证相关路由""" import logging +from datetime import UTC, datetime from pathlib import Path from typing import Annotated @@ -13,6 +14,7 @@ from apps.common.config import config from apps.common.oidc import oidc_provider from apps.dependency import verify_personal_token, verify_session from apps.schemas.personal_token import PostPersonalTokenMsg, PostPersonalTokenRsp +from apps.schemas.request_data import UserUpdateRequest from apps.schemas.response_data import ( AuthUserMsg, AuthUserRsp, @@ -20,25 +22,27 @@ from apps.schemas.response_data import ( OidcRedirectRsp, ResponseData, ) -from apps.services.personal_token import PersonalTokenManager -from apps.services.session import SessionManager -from apps.services.token import TokenManager -from apps.services.user import UserManager +from apps.services import ( + PersonalTokenManager, + SessionManager, + TokenManager, + UserManager, +) -router = APIRouter( +admin_router = APIRouter( prefix="/api/auth", tags=["auth"], ) -user_router = APIRouter( +router = APIRouter( prefix="/api/auth", tags=["auth"], dependencies=[Depends(verify_session), Depends(verify_personal_token)], ) -logger = logging.getLogger(__name__) -templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates") +_logger = logging.getLogger(__name__) +_templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates") -@router.get("/login") +@admin_router.get("/login") async def oidc_login(request: Request, code: str) -> HTMLResponse: """ GET /auth/login?code=xxx: 用户登录EulerCopilot @@ -55,41 +59,40 @@ async def oidc_login(request: Request, code: str) -> HTMLResponse: if user_sub: await oidc_provider.set_token(user_sub, token["access_token"], token["refresh_token"]) except Exception as e: - logger.exception("User login failed") + _logger.exception("User login failed") status_code = status.HTTP_400_BAD_REQUEST if "auth error" in str(e) else status.HTTP_403_FORBIDDEN - return templates.TemplateResponse( + return _templates.TemplateResponse( "login_failed.html.j2", {"request": request, "reason": "无法验证登录信息,请关闭本窗口并重试。"}, status_code=status_code, ) - + # 获取用户信息 if not request.client: - return templates.TemplateResponse( + return _templates.TemplateResponse( "login_failed.html.j2", {"request": request, "reason": "无法获取用户信息,请关闭本窗口并重试。"}, status_code=status.HTTP_403_FORBIDDEN, ) user_host = request.client.host - + # 获取用户sub if not user_sub: - logger.error("OIDC no user_sub associated.") - return templates.TemplateResponse( + _logger.error("OIDC no user_sub associated.") + return _templates.TemplateResponse( "login_failed.html.j2", {"request": request, "reason": "未能获取用户信息,请关闭本窗口并重试。"}, status_code=status.HTTP_403_FORBIDDEN, ) - - await UserManager.update_user(user_sub) - + # 更新用户信息 + await UserManager.update_user(user_sub, UserUpdateRequest(lastLogin=datetime.now(UTC))) + # 创建会话 current_session = await SessionManager.create_session(user_sub, user_host) - return templates.TemplateResponse( + return _templates.TemplateResponse( "login_success.html.j2", {"request": request, "current_session": current_session}, ) - # 用户主动logout -@user_router.get("/logout", response_model=ResponseData) +@router.get("/logout", response_model=ResponseData) async def logout(request: Request) -> JSONResponse: """GET /auth/logout: 用户登出EulerCopilot""" if not request.client: @@ -116,7 +119,7 @@ async def logout(request: Request) -> JSONResponse: ) -@router.get("/redirect", response_model=OidcRedirectRsp) +@admin_router.get("/redirect", response_model=OidcRedirectRsp) async def oidc_redirect() -> JSONResponse: """GET /auth/redirect: 前端获取OIDC重定向URL""" redirect_url = await oidc_provider.get_redirect_url() @@ -131,7 +134,7 @@ async def oidc_redirect() -> JSONResponse: # TODO(zwt): OIDC主动触发logout -@router.post("/logout", response_model=ResponseData) +@admin_router.post("/logout", response_model=ResponseData) async def oidc_logout(token: Annotated[str, Body()]) -> JSONResponse: """POST /auth/logout: OIDC主动告知后端用户已在其他SSO站点登出""" return JSONResponse( @@ -144,7 +147,7 @@ async def oidc_logout(token: Annotated[str, Body()]) -> JSONResponse: ) -@user_router.get("/user", response_model=AuthUserRsp) +@router.get("/user", response_model=AuthUserRsp) async def userinfo(request: Request) -> JSONResponse: """GET /auth/user: 获取当前用户信息""" user = await UserManager.get_user(user_sub=request.state.user_sub) @@ -173,7 +176,7 @@ async def userinfo(request: Request) -> JSONResponse: ) -@user_router.post("/key", responses={ +@router.post("/key", responses={ 400: {"model": ResponseData}, }, response_model=PostPersonalTokenRsp) async def change_personal_token(request: Request) -> JSONResponse: diff --git a/apps/routers/blacklist.py b/apps/routers/blacklist.py index bc2a3147b48b528f6e11befbcff113521bc92c20..194aa2207a2bd0bb118c6392a741fe490f0fbe2c 100644 --- a/apps/routers/blacklist.py +++ b/apps/routers/blacklist.py @@ -18,7 +18,7 @@ from apps.schemas.blacklist import ( from apps.schemas.response_data import ( ResponseData, ) -from apps.services.blacklist import ( +from apps.services import ( AbuseManager, QuestionBlacklistManager, UserBlacklistManager, @@ -33,7 +33,7 @@ admin_router = APIRouter( Depends(verify_admin), ], ) -user_router = APIRouter( +router = APIRouter( prefix="/api/blacklist", tags=["blacklist"], dependencies=[ @@ -141,7 +141,7 @@ async def change_blacklist_question(request: QuestionBlacklistRequest) -> JSONRe ).model_dump(exclude_none=True, by_alias=True)) -@user_router.post("/complaint", response_model=ResponseData) +@router.post("/complaint", response_model=ResponseData) async def abuse_report(raw_request: Request, request: AbuseRequest) -> JSONResponse: """POST /blacklist/complaint: 用户实施举报""" result = await AbuseManager.change_abuse_report( diff --git a/apps/routers/chat.py b/apps/routers/chat.py index 791936f770947f418eaba119dc1ade909d00911b..7ae6287a69c0d9aa31c5624aabf56c368d1ad50d 100644 --- a/apps/routers/chat.py +++ b/apps/routers/chat.py @@ -13,20 +13,23 @@ from fastapi.responses import JSONResponse, StreamingResponse from apps.common.queue import MessageQueue from apps.common.wordscheck import WordsCheck from apps.dependency import verify_personal_token, verify_session +from apps.models import ExecutorStatus from apps.scheduler.scheduler import Scheduler from apps.scheduler.scheduler.context import save_data -from apps.schemas.enum_var import ExecutorStatus from apps.schemas.request_data import RequestData, RequestDataApp from apps.schemas.response_data import ResponseData -from apps.services.activity import Activity -from apps.services.blacklist import QuestionBlacklistManager, UserBlacklistManager -from apps.services.conversation import ConversationManager -from apps.services.flow import FlowManager -from apps.services.record import RecordManager -from apps.services.task import TaskManager +from apps.services import ( + Activity, + ConversationManager, + FlowManager, + QuestionBlacklistManager, + RecordManager, + TaskManager, + UserBlacklistManager, +) RECOMMEND_TRES = 5 -logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) router = APIRouter( prefix="/api", tags=["chat"], @@ -73,7 +76,7 @@ async def chat_generator(post_body: RequestData, user_sub: str, session_id: str) # 敏感词检查 if await WordsCheck().check(post_body.question) != 1: yield "data: [SENSITIVE]\n\n" - logger.info("[Chat] 问题包含敏感词!") + _logger.info("[Chat] 问题包含敏感词!") await Activity.remove_active(user_sub) return @@ -85,7 +88,7 @@ async def chat_generator(post_body: RequestData, user_sub: str, session_id: str) # 在单独Task中运行Scheduler,拉齐queue.get的时机 scheduler = Scheduler(task, queue, post_body) - logger.info(f"[Chat] 用户是否活跃: {await Activity.is_active(user_sub)}") + _logger.info(f"[Chat] 用户是否活跃: {await Activity.is_active(user_sub)}") scheduler_task = asyncio.create_task(scheduler.run()) # 处理每一条消息 @@ -100,7 +103,7 @@ async def chat_generator(post_body: RequestData, user_sub: str, session_id: str) # 获取最终答案 task = scheduler.task if task.state.flow_status == ExecutorStatus.ERROR: - logger.error("[Chat] 生成答案失败") + _logger.error("[Chat] 生成答案失败") yield "data: [ERROR]\n\n" await Activity.remove_active(user_sub) return @@ -108,7 +111,7 @@ async def chat_generator(post_body: RequestData, user_sub: str, session_id: str) # 对结果进行敏感词检查 if await WordsCheck().check(task.runtime.answer) != 1: yield "data: [SENSITIVE]\n\n" - logger.info("[Chat] 答案包含敏感词!") + _logger.info("[Chat] 答案包含敏感词!") await Activity.remove_active(user_sub) return @@ -125,7 +128,7 @@ async def chat_generator(post_body: RequestData, user_sub: str, session_id: str) yield "data: [DONE]\n\n" except Exception: - logger.exception("[Chat] 生成答案失败") + _logger.exception("[Chat] 生成答案失败") yield "data: [ERROR]\n\n" finally: diff --git a/apps/routers/comment.py b/apps/routers/comment.py index 77cde798ada4f7ce3337b4d576d9d7475b66684d..ea79c720917b6cac715c2cf3972050883f773404 100644 --- a/apps/routers/comment.py +++ b/apps/routers/comment.py @@ -11,9 +11,9 @@ from apps.dependency import verify_personal_token, verify_session from apps.schemas.comment import AddCommentData from apps.schemas.record import RecordComment from apps.schemas.response_data import ResponseData -from apps.services.comment import CommentManager +from apps.services import CommentManager -logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) router = APIRouter( prefix="/api/comment", tags=["comment"], diff --git a/apps/routers/conversation.py b/apps/routers/conversation.py index 562e3e7643141badb4402137315a408a4bbc8498..fa99262ebdc539dfa8eaaef2bdaff223bd90ed03 100644 --- a/apps/routers/conversation.py +++ b/apps/routers/conversation.py @@ -3,17 +3,13 @@ import logging import uuid -from datetime import UTC, datetime from typing import Annotated from fastapi import APIRouter, Depends, Query, Request, status from fastapi.responses import JSONResponse from apps.dependency import verify_personal_token, verify_session -from apps.models.conversation import Conversation from apps.schemas.conversation import ( - AddConversationMsg, - AddConversationRsp, ChangeConversationData, ConversationListItem, ConversationListMsg, @@ -24,9 +20,7 @@ from apps.schemas.conversation import ( UpdateConversationRsp, ) from apps.schemas.response_data import ResponseData -from apps.services.appcenter import AppCenterManager -from apps.services.conversation import ConversationManager -from apps.services.document import DocumentManager +from apps.services import ConversationManager, DocumentManager router = APIRouter( prefix="/api/conversation", @@ -36,30 +30,9 @@ router = APIRouter( Depends(verify_personal_token), ], ) -logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) -async def create_new_conversation( - title: str, user_sub: str, app_id: uuid.UUID | None = None, - *, - debug: bool = False, -) -> Conversation: - """判断并创建新对话""" - # 新建对话 - if app_id and not await AppCenterManager.validate_user_app_access(user_sub, app_id): - err = "Invalid app_id." - raise RuntimeError(err) - new_conv = await ConversationManager.add_conversation_by_user_sub( - title=title, - user_sub=user_sub, - app_id=app_id, - debug=debug, - ) - if not new_conv: - err = "Create new conversation failed." - raise RuntimeError(err) - return new_conv - @router.get("", response_model=ConversationListRsp, responses={ status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": ResponseData}, }, @@ -92,55 +65,17 @@ async def get_conversation_list(request: Request) -> JSONResponse: ) -@router.post("", response_model=AddConversationRsp) -async def add_conversation( - request: Request, - appId: Annotated[uuid.UUID | None, Query()] = None, # noqa: N803 - title: str = "New Chat", - *, - debug: Annotated[bool, Query()] = False, -) -> JSONResponse: - """POST /conversation: 手动创建新对话""" - # 尝试创建新对话 - try: - debug = debug if debug is not None else False - new_conv = await create_new_conversation( - title=title, - user_sub=request.state.user_sub, - app_id=appId, - debug=debug, - ) - except RuntimeError as e: - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=ResponseData( - code=status.HTTP_500_INTERNAL_SERVER_ERROR, - message=str(e), - result={}, - ).model_dump(exclude_none=True, by_alias=True), - ) - - return JSONResponse( - status_code=status.HTTP_200_OK, - content=AddConversationRsp( - code=status.HTTP_200_OK, - message="success", - result=AddConversationMsg(conversationId=new_conv.id), - ).model_dump(exclude_none=True, by_alias=True), - ) - - -@router.put("", response_model=UpdateConversationRsp) +@router.post("", response_model=UpdateConversationRsp) async def update_conversation( - user_sub: Annotated[str, Depends(get_user)], - conversation_id: Annotated[str, Query(..., alias="conversationId")], + request: Request, + conversation_id: Annotated[uuid.UUID, Query(alias="conversationId")], post_body: ChangeConversationData, ) -> JSONResponse: - """PUT /conversation: 更新特定Conversation的数据""" + """PUT /conversation: 更新特定Conversation的信息""" # 判断Conversation是否合法 - conv = await ConversationManager.get_conversation_by_conversation_id(user_sub, conversation_id) - if not conv or conv.userSub != user_sub: - logger.error("[Conversation] conversation_id 不存在") + conv = await ConversationManager.get_conversation_by_conversation_id(request.state.user_sub, conversation_id) + if not conv or conv.userSub != request.state.user_sub: + _logger.error("[Conversation] conversation_id 不存在") return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content=ResponseData( @@ -153,13 +88,14 @@ async def update_conversation( # 更新Conversation数据 try: await ConversationManager.update_conversation_by_conversation_id( - user_sub, + request.state.user_sub, conversation_id, { "title": post_body.title, }, ) - except Exception as e: + except Exception: + _logger.exception("[Conversation] 更新对话数据失败") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -177,12 +113,10 @@ async def update_conversation( result=ConversationListItem( conversationId=conv.id, title=conv.title, - docCount=await DocumentManager.get_doc_count(user_sub, conv.id), - createdTime=datetime.fromtimestamp(conv.createdAt, tz=UTC).strftime( - "%Y-%m-%d %H:%M:%S", - ), - appId=conv.appId if conv.appId else "", - debug=conv.debug if conv.debug else False, + docCount=await DocumentManager.get_doc_count(conv.id), + createdTime=conv.createdAt.strftime("%Y-%m-%d %H:%M:%S"), + appId=conv.appId, + debug=conv.isTemporary, ), ).model_dump(exclude_none=True, by_alias=True), ) diff --git a/apps/routers/document.py b/apps/routers/document.py index 27d0ed7fc3bcc4a6ef35179d2cb64304628d7597..2e816fe5b5f858e97fca320c1c7b14f9f9849222 100644 --- a/apps/routers/document.py +++ b/apps/routers/document.py @@ -21,9 +21,7 @@ from apps.schemas.document import ( ) from apps.schemas.enum_var import DocumentStatus from apps.schemas.response_data import ResponseData -from apps.services.conversation import ConversationManager -from apps.services.document import DocumentManager -from apps.services.knowledge_base import KnowledgeBaseService +from apps.services import ConversationManager, DocumentManager, KnowledgeBaseService router = APIRouter( prefix="/api/document", diff --git a/apps/routers/flow.py b/apps/routers/flow.py index 2d87351e2a612da12b7356aff62c846e7c4f695d..ab6a084cdca14d21c3d39aa776ec4c57a37c7ebb 100644 --- a/apps/routers/flow.py +++ b/apps/routers/flow.py @@ -19,9 +19,7 @@ from apps.schemas.flow import ( from apps.schemas.request_data import PutFlowReq from apps.schemas.response_data import ResponseData from apps.schemas.service import NodeServiceListMsg, NodeServiceListRsp -from apps.services.appcenter import AppCenterManager -from apps.services.flow import FlowManager -from apps.services.flow_validate import FlowService +from apps.services import AppCenterManager, FlowManager, FlowServiceManager router = APIRouter( prefix="/api/flow", @@ -114,9 +112,9 @@ async def put_flow( result=FlowStructurePutMsg(), ).model_dump(exclude_none=True, by_alias=True), ) - put_body.flow = await FlowService.remove_excess_structure_from_flow(put_body.flow) - await FlowService.validate_flow_illegal(put_body.flow) - put_body.flow.check_status.connectivity = await FlowService.validate_flow_connectivity(put_body.flow) + put_body.flow = await FlowServiceManager.remove_excess_structure_from_flow(put_body.flow) + await FlowServiceManager.validate_flow_illegal(put_body.flow) + put_body.flow.check_status.connectivity = await FlowServiceManager.validate_flow_connectivity(put_body.flow) try: await FlowManager.put_flow_by_app_and_flow_id(appId, flowId, put_body.flow) diff --git a/apps/routers/knowledge.py b/apps/routers/knowledge.py index 4e4ecc4ee95b683e144e4eca3e240a34b52d48c7..b3aeadf4e9543dd5c05d79642079e79403da4b6c 100644 --- a/apps/routers/knowledge.py +++ b/apps/routers/knowledge.py @@ -13,7 +13,7 @@ from apps.schemas.response_data import ( ListTeamKnowledgeRsp, ResponseData, ) -from apps.services.knowledge import KnowledgeBaseManager +from apps.services import KnowledgeBaseManager router = APIRouter( prefix="/api/knowledge", diff --git a/apps/routers/llm.py b/apps/routers/llm.py index ab9cf0d9a891a7f76acb154cfdb9a60eac227117..8c644736cbc269636b4c98a9195a83d1a4324934 100644 --- a/apps/routers/llm.py +++ b/apps/routers/llm.py @@ -12,7 +12,7 @@ from apps.schemas.response_data import ( ListLLMRsp, ResponseData, ) -from apps.services.llm import LLMManager +from apps.services import LLMManager router = APIRouter( prefix="/api/llm", diff --git a/apps/routers/mcp_service.py b/apps/routers/mcp_service.py index b16022a2d60f28784a3183ef2744b5ef9b8bef09..25127158b3059379e13023c3183c6ac39f831c4b 100644 --- a/apps/routers/mcp_service.py +++ b/apps/routers/mcp_service.py @@ -25,9 +25,9 @@ from apps.schemas.mcp_service import ( UploadMCPServiceIconRsp, ) from apps.schemas.response_data import ResponseData -from apps.services.mcp_service import MCPServiceManager +from apps.services import MCPServiceManager -logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) router = APIRouter( prefix="/api/mcp", tags=["mcp-service"], @@ -70,7 +70,7 @@ async def get_mcpservice_list( # noqa: PLR0913 ) except Exception as e: err = f"[MCPServiceCenter] 获取MCP服务列表失败: {e}" - logger.exception(err) + _logger.exception(err) return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -103,7 +103,7 @@ async def create_or_update_mcpservice( try: service_id = await MCPServiceManager.create_mcpservice(data, request.state.user_sub) except Exception as e: - logger.exception("[MCPServiceCenter] MCP服务创建失败") + _logger.exception("[MCPServiceCenter] MCP服务创建失败") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -116,7 +116,7 @@ async def create_or_update_mcpservice( try: service_id = await MCPServiceManager.update_mcpservice(data, request.state.user_sub) except Exception as e: - logger.exception("[MCPService] 更新MCP服务失败") + _logger.exception("[MCPService] 更新MCP服务失败") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -135,7 +135,7 @@ async def create_or_update_mcpservice( ).model_dump(exclude_none=True, by_alias=True)) -@router.post("/{serviceId}/install") +@admin_router.post("/{serviceId}/install") async def install_mcp_service( request: Request, service_id: Annotated[str, Path(..., alias="serviceId", description="服务ID")], @@ -147,7 +147,7 @@ async def install_mcp_service( await MCPServiceManager.install_mcpservice(request.state.user_sub, service_id, install=install) except Exception as e: err = f"[MCPService] 安装mcp服务失败: {e!s}" if install else f"[MCPService] 卸载mcp服务失败: {e!s}" - logger.exception(err) + _logger.exception(err) return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -179,7 +179,7 @@ async def get_service_detail( config, icon = await MCPServiceManager.get_mcp_config(service_id) except Exception as e: err = f"[MCPService] 获取MCP服务API失败: {e}" - logger.exception(err) + _logger.exception(err) return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -241,7 +241,7 @@ async def delete_service(serviceId: Annotated[str, Path()]) -> JSONResponse: # await MCPServiceManager.delete_mcpservice(serviceId) except Exception as e: err = f"[MCPServiceManager] 删除MCP服务失败: {e}" - logger.exception(err) + _logger.exception(err) return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -286,7 +286,7 @@ async def update_mcp_icon( url = await MCPServiceManager.save_mcp_icon(serviceId, icon) except Exception as e: err = f"[MCPServiceManager] 更新MCP服务图标失败: {e}" - logger.exception(err) + _logger.exception(err) return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -319,7 +319,7 @@ async def active_or_deactivate_mcp_service( await MCPServiceManager.deactive_mcpservice(request.state.user_sub, mcpId) except Exception as e: err = f"[MCPService] 激活mcp服务失败: {e!s}" if data.active else f"[MCPService] 取消激活mcp服务失败: {e!s}" - logger.exception(err) + _logger.exception(err) return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( diff --git a/apps/routers/parameter.py b/apps/routers/parameter.py index 60ee123e8f2fa80e98d19e44e672d77ab4556175..53686ba49c90300868c0627928b466239449f3c6 100644 --- a/apps/routers/parameter.py +++ b/apps/routers/parameter.py @@ -8,9 +8,7 @@ from fastapi.responses import JSONResponse from apps.dependency.user import verify_personal_token, verify_session from apps.schemas.parameters import Type from apps.schemas.response_data import GetOperaRsp, GetParamsRsp -from apps.services.appcenter import AppCenterManager -from apps.services.flow import FlowManager -from apps.services.parameter import ParameterManager +from apps.services import AppCenterManager, FlowManager, ParameterManager router = APIRouter( prefix="/api/parameter", diff --git a/apps/routers/record.py b/apps/routers/record.py index 017926ec27cf4a8f40b63cf88177196e080678db..d6ad825955eb72c90c5efba71cd1996684950cae 100644 --- a/apps/routers/record.py +++ b/apps/routers/record.py @@ -10,7 +10,7 @@ from fastapi.responses import JSONResponse from apps.common.security import Security from apps.dependency import verify_personal_token, verify_session -from apps.models.task import ExecutorHistory +from apps.models import ExecutorHistory from apps.schemas.record import ( RecordContent, RecordData, @@ -23,10 +23,12 @@ from apps.schemas.response_data import ( RecordListRsp, ResponseData, ) -from apps.services.conversation import ConversationManager -from apps.services.document import DocumentManager -from apps.services.record import RecordManager -from apps.services.task import TaskManager +from apps.services import ( + ConversationManager, + DocumentManager, + RecordManager, + TaskManager, +) router = APIRouter( prefix="/api/record", diff --git a/apps/routers/service.py b/apps/routers/service.py index ae5afdcd7e1c3a3033a31ae7682e4f4bef2492b7..d5fa7aa942435758eb4a023cce7ff206d0b2d739 100644 --- a/apps/routers/service.py +++ b/apps/routers/service.py @@ -8,7 +8,11 @@ from typing import Annotated from fastapi import APIRouter, Depends, Path, Request, status from fastapi.responses import JSONResponse -from apps.dependency.user import verify_personal_token, verify_session +from apps.dependency.user import ( + verify_admin, + verify_personal_token, + verify_session, +) from apps.exceptions import InstancePermissionError from apps.schemas.enum_var import SearchType from apps.schemas.response_data import ResponseData @@ -26,9 +30,9 @@ from apps.schemas.service import ( UpdateServiceRequest, UpdateServiceRsp, ) -from apps.services.service import ServiceCenterManager +from apps.services import ServiceCenterManager -logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) router = APIRouter( prefix="/api/service", tags=["service-center"], @@ -37,6 +41,15 @@ router = APIRouter( Depends(verify_personal_token), ], ) +admin_router = APIRouter( + prefix="/api/admin/service", + tags=["service-center"], + dependencies=[ + Depends(verify_session), + Depends(verify_personal_token), + Depends(verify_admin), + ], +) @router.get("", response_model=GetServiceListRsp | ResponseData) @@ -85,7 +98,7 @@ async def get_service_list( # noqa: PLR0913 pageSize, ) except Exception: - logger.exception("[ServiceCenter] 获取服务列表失败") + _logger.exception("[ServiceCenter] 获取服务列表失败") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -117,14 +130,14 @@ async def get_service_list( # noqa: PLR0913 ) -@router.post("", response_model=UpdateServiceRsp) +@admin_router.post("", response_model=UpdateServiceRsp) async def update_service(request: Request, data: UpdateServiceRequest) -> JSONResponse: """POST /service: 上传并解析服务""" if not data.service_id: try: service_id = await ServiceCenterManager.create_service(request.state.user_sub, data.data) except Exception as e: - logger.exception("[ServiceCenter] 创建服务失败") + _logger.exception("[ServiceCenter] 创建服务失败") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -146,7 +159,7 @@ async def update_service(request: Request, data: UpdateServiceRequest) -> JSONRe ).model_dump(exclude_none=True, by_alias=True), ) except Exception as e: - logger.exception("[ServiceCenter] 更新服务失败") + _logger.exception("[ServiceCenter] 更新服务失败") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -158,7 +171,7 @@ async def update_service(request: Request, data: UpdateServiceRequest) -> JSONRe try: name, apis = await ServiceCenterManager.get_service_apis(service_id) except Exception as e: - logger.exception("[ServiceCenter] 获取服务API失败") + _logger.exception("[ServiceCenter] 获取服务API失败") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -192,7 +205,7 @@ async def get_service_detail( ).model_dump(exclude_none=True, by_alias=True), ) except Exception: - logger.exception("[ServiceCenter] 获取服务数据失败") + _logger.exception("[ServiceCenter] 获取服务数据失败") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -206,7 +219,7 @@ async def get_service_detail( try: name, apis = await ServiceCenterManager.get_service_apis(serviceId) except Exception: - logger.exception("[ServiceCenter] 获取服务API失败") + _logger.exception("[ServiceCenter] 获取服务API失败") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -220,7 +233,7 @@ async def get_service_detail( return JSONResponse(status_code=status.HTTP_200_OK, content=rsp.model_dump(exclude_none=True, by_alias=True)) -@router.delete("/{serviceId}", response_model=DeleteServiceRsp) +@admin_router.delete("/{serviceId}", response_model=DeleteServiceRsp) async def delete_service(request: Request, serviceId: Annotated[uuid.UUID, Path()]) -> JSONResponse: # noqa: N803 """DELETE /service/{serviceId}: 删除服务""" try: @@ -235,7 +248,7 @@ async def delete_service(request: Request, serviceId: Annotated[uuid.UUID, Path( ).model_dump(exclude_none=True, by_alias=True), ) except Exception: - logger.exception("[ServiceCenter] 删除服务失败") + _logger.exception("[ServiceCenter] 删除服务失败") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( @@ -272,7 +285,7 @@ async def modify_favorite_service( ).model_dump(exclude_none=True, by_alias=True), ) except Exception: - logger.exception("[ServiceCenter] 修改服务收藏状态失败") + _logger.exception("[ServiceCenter] 修改服务收藏状态失败") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ResponseData( diff --git a/apps/routers/tag.py b/apps/routers/tag.py index 72bc27b67cff5c07a80bdf2d9ff935a8fa34d918..59dc9349269d4281ef6177997454a8268932c21f 100644 --- a/apps/routers/tag.py +++ b/apps/routers/tag.py @@ -7,7 +7,7 @@ from fastapi.responses import JSONResponse from apps.dependency.user import verify_admin, verify_personal_token, verify_session from apps.schemas.request_data import PostTagData from apps.schemas.response_data import ResponseData -from apps.services.tag import TagManager +from apps.services import TagManager admin_router = APIRouter( prefix="/api/admin/tag", diff --git a/apps/routers/user.py b/apps/routers/user.py index 7316ffb29db14ce46f6981d4bf3b2aa47c7babf2..ef991246b851b4e99ce45cea150359acbc2a05d1 100644 --- a/apps/routers/user.py +++ b/apps/routers/user.py @@ -9,9 +9,7 @@ from apps.schemas.request_data import UpdateUserSelectedLLMReq, UserUpdateReques from apps.schemas.response_data import ResponseData, UserGetMsp, UserGetRsp, UserSelectedLLMData from apps.schemas.tag import UserTagListResponse from apps.schemas.user import UserInfo -from apps.services.llm import LLMManager -from apps.services.user import UserManager -from apps.services.user_tag import UserTagManager +from apps.services import LLMManager, UserManager, UserTagManager router = APIRouter( prefix="/api/user", diff --git a/apps/schemas/agent.py b/apps/schemas/agent.py index 3b7cc673ff727b37c9ec1e4895d83f62be12bfc1..d5b74a88547848919ffa7740f534b4927abd6d99 100644 --- a/apps/schemas/agent.py +++ b/apps/schemas/agent.py @@ -3,10 +3,9 @@ from pydantic import Field -from .enum_var import ( - AppType, - MetadataType, -) +from apps.models import AppType + +from .enum_var import MetadataType from .flow import MetadataBase, Permission diff --git a/apps/schemas/appcenter.py b/apps/schemas/appcenter.py index cf953c3d35f0acca390b1910722990ddbbebe2cc..d549ef563963bf03ea41e0fb535a8c978cc267f5 100644 --- a/apps/schemas/appcenter.py +++ b/apps/schemas/appcenter.py @@ -5,7 +5,8 @@ import uuid from pydantic import BaseModel, Field -from .enum_var import AppType, PermissionType +from apps.models import AppType, PermissionType + from .response_data import ResponseData diff --git a/apps/schemas/blacklist.py b/apps/schemas/blacklist.py index ec8a8e061517c5ba4792f21758d55a973387794a..145f0e45d92c2f11dcf9f4c59703f1c3d397d849 100644 --- a/apps/schemas/blacklist.py +++ b/apps/schemas/blacklist.py @@ -5,7 +5,7 @@ import uuid from pydantic import BaseModel -from apps.models.blacklist import Blacklist +from apps.models import Blacklist from apps.schemas.response_data import ResponseData diff --git a/apps/schemas/comment.py b/apps/schemas/comment.py index db5baaaa00c7456a21be0284fc33567701df4d40..1111f2091ebe0e161a39f49a9187e0e74dedfdde 100644 --- a/apps/schemas/comment.py +++ b/apps/schemas/comment.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, Field -from apps.schemas.enum_var import CommentType +from apps.models import CommentType class AddCommentData(BaseModel): diff --git a/apps/schemas/conversation.py b/apps/schemas/conversation.py index 3c310af859213f761cb7cd0a58efc039a191fa46..b43e5c5107cd8a7c73af2fbc623e8670a3c80e46 100644 --- a/apps/schemas/conversation.py +++ b/apps/schemas/conversation.py @@ -26,7 +26,7 @@ class ConversationListItem(BaseModel): title: str doc_count: int = Field(alias="docCount") created_time: str = Field(alias="createdTime") - app_id: str = Field(alias="appId") + app_id: uuid.UUID | None = Field(alias="appId") debug: bool = Field(alias="debug") diff --git a/apps/schemas/enum_var.py b/apps/schemas/enum_var.py index 53304d2eb72fd4e6d04ab157f2c55d398aad18d3..d5a8ba264048cbecff46cbfedfdeed11ec32bff7 100644 --- a/apps/schemas/enum_var.py +++ b/apps/schemas/enum_var.py @@ -12,31 +12,6 @@ class SlotType(str, Enum): KEYWORD = "keyword" -class StepStatus(str, Enum): - """步骤状态""" - - UNKNOWN = "unknown" - INIT = "init" - WAITING = "waiting" - RUNNING = "running" - SUCCESS = "success" - ERROR = "error" - PARAM = "param" - CANCELLED = "cancelled" - - -class ExecutorStatus(str, Enum): - """执行器状态""" - - UNKNOWN = "unknown" - INIT = "init" - WAITING = "waiting" - RUNNING = "running" - SUCCESS = "success" - ERROR = "error" - CANCELLED = "cancelled" - - class DocumentStatus(str, Enum): """文档状态""" @@ -101,14 +76,6 @@ class NodeType(str, Enum): CHOICE = "Choice" -class PermissionType(str, Enum): - """权限类型""" - - PROTECTED = "protected" - PUBLIC = "public" - PRIVATE = "private" - - class SearchType(str, Enum): """搜索类型""" @@ -156,31 +123,9 @@ class SpecialCallType(str, Enum): CHOICE = "Choice" -class CommentType(str, Enum): - """点赞点踩类型""" - - LIKE = "liked" - DISLIKE = "disliked" - NONE = "none" - - -class AppType(str, Enum): - """应用中心应用类型""" - - FLOW = "flow" - AGENT = "agent" - - class AppFilterType(str, Enum): """应用过滤类型""" ALL = "all" USER = "user" FAVORITE = "favorite" - - -class LanguageType(str, Enum): - """语言""" - - CHINESE = "zh" - ENGLISH = "en" diff --git a/apps/schemas/flow.py b/apps/schemas/flow.py index 46f750c86ec82666630b1d48278fa5cb49a2d8fb..26152f0e07808f5adcf0d1514b893a03d0fb0aec 100644 --- a/apps/schemas/flow.py +++ b/apps/schemas/flow.py @@ -6,13 +6,10 @@ from typing import Any from pydantic import BaseModel, Field +from apps.models import AppType, PermissionType + from .appcenter import AppLink -from .enum_var import ( - AppType, - EdgeType, - MetadataType, - PermissionType, -) +from .enum_var import EdgeType, MetadataType from .flow_topology import FlowItem from .response_data import ResponseData @@ -32,6 +29,7 @@ class PositionItem(BaseModel): x: float = Field(default=0.0) y: float = Field(default=0.0) + class Step(BaseModel): """Flow中Step的数据""" diff --git a/apps/schemas/llm.py b/apps/schemas/llm.py index 40aeceb625092719a2b953e87c823c2ff88ec293..d35a655cd1c5cc0aac8a0cef03dd41a7ba5b7136 100644 --- a/apps/schemas/llm.py +++ b/apps/schemas/llm.py @@ -1,7 +1,6 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """大模型返回的chunk""" -from enum import Enum from typing import Any from pydantic import BaseModel, Field @@ -15,32 +14,6 @@ class LLMChunk(BaseModel): tool_call: dict[str, Any] | None = None -class LLMType(str, Enum): - """大模型类型""" - - CHAT = "chat" - """模型支持Chat""" - FUNCTION = "function" - """模型支持Function Call""" - EMBEDDING = "embedding" - """模型支持Embedding""" - VISION = "vision" - """模型支持图片理解""" - THINKING = "thinking" - """模型支持思考推理""" - - -class LLMProvider(str, Enum): - """Function Call后端""" - - OLLAMA = "ollama" - """Ollama""" - OPENAI = "openai" - """OpenAI""" - TEI = "tei" - """TEI""" - - class LLMFunctions(BaseModel): """大模型可选调用的函数""" diff --git a/apps/schemas/message.py b/apps/schemas/message.py index 1101ae4f7dd800ecc26b3d59f421eb0ed9581d51..1e36cd275cbd40b75a2124a8004f1ebec083dd8c 100644 --- a/apps/schemas/message.py +++ b/apps/schemas/message.py @@ -7,7 +7,8 @@ from typing import Any from pydantic import BaseModel, Field -from apps.schemas.enum_var import EventType, ExecutorStatus, StepStatus +from apps.models import ExecutorStatus, StepStatus +from apps.schemas.enum_var import EventType from .record import RecordMetadata diff --git a/apps/schemas/record.py b/apps/schemas/record.py index 27673e51e60f4957fa0bd0d3d7ab8972b2e12545..9cfb87c7d12e15e6292bf9920540feb898df0256 100644 --- a/apps/schemas/record.py +++ b/apps/schemas/record.py @@ -7,7 +7,7 @@ from typing import Any, Literal from pydantic import BaseModel, Field -from apps.schemas.enum_var import CommentType, ExecutorStatus, StepStatus +from apps.models import CommentType, ExecutorStatus, StepStatus class RecordDocument(BaseModel): diff --git a/apps/schemas/request_data.py b/apps/schemas/request_data.py index de9d979d8acdfe1dda188971d51a4d774df579b1..b59918b9f68b1f6220e61ea1217463342e20d7c7 100644 --- a/apps/schemas/request_data.py +++ b/apps/schemas/request_data.py @@ -2,13 +2,14 @@ """FastAPI 请求体""" import uuid +from datetime import datetime from typing import Any from pydantic import BaseModel, Field -from .enum_var import LanguageType +from apps.models import LanguageType, LLMProvider + from .flow_topology import FlowItem -from .llm import LLMProvider from .message import FlowParams @@ -80,3 +81,4 @@ class UserUpdateRequest(BaseModel): user_name: str | None = Field(default=None, description="用户名", alias="userName") auto_execute: bool = Field(default=False, description="是否自动执行", alias="autoExecute") agreement_confirmed: bool | None = Field(default=None, description="协议确认状态", alias="agreementConfirmed") + last_login: datetime | None = Field(default=None, description="最后一次登录时间", alias="lastLogin") diff --git a/apps/schemas/response_data.py b/apps/schemas/response_data.py index 1b55b209c58ab5fbe006f0f15893485bb5489286..cdf370f289a1943ae40a1791a0d8986fb65723c1 100644 --- a/apps/schemas/response_data.py +++ b/apps/schemas/response_data.py @@ -98,21 +98,6 @@ class UserGetRsp(ResponseData): result: UserGetMsp -class LLMProvider(BaseModel): - """LLM提供商数据结构""" - - provider: str = Field(description="LLM提供商") - description: str = Field(description="LLM提供商描述") - url: str | None = Field(default=None, description="LLM提供商URL") - icon: str = Field(description="LLM提供商图标") - - -class ListLLMProviderRsp(ResponseData): - """GET /api/llm/provider 返回数据结构""" - - result: list[LLMProvider] = Field(default=[], title="Result") - - class LLMProviderInfo(BaseModel): """LLM数据结构""" diff --git a/apps/schemas/scheduler.py b/apps/schemas/scheduler.py index f99013c4e59dbc0e70a3bee124d4e1c69409d9ab..e78430589aa3e2f53e073745304a036ff7c8c5a0 100644 --- a/apps/schemas/scheduler.py +++ b/apps/schemas/scheduler.py @@ -6,12 +6,10 @@ from typing import Any from pydantic import BaseModel, Field -from apps.llm.embedding import Embedding -from apps.llm.function import FunctionLLM -from apps.llm.reasoning import ReasoningLLM -from apps.models.task import ExecutorHistory +from apps.llm import Embedding, FunctionLLM, ReasoningLLM +from apps.models import ExecutorHistory, LanguageType -from .enum_var import CallOutputType, LanguageType +from .enum_var import CallOutputType from .scheduler import ExecutorBackground diff --git a/deploy/chart/authhub/templates/NOTES.txt b/deploy/chart/authhub/templates/NOTES.txt index 2589763fe3986b0ab74926792e9153c0d9d4ec34..0d517be75bfa56dd02d705d19d1159e298559b6f 100644 --- a/deploy/chart/authhub/templates/NOTES.txt +++ b/deploy/chart/authhub/templates/NOTES.txt @@ -1,5 +1,5 @@ 感谢您使用openEuler Intelligence! -当前为0.9.6版本。 +当前为0.10.0版本。 当前Chart的功能为:AuthHub统一登录系统部署。 说明: diff --git a/deploy/chart/databases/templates/NOTES.txt b/deploy/chart/databases/templates/NOTES.txt index c3d1f59454b90bd0261ce8f4bd393f3760dc6785..5447b7de6e8c5bfda073a301b170f8aa6a779514 100644 --- a/deploy/chart/databases/templates/NOTES.txt +++ b/deploy/chart/databases/templates/NOTES.txt @@ -1,3 +1,3 @@ 感谢您使用openEuler Intelligence! -当前为0.9.6版本。 +当前为0.10.0版本。 当前Chart的功能为:数据库部署。 diff --git a/deploy/chart/euler_copilot/templates/NOTES.txt b/deploy/chart/euler_copilot/templates/NOTES.txt index ec7debb6b984f3e6a2aa2b3128301cd763bce5e6..fdb725dd2a450fde9946a1b45c00084b37c9e712 100644 --- a/deploy/chart/euler_copilot/templates/NOTES.txt +++ b/deploy/chart/euler_copilot/templates/NOTES.txt @@ -1,5 +1,5 @@ 感谢您使用openEuler Intelligence! -当前为0.9.6版本。 +当前为0.10.0版本。 当前Chart的功能为:openEuler Intelligence核心组件部署。 更多项目动态和分享会议,请关注openEuler sig-intelligence:https://www.openeuler.org/en/sig/sig-detail/?name=sig-intelligence diff --git a/deploy/chart/euler_copilot/templates/framework/framework.yaml b/deploy/chart/euler_copilot/templates/framework/framework.yaml index 034779540664714efb37287101bb70688ed2a394..cc94cdccdabb7cb9a2146385af15a6fbede2a2d2 100644 --- a/deploy/chart/euler_copilot/templates/framework/framework.yaml +++ b/deploy/chart/euler_copilot/templates/framework/framework.yaml @@ -37,7 +37,7 @@ spec: automountServiceAccountToken: false containers: - name: framework - image: {{ .Values.euler_copilot.framework.image | default (printf "%s/neocopilot/euler-copilot-framework:0.9.6-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} + image: {{ .Values.euler_copilot.framework.image | default (printf "%s/neocopilot/euler-copilot-framework:0.10.0-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} imagePullPolicy: {{ default "IfNotPresent" .Values.globals.imagePullPolicy }} ports: - containerPort: 8002 diff --git a/deploy/chart/euler_copilot/templates/rag-web/rag-web.yaml b/deploy/chart/euler_copilot/templates/rag-web/rag-web.yaml index fd9daf5379327bef3f0a470785bebf3ab3835a08..2b0ee2b83f3c3f57ef0f2b583102e2bdd7f6239d 100644 --- a/deploy/chart/euler_copilot/templates/rag-web/rag-web.yaml +++ b/deploy/chart/euler_copilot/templates/rag-web/rag-web.yaml @@ -37,7 +37,7 @@ spec: automountServiceAccountToken: false containers: - name: rag-web - image: {{ .Values.euler_copilot.rag_web.image | default (printf "%s/neocopilot/data_chain_web:0.9.6-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} + image: {{ .Values.euler_copilot.rag_web.image | default (printf "%s/neocopilot/data_chain_web:0.10.0-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} imagePullPolicy: {{ default "IfNotPresent" .Values.globals.imagePullPolicy }} ports: - containerPort: 9888 diff --git a/deploy/chart/euler_copilot/templates/rag/rag-config.yaml b/deploy/chart/euler_copilot/templates/rag/rag-config.yaml index 7ffc922d10808077cf1667d58d0dbb3c19c4fa96..6fb273af1be266c54905e9aa3ff07415afd1eaac 100644 --- a/deploy/chart/euler_copilot/templates/rag/rag-config.yaml +++ b/deploy/chart/euler_copilot/templates/rag/rag-config.yaml @@ -14,8 +14,8 @@ data: - from: /config/.env to: /config-rw/.env mode: - uid: 1001 - gid: 1001 + uid: 0 + gid: 0 mode: "0o650" secrets: - /db-secrets @@ -23,10 +23,10 @@ data: - from: /config/.env-sql to: /config-rw/.env-sql mode: - uid: 1001 - gid: 1001 + uid: 0 + gid: 0 mode: "0o650" secrets: - /db-secrets - /system-secrets -{{- end -}} \ No newline at end of file +{{- end -}} diff --git a/deploy/chart/euler_copilot/templates/rag/rag.yaml b/deploy/chart/euler_copilot/templates/rag/rag.yaml index 761623ef715b91cc70508aaa2a42bb4d75e824a1..334ec6d733b1c016673db096fcfd66edffeff480 100644 --- a/deploy/chart/euler_copilot/templates/rag/rag.yaml +++ b/deploy/chart/euler_copilot/templates/rag/rag.yaml @@ -39,21 +39,10 @@ spec: app: rag spec: automountServiceAccountToken: false - securityContext: - fsGroup: 1001 containers: - name: rag - image: {{ .Values.euler_copilot.rag.image | default (printf "%s/neocopilot/data_chain_back_end:0.9.6-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} + image: {{ .Values.euler_copilot.rag.image | default (printf "%s/neocopilot/data_chain_back_end:0.10.0-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} imagePullPolicy: {{ default "IfNotPresent" .Values.globals.imagePullPolicy }} - securityContext: - readOnlyRootFilesystem: {{ default false .Values.euler_copilot.framework.readOnly }} - capabilities: - drop: - - ALL - runAsUser: 1001 - runAsGroup: 1001 - runAsNonRoot: true - allowPrivilegeEscalation: false ports: - containerPort: 9988 protocol: TCP @@ -91,13 +80,6 @@ spec: - --config - config.yaml - --copy - securityContext: - capabilities: - drop: - - ALL - runAsUser: 1001 - runAsGroup: 1001 - runAsNonRoot: true volumeMounts: - mountPath: /config/.env name: rag-config-vl diff --git a/deploy/chart/euler_copilot/templates/web/web.yaml b/deploy/chart/euler_copilot/templates/web/web.yaml index c20c045dd686f0c1607467f5c680821afe7e7994..3a027427dd9939792f41bc2918840b8a9b1a8f38 100644 --- a/deploy/chart/euler_copilot/templates/web/web.yaml +++ b/deploy/chart/euler_copilot/templates/web/web.yaml @@ -36,7 +36,7 @@ spec: automountServiceAccountToken: false containers: - name: web - image: {{ .Values.euler_copilot.web.image | default (printf "%s/neocopilot/euler-copilot-web:0.9.6-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} + image: {{ .Values.euler_copilot.web.image | default (printf "%s/neocopilot/euler-copilot-web:0.10.0-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} imagePullPolicy: {{ default "IfNotPresent" .Values.globals.imagePullPolicy }} ports: - containerPort: 8080 diff --git a/deploy/chart/euler_copilot/values.yaml b/deploy/chart/euler_copilot/values.yaml index 64566314a46dda5e147c6203420e85ce1dafcaa1..2990ca90b59b3dd062c5578afa9039365490454a 100644 --- a/deploy/chart/euler_copilot/values.yaml +++ b/deploy/chart/euler_copilot/values.yaml @@ -25,7 +25,7 @@ models: # 用于Function Call的模型;建议使用特定推理框架 functionCall: # 推理框架类型,默认为ollama - # 可用的框架类型:["vllm", "ollama", "function_call", "json_mode", "structured_output"] + # 可用的框架类型:["vllm", "sglang", "ollama", "openai"] backend: # [必填] 模型地址;请根据 API 提供商文档确定是否需要带上“v1”后缀 # 选择不填则与问答模型一致 @@ -89,8 +89,8 @@ euler_copilot: framework: # [必填] 是否部署Framework后端框架服务 enabled: true - # 镜像设置;默认为hub.oepkgs.net/neocopilot/euler-copilot-framework:0.9.6-x86 - # 镜像标签:["0.9.6-x86", "0.9.6-arm"] + # 镜像设置;默认为hub.oepkgs.net/neocopilot/euler-copilot-framework:0.10.0-x86 + # 镜像标签:["0.10.0-x86", "0.10.0-arm"] image: # 容器根目录只读 readOnly: @@ -106,8 +106,8 @@ euler_copilot: web: # [必填] 是否部署Web前端用户界面 enabled: true - # 镜像设置;默认为hub.oepkgs.net/neocopilot/euler-copilot-web:0.9.6-x86 - # 镜像标签:["0.9.6-x86", "0.9.6-arm"] + # 镜像设置;默认为hub.oepkgs.net/neocopilot/euler-copilot-web:0.10.0-x86 + # 镜像标签:["0.10.0-x86", "0.10.0-arm"] image: # 容器根目录只读 readOnly: @@ -123,8 +123,8 @@ euler_copilot: rag_web: # [必填] 是否部署RAG Web前端用户界面 enabled: true - # 镜像设置;默认为hub.oepkgs.net/neocopilot/data_chain_web:0.9.6-x86 - # 镜像标签:["0.9.6-x86", "0.9.6-arm"] + # 镜像设置;默认为hub.oepkgs.net/neocopilot/data_chain_web:0.10.0-x86 + # 镜像标签:["0.10.0-x86", "0.10.0-arm"] image: # 容器根目录只读 readOnly: @@ -140,8 +140,8 @@ euler_copilot: rag: # [必填] 是否部署RAG后端服务 enabled: true - # 镜像设置;默认为hub.oepkgs.net/neocopilot/data_chain_back_end:0.9.6-x86 - # 镜像标签:["0.9.6-x86", "0.9.6-arm"] + # 镜像设置;默认为hub.oepkgs.net/neocopilot/data_chain_back_end:0.10.0-x86 + # 镜像标签:["0.10.0-x86", "0.10.0-arm"] image: # 容器根目录只读 readOnly: @@ -153,3 +153,5 @@ euler_copilot: type: # 当类型为NodePort时,填写主机的端口号 nodePort: + + diff --git a/deploy/scripts/0-one-click-deploy/one-click-deploy.sh b/deploy/scripts/0-one-click-deploy/one-click-deploy.sh index d1e2d6cdadfa547419b224ff7ecdd1879b0aa003..9e7ff827bf6d051fab9833a2c131c26e9f1451ed 100755 --- a/deploy/scripts/0-one-click-deploy/one-click-deploy.sh +++ b/deploy/scripts/0-one-click-deploy/one-click-deploy.sh @@ -21,6 +21,66 @@ NAMESPACE="euler-copilot" TIMEOUT=300 # 最大等待时间(秒) INTERVAL=10 # 检查间隔(秒) +# 全局变量声明 +authhub_address="" +eulercopilot_address="" + +# 解析命令行参数 +parse_arguments() { + while [[ $# -gt 0 ]]; do + case "$1" in + --eulercopilot_address) + if [ -n "$2" ]; then + eulercopilot_address="$2" + shift 2 + else + echo -e "${RED}错误: --eulercopilot_address 需要提供一个值${RESET}" >&2 + exit 1 + fi + ;; + --authhub_address) + if [ -n "$2" ]; then + authhub_address="$2" + shift 2 + else + echo -e "${RED}错误: --authhub_address 需要提供一个值${RESET}" >&2 + exit 1 + fi + ;; + *) + echo -e "${RED}未知选项: $1${RESET}" >&2 + exit 1 + ;; + esac + done +} + +# 提示用户输入必要参数 +prompt_for_addresses() { + # 如果未通过命令行参数提供eulercopilot_address,则提示用户输入 + if [ -z "$eulercopilot_address" ]; then + echo -e "${YELLOW}未提供 EulerCopilot 访问地址${RESET}" + read -p "$(echo -e "${CYAN}请输入 EulerCopilot 访问地址 (格式如: http://myhost:30080): ${RESET}")" eulercopilot_address + + # 验证输入是否为空 + while [ -z "$eulercopilot_address" ]; do + echo -e "${RED}错误: EulerCopilot 访问地址不能为空${RESET}" + read -p "$(echo -e "${CYAN}请输入 EulerCopilot 访问地址 (格式如: http://myhost:30080): ${RESET}")" eulercopilot_address + done + fi + + # 如果未通过命令行参数提供authhub_address,则提示用户输入 + if [ -z "$authhub_address" ]; then + echo -e "${YELLOW}未提供 Authhub 访问地址${RESET}" + read -p "$(echo -e "${CYAN}请输入 Authhub 访问地址 (格式如: http://myhost:30081): ${RESET}")" authhub_address + + # 验证输入是否为空 + while [ -z "$authhub_address" ]; do + echo -e "${RED}错误: Authhub 访问地址不能为空${RESET}" + read -p "$(echo -e "${CYAN}请输入 Authhub 访问地址 (格式如: http://myhost:30081): ${RESET}")" authhub_address + done + fi +} # 带颜色输出的进度条函数 colorful_progress() { @@ -52,12 +112,13 @@ print_step_title() { MAIN_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) cd "$MAIN_DIR" || exit 1 -# 带错误检查的脚本执行函数(改进版) run_script_with_check() { local script_path=$1 local script_name=$2 local step_number=$3 local auto_input=${4:-false} + shift 4 + local extra_args=("$@") # 使用数组来存储额外参数 # 前置检查:脚本是否存在 if [ ! -f "$script_path" ]; then @@ -74,6 +135,7 @@ run_script_with_check() { echo -e "${DIM}${BLUE}🠖 脚本绝对路径:${YELLOW}${script_abs_path}${RESET}" echo -e "${DIM}${BLUE}🠖 执行工作目录:${YELLOW}${script_dir}${RESET}" + echo -e "${DIM}${BLUE}🠖 额外参数:${YELLOW}${extra_args[*]}${RESET}" echo -e "${DIM}${BLUE}🠖 开始执行时间:${YELLOW}$(date +'%Y-%m-%d %H:%M:%S')${RESET}" # 创建临时日志文件 @@ -83,9 +145,9 @@ run_script_with_check() { # 执行脚本(带自动输入处理和实时日志输出) local exit_code=0 if $auto_input; then - (cd "$script_dir" && yes "" | bash "$script_base" 2>&1 | tee "$log_file") + (cd "$script_dir" && yes "" | bash "./$script_base" "${extra_args[@]}" 2>&1 | tee "$log_file") else - (cd "$script_dir" && bash "$script_base" 2>&1 | tee "$log_file") + (cd "$script_dir" && bash "./$script_base" "${extra_args[@]}" 2>&1 | tee "$log_file") fi exit_code=${PIPESTATUS[0]} @@ -160,15 +222,16 @@ show_header() { echo -e "\n${BOLD}${MAGENTA}$(printf '✧%.0s' $(seq 1 $(tput cols)))${RESET}" echo -e "${BOLD}${WHITE} Euler Copilot 一键部署系统 ${RESET}" echo -e "${BOLD}${MAGENTA}$(printf '✧%.0s' $(seq 1 $(tput cols)))${RESET}" - echo -e "${CYAN}◈ 主工作目录:${YELLOW}${MAIN_DIR}${RESET}\n" + echo -e "${CYAN}◈ 主工作目录:${YELLOW}${MAIN_DIR}${RESET}" + echo -e "${CYAN}◈ EulerCopilot地址:${YELLOW}${eulercopilot_address:-未设置}${RESET}" + echo -e "${CYAN}◈ Authhub地址:${YELLOW}${authhub_address:-未设置}${RESET}\n" } - -# 初始化部署流程 +# 修改后的start_deployment函数中的步骤配置 start_deployment() { local total_steps=8 local current_step=1 - # 步骤配置(脚本路径 脚本名称 自动输入) + # 步骤配置(脚本路径 脚本名称 自动输入 额外参数数组) local steps=( "../1-check-env/check_env.sh 环境检查 false" "_conditional_tools_step 基础工具安装(k3s+helm) true" @@ -176,27 +239,29 @@ start_deployment() { "../4-deploy-deepseek/deploy_deepseek.sh Deepseek模型部署 false" "../5-deploy-embedding/deploy-embedding.sh Embedding服务部署 false" "../6-install-databases/install_databases.sh 数据库集群部署 false" - "../7-install-authhub/install_authhub.sh Authhub部署 true" - "_conditional_eulercopilot_step EulerCopilot部署 true" + "../7-install-authhub/install_authhub.sh Authhub部署 true --authhub_address ${authhub_address}" + "_conditional_eulercopilot_step EulerCopilot部署 true" ) for step in "${steps[@]}"; do local script_path=$(echo "$step" | awk '{print $1}') - local script_name=$(echo "$step" | awk '{sub($1 OFS, ""); print $1}') - local auto_input=$(echo "$step" | awk '{print $NF}') - if [[ "$script_path" == "_conditional_tools_step" ]]; then + local script_name=$(echo "$step" | awk '{print $2}') + local auto_input=$(echo "$step" | awk '{print $3}') + local extra_args=$(echo "$step" | awk '{for(i=4;i<=NF;i++) printf $i" "}') + + # 特殊步骤处理 + if [[ "$script_path" == "_conditional_tools_step" ]]; then handle_tools_step $current_step elif [[ "$script_path" == "_conditional_eulercopilot_step" ]]; then + sleep 60 handle_eulercopilot_step $current_step - sleep 60 - elif ! run_script_with_check "$script_path" "$script_name" $current_step $auto_input; then - echo "Error: Script execution failed" + else + run_script_with_check "$script_path" "$script_name" $current_step $auto_input $extra_args fi colorful_progress $current_step $total_steps ((current_step++)) done - } # 处理工具安装步骤 @@ -210,14 +275,19 @@ handle_tools_step() { fi } -# 处理工具安装步骤 handle_eulercopilot_step() { local current_step=$1 - sleep 60 - run_script_with_check "../8-install-EulerCopilot/install_eulercopilot.sh" "EulerCopilot部署" $current_step true - + local extra_args=() + + # 构建额外参数数组 + [ -n "$authhub_address" ] && extra_args+=(--authhub_address "$authhub_address") + [ -n "$eulercopilot_address" ] && extra_args+=(--eulercopilot_address "$eulercopilot_address") + + run_script_with_check "../8-install-EulerCopilot/install_eulercopilot.sh" "EulerCopilot部署" $current_step true "${extra_args[@]}" } # 主执行流程 +parse_arguments "$@" +prompt_for_addresses show_header start_deployment diff --git a/deploy/scripts/2-install-tools/install_tools.sh b/deploy/scripts/2-install-tools/install_tools.sh index 8536d036514b1d814fb13265fc911c3fb4f45c65..3b19749f3394e0796d5dce44b070132a05c7115f 100755 --- a/deploy/scripts/2-install-tools/install_tools.sh +++ b/deploy/scripts/2-install-tools/install_tools.sh @@ -3,7 +3,7 @@ GITHUB_MIRROR="https://gh-proxy.com" ARCH=$(uname -m) TOOLS_DIR="/home/eulercopilot/tools" -eulercopilot_version=0.9.6 +eulercopilot_version=0.10.0 SCRIPT_PATH="$( cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 @@ -338,16 +338,6 @@ function check_k3s_status() { fi } -check_hub_connection() { - if curl -sSf http://hub.oepkgs.net >/dev/null 2>&1; then - echo -e "[Info] 镜像站连接正常" - return 0 - else - echo -e "[Error] 镜像站连接失败" - return 1 - fi -} - function main { # 创建工具目录 mkdir -p "$TOOLS_DIR" @@ -366,6 +356,13 @@ function main { else echo -e "[Info] K3s 已经安装,跳过安装步骤" fi + # 优先检查网络 + if check_network; then + echo -e "\033[32m[Info] 在线环境,跳过镜像导入\033[0m" + else + echo -e "\033[33m[Info] 离线环境,开始导入本地镜像,请确保本地目录已存在所有镜像文件\033[0m" + bash "$IMPORT_SCRIPT/9-other-script/import_images.sh" -v "$eulercopilot_version" + fi # 安装Helm(如果尚未安装) if ! command -v helm &> /dev/null; then @@ -377,14 +374,6 @@ function main { ln -sf /etc/rancher/k3s/k3s.yaml ~/.kube/config check_k3s_status - # 优先检查网络 - if check_hub_connection; then - echo -e "\033[32m[Info] 在线环境,跳过镜像导入\033[0m" - else - echo -e "\033[33m[Info] 离线环境,开始导入本地镜像,请确保本地目录已存在所有镜像文件\033[0m" - bash "$IMPORT_SCRIPT/9-other-script/import_images.sh" -v "$eulercopilot_version" - fi - echo -e "\n\033[32m=== 全部工具安装完成 ===\033[0m" echo -e "K3s 版本:$(k3s --version | head -n1)" echo -e "Helm 版本:$(helm version --short)" diff --git a/deploy/scripts/7-install-authhub/install_authhub.sh b/deploy/scripts/7-install-authhub/install_authhub.sh index 8c5bb37d4a28ec1e7602773ee794d1db64d6b67d..5c8d26a7c70efcdb024606303c845aef613863ea 100755 --- a/deploy/scripts/7-install-authhub/install_authhub.sh +++ b/deploy/scripts/7-install-authhub/install_authhub.sh @@ -18,6 +18,18 @@ CHART_DIR="$( dirname "$(dirname "$(dirname "$canonical_path")")" )/chart" +# 打印帮助信息 +print_help() { + echo -e "${GREEN}用法: $0 [选项]" + echo -e "选项:" + echo -e " --help 显示帮助信息" + echo -e " --authhub_address <地址> 指定Authhub的访问地址(例如:http://myhost:30081)" + echo -e "" + echo -e "示例:" + echo -e " $0 --authhub_address http://myhost:30081${NC}" + exit 0 +} + # 获取系统架构 get_architecture() { local arch=$(uname -m) @@ -120,7 +132,7 @@ helm_install() { echo -e "${BLUE}正在安装 authhub...${NC}" helm upgrade --install authhub -n euler-copilot ./authhub \ --set globals.arch="$arch" \ - --set domain.authhub="${authhub_address}" || { + --set domain.authhub="${authhub_address}" || { echo -e "${RED}Helm 安装 authhub 失败!${NC}" return 1 } @@ -165,12 +177,20 @@ check_pods_status() { done } -main() { +deploy() { local arch arch=$(get_architecture) || exit 1 create_namespace || exit 1 uninstall_authhub || exit 1 - get_authhub_address|| exit 1 + + # 如果未通过参数提供地址,则提示用户输入 + if [ -z "$authhub_address" ]; then + echo -e "${YELLOW}未提供 --authhub_address 参数,需要手动输入地址${NC}" + get_authhub_address || exit 1 + else + echo -e "${GREEN}使用参数指定的Authhub地址:$authhub_address${NC}" + fi + helm_install "$arch" || exit 1 check_pods_status || { echo -e "${RED}部署失败:Pod状态检查未通过!${NC}" @@ -185,5 +205,36 @@ main() { echo -e "=========================${NC}" } +# 解析命令行参数 +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --help) + print_help + exit 0 + ;; + --authhub_address) + if [ -n "$2" ]; then + authhub_address="$2" + shift 2 + else + echo -e "${RED}错误:--authhub_address 需要提供一个参数${NC}" >&2 + exit 1 + fi + ;; + *) + echo -e "${RED}未知参数: $1${NC}" >&2 + print_help + exit 1 + ;; + esac + done +} + +main() { + parse_args "$@" + deploy +} + trap 'echo -e "${RED}操作被中断!${NC}"; exit 1' INT main "$@" diff --git a/deploy/scripts/8-install-EulerCopilot/install_eulercopilot.sh b/deploy/scripts/8-install-EulerCopilot/install_eulercopilot.sh index b7961ebecfc8ff1921a84291af412ffe965eb507..2478656b41085dcde8ef87d7803dc1cfcaff4ea0 100755 --- a/deploy/scripts/8-install-EulerCopilot/install_eulercopilot.sh +++ b/deploy/scripts/8-install-EulerCopilot/install_eulercopilot.sh @@ -76,32 +76,6 @@ parse_arguments() { done } - -# 安装成功信息显示函数 -show_success_message() { - local host=$1 - local arch=$2 - - echo -e "\n${GREEN}==================================================${NC}" - echo -e "${GREEN} EulerCopilot 部署完成! ${NC}" - echo -e "${GREEN}==================================================${NC}" - - echo -e "${YELLOW}访问信息:${NC}" - echo -e "EulerCopilot UI: ${eulercopilot_address}" - echo -e "AuthHub 管理界面: ${authhub_address}" - - echo -e "\n${YELLOW}系统信息:${NC}" - echo -e "内网IP: ${host}" - echo -e "系统架构: $(uname -m) (识别为: ${arch})" - echo -e "插件目录: ${PLUGINS_DIR}" - echo -e "Chart目录: ${DEPLOY_DIR}/chart/" - - echo -e "${BLUE}操作指南:${NC}" - echo -e "1. 查看集群状态: kubectl get all -n $NAMESPACE" - echo -e "2. 查看实时日志: kubectl logs -n $NAMESPACE -f deployment/$NAMESPACE" - echo -e "3. 查看POD状态:kubectl get pods -n $NAMESPACE" -} - # 获取系统架构 get_architecture() { local arch=$(uname -m) @@ -284,19 +258,17 @@ uninstall_eulercopilot() { echo -e "${YELLOW}未找到需要清理的Helm Release: euler-copilot${NC}" fi - # 删除 PVC: framework-semantics-claim 和 web-static - local pvc_names=("framework-semantics-claim" "web-static") - for pvc_name in "${pvc_names[@]}"; do - if kubectl get pvc "$pvc_name" -n euler-copilot &>/dev/null; then - echo -e "${GREEN}找到PVC: ${pvc_name},开始清理...${NC}" - if ! kubectl delete pvc "$pvc_name" -n euler-copilot --force --grace-period=0; then - echo -e "${RED}错误:删除PVC ${pvc_name} 失败!${NC}" >&2 - return 1 - fi - else - echo -e "${YELLOW}未找到需要清理的PVC: ${pvc_name}${NC}" + # 删除 PVC: framework-semantics-claim + local pvc_name="framework-semantics-claim" + if kubectl get pvc "$pvc_name" -n euler-copilot &>/dev/null; then + echo -e "${GREEN}找到PVC: ${pvc_name},开始清理...${NC}" + if ! kubectl delete pvc "$pvc_name" -n euler-copilot --force --grace-period=0; then + echo -e "${RED}错误:删除PVC ${pvc_name} 失败!${NC}" >&2 + return 1 fi - done + else + echo -e "${YELLOW}未找到需要清理的PVC: ${pvc_name}${NC}" + fi # 删除 Secret: euler-copilot-system local secret_name="euler-copilot-system" @@ -323,7 +295,6 @@ modify_yaml() { # 添加其他必填参数 set_args+=( - "--set" "globals.arch=$arch" "--set" "login.client.id=${client_id}" "--set" "login.client.secret=${client_secret}" "--set" "domain.euler_copilot=${eulercopilot_address}" @@ -379,10 +350,11 @@ pre_install_checks() { # 执行安装 execute_helm_install() { + local arch=$1 echo -e "${BLUE}开始部署EulerCopilot(架构: $arch)...${NC}" >&2 enter_chart_directory - helm upgrade --install $NAMESPACE -n $NAMESPACE ./euler_copilot --create-namespace || { + helm upgrade --install $NAMESPACE -n $NAMESPACE ./euler_copilot --set globals.arch=$arch --create-namespace || { echo -e "${RED}Helm 安装 EulerCopilot 失败!${NC}" >&2 exit 1 } @@ -467,7 +439,7 @@ main() { modify_yaml "$host" "$preserve_models" echo -e "${BLUE}开始Helm安装...${NC}" - execute_helm_install + execute_helm_install "$arch" if check_pods_status; then echo -e "${GREEN}所有组件已就绪!${NC}" @@ -477,4 +449,31 @@ main() { fi } -main "$@" \ No newline at end of file +# 添加安装成功信息显示函数 +show_success_message() { + local host=$1 + local arch=$2 + + + echo -e "\n${GREEN}==================================================${NC}" + echo -e "${GREEN} EulerCopilot 部署完成! ${NC}" + echo -e "${GREEN}==================================================${NC}" + + echo -e "${YELLOW}访问信息:${NC}" + echo -e "EulerCopilot UI: ${eulercopilot_address}" + echo -e "AuthHub 管理界面: ${authhub_address}" + + echo -e "\n${YELLOW}系统信息:${NC}" + echo -e "内网IP: ${host}" + echo -e "系统架构: $(uname -m) (识别为: ${arch})" + echo -e "插件目录: ${PLUGINS_DIR}" + echo -e "Chart目录: ${DEPLOY_DIR}/chart/" + + echo -e "${BLUE}操作指南:${NC}" + echo -e "1. 查看集群状态: kubectl get pod -n $NAMESPACE" + echo -e "2. 查看实时日志: kubectl logs -f -n $NAMESPACE " + echo -e " 例如:kubectl logs -f framework-deploy-5577c87b6-h82g8 -n euler-copilot" + echo -e "3. 查看POD状态:kubectl get pods -n $NAMESPACE" +} + +main "$@" diff --git a/deploy/scripts/9-other-script/save_images.sh b/deploy/scripts/9-other-script/save_images.sh index edb06bc566c09d31c96e5d410b398b437a6f18fe..489560bd66a7e1d5d98a9a1cfd145dd325e480a0 100755 --- a/deploy/scripts/9-other-script/save_images.sh +++ b/deploy/scripts/9-other-script/save_images.sh @@ -8,7 +8,7 @@ BLUE='\033[0;34m' NC='\033[0m' # 恢复默认颜色 # 默认配置 -eulercopilot_version="0.9.6" +eulercopilot_version="0.10.0" ARCH_SUFFIX="" OUTPUT_DIR="/home/eulercopilot/images/${eulercopilot_version}" @@ -23,7 +23,7 @@ show_help() { echo -e " --arch <架构> 指定系统架构 (arm/x86, 默认自动检测)" echo -e "" echo -e "${YELLOW}示例:${NC}" - echo -e " $0 --version 0.9.6 --arch arm" + echo -e " $0 --version ${eulercopilot_version} --arch arm" echo -e " $0 --help" exit 0 } diff --git a/docs/en/intelligent_foundation/syshax/deploy_guide/syshax_deployment_guide.md b/docs/en/intelligent_foundation/syshax/deploy_guide/syshax_deployment_guide.md index e6f6e3f4c44cb1ac82bc04d17de0bf92326030a5..fd38118821511e314aae8f84090a6cc5fc417315 100644 --- a/docs/en/intelligent_foundation/syshax/deploy_guide/syshax_deployment_guide.md +++ b/docs/en/intelligent_foundation/syshax/deploy_guide/syshax_deployment_guide.md @@ -2,63 +2,65 @@ ## Overview -sysHAX is positioned as K+X heterogeneous fusion inference acceleration, mainly containing two functional components: +sysHAX is positioned as K+X heterogeneous fusion inference acceleration, mainly consisting of two functional components: -- Dynamic inference scheduling +- Inference dynamic scheduling - CPU inference acceleration -**Dynamic inference scheduling**: For inference tasks, the prefill phase belongs to compute-intensive tasks, while the decode phase belongs to memory-intensive tasks. Therefore, from the perspective of computational resources, the prefill phase is suitable for execution on GPU/NPU and other hardware, while the decode phase can be executed on CPU and other hardware. +**Inference dynamic scheduling**: For inference tasks, the prefill stage is a compute-intensive task, while the decode stage is memory-access intensive. Therefore, from the perspective of computing resources, the prefill stage is suitable for execution on hardware such as GPU/NPU, whereas the decode stage can be executed on hardware like CPU. -**CPU inference acceleration**: Accelerates CPU inference performance through NUMA affinity, parallel optimization, operator optimization, and other methods on the CPU. +**CPU inference acceleration**: Accelerate CPU inference performance through NUMA affinity, parallel optimization, and operator optimization. -sysHAX consists of two delivery components: +sysHAX includes two delivery components: -![syshax-deploy](pictures/syshax-deploy.png) +![syshax-deploy](pictures/syshax-deploy.png "syshax-deploy") The delivery components include: -- sysHAX: Responsible for request processing and scheduling of prefill and decode requests -- vllm: vllm is a large model inference service that includes both GPU/NPU and CPU during deployment, used for processing prefill and decode requests respectively. From the perspective of developer usability, vllm will be released using containerization. +- sysHAX: responsible for request processing and scheduling of prefill and decode requests +- vllm: vllm is a large model inference service that includes both GPU/NPU and CPU versions during deployment, used for handling prefill and decode requests respectively. From the developer's usability perspective, vllm will be released in containerized form. -vllm is a **high-throughput, low-memory-usage large language model (LLM) inference and service engine** that supports **CPU computation acceleration** and provides efficient operator dispatch mechanisms, including: +vllm is a **high-throughput, low-memory footprint** **large language model (LLM) inference and service engine** that supports **CPU computation acceleration**, providing an efficient operator dispatch mechanism, including: -- Schedule: Optimizes task distribution to improve parallel computation efficiency -- Prepare Input: Efficient data preprocessing to accelerate input construction -- Ray framework: Utilizes distributed computing to improve inference throughput -- Sample (model post-processing): Optimizes sampling strategies to improve generation quality -- Framework post-processing: Integrates multiple optimization strategies to improve overall inference performance +- Schedule: Optimize task distribution to improve parallel computing efficiency +- Prepare Input: Efficient data preprocessing to accelerate input building +- Ray Framework: Utilize distributed computing to enhance inference throughput +- Sample (Model Post-processing): Optimize sampling strategies to improve generation quality +- Framework Post-processing: Integrate multiple optimization strategies to enhance overall inference performance -This engine combines **efficient computation scheduling and optimization strategies** to provide **faster, more stable, and more scalable** solutions for LLM inference. +This engine combines **efficient computational scheduling and optimization strategies** to provide **faster, more stable, and scalable** solutions for LLM inference. ## Environment Preparation -| Server Model | Kunpeng 920 Series CPU | -| ------------ | ---------------------- | -| GPU | Nvidia A100 | -| OS | openEuler 22.03 LTS and above | -| Python | 3.9 and above | -| Docker | 25.0.3 and above | +| KEY | VALUE | +| ---------- | ---------------------------------------- | +| Server Model | Kunpeng 920 series CPU | +| GPU | Nvidia A100 | +| Operating System | openEuler 24.03 LTS SP1 | +| Python | 3.9 and above | +| Docker | 25.0.3 and above | - Docker 25.0.3 can be installed via `dnf install moby`. +- Note: sysHAX currently only supports NVIDIA GPU adaptation on AI acceleration cards, ASCEND NPU adaptation is in progress. ## Deployment Process -First, check whether NVIDIA drivers and CUDA drivers are already installed using `nvidia-smi` and `nvcc -V`. If not, you need to install NVIDIA drivers and CUDA drivers first. +First, you need to check if the NVIDIA driver and CUDA driver are installed via `nvidia-smi` and `nvcc -V`. If not, you need to install the NVIDIA driver and CUDA driver first. ### Install NVIDIA Container Toolkit (Container Engine Plugin) -If NVIDIA Container Toolkit is already installed, you can skip this step. Otherwise, follow the installation process below: +If NVIDIA Container Toolkit is already installed, you can skip this step. Otherwise, install it following the process below: -- Execute the `systemctl restart docker` command to restart docker, making the container engine plugin configuration in the docker config file effective. +- Execute the `systemctl restart docker` command to restart Docker, so that the content added by the container engine plugin in the Docker configuration file takes effect. -### Container-based vllm Setup +### Container Scenario vllm Setup The following process deploys vllm in a GPU container. ```shell -docker pull hub.oepkgs.net/neocopilot/syshax/syshax-vllm-gpu:0.2.0 +docker pull hub.oepkgs.net/neocopilot/syshax/syshax-vllm-gpu:0.2.1 docker run --name vllm_gpu \ --ipc="shareable" \ @@ -67,40 +69,42 @@ docker run --name vllm_gpu \ -p 8001:8001 \ -v /home/models:/home/models \ -w /home/ \ - -itd hub.oepkgs.net/neocopilot/syshax/syshax-vllm-gpu:0.2.0 bash + -itd hub.oepkgs.net/neocopilot/syshax/syshax-vllm-gpu:0.2.1 bash ``` In the above script: -- `--ipc="shareable"`: Allows the container to share IPC namespace for inter-process communication. -- `--shm-size=64g`: Sets the container shared memory to 64G. -- `--gpus=all`: Allows the container to use all GPU devices on the host -- `-p 8001:8001`: Port mapping, mapping the host's port 8001 to the container's port 8001. Developers can modify this as needed. -- `-v /home/models:/home/models`: Directory mounting, mapping the host's `/home/models` to `/home/models` inside the container for model sharing. Developers can modify the mapping directory as needed. +- `--ipc="shareable"`: Allows containers to share IPC namespaces for inter-process communication. +- `--shm-size=64g`: Sets container shared memory to 64G. +- `--gpus=all`: Allows containers to use all GPU devices on the host machine. +- `-p 8001:8001`: Port mapping, mapping port 8001 of the host machine to port 8001 of the container, which can be modified by developers. +- `-v /home/models:/home/models`: Directory mounting, mapping the host's `/home/models` to the container's `/home/models`, achieving model sharing. Developers can modify the mapping directory as needed. ```shell vllm serve /home/models/DeepSeek-R1-Distill-Qwen-32B \ --served-model-name=ds-32b \ --host 0.0.0.0 \ --port 8001 \ - --dtype=half \ + --dtype=auto \ --swap_space=16 \ --block_size=16 \ --preemption_mode=swap \ --max_model_len=8192 \ --tensor-parallel-size 2 \ - --gpu_memory_utilization=0.8 + --gpu_memory_utilization=0.8 \ + --enable-auto-pd-offload ``` In the above script: -- `--tensor-parallel-size 2`: Enables tensor parallelism, splitting the model across 2 GPUs. Requires at least 2 GPUs. Developers can modify this as needed. -- `--gpu_memory_utilization=0.8`: Limits GPU memory usage to 80% to avoid service crashes due to memory exhaustion. Developers can modify this as needed. +- `--tensor-parallel-size 2`: Enable tensor parallelism, splitting the model to run on 2 GPUs, requiring at least 2 GPUs, which can be modified by developers. +- `--gpu_memory_utilization=0.8`: Limit GPU memory utilization to 80% to prevent service crashes due to memory exhaustion, which can be modified by developers. +- `--enable-auto-pd-offload`: Enable PD separation when triggering swap out. The following process deploys vllm in a CPU container. ```shell -docker pull hub.oepkgs.net/neocopilot/syshax/syshax-vllm-cpu:0.2.0 +docker pull hub.oepkgs.net/neocopilot/syshax/syshax-vllm-cpu:0.2.1 docker run --name vllm_cpu \ --ipc container:vllm_gpu \ @@ -109,15 +113,15 @@ docker run --name vllm_cpu \ -p 8002:8002 \ -v /home/models:/home/models \ -w /home/ \ - -itd hub.oepkgs.net/neocopilot/syshax/syshax-vllm-cpu:0.2.0 bash + -itd hub.oepkgs.net/neocopilot/syshax/syshax-vllm-cpu:0.2.1 bash ``` In the above script: -- `--ipc container:vllm_gpu`: Shares the IPC (inter-process communication) namespace with the container named vllm_gpu. Allows this container to exchange data directly through shared memory, avoiding cross-container copying. +- `--ipc container:vllm_gpu` shares the IPC (inter-process communication) namespace of the container named vllm_gpu. This allows this container to exchange data directly through shared memory, avoiding cross-container copying. ```shell -INFERENCE_OP_MODE=fused OMP_NUM_THREADS=160 CUSTOM_CPU_AFFINITY=0-159 SYSHAX_QUANTIZE=q8_0 \ +NRC=4 INFERENCE_OP_MODE=fused OMP_NUM_THREADS=160 CUSTOM_CPU_AFFINITY=0-159 SYSHAX_QUANTIZE=q4_0 \ vllm serve /home/models/DeepSeek-R1-Distill-Qwen-32B \ --served-model-name=ds-32b \ --host 0.0.0.0 \ @@ -125,19 +129,21 @@ vllm serve /home/models/DeepSeek-R1-Distill-Qwen-32B \ --dtype=half \ --block_size=16 \ --preemption_mode=swap \ - --max_model_len=8192 + --max_model_len=8192 \ + --enable-auto-pd-offload ``` In the above script: -- `INFERENCE_OP_MODE=fused`: Enables CPU inference acceleration -- `OMP_NUM_THREADS=160`: Specifies the number of CPU inference threads to start as 160. This environment variable only takes effect after specifying INFERENCE_OP_MODE=fused. -- `CUSTOM_CPU_AFFINITY=0-159`: Specifies the CPU binding scheme, which will be explained in detail later. -- `SYSHAX_QUANTIZE=q8_0`: Specifies the quantization scheme as q8_0. The current version supports 2 quantization schemes: `q8_0`, `q4_0`. +- `INFERENCE_OP_MODE=fused`: Enable CPU inference acceleration +- `OMP_NUM_THREADS=160`: Specify the number of threads for CPU inference startup as 160. This environment variable only takes effect after setting INFERENCE_OP_MODE=fused. +- `CUSTOM_CPU_AFFINITY=0-159`: Specify the CPU binding scheme, which will be described in detail later. +- `SYSHAX_QUANTIZE=q4_0`: Specify the quantization scheme as q4_0. The current version supports 2 quantization schemes: `q8_0` and `q4_0`. +- `NRC=4`: GEMV operator block mode. This environment variable provides good acceleration effects on 920 series processors. -Note: The GPU container must be started first before starting the CPU container. +Note that the GPU container must be started before the CPU container can be started. -Use lscpu to check the current machine's hardware configuration, focusing on: +Use lscpu to check the current machine's hardware situation, focusing on: ```shell Architecture: aarch64 @@ -160,26 +166,33 @@ NUMA: NUMA node3 CPU(s): 120-159 ``` -This machine has 160 physical cores, no SMT enabled, 4 NUMA nodes, with 40 cores on each NUMA. +This machine has a total of 160 physical cores, with SMT disabled, and has 4 NUMA nodes, each with 40 cores. -Use these two environment variables to set the CPU binding scheme: `OMP_NUM_THREADS=160 CUSTOM_CPU_AFFINITY=0-159`. In these two environment variables, the first one is the number of CPU inference threads to start, and the second one is the IDs of the CPUs to bind. In CPU inference acceleration, to achieve NUMA affinity, CPU binding operations are required, following these rules: +Set the CPU binding scheme using these two scripts: `OMP_NUM_THREADS=160 CUSTOM_CPU_AFFINITY=0-159`. In these two environment variables, the first specifies the number of threads for CPU inference startup, and the second specifies the IDs of the bound CPUs. To achieve NUMA affinity in CPU inference acceleration, CPU binding operations are required, following the rules below: -- The number of started threads must match the number of bound CPUs; +- The number of startup threads must match the number of bound CPUs; - The number of CPUs used on each NUMA must be the same to maintain load balancing. -For example, in the above script, CPUs 0-159 are bound. Among them, 0-39 belong to NUMA node 0, 40-79 belong to NUMA node 1, 80-119 belong to NUMA node 2, and 120-159 belong to NUMA node 3. Each NUMA uses 40 CPUs, ensuring load balancing across all NUMAs. +For example, in the above script, CPUs 0-159 are bound. Among them, 0-39 belongs to NUMA node 0, 40-79 belongs to NUMA node 1, 80-119 belongs to NUMA node 2, and 120-159 belongs to NUMA node 3. Each NUMA uses 40 CPUs, ensuring load balancing for each NUMA. ### sysHAX Installation -sysHAX installation: +There are two ways to install sysHAX: you can install the rpm package via dnf. Note that this method requires upgrading openEuler to openEuler 24.03 LTS SP2 or higher version: ```shell dnf install sysHAX ``` -Before starting sysHAX, some basic configuration is needed: +Or directly start using the source code: ```shell +git clone -b v0.2.0 https://gitee.com/openeuler/sysHAX.git +``` + +Before starting sysHAX, some basic configurations are required: + +```shell +# When installing sysHAX via dnf install sysHAX syshax init syshax config services.gpu.port 8001 syshax config services.cpu.port 8002 @@ -187,15 +200,30 @@ syshax config services.conductor.port 8010 syshax config models.default ds-32b ``` -Additionally, you can use `syshax config --help` to view all configuration commands. +```shell +# When using git clone -b v0.2.0 https://gitee.com/openeuler/sysHAX.git +python3 cli.py init +python3 cli.py config services.gpu.port 8001 +python3 cli.py config services.cpu.port 8002 +python3 cli.py config services.conductor.port 8010 +python3 cli.py config models.default ds-32b +``` + +Additionally, you can use `syshax config --help` or `python3 cli.py config --help` to view all configuration commands. -After configuration is complete, start the sysHAX service with the following command: +After configuration, start the sysHAX service using the following command: ```shell +# When installing sysHAX via dnf install sysHAX syshax run ``` -When starting the sysHAX service, service connectivity testing will be performed. sysHAX complies with openAPI standards. Once the service is started, you can use APIs to call the large model service. You can test it with the following script: +```shell +# When using git clone -b v0.2.0 https://gitee.com/openeuler/sysHAX.git +python3 main.py +``` + +When starting the sysHAX service, connectivity testing will be performed. sysHAX complies with the openAPI standard. After the service starts, you can call the large model service via API. The following script can be used for testing: ```shell curl http://0.0.0.0:8010/v1/chat/completions -H "Content-Type: application/json" -d '{ @@ -203,10 +231,9 @@ curl http://0.0.0.0:8010/v1/chat/completions -H "Content-Type: application/json" "messages": [ { "role": "user", - "content": "Introduce openEuler." + "content": "介绍一下openEuler。" } ], "stream": true, "max_tokens": 1024 }' -``` diff --git a/docs/en/openeuler_intelligence/mcp_agent/_toc.yaml b/docs/en/openeuler_intelligence/mcp_agent/_toc.yaml index 7af448ed9960b64b9cf3359549c785b3ca155d09..c69c3f2d2bc0b0139e629ef3d2695bc2c2963b6a 100644 --- a/docs/en/openeuler_intelligence/mcp_agent/_toc.yaml +++ b/docs/en/openeuler_intelligence/mcp_agent/_toc.yaml @@ -1,6 +1,6 @@ -label: openEuler MCP Service Guide +label: openEuler MCP 服务指南 isManual: true -description: openEuler Intelligent MCP +description: openEuler智能化MCP sections: - - label: openEuler MCP Service Guide + - label: openEuler MCP服务指南 href: ./mcp_guide.md \ No newline at end of file diff --git a/docs/en/openeuler_intelligence/mcp_agent/mcp_guide.md b/docs/en/openeuler_intelligence/mcp_agent/mcp_guide.md index 68b35325d03ddabed51005746b9008b6fb89a0fc..2e437b1c4972880fb08afe09492c87062a4c905d 100644 --- a/docs/en/openeuler_intelligence/mcp_agent/mcp_guide.md +++ b/docs/en/openeuler_intelligence/mcp_agent/mcp_guide.md @@ -1,3 +1,90 @@ # MCP Service Guide -(Current content is being updated, please wait.) +## 1. Overview + +The current version of openEuler intelligence has enhanced support for MCP. The usage process is mainly divided into the following steps: + +1. Register MCP +2. Install MCP +3. Activate MCP and load configuration files +4. Build Agent based on the activated MCP +5. Test Agent +6. Publish Agent +7. Use Agent + +> **Note**: +> +> - Registration, installation, and activation of MCP require administrator privileges +> - Building, testing, publishing, and using Agent are ordinary user privilege operations +> - All Agent-related operations must be performed based on the activated MCP + +## 2. Registration, Installation and Activation of MCP + +The following process uses an administrator account as an example to demonstrate the complete management process of MCP: + +1. **Register MCP** + Register MCP to the openEuler intelligence system through the "MCP Registration" button in the plugin center + ![mcp register button](pictures/regeister_mcp_button.png) + + Clicking the button pops up the registration window (the default configurations for SSE and STDIO are as follows): + ![sse register window](pictures/sse_mcp_register.png) + ![stdio register window](pictures/stdio_mcp_register.png) + + Taking SSE registration as an example, fill in the configuration information and click "Save" + ![fill in mcp configuration file](pictures/add_mcp.png) + +2. **Install MCP** + + > **Note**: Before installing STDIO, you can adjust service dependency files and permissions in the `/opt/copilot/semantics/mcp/template` directory on the corresponding container or server + + Click the "Install" button on the registered MCP card to install + ![mcp install button](pictures/sse_mcp_intstalling.png) + +3. **View MCP Tools** + After successful installation, click the MCP card to view the tools supported by this service + ![mcp service tools](pictures/mcp_details.png) + +4. **Activate MCP** + Click the "Activate" button to enable the MCP service + ![mcp activate button](pictures/activate_mcp.png) + +## 3. Creation, Testing, Publishing and Using of Agent Applications + +The following operations can be completed by ordinary users, and all operations must be performed based on the activated MCP: + +1. **Create Agent Application** + Click the "Create Application" button in the application center + ![create agent application](pictures/create_app_button.png) + +2. **Configure Agent Application** + After successful creation, click the application card to enter the details page, where you can modify the application configuration information + ![modify agent application configuration](pictures/edit_Agent_app_message.png) + +3. **Associate MCP** + Click the "Add MCP" button, select the activated MCP from the list that pops up on the left to associate + ![add mcp](pictures/add_mcp_button.png) + ![mcp list](pictures/add_mcp.png) + +4. **Test Agent Application** + After completing MCP association and information configuration, click the "Test" button in the lower right corner to perform functional testing + ![test agent application](pictures/test_mcp.png) + +5. **Publish Agent Application** + After passing the test, click the "Publish" button in the lower right corner to publish the application + ![publish agent application](pictures/publish_app.png) + +6. **Use Agent Application** + The published application will be displayed in the application market, double-click to use + ![use agent application](pictures/click_and_use.png) + + Agent applications have two usage modes: + + - **Automatic Mode**: Operations are executed automatically without manual user confirmation + ![automatic use agent application](pictures/chat_with_auto_excute.png) + + - **Manual Mode**: Risks are prompted before execution, and execution requires user confirmation + ![manual use agent application](pictures/chat_with_not_auto_excute.png) + +## 4. Summary + +Through the above process, users can build and use customized Agent applications based on MCP. Welcome to experience and explore more functional scenarios. diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/activate_mcp.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/activate_mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..61533013f8629ce483c3ccb074ab0c1041c430cb Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/activate_mcp.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/add_mcp.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/add_mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..32f0f84879dcef4193cb912878bea5d87a86dd1c Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/add_mcp.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/add_mcp_button.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/add_mcp_button.png new file mode 100644 index 0000000000000000000000000000000000000000..ad6ceb728d861cf16bcc3fbe1071c1ff376e0956 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/add_mcp_button.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/chat_with_auto_excute.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/chat_with_auto_excute.png new file mode 100644 index 0000000000000000000000000000000000000000..f7eb8e06eeb54e3f5c038b60bfd603953cae30f3 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/chat_with_auto_excute.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/chat_with_not_auto_excute.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/chat_with_not_auto_excute.png new file mode 100644 index 0000000000000000000000000000000000000000..1bfa31b6396680e0359f2176750462ca33862594 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/chat_with_not_auto_excute.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/choose_Agent_app_to_create.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/choose_Agent_app_to_create.png new file mode 100644 index 0000000000000000000000000000000000000000..b1c200e6b0092c043b3cb99f3c7def4fe101ad94 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/choose_Agent_app_to_create.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/choose_mcp.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/choose_mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..4d50d4e3f6330062846d85d7bf7b66e34b1a3b98 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/choose_mcp.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/choose_mcp_2.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/choose_mcp_2.png new file mode 100644 index 0000000000000000000000000000000000000000..693d6e607c2872123e1a506237f53cacebad3af8 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/choose_mcp_2.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/click_and_use.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/click_and_use.png new file mode 100644 index 0000000000000000000000000000000000000000..281e8c736baf98c29cf3991e43dfe18cb4fc89be Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/click_and_use.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/create_app_button.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/create_app_button.png new file mode 100644 index 0000000000000000000000000000000000000000..fce7d20de9e28a6e066f0cd9be16fc2dcdd350c7 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/create_app_button.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/edit_Agent_app_message.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/edit_Agent_app_message.png new file mode 100644 index 0000000000000000000000000000000000000000..eea98c7db7b4c93ea54bd83b18687159ed7e4c6b Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/edit_Agent_app_message.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/installed_sse_mcp.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/installed_sse_mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..f9d98d5f7aa424e0ceef901688424538cd26b18d Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/installed_sse_mcp.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/mcp_details.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/mcp_details.png new file mode 100644 index 0000000000000000000000000000000000000000..f374a96687a4d98d65ee199be059212a61cf2692 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/mcp_details.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/publish_app.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/publish_app.png new file mode 100644 index 0000000000000000000000000000000000000000..3785369ddd009e4c0e2d08d2134dfffde5a101b6 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/publish_app.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/regeister_mcp_button.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/regeister_mcp_button.png new file mode 100644 index 0000000000000000000000000000000000000000..f3145eb9c93123b7b7417239f71d564f8ce4558c Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/regeister_mcp_button.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/sse_mcp_config.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/sse_mcp_config.png new file mode 100644 index 0000000000000000000000000000000000000000..2f9fba9632ebf4da993ac62283c4cf3120a45faf Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/sse_mcp_config.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/sse_mcp_intstalling.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/sse_mcp_intstalling.png new file mode 100644 index 0000000000000000000000000000000000000000..52f6ab4d5a51f3af5710c8c024e127fdff2a7e00 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/sse_mcp_intstalling.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/sse_mcp_register.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/sse_mcp_register.png new file mode 100644 index 0000000000000000000000000000000000000000..8ed20b064a11c3442ec7fafcf76bbd2f6fab0a64 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/sse_mcp_register.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/stdio_mcp_register.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/stdio_mcp_register.png new file mode 100644 index 0000000000000000000000000000000000000000..620a664fd63666ab224ea3d64d90417f482cba7a Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/stdio_mcp_register.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/test_mcp.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/test_mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..18143d4ea7e65f1b61b2618c06ac579ec516a018 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/test_mcp.png differ diff --git a/docs/en/openeuler_intelligence/mcp_agent/pictures/uninstalled_stdio_mcp.png b/docs/en/openeuler_intelligence/mcp_agent/pictures/uninstalled_stdio_mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..101ab687f2e28593fcce5dcfc27c1d2b8866d640 Binary files /dev/null and b/docs/en/openeuler_intelligence/mcp_agent/pictures/uninstalled_stdio_mcp.png differ diff --git a/docs/zh/intelligent_foundation/syshax/deploy_guide/syshax_deployment_guide.md b/docs/zh/intelligent_foundation/syshax/deploy_guide/syshax_deployment_guide.md index 452ccd2f4885015bcbf12a0087d6409e3f4f3b8d..4faa2e431f6192f7795df9a8e468af5573383c0b 100644 --- a/docs/zh/intelligent_foundation/syshax/deploy_guide/syshax_deployment_guide.md +++ b/docs/zh/intelligent_foundation/syshax/deploy_guide/syshax_deployment_guide.md @@ -12,7 +12,7 @@ sysHAX功能定位为K+X异构融合推理加速,主要包含两部分功能 sysHAX共包含两部分交付件: -![syshax-deploy](pictures/syshax-deploy.png) +![syshax-deploy](pictures/syshax-deploy.png "syshax-deploy") 交付件包括: - sysHAX:负责请求的处理和prefill、decode请求的调度 @@ -30,14 +30,16 @@ vllm是一款**高吞吐、低内存占用**的**大语言模型(LLM)推理 ## 环境准备 +| KEY | VALUE | +| ---------- | ---------------------------------------- | | 服务器型号 | 鲲鹏920系列CPU | -| ---------- | ----------------------------------------- | | GPU | Nvidia A100 | -| 操作系统 | openEuler 22.03 LTS及以上 | +| 操作系统 | openEuler 24.03 LTS SP1 | | python | 3.9及以上 | | docker | 25.0.3及以上 | - docker 25.0.3可通过 `dnf install moby` 进行安装。 +- 请注意,sysHAX目前在AI加速卡侧只对NVIDIA GPU进行了适配,ASCEND NPU适配正在进行中。 ## 部署流程 @@ -56,7 +58,7 @@ vllm是一款**高吞吐、低内存占用**的**大语言模型(LLM)推理 如下流程为在GPU容器中部署vllm。 ```shell -docker pull hub.oepkgs.net/neocopilot/syshax/syshax-vllm-gpu:0.2.0 +docker pull hub.oepkgs.net/neocopilot/syshax/syshax-vllm-gpu:0.2.1 docker run --name vllm_gpu \ --ipc="shareable" \ @@ -65,7 +67,7 @@ docker run --name vllm_gpu \ -p 8001:8001 \ -v /home/models:/home/models \ -w /home/ \ - -itd hub.oepkgs.net/neocopilot/syshax/syshax-vllm-gpu:0.2.0 bash + -itd hub.oepkgs.net/neocopilot/syshax/syshax-vllm-gpu:0.2.1 bash ``` 在上述脚本中: @@ -81,24 +83,26 @@ vllm serve /home/models/DeepSeek-R1-Distill-Qwen-32B \ --served-model-name=ds-32b \ --host 0.0.0.0 \ --port 8001 \ - --dtype=half \ + --dtype=auto \ --swap_space=16 \ --block_size=16 \ --preemption_mode=swap \ --max_model_len=8192 \ --tensor-parallel-size 2 \ - --gpu_memory_utilization=0.8 + --gpu_memory_utilization=0.8 \ + --enable-auto-pd-offload ``` 在上述脚本中: - `--tensor-parallel-size 2`:启用张量并行,将模型拆分到2张GPU上运行,需至少2张GPU,开发者可自行修改。 - `--gpu_memory_utilization=0.8`:限制显存使用率为80%,避免因为显存耗尽而导致服务崩溃,开发者可自行修改。 +- `--enable-auto-pd-offload`:启动在swap out时触发PD分离。 如下流程为在CPU容器中部署vllm。 ```shell -docker pull hub.oepkgs.net/neocopilot/syshax/syshax-vllm-cpu:0.2.0 +docker pull hub.oepkgs.net/neocopilot/syshax/syshax-vllm-cpu:0.2.1 docker run --name vllm_cpu \ --ipc container:vllm_gpu \ @@ -107,7 +111,7 @@ docker run --name vllm_cpu \ -p 8002:8002 \ -v /home/models:/home/models \ -w /home/ \ - -itd hub.oepkgs.net/neocopilot/syshax/syshax-vllm-cpu:0.2.0 bash + -itd hub.oepkgs.net/neocopilot/syshax/syshax-vllm-cpu:0.2.1 bash ``` 在上述脚本中: @@ -115,7 +119,7 @@ docker run --name vllm_cpu \ - `--ipc container:vllm_gpu`共享名为vllm_gpu的容器的IPC(进程间通信)命名空间。允许此容器直接通过共享内存交换数据,避免跨容器复制。 ```shell -INFERENCE_OP_MODE=fused OMP_NUM_THREADS=160 CUSTOM_CPU_AFFINITY=0-159 SYSHAX_QUANTIZE=q8_0 \ +NRC=4 INFERENCE_OP_MODE=fused OMP_NUM_THREADS=160 CUSTOM_CPU_AFFINITY=0-159 SYSHAX_QUANTIZE=q4_0 \ vllm serve /home/models/DeepSeek-R1-Distill-Qwen-32B \ --served-model-name=ds-32b \ --host 0.0.0.0 \ @@ -123,7 +127,8 @@ vllm serve /home/models/DeepSeek-R1-Distill-Qwen-32B \ --dtype=half \ --block_size=16 \ --preemption_mode=swap \ - --max_model_len=8192 + --max_model_len=8192 \ + --enable-auto-pd-offload ``` 在上述脚本中: @@ -131,7 +136,8 @@ vllm serve /home/models/DeepSeek-R1-Distill-Qwen-32B \ - `INFERENCE_OP_MODE=fused`:启动CPU推理加速 - `OMP_NUM_THREADS=160`:指定CPU推理启动线程数为160,该环境变量需要在指定INFERENCE_OP_MODE=fused后才能生效 - `CUSTOM_CPU_AFFINITY=0-159`:指定CPU绑核方案,后续会详细介绍。 -- `SYSHAX_QUANTIZE=q8_0`:指定量化方案为q8_0。当前版本支持2种量化方案:`q8_0`、`q4_0`。 +- `SYSHAX_QUANTIZE=q4_0`:指定量化方案为q4_0。当前版本支持2种量化方案:`q8_0`、`q4_0`。 +- `NRC=4`:GEMV算子分块方式,该环境变量在920系列处理器上具有较好的加速效果。 需要注意的是,必须先启动GPU的容器,才能启动CPU的容器。 @@ -169,15 +175,22 @@ NUMA: ### sysHAX安装 -sysHAX安装: +sysHAX安装有两种方式,可以通过dnf安装rpm包。注意,使用该方法需要将openEuler升级至openEuler 24.03 LTS SP2及以上版本: ```shell dnf install sysHAX ``` +或者直接使用源码启动: + +```shell +git clone -b v0.2.0 https://gitee.com/openeuler/sysHAX.git +``` + 在启动sysHAX之前需要进行一些基础配置: ```shell +# 使用 dnf install sysHAX 安装sysHAX时 syshax init syshax config services.gpu.port 8001 syshax config services.cpu.port 8002 @@ -185,14 +198,29 @@ syshax config services.conductor.port 8010 syshax config models.default ds-32b ``` -此外,也可以通过 `syshax config --help` 来查看全部配置命令。 +```shell +# 使用 git clone -b v0.2.0 https://gitee.com/openeuler/sysHAX.git 时 +python3 cli.py init +python3 cli.py config services.gpu.port 8001 +python3 cli.py config services.cpu.port 8002 +python3 cli.py config services.conductor.port 8010 +python3 cli.py config models.default ds-32b +``` + +此外,也可以通过 `syshax config --help` 或者 `python3 cli.py config --help` 来查看全部配置命令。 配置完成后,通过如下命令启动sysHAX服务: ```shell +# 使用 dnf install sysHAX 安装sysHAX时 syshax run ``` +```shell +# 使用 git clone -b v0.2.0 https://gitee.com/openeuler/sysHAX.git 时 +python3 main.py +``` + 启动sysHAX服务的时候,会进行服务连通性测试。sysHAX符合openAPI标准,待服务启动完成后,即可API来调用大模型服务。可通过如下脚本进行测试: ```shell diff --git a/docs/zh/openeuler_intelligence/intelligent_assistant/quick_start/smart_web/deploy_guide/offline.md b/docs/zh/openeuler_intelligence/intelligent_assistant/quick_start/smart_web/deploy_guide/offline.md index c79e5f562f408d50828876c54f5e466830706434..c40d99becf2f78414e20864982857df5da446df4 100644 --- a/docs/zh/openeuler_intelligence/intelligent_assistant/quick_start/smart_web/deploy_guide/offline.md +++ b/docs/zh/openeuler_intelligence/intelligent_assistant/quick_start/smart_web/deploy_guide/offline.md @@ -21,7 +21,7 @@ EulerCopilot 是一款智能问答工具,使用 EulerCopilot 可以解决操 | authhub-web-service | 8000 | 鉴权服务前端 | | mysql | 3306 (内部端口) | MySQL数据库 | | redis | 6379 (内部端口) | Redis数据库 | -| minio | 9000 (内部端口) 9001(外部部端口) | minio数据库 | +| minio | 9000 (内部端口) 9001(外部端口) | minio数据库 | | mongo | 27017 (内部端口) | mongo数据库 | | postgres | 5432 (内部端口) | 向量数据库 | | secret_inject | 无 | 配置文件安全复制工具 | diff --git a/docs/zh/openeuler_intelligence/intelligent_assistant/quick_start/smart_web/deploy_guide/online.md b/docs/zh/openeuler_intelligence/intelligent_assistant/quick_start/smart_web/deploy_guide/online.md index 59527296199c2a1f06b81575815e481cbf54c6c2..45e7de3c8ee098ab4bebdbd43d138a2106e32dc7 100644 --- a/docs/zh/openeuler_intelligence/intelligent_assistant/quick_start/smart_web/deploy_guide/online.md +++ b/docs/zh/openeuler_intelligence/intelligent_assistant/quick_start/smart_web/deploy_guide/online.md @@ -21,7 +21,7 @@ EulerCopilot 是一款智能问答工具,使用 EulerCopilot 可以解决操 | authhub-web-service | 8000 | 鉴权服务前端 | | mysql | 3306 (内部端口) | MySQL数据库 | | redis | 6379 (内部端口) | Redis数据库 | -| minio | 9000 (内部端口) 9001(外部部端口) | minio数据库 | +| minio | 9000 (内部端口) 9001(外部端口) | minio数据库 | | mongo | 27017 (内部端口) | mongo数据库 | | postgres | 5432 (内部端口) | 向量数据库 | | secret_inject | 无 | 配置文件安全复制工具 | diff --git a/docs/zh/openeuler_intelligence/mcp_agent/mcp_guide.md b/docs/zh/openeuler_intelligence/mcp_agent/mcp_guide.md index 1518c6b91c4729d8fc0b7d4234b62a6cb78df5b2..fe12cab4e6a39efb6e0fe27bede4487559317655 100644 --- a/docs/zh/openeuler_intelligence/mcp_agent/mcp_guide.md +++ b/docs/zh/openeuler_intelligence/mcp_agent/mcp_guide.md @@ -1,3 +1,90 @@ # MCP 服务指南 -(当前内容待更新,请等待) +## 1. 概述 + +openEuler intelligence 当前版本对 MCP 的支持已得到增强,使用流程主要分为以下步骤: + +1. 注册 MCP +2. 安装 MCP +3. 激活 MCP 并载入配置文件 +4. 基于已激活的 MCP 构建 Agent +5. 测试 Agent +6. 发布 Agent +7. 使用 Agent + +> **说明**: +> +> - 注册、安装和激活 MCP 需管理员权限操作 +> - 构建、测试、发布和使用 Agent 为普通用户权限操作 +> - 所有 Agent 相关操作均需基于已激活的 MCP 进行 + +## 2. MCP 的注册、安装与激活 + +以下流程以管理员账号为例,展示 MCP 的完整管理流程: + +1. **注册 MCP** + 通过插件中心的 "MCP 注册" 按钮,将 MCP 注册到 openEuler intelligence 系统中 + ![mcp注册按钮](pictures/regeister_mcp_button.png) + + 点击按钮后弹出注册窗口(SSE 和 STDIO 的默认配置如下): + ![sse注册窗口](pictures/sse_mcp_register.png) + ![stdio注册窗口](pictures/stdio_mcp_register.png) + + 以 SSE 注册为例,填写配置信息后点击"保存" + ![填入mcp配置文件](pictures/add_mcp.png) + +2. **安装 MCP** + + > **注意**:安装 STDIO 前,可在对应容器或服务器的 `/opt/copilot/semantics/mcp/template` 目录下调整服务依赖文件及权限 + + 点击已注册的 MCP 卡片上的"安装"按钮进行安装 + ![mcp安装按钮](pictures/sse_mcp_intstalling.png) + +3. **查看 MCP 工具** + 安装成功后,点击 MCP 卡片可查看该服务支持的工具 + ![mcp服务工具](pictures/mcp_details.png) + +4. **激活 MCP** + 点击"激活"按钮启用 MCP 服务 + ![mcp激活按钮](pictures/activate_mcp.png) + +## 3. Agent 应用的创建、测试、发布与使用 + +以下操作可由普通用户完成,所有操作均需基于已激活的 MCP 进行: + +1. **创建 Agent 应用** + 点击应用中心的"创建应用"按钮 + ![创建agent应用](pictures/create_app_button.png) + +2. **配置 Agent 应用** + 创建成功后,点击应用卡片进入详情页,可修改应用配置信息 + ![修改agent应用配置](pictures/edit_Agent_app_message.png) + +3. **关联 MCP** + 点击"添加 MCP"按钮,在左侧弹出的列表中选择已激活的 MCP 进行关联 + ![添加mcp](pictures/add_mcp_button.png) + ![mcp列表](pictures/add_mcp.png) + +4. **测试 Agent 应用** + 完成 MCP 关联和信息配置后,点击右下角"测试"按钮进行功能测试 + ![测试agent应用](pictures/test_mcp.png) + +5. **发布 Agent 应用** + 测试通过后,点击右下角"发布"按钮发布应用 + ![发布agent应用](pictures/publish_app.png) + +6. **使用 Agent 应用** + 发布后的应用将显示在应用市场中,双击即可使用 + ![使用agent应用](pictures/click_and_use.png) + + Agent 应用有两种使用模式: + + - **自动模式**:无需用户手动确认,自动执行操作 + ![自动使用agent应用](pictures/chat_with_auto_excute.png) + + - **手动模式**:执行前会提示风险,需用户确认后才执行 + ![手动使用agent应用](pictures/chat_with_not_auto_excute.png) + +## 4. 总结 + +通过上述流程,用户可基于 MCP 构建并使用自定义的 Agent 应用。欢迎体验并探索更多功能场景。 diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/activate_mcp.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/activate_mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..61533013f8629ce483c3ccb074ab0c1041c430cb Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/activate_mcp.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/add_mcp.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/add_mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..32f0f84879dcef4193cb912878bea5d87a86dd1c Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/add_mcp.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/add_mcp_button.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/add_mcp_button.png new file mode 100644 index 0000000000000000000000000000000000000000..ad6ceb728d861cf16bcc3fbe1071c1ff376e0956 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/add_mcp_button.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/chat_with_auto_excute.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/chat_with_auto_excute.png new file mode 100644 index 0000000000000000000000000000000000000000..f7eb8e06eeb54e3f5c038b60bfd603953cae30f3 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/chat_with_auto_excute.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/chat_with_not_auto_excute.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/chat_with_not_auto_excute.png new file mode 100644 index 0000000000000000000000000000000000000000..1bfa31b6396680e0359f2176750462ca33862594 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/chat_with_not_auto_excute.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/choose_Agent_app_to_create.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/choose_Agent_app_to_create.png new file mode 100644 index 0000000000000000000000000000000000000000..b1c200e6b0092c043b3cb99f3c7def4fe101ad94 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/choose_Agent_app_to_create.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/choose_mcp.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/choose_mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..4d50d4e3f6330062846d85d7bf7b66e34b1a3b98 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/choose_mcp.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/choose_mcp_2.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/choose_mcp_2.png new file mode 100644 index 0000000000000000000000000000000000000000..693d6e607c2872123e1a506237f53cacebad3af8 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/choose_mcp_2.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/click_and_use.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/click_and_use.png new file mode 100644 index 0000000000000000000000000000000000000000..281e8c736baf98c29cf3991e43dfe18cb4fc89be Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/click_and_use.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/create_app_button.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/create_app_button.png new file mode 100644 index 0000000000000000000000000000000000000000..fce7d20de9e28a6e066f0cd9be16fc2dcdd350c7 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/create_app_button.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/edit_Agent_app_message.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/edit_Agent_app_message.png new file mode 100644 index 0000000000000000000000000000000000000000..eea98c7db7b4c93ea54bd83b18687159ed7e4c6b Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/edit_Agent_app_message.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/installed_sse_mcp.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/installed_sse_mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..f9d98d5f7aa424e0ceef901688424538cd26b18d Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/installed_sse_mcp.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/mcp_details.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/mcp_details.png new file mode 100644 index 0000000000000000000000000000000000000000..f374a96687a4d98d65ee199be059212a61cf2692 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/mcp_details.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/publish_app.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/publish_app.png new file mode 100644 index 0000000000000000000000000000000000000000..3785369ddd009e4c0e2d08d2134dfffde5a101b6 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/publish_app.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/regeister_mcp_button.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/regeister_mcp_button.png new file mode 100644 index 0000000000000000000000000000000000000000..f3145eb9c93123b7b7417239f71d564f8ce4558c Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/regeister_mcp_button.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/sse_mcp_config.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/sse_mcp_config.png new file mode 100644 index 0000000000000000000000000000000000000000..2f9fba9632ebf4da993ac62283c4cf3120a45faf Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/sse_mcp_config.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/sse_mcp_intstalling.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/sse_mcp_intstalling.png new file mode 100644 index 0000000000000000000000000000000000000000..52f6ab4d5a51f3af5710c8c024e127fdff2a7e00 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/sse_mcp_intstalling.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/sse_mcp_register.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/sse_mcp_register.png new file mode 100644 index 0000000000000000000000000000000000000000..8ed20b064a11c3442ec7fafcf76bbd2f6fab0a64 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/sse_mcp_register.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/stdio_mcp_register.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/stdio_mcp_register.png new file mode 100644 index 0000000000000000000000000000000000000000..620a664fd63666ab224ea3d64d90417f482cba7a Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/stdio_mcp_register.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/test_mcp.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/test_mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..18143d4ea7e65f1b61b2618c06ac579ec516a018 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/test_mcp.png differ diff --git a/docs/zh/openeuler_intelligence/mcp_agent/pictures/uninstalled_stdio_mcp.png b/docs/zh/openeuler_intelligence/mcp_agent/pictures/uninstalled_stdio_mcp.png new file mode 100644 index 0000000000000000000000000000000000000000..101ab687f2e28593fcce5dcfc27c1d2b8866d640 Binary files /dev/null and b/docs/zh/openeuler_intelligence/mcp_agent/pictures/uninstalled_stdio_mcp.png differ diff --git "a/documents/user-guide/\351\203\250\347\275\262\346\214\207\345\215\227/\347\275\221\347\273\234\347\216\257\345\242\203\344\270\213\351\203\250\347\275\262\346\214\207\345\215\227.md" "b/documents/user-guide/\351\203\250\347\275\262\346\214\207\345\215\227/\347\275\221\347\273\234\347\216\257\345\242\203\344\270\213\351\203\250\347\275\262\346\214\207\345\215\227.md" index 708847f1c739b6f62f9ddb04a68d46ed5257bb7c..def52f6863e28a3e5b18e9f566e74b1c9501cfec 100644 --- "a/documents/user-guide/\351\203\250\347\275\262\346\214\207\345\215\227/\347\275\221\347\273\234\347\216\257\345\242\203\344\270\213\351\203\250\347\275\262\346\214\207\345\215\227.md" +++ "b/documents/user-guide/\351\203\250\347\275\262\346\214\207\345\215\227/\347\275\221\347\273\234\347\216\257\345\242\203\344\270\213\351\203\250\347\275\262\346\214\207\345\215\227.md" @@ -322,6 +322,7 @@ k3s ctr image import $image_tar ```bash # 重启framework服务 kubectl get pod -n euler-copilot + ```bash kubectl delete pod framework-deploy-65b669fc58-q9bw7 -n euler-copilot ``` diff --git a/tests/entities/test_user.py b/tests/entities/test_user.py deleted file mode 100644 index 01ac7ea340a027427786de868ecacfddc51e9b53..0000000000000000000000000000000000000000 --- a/tests/entities/test_user.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. - -import unittest -from apps.schemas.user import User - - -class TestUser(unittest.TestCase): - - def test_valid_user(self): - user = { - "user_sub": "1", - "passwd": None, - "organization": "openEuler", - "revision_number": "0.0.0.0" - } - - user_obj = User.model_validate(user) - self.assertEqual(user_obj.user_sub, "1") - self.assertEqual(user_obj.organization, "openEuler") - self.assertEqual(user_obj.revision_number, "0.0.0.0") - self.assertIsNone(user_obj.passwd) - - def test_invalid_user(self): - user = { - "user_sub": 1000, - "passwd": "123456", - "organization": None, - "revision_number": "0.0.0.0" - } - - self.assertRaises(Exception, User.model_validate, user) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/manager/__init__.py b/tests/manager/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/tests/manager/test_user.py b/tests/manager/test_user.py deleted file mode 100644 index 712cc9e6c791333098462286eae85a4eb5f60025..0000000000000000000000000000000000000000 --- a/tests/manager/test_user.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. - -import unittest -from unittest.mock import patch, MagicMock -from datetime import datetime -from apps.services.user import UserManager, User -from apps.db.mysql import User as UserModel, MysqlDB - - -class TestUserManager(unittest.TestCase): - - @patch('apps.models.mysql_db.MysqlDB') - @patch('apps.logger.get_logger') - def test_add_userinfo_failure(self, mock_get_logger, mock_mysql_db): - # 创建模拟的 logger 对象 - mock_logger = MagicMock() - - # 将模拟的 logger 对象分配给 UserManager.logger - UserManager.logger = mock_logger - - userinfo = User(userSub="test_user_sub", organization="test_org", revision_number="123") - - # 创建模拟的数据库会话对象 - mock_session = MagicMock() - - # 模拟 get_session 方法并返回模拟的数据库会话对象 - mock_get_session = MagicMock(return_value=mock_session) - mock_mysql_db.return_value.get_session = mock_get_session - - # 修改 mock_session,使其支持上下文管理器协议 - mock_session.__enter__.return_value = mock_session - mock_session.__exit__.return_value = False - - # 调用被测试方法 - UserManager.add_userinfo(userinfo) - - # 断言模拟的 logger 对象的 info 方法被调用一次,并且调用时传入了预期的日志信息 - mock_logger.info.assert_called_once_with("Add userinfo failed due to error: __enter__") - - @patch.object(MysqlDB, 'get_session') - def test_get_userinfo_by_user_sub_success(self, mock_get_session): - user_sub = "test_user_sub" - revision_number = 1 # 设置一个合适的修订号 - - # 创建模拟对象,确保与被测试的方法中使用的查询一致 - mock_query = MagicMock() - mock_user = UserModel(user_sub=user_sub, revision_number=revision_number) - mock_query.filter.return_value.first.side_effect = [mock_user, None] - mock_session = MagicMock(query=mock_query) - mock_get_session.return_value.__enter__.return_value = mock_session - - # 调用被测试的方法 - result = UserManager.get_user(user_sub) - - # 断言结果不为 None - self.assertIsNotNone(result) - - @patch('apps.models.mysql_db.MysqlDB') - @patch('apps.manager.user_manager.UserManager.get_userinfo_by_user_sub') - def test_update_userinfo_by_user_sub_success(self, mock_get_userinfo, mock_mysql_db): - # 创建测试数据 - userinfo = User(userSub="test_user_sub", organization="test_org", revision_number="123") - - # 模拟 get_userinfo_by_user_sub 方法返回一个已存在的用户信息对象 - mock_get_userinfo.return_value = userinfo - - # 模拟 MysqlDB 类的实例和方法 - mock_session = MagicMock() - mock_query = MagicMock() - mock_query.filter.return_value.first.return_value = None # 模拟用户信息不存在的情况 - mock_session.query.return_value = mock_query - mock_mysql_db_instance = mock_mysql_db.return_value - mock_mysql_db_instance.get_session.return_value = mock_session - - # 调用被测方法 - updated_userinfo = UserManager.update_user(userinfo, refresh_revision=True) - - # 断言返回的用户信息的 revision_number 是否与原始用户信息一致 - self.assertEqual(updated_userinfo.revision_number, userinfo.revision_number) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/routers/test_auth.py b/tests/routers/test_auth.py index 4c3d4b0806e94d0dad4ec142e630dbbc2d5e3198..6ab05d0e8d10dae9637019a4d58e2f9ce68c190d 100644 --- a/tests/routers/test_auth.py +++ b/tests/routers/test_auth.py @@ -6,7 +6,7 @@ from fastapi.testclient import TestClient from jwt import encode -from apps.routers.auth import router +from apps.routers.auth import admin_router access_token = encode({"sub": "user_id"}, "secret_key", algorithm="HS256") @@ -19,7 +19,7 @@ class TestAuthorizeRouter(unittest.TestCase): @patch('apps.routers.authorize.UserManager.update_userinfo_by_user_sub') def test_oidc_login_success(self, mock_update_userinfo, mock_get_redis_connection, mock_get_oidc_user, mock_get_oidc_token): - client = TestClient(router) + client = TestClient(admin_router) mock_update_userinfo.return_value = None mock_get_oidc_token.return_value = "access_token" mock_get_oidc_user.return_value = {'user_sub': '123'} @@ -33,7 +33,7 @@ class TestAuthorizeRouter(unittest.TestCase): @patch('apps.routers.authorize.RedisConnectionPool.get_redis_connection') def test_oidc_login_fail(self, mock_get_redis_connection): - client = TestClient(router) + client = TestClient(admin_router) mock_redis = MagicMock() mock_get_redis_connection.return_value = mock_redis mock_redis.setex.return_value = None @@ -47,7 +47,7 @@ class TestAuthorizeRouter(unittest.TestCase): @patch('apps.routers.authorize.RedisConnectionPool.get_redis_connection') def test_logout(self, mock_get_redis_connection): - client = TestClient(router) + client = TestClient(admin_router) mock_redis = MagicMock() mock_get_redis_connection.return_value = mock_redis mock_redis.delete.return_value = None @@ -62,7 +62,7 @@ class TestAuthorizeRouter(unittest.TestCase): @patch('apps.routers.authorize.UserManager.get_revision_number_by_user_sub') def test_userinfo(self, mock_get_revision_number_by_user_sub): - client = TestClient(router) + client = TestClient(admin_router) mock_get_revision_number_by_user_sub.return_value = "123" response = client.get("/authorize/user", cookies={"_t": "access_token"}) assert response.status_code == 200 @@ -74,7 +74,7 @@ class TestAuthorizeRouter(unittest.TestCase): @patch('apps.routers.authorize.UserManager.update_userinfo_by_user_sub') def test_update_revision_number(self, mock_update_userinfo_by_user_sub): - client = TestClient(router) + client = TestClient(admin_router) mock_update_userinfo_by_user_sub.return_value = None response = client.post("/authorize/update_revision_number", json={"revision_num": "123"}, cookies={"_t": "access_token"}) diff --git a/tests/entities/__init__.py b/tests/schemas/__init__.py similarity index 100% rename from tests/entities/__init__.py rename to tests/schemas/__init__.py diff --git a/tests/entities/test_blacklist.py b/tests/schemas/test_blacklist.py similarity index 100% rename from tests/entities/test_blacklist.py rename to tests/schemas/test_blacklist.py diff --git a/tests/entities/test_plugin.py b/tests/schemas/test_plugin.py similarity index 100% rename from tests/entities/test_plugin.py rename to tests/schemas/test_plugin.py diff --git a/tests/entities/test_request_data.py b/tests/schemas/test_request_data.py similarity index 100% rename from tests/entities/test_request_data.py rename to tests/schemas/test_request_data.py diff --git a/tests/entities/test_response_data.py b/tests/schemas/test_response_data.py similarity index 100% rename from tests/entities/test_response_data.py rename to tests/schemas/test_response_data.py diff --git a/tests/entities/test_session.py b/tests/schemas/test_session.py similarity index 100% rename from tests/entities/test_session.py rename to tests/schemas/test_session.py diff --git a/tests/schemas/test_user.py b/tests/schemas/test_user.py new file mode 100644 index 0000000000000000000000000000000000000000..160fb2c102d9f932aaa4c794120d2dcb0dfc4967 --- /dev/null +++ b/tests/schemas/test_user.py @@ -0,0 +1,48 @@ +"""UserInfo模型的单元测试""" + +from apps.schemas.user import UserInfo + + +def test_user_info_creation() -> None: + """测试UserInfo对象的创建""" + # 测试默认值 + user = UserInfo() + assert user.user_sub == "" + assert user.user_name == "" + + # 测试指定值 + user = UserInfo(user_sub="sub123", user_name="test_user") # pyright: ignore[reportCallIssue] + assert user.user_sub == "sub123" + assert user.user_name == "test_user" + + +def test_user_info_alias() -> None: + """测试UserInfo的别名功能""" + # 测试使用别名创建对象 + user = UserInfo(userSub="sub123", userName="test_user") + assert user.user_sub == "sub123" + assert user.user_name == "test_user" + + +def test_user_info_validation() -> None: + """测试UserInfo的数据验证""" + # 测试正常情况 + user = UserInfo(userSub="sub123", userName="test_user") + assert user.user_sub == "sub123" + assert user.user_name == "test_user" + + # 测试空字符串 + user = UserInfo(userSub="", userName="") + assert user.user_sub == "" + assert user.user_name == "" + + +def test_user_info_str_representation() -> None: + """测试UserInfo的字符串表示""" + user = UserInfo(userSub="sub123", userName="test_user") + # 确保对象可以正确转换为字符串 + str_repr = str(user) + assert "UserInfo" in str_repr + assert "sub123" in str_repr + assert "test_user" in str_repr + diff --git a/tests/scripts/__init__.py b/tests/scripts/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/tests/scripts/test_user_exporter.py b/tests/scripts/test_user_exporter.py deleted file mode 100644 index 7c843924fbb8f88e29b1802db957155ee2f5ee17..0000000000000000000000000000000000000000 --- a/tests/scripts/test_user_exporter.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. - -import unittest -import os -import shutil -from unittest.mock import patch, mock_open, MagicMock -from apps.utils.user_exporter import UserExporter - - -class TestUserExporter(unittest.TestCase): - - def setUp(self): - self.user_sub = "test_user_sub" - - @patch('apps.utils.user_exporter.UserManager.get_userinfo_by_user_sub') - @patch('apps.utils.user_exporter.UserQaRecordManager.get_user_qa_record_by_user_sub') - @patch('apps.utils.user_exporter.QaManager.query_encrypted_qa_pair_by_sessionid') - def test_export_user_data(self, mock_query_encrypted_qa_pair, mock_get_user_qa_record, mock_get_userinfo): - mock_get_userinfo.return_value = MagicMock() - mock_get_user_qa_record.return_value = [MagicMock()] - mock_query_encrypted_qa_pair.return_value = [MagicMock(), MagicMock()] - zip_file_path = UserExporter.export_user_data(self.user_sub) - assert os.path.exists(zip_file_path) - os.remove(zip_file_path) - - @patch('apps.utils.user_exporter.UserManager.get_userinfo_by_user_sub') - def test_export_user_info_to_xlsx(self, mock_get_userinfo): - mock_get_userinfo.return_value = MagicMock() - tmp_out_dir = './temp_dir' - if not os.path.exists(tmp_out_dir): - os.mkdir(tmp_out_dir) - xlsx_file_path = os.path.join(tmp_out_dir, 'user_info_' + self.user_sub + '.xlsx') - UserExporter.export_user_info_to_xlsx(tmp_out_dir, self.user_sub) - assert os.path.exists(xlsx_file_path) - os.remove(xlsx_file_path) - os.rmdir(tmp_out_dir) - - @patch('apps.utils.user_exporter.UserQaRecordManager.get_user_qa_record_by_user_sub') - @patch('apps.utils.user_exporter.QaManager.query_encrypted_qa_pair_by_sessionid') - def test_export_chats_to_xlsx(self, mock_query_encrypted_qa_pair, mock_get_user_qa_record): - mock_get_user_qa_record.return_value = [MagicMock()] - mock_query_encrypted_qa_pair.return_value = [MagicMock(), MagicMock()] - tmp_out_dir = './temp_dir' - if not os.path.exists(tmp_out_dir): - os.mkdir(tmp_out_dir) - xlsx_file_path = os.path.join(tmp_out_dir, 'chat_title_2024-02-27 12:00:00.xlsx') - UserExporter.export_chats_to_xlsx(tmp_out_dir, self.user_sub) - assert os.path.exists(xlsx_file_path) - os.remove(xlsx_file_path) - os.rmdir(tmp_out_dir) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/manager/test_abuse.py b/tests/services/test_abuse.py similarity index 100% rename from tests/manager/test_abuse.py rename to tests/services/test_abuse.py diff --git a/tests/manager/test_blacklist.py b/tests/services/test_blacklist.py similarity index 100% rename from tests/manager/test_blacklist.py rename to tests/services/test_blacklist.py diff --git a/tests/manager/test_comment.py b/tests/services/test_comment.py similarity index 100% rename from tests/manager/test_comment.py rename to tests/services/test_comment.py diff --git a/tests/manager/test_conversation.py b/tests/services/test_conversation.py similarity index 100% rename from tests/manager/test_conversation.py rename to tests/services/test_conversation.py diff --git a/tests/manager/test_record.py b/tests/services/test_record.py similarity index 100% rename from tests/manager/test_record.py rename to tests/services/test_record.py diff --git a/tests/services/test_user.py b/tests/services/test_user.py new file mode 100644 index 0000000000000000000000000000000000000000..f6704cf2ce15ffdc17b7c2a979b302bc78ba6420 --- /dev/null +++ b/tests/services/test_user.py @@ -0,0 +1,163 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. + +from unittest.mock import AsyncMock, patch + +import pytest +from sqlalchemy.ext.asyncio import AsyncSession + +from apps.models import User +from apps.schemas.request_data import UserUpdateRequest +from apps.services.user import UserManager + + +@pytest.fixture +def mock_session(): + """创建模拟的数据库会话""" + session = AsyncMock(spec=AsyncSession) + return session + + +@pytest.fixture +def mock_user(): + """创建测试用户对象""" + user = User( + userSub="test_user_sub", + isActive=True, + isWhitelisted=False, + credit=100, + ) + return user + + +@pytest.mark.asyncio +class TestUserManager: + """测试 UserManager 类""" + + @patch("apps.services.user.postgres.session") + async def test_list_user(self, mock_session_maker, mock_session): + """测试 list_user 方法""" + # 准备测试数据 + mock_users = [ + User(userSub="user1", isActive=True, isWhitelisted=False, credit=100), + User(userSub="user2", isActive=False, isWhitelisted=True, credit=200), + ] + + # 配置模拟会话 + mock_session_maker.return_value.__aenter__.return_value = mock_session + mock_session.scalar.return_value = 2 + mock_session.scalars.return_value.all.return_value = mock_users + + # 调用被测试方法 + users, count = await UserManager.list_user(n=10, page=1) + + # 验证结果 + assert len(users) == 2 + assert count == 2 + mock_session.scalar.assert_called_once() + mock_session.scalars.assert_called_once() + + @patch("apps.services.user.postgres.session") + async def test_get_user_found(self, mock_session_maker, mock_user): + """测试 get_user 方法 - 找到用户""" + # 配置模拟会话 + mock_session_maker.return_value.__aenter__.return_value = mock_user + mock_session = AsyncMock() + mock_session_maker.return_value.__aenter__.return_value = mock_session + mock_session.scalars.return_value.one_or_none.return_value = mock_user + + # 调用被测试方法 + result = await UserManager.get_user("test_user_sub") + + # 验证结果 + assert result == mock_user + mock_session.scalars.assert_called_once() + + @patch("apps.services.user.postgres.session") + async def test_get_user_not_found(self, mock_session_maker): + """测试 get_user 方法 - 未找到用户""" + # 配置模拟会话 + mock_session = AsyncMock() + mock_session_maker.return_value.__aenter__.return_value = mock_session + mock_session.scalars.return_value.one_or_none.return_value = None + + # 调用被测试方法 + result = await UserManager.get_user("nonexistent_user") + + # 验证结果 + assert result is None + mock_session.scalars.assert_called_once() + + @patch("apps.services.user.postgres.session") + async def test_update_user_create_new(self, mock_session_maker, mock_user): + """测试 update_user 方法 - 创建新用户""" + # 配置模拟会话 + mock_session = AsyncMock() + mock_session_maker.return_value.__aenter__.return_value = mock_session + mock_session.scalars.return_value.one_or_none.return_value = None + + # 准备测试数据 + update_data = UserUpdateRequest( + isActive=True, + isWhitelisted=False, + credit=150, + ) + + # 调用被测试方法 + await UserManager.update_user("new_user_sub", update_data) + + # 验证调用 + mock_session.merge.assert_called_once() + mock_session.commit.assert_called_once() + + @patch("apps.services.user.postgres.session") + async def test_update_user_update_existing(self, mock_session_maker, mock_user): + """测试 update_user 方法 - 更新现有用户""" + # 配置模拟会话 + mock_session = AsyncMock() + mock_session_maker.return_value.__aenter__.return_value = mock_session + mock_session.scalars.return_value.one_or_none.return_value = mock_user + + # 准备测试数据 + update_data = UserUpdateRequest( + isActive=False, + credit=200 + ) + + # 调用被测试方法 + await UserManager.update_user("test_user_sub", update_data) + + # 验证调用 + assert mock_user.isActive == False + assert mock_user.credit == 200 + assert mock_user.lastLogin is not None + mock_session.commit.assert_called_once() + + @patch("apps.services.user.postgres.session") + async def test_delete_user_found(self, mock_session_maker, mock_user): + """测试 delete_user 方法 - 找到并删除用户""" + # 配置模拟会话 + mock_session = AsyncMock() + mock_session_maker.return_value.__aenter__.return_value = mock_session + mock_session.scalars.return_value.one_or_none.return_value = mock_user + + # 调用被测试方法 + await UserManager.delete_user("test_user_sub") + + # 验证调用 + mock_session.delete.assert_called_once_with(mock_user) + mock_session.commit.assert_called_once() + + @patch("apps.services.user.postgres.session") + async def test_delete_user_not_found(self, mock_session_maker): + """测试 delete_user 方法 - 未找到用户""" + # 配置模拟会话 + mock_session = AsyncMock() + mock_session_maker.return_value.__aenter__.return_value = mock_session + mock_session.scalars.return_value.one_or_none.return_value = None + + # 调用被测试方法 + await UserManager.delete_user("nonexistent_user") + + # 验证调用 + mock_session.delete.assert_not_called() + mock_session.commit.assert_not_called()