(9-5-1)智能客服Agent开发:API服务(1)

9.6  API服务

“API服务”模块提供了处理各种API相关操作的后端服务,包括对话管理、路由事件、处理消息、与数据库交互以及建立WebSocket连接等功能。该模块与对话代理和消息处理器等其他组件集成,实现系统与外部客户端之间的高效通信和数据管理。该模块在促进无缝API交互和实时通信管理方面发挥了重要作用。

9.6.1  FastAPI服务

文件api.py实现了一个基于 FastAPI 的 API 服务,提供了多种与对话管理和数据集更新相关的功能。能够处理对话和消息的管理、数据集的重新加载、以及相关的 API 操作,适用于基于对话系统的应用。

cfg = Config()

dialog_agent = DialogAgent(cfg=cfg)
logger = cfg.logger("server")

def run_migrations():
    alembic_cfg = AlembicConfig("alembic.ini")
    command.upgrade(alembic_cfg, "head")

@asynccontextmanager
async def lifespan(_app: FastAPI):
    print(
        json.dumps(
            cfg.model_dump(),
            indent=4,
        )
    )
    run_migrations()
    await dialog_agent.load()
    try:
        yield
    finally:
        await dialog_agent.unload()

app = FastAPI(lifespan=lifespan)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.middleware("http")
async def check_api_key(request, call_next):
    try:
        if cfg.server.api_key is not None:
            if "Authorization" in request.headers:
                api_key = request.headers["Authorization"].split(" ")[-1]
                if api_key != cfg.server.api_key:
                    return JSONResponse(status_code=401, content={"detail": "Неверный api_key"})
        return await call_next(request)
    except Exception as e:
        if cfg.server.debug:
            raise e
        return JSONResponse(status_code=500, content={"detail": "Внутренняя ошибка сервера"})

@app.post("/dialog/{external_id}/ask/async")
async def ask_async(external_id: int, message: CreateMessageRequst):
    try:
        message = Message(text=message.text, media=message.media, sender=MSG_SENDER_USER)
        message_id = await dialog_agent.add_new_message(external_id, message)

        return {"message_id": message_id}
    except Exception as e:
        if cfg.server.debug:
            raise e
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/dialog/{external_id}/ask/sync")
async def ask_sync(external_id: int, message: CreateMessageRequst):
    try:
        message = Message(text=message.text, media=message.media, sender=MSG_SENDER_USER)
        return await dialog_agent.add_new_message(external_id, message, False)
    except Exception as e:
        if cfg.server.debug:
            raise e
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/agent/dataset/search")
async def search(message: CreateMessageRequst):
    try:
        message = Message(text=message.text, media=message.media, sender=MSG_SENDER_USER)
        result = await dialog_agent.vector_store.retrieval(dialog_agent.agent_instance, [message.text], use_consolidation=False)
        return {"result": result}
    except Exception as e:
        if cfg.server.debug:
            raise e
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/dialog/{external_id}")
async def get_chat(external_id: int):
    dialog: Dialog | None = dialog_agent.dialogs_manager.get_dialog(external_id)
    if dialog is None:
        raise HTTPException(status_code=404, detail="Диалог не найден")
    return {
        "dialog_id": dialog.id,
        "external_id": dialog.external_id,
        "user": {
            "id": dialog.user_id,
            "type": dialog.user_type,
            "name": dialog.meta.get("name", "Пользователь"),
        },
        "thread_id": dialog.thread_id,
        "started_at": dialog.meta.get("started_at"),
        "ended_at": dialog.meta.get("ended_at"),
        "meta": dialog.meta,
        "price": dialog.price,
    }

@app.get("/dialogs")
async def get_chats():
    dialogs = []
    for dialog in dialog_agent.dialogs_manager.dialogs.values():
        dialogs.append(
            {
                "dialog_id": dialog.id,
                "external_id": dialog.external_id,
                "user": {
                    "id": dialog.user_id,
                    "type": dialog.user_type,
                    "name": dialog.meta.get("name", "Пользователь"),
                },
                "thread_id": dialog.thread_id,
                "started_at": dialog.meta.get("started_at"),
                "ended_at": dialog.meta.get("ended_at"),
                "meta": dialog.meta,
                "price": dialog.price,
            }
        )
    dialogs.sort(key=lambda x: x["dialog_id"], reverse=True)
    return dialogs

@app.get("/dialog/{external_id}/messages")
async def get_messages(external_id: int, before_id: int, after_id: int, limit: int = 10):
    dialog = dialog_agent.dialogs_manager.get_dialog(external_id)
    return await dialog_agent.db.get_messages(dialog_id=dialog.id, before_id=before_id, after_id=after_id, limit=limit)

@app.post("/dialog/{external_id}")
async def open_dialog(external_id: int, request: OpenDialogRequest):
    try:
        await dialog_agent.open_dialog(external_id, request)
    except Exception as e:
        if cfg.server.debug:
            raise e
        raise HTTPException(status_code=500, detail=str(e))

@app.delete("/dialog/{external_id}")
async def close_dialog(external_id: int):
    try:
        await dialog_agent.close_dialog(external_id)
    except Exception as e:
        if cfg.server.debug:
            raise e
        raise HTTPException(status_code=500, detail=str(e))

@app.patch("/dialog/{external_id}")
async def update_dialog(external_id: int, request: UpdateDialogRequest):
    try:
        await dialog_agent.update_dialog(external_id, request.user_id)
    except Exception as e:
        if cfg.server.debug:
            raise e
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/dialog/typing/{user_id}/{user_type}")
async def user_typing(user_id: int, user_type: str):
    try:
        dialog_id = dialog_agent.dialogs_manager.get_dialog_id_by_user(user_id, user_type)
        if dialog_id:
            dialog = dialog_agent.dialogs_manager.get_dialog(dialog_id)
            if dialog:
                await dialog_agent.message_processor.trigger_processing(dialog)
                return {"status": "ok"}
        raise HTTPException(status_code=404, detail="Диалог не найден")
    except Exception as e:
        if cfg.server.debug:
            raise e
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/agent/dataset/reload/state")
async def get_reload_state():
    return {"reload_state": dialog_agent.reload_state}


@app.post("/agent/dataset/reload/file/sync")
async def make_reload_sync(files: List[UploadFile] = File(..., max_length=300), group: str | None = None):
    # Проверка наличия файлов
    if not files:
        raise HTTPException(status_code=400, detail="Требуется хотя бы один файл")

    # Проверка формата всех файлов
    for file in files:
        allowed_extensions = ["txt", "json"]
        if file.filename.split(".")[-1] not in allowed_extensions:
            raise HTTPException(status_code=400, detail=f"Файл {file.filename} имеет неверное расширение. Допустимы только " + ", ".join(allowed_extensions))

    # Чтение и декодирование файлов
    if len(files) > 1:
        contents = []

        for file in files:
            content_bytes = await file.read()

            if file.filename.split(".")[-1] == "txt":
                contents.append(content_bytes.decode("utf-8"))
            if file.filename.split(".")[-1] == "json":
                contents.append(json.loads(content_bytes.decode("utf-8")))

    else:
        contents = (await files[0].read()).decode("utf-8")

    # Если загружен более чем один файл, возвращаем массив содержимого файлов
    await dialog_agent.reload_dataset(contents, group)

    return {"reload_state": dialog_agent.reload_state}


@app.post("/agent/dataset/reload/sync")
async def make_reload_sync():
    await dialog_agent.reload_dataset()

    return {"reload_state": dialog_agent.reload_state}

@app.post("/agent/dataset/reload/async")
async def make_reload_async(background_tasks: BackgroundTasks):
    background_tasks.add_task(dialog_agent.reload_dataset)
    return {"status": "ok"}

@app.put("/agent")
async def make_create_agent():
    cloud_id = await dialog_agent.agent.create_cloud()
    if cloud_id != dialog_agent.agent_instance.cloud_id:
        await dialog_agent.db.update_agent_instance(dialog_agent.agent_instance.id, cloud_id=dialog_agent.agent_instance.cloud_id)
    return {"status": "ok"}

@app.get("/status")
async def get_status():
    return {"status": "ok"}

if __name__ == "__main__":
    import uvicorn
    log_config = uvicorn.config.LOGGING_CONFIG
    log_config["formatters"]["default"]["fmt"] = "%(asctime)s - %(levelname)s - %(message)s"
    log_config["formatters"]["default"]["use_colors"] = True
    log_config["formatters"]["default"]["datefmt"] = "%Y-%m-%d %H:%M:%S,000"
    log_config["formatters"]["access"]["fmt"] = "%(asctime)s - %(levelname)s - %(message)s"
    log_config["formatters"]["access"]["use_colors"] = True
    log_config["formatters"]["access"]["datefmt"] = "%Y-%m-%d %H:%M:%S,000"
    uvicorn.run("app.cli.api:app", host="0.0.0.0", port=cfg.server.port, log_level="debug" if cfg.server.debug else "error", log_config=log_config)

总体而言,上述代码实现了一个高效的后端服务,具体功能包括:

  1. 对话管理:通过 /dialog/{external_id} 路由实现对话的查询、创建、更新和关闭功能,支持获取对话的详细信息以及与之相关的消息。
  2. 消息处理:通过 ask_async 和 ask_sync 接口处理用户的提问,并根据请求方式(同步或异步)返回相应的结果。
  3. 数据集操作:提供了接口来搜索数据集、同步或异步重新加载数据集,以及上传和处理文件(如 .txt 和 .json 格式的文件)。
  4. API 密钥验证:使用中间件 check_api_key 对传入请求的 API 密钥进行验证,确保只有授权用户才能访问服务。
  5. 状态查询:通过 /status 接口返回当前服务状态,确保服务处于正常运行状态。

9.6.2  对话Agent

文件dialog_agent.py实现了对话代理(DialogAgent)的核心功能,负责管理对话的生命周期和消息处理。此文件包含了数据集重载、对话加载与卸载、消息添加、对话开启、关闭和更新等操作,同时还负责初始化与外部 API、数据库、WebSocket 以及向量存储等组件的交互,确保整个对话系统能够高效、稳定地运作。

import ssl
from datetime import datetime
# 请确保导入所需的类和函数,例如 AgentInstance, BaseLLM, VectorStore, WebSocket, BackendClient, Database, DialogsManager, MessageProcessor, EventRouter, BaseAgent, create_llm, create_vector_store, create_agent, Message, MSG_SENDER_USER, OpenDialogRequest, etc.

# 创建默认的 SSL 上下文
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE


class DialogAgent:
    # 类属性声明
    agent_instance: AgentInstance
    llm: BaseLLM
    vector_store: VectorStore
    ws: WebSocket
    bc: BackendClient
    db: Database
    dialogs_manager: DialogsManager
    message_processor: MessageProcessor
    event_router: EventRouter
    agent: BaseAgent

    def __init__(self, cfg: Config):
        self.cfg = cfg
        self.logger = cfg.logger("agent_instance")
        self.reload_state = "loaded"  # 数据集初始状态为 loaded

    async def reload_dataset(self, dataset: list[dict] | str | list[str] | None = None, group: str | None = None):
        # 将数据集重载状态设置为进行中
        self.reload_state = "in_progress"

        # 如果未提供数据集,则通过 BackendClient 获取数据集
        dataset = await self.bc.get_dataset() if not dataset else dataset

        try:
            # 构建向量存储数据集
            await self.vector_store.build_dataset(dataset, group)
            self.agent_instance.config["vs_loaded"] = True
            await self.db.update_agent_instance(self.agent_instance.id, config=self.agent_instance.config)
            self.reload_state = "loaded"
        except Exception as e:
            self.logger.error(f"Ошибка при перезагрузке датасета: {e}")
            self.reload_state = "error"

        # 通知外部服务当前数据集重载状态
        await self.bc.notify_reload_state(self.reload_state)
        self.logger.info("Датасет успешно перезагружен.")  # 数据集成功重载

    async def load(self):
        # 初始化数据库客户端
        self.db = Database(
            logger=self.cfg.logger("db"),
            url=self.cfg.db.url,
            name=self.cfg.db.name,
            pool_size=self.cfg.db.pool_size,
            max_overflow=self.cfg.db.max_overflow,
            pool_timeout=self.cfg.db.pool_timeout,
            pool_recycle=self.cfg.db.pool_recycle,
        )

        # 初始化外部 API 客户端
        self.ws = WebSocket(self.cfg)
        self.bc = BackendClient(self.cfg)

        # 初始化代理
        self.agent_instance = await self.db.get_agent_instance(self.cfg.agent.id)
        if not self.agent_instance:
            raise Exception(f"Агент с id={self.cfg.agent.id} не найден.")  # 未找到指定 ID 的代理
        self.llm = create_llm(self.cfg, self.agent_instance.type)
        self.vector_store = create_vector_store(cfg=self.cfg, llm=self.llm, preset=self.agent_instance.config.get("vs_preset"))
        self.agent = create_agent(self.agent_instance, self.cfg, self.vector_store, self.llm)

        self.agent_instance.cloud_id = await self.agent.create_cloud()
        await self.db.update_agent_instance(self.agent_instance.id, cloud_id=self.agent_instance.cloud_id)

        # 创建用于分离逻辑的管理器
        self.dialogs_manager = DialogsManager(self.db, self.agent, self.logger)
        self.message_processor = MessageProcessor(self.cfg, self.agent, self.db, self.bc, self.ws)
        self.event_router = EventRouter(self.cfg)

        await self.dialogs_manager.load_active()

    async def unload(self):
        # 更新活动对话状态
        await self.dialogs_manager.update_active()

    async def add_new_message(self, external_id: int, message: Message, is_async: bool = True) -> dict:
        dialog = self.dialogs_manager.get_dialog(external_id)
        if not dialog:
            raise Exception(f"Диалог с external_id={external_id} не найден.")  # 未找到对应 external_id 的对话

        dialog.queue.append(message)

        message = await self.db.create_message(
            Message(
                dialog_id=dialog.id,
                sender=MSG_SENDER_USER,
                text=message.text,
                media=message.media,
                created_at=datetime.now(),
            )
        )

        if is_async:
            await self.message_processor.trigger_processing(dialog)
            return {"id": message.id}

        return await self.message_processor.process_messages_sync(dialog)

    async def close_dialog(self, external_id: int):
        await self.dialogs_manager.close_dialog(external_id)

    async def update_dialog(self, external_id: int, user_id: int):
        await self.dialogs_manager.update_dialog(external_id, user_id)

    async def open_dialog(self, external_id: int, request: OpenDialogRequest):
        dialog = await self.dialogs_manager.open_dialog(external_id, request.user_id, request.user_type, request.meta)
        self.logger.info(f"Открыт диалог: {external_id} -> {dialog.id}")  # 对话已开启
        return dialog

    async def process_typing(self, user_id: int, user_type: str):
        dialog_id = self.dialogs_manager.get_dialog_id_by_user(user_id, user_type)
        if dialog_id:
            dialog = self.dialogs_manager.get_dialog(dialog_id)
            if dialog:
                await self.message_processor.trigger_processing(dialog)

    async def create_cloud(self):
        cloud_id = await self.agent.create_cloud()
        if cloud_id != self.agent_instance.cloud_id:
            await self.db.update_agent_instance(self.agent_instance.id, cloud_id=self.agent_instance.cloud_id)

        self.agent_instance = await self.db.get_agent_instance(self.cfg.agent.id)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农三叔

感谢鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值