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)
总体而言,上述代码实现了一个高效的后端服务,具体功能包括:
- 对话管理:通过 /dialog/{external_id} 路由实现对话的查询、创建、更新和关闭功能,支持获取对话的详细信息以及与之相关的消息。
- 消息处理:通过 ask_async 和 ask_sync 接口处理用户的提问,并根据请求方式(同步或异步)返回相应的结果。
- 数据集操作:提供了接口来搜索数据集、同步或异步重新加载数据集,以及上传和处理文件(如 .txt 和 .json 格式的文件)。
- API 密钥验证:使用中间件 check_api_key 对传入请求的 API 密钥进行验证,确保只有授权用户才能访问服务。
- 状态查询:通过 /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)