refactor: Repository 重构(agent_config → agent)+ 新增 agent_router
- 删除旧的 agent_config_repository,新增 agent_repository - 更新 models_business.py 中 Agent 模型 - 新增 agent_router,将 agent 配置管理从 chat_router 拆分 - 删除废弃的迁移文件 20260518_0001_agent_config_uid.py - 更新 lifespan、manager.py 中的 Agent 表结构迁移
This commit is contained in:
parent
01a052de8a
commit
20ac022cf3
@ -1,265 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from yuxi.storage.postgres.models_business import AgentConfig
|
||||
from yuxi.utils.datetime_utils import utc_now_naive
|
||||
|
||||
# 默认配置名称
|
||||
DEFAULT_CONFIG_NAME = "初始配置"
|
||||
|
||||
|
||||
def _merge_skill_slugs(current_slugs: object, new_slugs: list[str]) -> list[str]:
|
||||
merged: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for value in [*(current_slugs if isinstance(current_slugs, list) else []), *new_slugs]:
|
||||
if not isinstance(value, str):
|
||||
continue
|
||||
slug = value.strip()
|
||||
if not slug or slug in seen:
|
||||
continue
|
||||
seen.add(slug)
|
||||
merged.append(slug)
|
||||
return merged
|
||||
|
||||
|
||||
class AgentConfigRepository:
|
||||
def __init__(self, db_session: AsyncSession):
|
||||
self.db = db_session
|
||||
|
||||
async def list_by_user_agent(self, *, uid: str, agent_id: str) -> list[AgentConfig]:
|
||||
result = await self.db.execute(
|
||||
select(AgentConfig)
|
||||
.where(AgentConfig.uid == uid, AgentConfig.agent_id == agent_id)
|
||||
.order_by(AgentConfig.is_default.desc(), AgentConfig.id.asc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_id(self, config_id: int) -> AgentConfig | None:
|
||||
result = await self.db.execute(select(AgentConfig).where(AgentConfig.id == config_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def _get_by_id_for_update(self, config_id: int) -> AgentConfig | None:
|
||||
result = await self.db.execute(select(AgentConfig).where(AgentConfig.id == config_id).with_for_update())
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def set_default(self, *, config: AgentConfig, updated_by: str | None = None) -> AgentConfig:
|
||||
now = utc_now_naive()
|
||||
|
||||
# 先清空该用户+智能体的所有默认配置
|
||||
await self.db.execute(
|
||||
update(AgentConfig)
|
||||
.where(
|
||||
AgentConfig.uid == config.uid,
|
||||
AgentConfig.agent_id == config.agent_id,
|
||||
)
|
||||
.values(is_default=False, updated_at=now, updated_by=updated_by)
|
||||
)
|
||||
|
||||
# 再设置目标配置为默认
|
||||
config.is_default = True
|
||||
config.updated_at = now
|
||||
config.updated_by = updated_by
|
||||
|
||||
await self.db.commit()
|
||||
await self.db.refresh(config)
|
||||
return config
|
||||
|
||||
async def get_default(self, *, uid: str, agent_id: str) -> AgentConfig | None:
|
||||
result = await self.db.execute(
|
||||
select(AgentConfig).where(
|
||||
AgentConfig.uid == uid,
|
||||
AgentConfig.agent_id == agent_id,
|
||||
AgentConfig.is_default.is_(True),
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_or_create_default(
|
||||
self,
|
||||
*,
|
||||
uid: str,
|
||||
agent_id: str,
|
||||
created_by: str | None = None,
|
||||
) -> AgentConfig:
|
||||
existing = await self.get_default(uid=uid, agent_id=agent_id)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
items = await self.list_by_user_agent(uid=uid, agent_id=agent_id)
|
||||
if items:
|
||||
return items[0]
|
||||
|
||||
config = AgentConfig(
|
||||
uid=uid,
|
||||
agent_id=agent_id,
|
||||
name=DEFAULT_CONFIG_NAME,
|
||||
description=None,
|
||||
icon=None,
|
||||
pics=[],
|
||||
examples=[],
|
||||
config_json={},
|
||||
is_default=True,
|
||||
created_by=created_by,
|
||||
updated_by=created_by,
|
||||
created_at=utc_now_naive(),
|
||||
updated_at=utc_now_naive(),
|
||||
)
|
||||
self.db.add(config)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(config)
|
||||
return config
|
||||
|
||||
async def _name_exists(self, *, uid: str, agent_id: str, name: str, exclude_id: int | None) -> bool:
|
||||
stmt = select(AgentConfig.id).where(
|
||||
AgentConfig.uid == uid,
|
||||
AgentConfig.agent_id == agent_id,
|
||||
AgentConfig.name == name,
|
||||
)
|
||||
if exclude_id is not None:
|
||||
stmt = stmt.where(AgentConfig.id != exclude_id)
|
||||
result = await self.db.execute(stmt)
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
async def ensure_unique_name(
|
||||
self,
|
||||
*,
|
||||
uid: str,
|
||||
agent_id: str,
|
||||
desired_name: str,
|
||||
exclude_id: int | None = None,
|
||||
) -> str:
|
||||
candidate = desired_name.strip() or "未命名配置"
|
||||
if not await self._name_exists(uid=uid, agent_id=agent_id, name=candidate, exclude_id=exclude_id):
|
||||
return candidate
|
||||
|
||||
base = f"{candidate}-副本"
|
||||
if not await self._name_exists(uid=uid, agent_id=agent_id, name=base, exclude_id=exclude_id):
|
||||
return base
|
||||
|
||||
idx = 2
|
||||
while True:
|
||||
candidate2 = f"{base}{idx}"
|
||||
if not await self._name_exists(uid=uid, agent_id=agent_id, name=candidate2, exclude_id=exclude_id):
|
||||
return candidate2
|
||||
idx += 1
|
||||
|
||||
async def create(
|
||||
self,
|
||||
*,
|
||||
uid: str,
|
||||
agent_id: str,
|
||||
name: str,
|
||||
description: str | None = None,
|
||||
icon: str | None = None,
|
||||
pics: list[str] | None = None,
|
||||
examples: list[str] | None = None,
|
||||
config_json: dict | None = None,
|
||||
is_default: bool = False,
|
||||
created_by: str | None = None,
|
||||
) -> AgentConfig:
|
||||
unique_name = await self.ensure_unique_name(
|
||||
uid=uid,
|
||||
agent_id=agent_id,
|
||||
desired_name=name,
|
||||
exclude_id=None,
|
||||
)
|
||||
config = AgentConfig(
|
||||
uid=uid,
|
||||
agent_id=agent_id,
|
||||
name=unique_name,
|
||||
description=description,
|
||||
icon=icon,
|
||||
pics=pics or [],
|
||||
examples=examples or [],
|
||||
config_json=config_json or {},
|
||||
is_default=False,
|
||||
created_by=created_by,
|
||||
updated_by=created_by,
|
||||
created_at=utc_now_naive(),
|
||||
updated_at=utc_now_naive(),
|
||||
)
|
||||
self.db.add(config)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(config)
|
||||
if is_default:
|
||||
return await self.set_default(config=config, updated_by=created_by)
|
||||
return config
|
||||
|
||||
async def update(
|
||||
self,
|
||||
config: AgentConfig,
|
||||
*,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
icon: str | None = None,
|
||||
pics: list[str] | None = None,
|
||||
examples: list[str] | None = None,
|
||||
config_json: dict | None = None,
|
||||
updated_by: str | None = None,
|
||||
) -> AgentConfig:
|
||||
if name is not None:
|
||||
config.name = await self.ensure_unique_name(
|
||||
uid=config.uid,
|
||||
agent_id=config.agent_id,
|
||||
desired_name=name,
|
||||
exclude_id=config.id,
|
||||
)
|
||||
if description is not None:
|
||||
config.description = description
|
||||
if icon is not None:
|
||||
config.icon = icon
|
||||
if pics is not None:
|
||||
config.pics = pics
|
||||
if examples is not None:
|
||||
config.examples = examples
|
||||
if config_json is not None:
|
||||
config.config_json = config_json
|
||||
|
||||
config.updated_by = updated_by
|
||||
config.updated_at = utc_now_naive()
|
||||
await self.db.commit()
|
||||
await self.db.refresh(config)
|
||||
return config
|
||||
|
||||
async def delete(self, *, config: AgentConfig, updated_by: str | None = None) -> None:
|
||||
uid = config.uid
|
||||
agent_id = config.agent_id
|
||||
was_default = bool(config.is_default)
|
||||
|
||||
await self.db.delete(config)
|
||||
await self.db.commit()
|
||||
|
||||
remaining = await self.list_by_user_agent(uid=uid, agent_id=agent_id)
|
||||
if not remaining:
|
||||
await self.get_or_create_default(uid=uid, agent_id=agent_id, created_by=updated_by)
|
||||
return
|
||||
|
||||
if was_default:
|
||||
await self.set_default(config=remaining[0], updated_by=updated_by)
|
||||
|
||||
async def add_skills_to_config_json(self, *, agent_config_id: int, new_slugs: list[str]) -> bool:
|
||||
"""在 config_json.context.skills 中追加 skills,并保持顺序去重。
|
||||
|
||||
Args:
|
||||
agent_config_id: AgentConfig 的 ID
|
||||
new_slugs: 要追加的技能 slug 列表,会自动去重
|
||||
|
||||
Returns:
|
||||
是否找到并更新了配置
|
||||
"""
|
||||
config = await self._get_by_id_for_update(agent_config_id)
|
||||
if not config:
|
||||
return False
|
||||
|
||||
config_json = dict(config.config_json or {})
|
||||
context = dict(config_json.get("context") or {})
|
||||
context["skills"] = _merge_skill_slugs(context.get("skills"), new_slugs)
|
||||
config_json["context"] = context
|
||||
|
||||
config.config_json = config_json
|
||||
config.updated_at = utc_now_naive()
|
||||
await self.db.commit()
|
||||
await self.db.refresh(config)
|
||||
return True
|
||||
324
backend/package/yuxi/repositories/agent_repository.py
Normal file
324
backend/package/yuxi/repositories/agent_repository.py
Normal file
@ -0,0 +1,324 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from yuxi.agents.buildin import agent_manager
|
||||
from yuxi.storage.postgres.models_business import Agent, User
|
||||
from yuxi.utils.datetime_utils import utc_now_naive
|
||||
|
||||
DEFAULT_AGENT_SLUG = "default-chatbot"
|
||||
DEFAULT_AGENT_NAME = "智能助手"
|
||||
DEFAULT_AGENT_BACKEND_ID = "ChatbotAgent"
|
||||
DEFAULT_SHARE_CONFIG = {"access_level": "global", "department_ids": [], "user_uids": []}
|
||||
ACCESS_LEVELS = {"global", "department", "user"}
|
||||
ADMIN_ROLES = {"admin", "superadmin"}
|
||||
|
||||
|
||||
def is_builtin_agent(agent: Agent) -> bool:
|
||||
return agent.slug == DEFAULT_AGENT_SLUG
|
||||
|
||||
|
||||
def _normalize_department_ids(department_ids: list | None) -> list[int]:
|
||||
return [int(department_id) for department_id in department_ids or []]
|
||||
|
||||
|
||||
def _normalize_user_uids(user_uids: list | None) -> list[str]:
|
||||
return [uid for uid in (str(uid).strip() for uid in user_uids or []) if uid]
|
||||
|
||||
|
||||
def normalize_agent_share_config(
|
||||
share_config: dict | None,
|
||||
*,
|
||||
user_uid: str | None = None,
|
||||
department_id: int | str | None = None,
|
||||
force_private: bool = False,
|
||||
) -> dict:
|
||||
if force_private:
|
||||
if not user_uid:
|
||||
raise ValueError("私有智能体必须绑定创建用户")
|
||||
return {"access_level": "user", "department_ids": [], "user_uids": [str(user_uid)]}
|
||||
|
||||
config = share_config or {}
|
||||
access_level = config.get("access_level") or "global"
|
||||
if access_level not in ACCESS_LEVELS:
|
||||
raise ValueError("无效的智能体权限等级")
|
||||
|
||||
if access_level == "global":
|
||||
return DEFAULT_SHARE_CONFIG.copy()
|
||||
|
||||
if access_level == "department":
|
||||
department_ids = _normalize_department_ids(config.get("department_ids"))
|
||||
if department_id is not None:
|
||||
department_ids.append(int(department_id))
|
||||
department_ids = sorted(set(department_ids))
|
||||
if not department_ids:
|
||||
raise ValueError("部门共享至少需要选择一个部门")
|
||||
return {"access_level": "department", "department_ids": department_ids, "user_uids": []}
|
||||
|
||||
user_uids = _normalize_user_uids(config.get("user_uids"))
|
||||
if user_uid:
|
||||
user_uids.append(str(user_uid))
|
||||
user_uids = sorted(set(user_uids))
|
||||
if not user_uids:
|
||||
raise ValueError("指定人可访问至少需要选择一个用户")
|
||||
return {"access_level": "user", "department_ids": [], "user_uids": user_uids}
|
||||
|
||||
|
||||
def user_can_access_agent(user: User, agent: Agent) -> bool:
|
||||
if user.role == "superadmin":
|
||||
return True
|
||||
user_uid = str(user.uid)
|
||||
if agent.created_by == user_uid:
|
||||
return True
|
||||
|
||||
share_config = agent.share_config or DEFAULT_SHARE_CONFIG.copy()
|
||||
access_level = share_config.get("access_level")
|
||||
if access_level == "global":
|
||||
return True
|
||||
|
||||
if access_level == "department":
|
||||
if user.department_id is None:
|
||||
return False
|
||||
try:
|
||||
return int(user.department_id) in [int(value) for value in share_config.get("department_ids") or []]
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
if access_level == "user":
|
||||
return user_uid in (share_config.get("user_uids") or [])
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def user_can_manage_agent(user: User, agent: Agent) -> bool:
|
||||
return user.role in ADMIN_ROLES or agent.created_by == str(user.uid)
|
||||
|
||||
|
||||
def _slugify(value: str | None) -> str:
|
||||
base = re.sub(r"[^a-zA-Z0-9_-]+", "-", (value or "").strip().lower()).strip("-")
|
||||
return base[:56] or f"agent-{uuid.uuid4().hex[:12]}"
|
||||
|
||||
|
||||
class AgentRepository:
|
||||
def __init__(self, db_session: AsyncSession):
|
||||
self.db = db_session
|
||||
|
||||
async def ensure_default_agent(self, *, created_by: str | None = None) -> Agent:
|
||||
agent = await self.get_by_slug(DEFAULT_AGENT_SLUG)
|
||||
if agent:
|
||||
needs_update = False
|
||||
if agent.share_config != DEFAULT_SHARE_CONFIG:
|
||||
agent.share_config = DEFAULT_SHARE_CONFIG.copy()
|
||||
needs_update = True
|
||||
if not agent.is_default:
|
||||
return await self.set_default(agent=agent, updated_by=created_by)
|
||||
if needs_update:
|
||||
agent.updated_by = created_by
|
||||
agent.updated_at = utc_now_naive()
|
||||
await self.db.commit()
|
||||
await self.db.refresh(agent)
|
||||
return agent
|
||||
|
||||
agent = Agent(
|
||||
slug=DEFAULT_AGENT_SLUG,
|
||||
backend_id=DEFAULT_AGENT_BACKEND_ID,
|
||||
name=DEFAULT_AGENT_NAME,
|
||||
description=None,
|
||||
icon=None,
|
||||
pics=[],
|
||||
config_json={"context": {}},
|
||||
share_config=DEFAULT_SHARE_CONFIG.copy(),
|
||||
is_default=True,
|
||||
created_by=created_by,
|
||||
updated_by=created_by,
|
||||
created_at=utc_now_naive(),
|
||||
updated_at=utc_now_naive(),
|
||||
)
|
||||
self.db.add(agent)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(agent)
|
||||
return agent
|
||||
|
||||
async def list_visible(self, *, user: User) -> list[Agent]:
|
||||
result = await self.db.execute(select(Agent).order_by(Agent.is_default.desc(), Agent.id.asc()))
|
||||
agents = list(result.scalars().all())
|
||||
if user.role == "superadmin":
|
||||
return agents
|
||||
return [agent for agent in agents if user_can_access_agent(user, agent)]
|
||||
|
||||
async def get_by_slug(self, slug: str) -> Agent | None:
|
||||
result = await self.db.execute(select(Agent).where(Agent.slug == slug))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_visible_by_slug(self, *, slug: str, user: User) -> Agent | None:
|
||||
agent = await self.get_by_slug(slug)
|
||||
if agent and user_can_access_agent(user, agent):
|
||||
return agent
|
||||
return None
|
||||
|
||||
async def get_default(self) -> Agent | None:
|
||||
result = await self.db.execute(select(Agent).where(Agent.is_default.is_(True)))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def set_default(self, *, agent: Agent, updated_by: str | None = None) -> Agent:
|
||||
if not is_builtin_agent(agent):
|
||||
raise ValueError("默认智能体已固定为内置智能助手")
|
||||
share_config = agent.share_config or DEFAULT_SHARE_CONFIG.copy()
|
||||
if share_config.get("access_level") != "global":
|
||||
raise ValueError("内置智能体必须全局共享")
|
||||
|
||||
now = utc_now_naive()
|
||||
await self.db.execute(update(Agent).where(Agent.is_default.is_(True)).values(is_default=False, updated_at=now))
|
||||
agent.is_default = True
|
||||
agent.updated_by = updated_by
|
||||
agent.updated_at = now
|
||||
await self.db.commit()
|
||||
await self.db.refresh(agent)
|
||||
return agent
|
||||
|
||||
async def _slug_exists(self, slug: str) -> bool:
|
||||
result = await self.db.execute(select(Agent.id).where(Agent.slug == slug))
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
async def _unique_slug(self, desired: str | None, name: str) -> str:
|
||||
base = _slugify(desired or name)
|
||||
candidate = base
|
||||
idx = 2
|
||||
while await self._slug_exists(candidate):
|
||||
suffix = f"-{idx}"
|
||||
candidate = f"{base[: 80 - len(suffix)]}{suffix}"
|
||||
idx += 1
|
||||
return candidate
|
||||
|
||||
async def create(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
backend_id: str,
|
||||
slug: str | None = None,
|
||||
description: str | None = None,
|
||||
icon: str | None = None,
|
||||
pics: list[str] | None = None,
|
||||
config_json: dict | None = None,
|
||||
share_config: dict | None = None,
|
||||
is_default: bool = False,
|
||||
created_by: str | None = None,
|
||||
creator: User | None = None,
|
||||
) -> Agent:
|
||||
normalized_share_config = normalize_agent_share_config(
|
||||
share_config,
|
||||
user_uid=str(creator.uid) if creator else created_by,
|
||||
department_id=creator.department_id if creator else None,
|
||||
force_private=bool(creator and creator.role not in ADMIN_ROLES),
|
||||
)
|
||||
if is_default and normalized_share_config.get("access_level") != "global":
|
||||
raise ValueError("默认智能体必须全局共享")
|
||||
|
||||
agent = Agent(
|
||||
slug=await self._unique_slug(slug, name),
|
||||
backend_id=backend_id,
|
||||
name=name.strip() or "未命名智能体",
|
||||
description=description,
|
||||
icon=icon,
|
||||
pics=pics or [],
|
||||
config_json=config_json or {"context": {}},
|
||||
share_config=normalized_share_config,
|
||||
is_default=False,
|
||||
created_by=created_by,
|
||||
updated_by=created_by,
|
||||
created_at=utc_now_naive(),
|
||||
updated_at=utc_now_naive(),
|
||||
)
|
||||
self.db.add(agent)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(agent)
|
||||
if is_default:
|
||||
return await self.set_default(agent=agent, updated_by=created_by)
|
||||
return agent
|
||||
|
||||
async def update(
|
||||
self,
|
||||
agent: Agent,
|
||||
*,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
icon: str | None = None,
|
||||
pics: list[str] | None = None,
|
||||
config_json: dict | None = None,
|
||||
share_config: dict | None = None,
|
||||
updated_by: str | None = None,
|
||||
updater: User | None = None,
|
||||
) -> Agent:
|
||||
if name is not None:
|
||||
agent.name = name.strip() or "未命名智能体"
|
||||
if description is not None:
|
||||
agent.description = description
|
||||
if icon is not None:
|
||||
agent.icon = icon
|
||||
if pics is not None:
|
||||
agent.pics = pics
|
||||
if config_json is not None:
|
||||
agent.config_json = config_json
|
||||
if share_config is not None:
|
||||
if is_builtin_agent(agent):
|
||||
agent.share_config = DEFAULT_SHARE_CONFIG.copy()
|
||||
else:
|
||||
normalized_share_config = normalize_agent_share_config(
|
||||
share_config,
|
||||
user_uid=str(updater.uid) if updater else updated_by,
|
||||
department_id=updater.department_id if updater else None,
|
||||
force_private=bool(updater and updater.role not in ADMIN_ROLES),
|
||||
)
|
||||
agent.share_config = normalized_share_config
|
||||
|
||||
agent.updated_by = updated_by
|
||||
agent.updated_at = utc_now_naive()
|
||||
await self.db.commit()
|
||||
await self.db.refresh(agent)
|
||||
return agent
|
||||
|
||||
async def delete(self, *, agent: Agent) -> None:
|
||||
await self.db.delete(agent)
|
||||
await self.db.commit()
|
||||
|
||||
async def serialize(
|
||||
self,
|
||||
agent: Agent,
|
||||
*,
|
||||
user: User,
|
||||
include_configurable_items: bool = False,
|
||||
backend_info_cache: dict[tuple[str, bool, str], dict] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
data = agent.to_dict()
|
||||
data["can_manage"] = user_can_manage_agent(user, agent)
|
||||
data["is_builtin"] = is_builtin_agent(agent)
|
||||
data["permission_locked"] = is_builtin_agent(agent)
|
||||
|
||||
backend = agent_manager.get_agent(agent.backend_id)
|
||||
if backend:
|
||||
cache_key = (agent.backend_id, include_configurable_items, user.role)
|
||||
backend_info = backend_info_cache.get(cache_key) if backend_info_cache is not None else None
|
||||
if backend_info is None:
|
||||
backend_info = await backend.get_info(
|
||||
include_configurable_items=include_configurable_items,
|
||||
user_role=user.role,
|
||||
db=self.db if include_configurable_items else None,
|
||||
user=user if include_configurable_items else None,
|
||||
)
|
||||
if backend_info_cache is not None:
|
||||
backend_info_cache[cache_key] = backend_info
|
||||
data["capabilities"] = backend_info.get("capabilities", [])
|
||||
data["metadata"] = backend_info.get("metadata", {})
|
||||
if include_configurable_items:
|
||||
data["configurable_items"] = backend_info.get("configurable_items", {})
|
||||
else:
|
||||
data["capabilities"] = []
|
||||
data["metadata"] = {}
|
||||
if include_configurable_items:
|
||||
data["configurable_items"] = {}
|
||||
return data
|
||||
@ -1,136 +0,0 @@
|
||||
"""Move agent configs from department scope to user scope.
|
||||
|
||||
Revision ID: 20260518_0001
|
||||
Revises:
|
||||
Create Date: 2026-05-18
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "20260518_0001"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("agent_configs", sa.Column("uid", sa.String(), nullable=True))
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE agent_configs ac
|
||||
SET uid = u.uid
|
||||
FROM users u
|
||||
WHERE ac.uid IS NULL
|
||||
AND ac.created_by ~ '^[0-9]+$'
|
||||
AND u.id = ac.created_by::integer
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE agent_configs ac
|
||||
SET uid = u.uid
|
||||
FROM users u
|
||||
WHERE ac.uid IS NULL
|
||||
AND ac.created_by = u.uid
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE agent_configs ac
|
||||
SET uid = (
|
||||
SELECT u.uid
|
||||
FROM users u
|
||||
WHERE u.department_id = ac.department_id
|
||||
AND u.is_deleted = 0
|
||||
ORDER BY
|
||||
CASE WHEN u.role = 'superadmin' THEN 0 WHEN u.role = 'admin' THEN 1 ELSE 2 END,
|
||||
u.id ASC
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE ac.uid IS NULL
|
||||
"""
|
||||
)
|
||||
op.execute("DELETE FROM agent_configs WHERE uid IS NULL")
|
||||
|
||||
op.execute("DROP INDEX IF EXISTS uq_agent_configs_department_agent_default")
|
||||
op.execute("DROP INDEX IF EXISTS ix_agent_configs_department_id")
|
||||
op.drop_constraint("uq_agent_configs_department_agent_name", "agent_configs", type_="unique")
|
||||
op.drop_constraint("agent_configs_department_id_fkey", "agent_configs", type_="foreignkey")
|
||||
op.drop_column("agent_configs", "department_id")
|
||||
|
||||
op.alter_column("agent_configs", "uid", nullable=False)
|
||||
op.execute(
|
||||
"""
|
||||
WITH ranked AS (
|
||||
SELECT id, ROW_NUMBER() OVER (PARTITION BY uid, agent_id, name ORDER BY id) AS rn
|
||||
FROM agent_configs
|
||||
)
|
||||
UPDATE agent_configs ac
|
||||
SET name = LEFT(ac.name, 90) || '-' || ac.id::text
|
||||
FROM ranked
|
||||
WHERE ac.id = ranked.id AND ranked.rn > 1
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
WITH ranked AS (
|
||||
SELECT id, ROW_NUMBER() OVER (PARTITION BY uid, agent_id ORDER BY id) AS rn
|
||||
FROM agent_configs
|
||||
WHERE is_default IS TRUE
|
||||
)
|
||||
UPDATE agent_configs ac
|
||||
SET is_default = FALSE
|
||||
FROM ranked
|
||||
WHERE ac.id = ranked.id AND ranked.rn > 1
|
||||
"""
|
||||
)
|
||||
op.create_foreign_key("agent_configs_uid_fkey", "agent_configs", "users", ["uid"], ["uid"])
|
||||
op.create_unique_constraint("uq_agent_configs_uid_agent_name", "agent_configs", ["uid", "agent_id", "name"])
|
||||
op.create_index("ix_agent_configs_uid", "agent_configs", ["uid"])
|
||||
op.create_index(
|
||||
"uq_agent_configs_uid_agent_default",
|
||||
"agent_configs",
|
||||
["uid", "agent_id"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("is_default IS TRUE"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.add_column("agent_configs", sa.Column("department_id", sa.Integer(), nullable=True))
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE agent_configs ac
|
||||
SET department_id = u.department_id
|
||||
FROM users u
|
||||
WHERE ac.uid = u.uid
|
||||
"""
|
||||
)
|
||||
op.execute("DELETE FROM agent_configs WHERE department_id IS NULL")
|
||||
|
||||
op.execute("DROP INDEX IF EXISTS uq_agent_configs_uid_agent_default")
|
||||
op.execute("DROP INDEX IF EXISTS ix_agent_configs_uid")
|
||||
op.drop_constraint("uq_agent_configs_uid_agent_name", "agent_configs", type_="unique")
|
||||
op.drop_constraint("agent_configs_uid_fkey", "agent_configs", type_="foreignkey")
|
||||
op.drop_column("agent_configs", "uid")
|
||||
|
||||
op.alter_column("agent_configs", "department_id", nullable=False)
|
||||
op.create_foreign_key("agent_configs_department_id_fkey", "agent_configs", "departments", ["department_id"], ["id"])
|
||||
op.create_unique_constraint(
|
||||
"uq_agent_configs_department_agent_name",
|
||||
"agent_configs",
|
||||
["department_id", "agent_id", "name"],
|
||||
)
|
||||
op.create_index("ix_agent_configs_department_id", "agent_configs", ["department_id"])
|
||||
op.create_index(
|
||||
"uq_agent_configs_department_agent_default",
|
||||
"agent_configs",
|
||||
["department_id", "agent_id"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("is_default IS TRUE"),
|
||||
)
|
||||
@ -14,7 +14,6 @@ from sqlalchemy import (
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
)
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
@ -154,52 +153,44 @@ class AgentEnv(Base):
|
||||
}
|
||||
|
||||
|
||||
class AgentConfig(Base):
|
||||
"""智能体配置(按用户隔离,多份可切换)"""
|
||||
class Agent(Base):
|
||||
"""用户可管理、可授权、可切换的智能体。"""
|
||||
|
||||
__tablename__ = "agent_configs"
|
||||
__tablename__ = "agents"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
uid = Column(String, ForeignKey("users.uid"), nullable=False, index=True)
|
||||
agent_id = Column(String(64), nullable=False, index=True)
|
||||
slug = Column(String(80), nullable=False, unique=True, index=True)
|
||||
backend_id = Column(String(64), nullable=False, index=True)
|
||||
|
||||
name = Column(String(100), nullable=False)
|
||||
description = Column(String(255), nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
icon = Column(String(255), nullable=True)
|
||||
|
||||
pics = Column(JSON, nullable=False, default=list)
|
||||
examples = Column(JSON, nullable=False, default=list)
|
||||
config_json = Column(JSON, nullable=False, default=dict)
|
||||
share_config = Column(JSON, nullable=False, default=dict)
|
||||
|
||||
is_default = Column(Boolean, nullable=False, default=False, index=True)
|
||||
|
||||
created_by = Column(String(64), nullable=True)
|
||||
created_by = Column(String(64), nullable=True, index=True)
|
||||
updated_by = Column(String(64), nullable=True)
|
||||
created_at = Column(DateTime, default=utc_now_naive)
|
||||
updated_at = Column(DateTime, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("uid", "agent_id", "name", name="uq_agent_configs_uid_agent_name"),
|
||||
Index(
|
||||
"uq_agent_configs_uid_agent_default",
|
||||
"uid",
|
||||
"agent_id",
|
||||
unique=True,
|
||||
postgresql_where=is_default.is_(True),
|
||||
),
|
||||
)
|
||||
__table_args__ = (Index("uq_agents_default", "is_default", unique=True, postgresql_where=is_default.is_(True)),)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"uid": self.uid,
|
||||
"agent_id": self.agent_id,
|
||||
"slug": self.slug,
|
||||
"agent_id": self.slug,
|
||||
"backend_id": self.backend_id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"icon": self.icon,
|
||||
"pics": self.pics or [],
|
||||
"examples": self.examples or [],
|
||||
"config_json": self.config_json or {},
|
||||
"share_config": self.share_config or {},
|
||||
"is_default": bool(self.is_default),
|
||||
"created_by": self.created_by,
|
||||
"updated_by": self.updated_by,
|
||||
|
||||
@ -3,6 +3,7 @@ import os
|
||||
from fastapi import APIRouter
|
||||
|
||||
from server.routers.auth_router import auth
|
||||
from server.routers.agent_router import agent_router
|
||||
from server.routers.chat_router import chat
|
||||
from server.routers.dashboard_router import dashboard
|
||||
from server.routers.auth_dept_router import department
|
||||
@ -25,7 +26,8 @@ router = APIRouter()
|
||||
# 基础系统接口:健康检查、配置、认证与聊天主链路。
|
||||
router.include_router(system) # /api/system/* 系统状态与全局配置
|
||||
router.include_router(auth) # /api/auth/* 登录与用户信息
|
||||
router.include_router(chat) # /api/chat/* 对话、消息流、运行态
|
||||
router.include_router(agent_router) # /api/agent/* 智能体管理与运行态
|
||||
router.include_router(chat) # /api/chat/* 对话线程、消息历史与附件
|
||||
|
||||
# 管理与工作台接口:后台任务、权限域以及工具体系配置。
|
||||
router.include_router(dashboard) # /api/dashboard/* 仪表盘聚合数据
|
||||
|
||||
328
backend/server/routers/agent_router.py
Normal file
328
backend/server/routers/agent_router.py
Normal file
@ -0,0 +1,328 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from server.routers.auth_router import get_admin_user
|
||||
from server.utils.auth_middleware import get_db, get_required_user
|
||||
from yuxi.agents.buildin import agent_manager
|
||||
from yuxi.agents.context import filter_config_by_role
|
||||
from yuxi.repositories.agent_repository import AgentRepository, is_builtin_agent, user_can_access_agent, user_can_manage_agent
|
||||
from yuxi.services.agent_run_service import (
|
||||
cancel_agent_run_view,
|
||||
create_agent_run_view,
|
||||
get_active_run_by_thread,
|
||||
get_agent_run_view,
|
||||
stream_agent_run_events,
|
||||
)
|
||||
from yuxi.services.chat_service import agent_chat, stream_agent_chat
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
agent_router = APIRouter(prefix="/agent", tags=["agent"])
|
||||
|
||||
|
||||
class AgentCreate(BaseModel):
|
||||
name: str
|
||||
backend_id: str = "ChatbotAgent"
|
||||
slug: str | None = None
|
||||
description: str | None = None
|
||||
icon: str | None = None
|
||||
pics: list[str] | None = None
|
||||
config_json: dict | None = None
|
||||
share_config: dict | None = None
|
||||
set_default: bool = False
|
||||
|
||||
|
||||
class AgentUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
icon: str | None = None
|
||||
pics: list[str] | None = None
|
||||
config_json: dict | None = None
|
||||
share_config: dict | None = None
|
||||
|
||||
|
||||
class AgentRunCreate(BaseModel):
|
||||
query: str = Field(..., description="用户输入的问题")
|
||||
agent_id: str = Field(..., description="智能体 ID")
|
||||
thread_id: str = Field(..., description="会话线程 ID")
|
||||
meta: dict = Field(default_factory=dict, description="可选,请求追踪信息,例如 request_id")
|
||||
image_content: str | None = Field(None, description="可选,base64 图片内容")
|
||||
|
||||
|
||||
class AgentChatRequest(BaseModel):
|
||||
query: str = Field(..., description="用户输入的问题")
|
||||
agent_id: str = Field(..., description="智能体 ID")
|
||||
thread_id: str | None = Field(None, description="可选,会话线程 ID;不传则自动创建")
|
||||
meta: dict = Field(default_factory=dict, description="可选,请求追踪信息,例如 request_id")
|
||||
image_content: str | None = Field(None, description="可选,base64 图片内容")
|
||||
|
||||
|
||||
def _backend_info(info: dict) -> dict:
|
||||
data = dict(info)
|
||||
data["backend_id"] = data.pop("id", None)
|
||||
data["type"] = "agent_backend"
|
||||
return data
|
||||
|
||||
|
||||
def _filter_agent_config_json(backend_id: str, config_json: dict | None, role: str | None) -> dict:
|
||||
backend = agent_manager.get_agent(backend_id)
|
||||
context_schema = backend.context_schema if backend else None
|
||||
return filter_config_by_role(config_json or {}, role, context_schema=context_schema)
|
||||
|
||||
|
||||
async def _serialize_agent(
|
||||
repo: AgentRepository,
|
||||
item,
|
||||
user: User,
|
||||
*,
|
||||
include_configurable_items: bool = False,
|
||||
backend_info_cache: dict[tuple[str, bool, str], dict] | None = None,
|
||||
) -> dict:
|
||||
data = await repo.serialize(
|
||||
item,
|
||||
user=user,
|
||||
include_configurable_items=include_configurable_items,
|
||||
backend_info_cache=backend_info_cache,
|
||||
)
|
||||
data["config_json"] = _filter_agent_config_json(item.backend_id, data.get("config_json"), user.role)
|
||||
return data
|
||||
|
||||
|
||||
@agent_router.get("/backends")
|
||||
async def list_agent_backends(current_user: User = Depends(get_required_user)):
|
||||
infos = await agent_manager.get_agents_info(include_configurable_items=False)
|
||||
return {"backends": [_backend_info(info) for info in infos]}
|
||||
|
||||
|
||||
@agent_router.get("/backends/{backend_id}")
|
||||
async def get_agent_backend(
|
||||
backend_id: str,
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
backend = agent_manager.get_agent(backend_id)
|
||||
if not backend:
|
||||
raise HTTPException(status_code=404, detail=f"智能体后端 {backend_id} 不存在")
|
||||
return _backend_info(await backend.get_info(user_role=current_user.role, db=db, user=current_user))
|
||||
|
||||
|
||||
@agent_router.get("")
|
||||
async def list_agents(current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)):
|
||||
repo = AgentRepository(db)
|
||||
await repo.ensure_default_agent()
|
||||
items = await repo.list_visible(user=current_user)
|
||||
backend_info_cache: dict[tuple[str, bool, str], dict] = {}
|
||||
agents = [
|
||||
await _serialize_agent(repo, item, current_user, backend_info_cache=backend_info_cache) for item in items
|
||||
]
|
||||
return {"agents": agents}
|
||||
|
||||
|
||||
@agent_router.get("/default")
|
||||
async def get_default_agent(current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)):
|
||||
repo = AgentRepository(db)
|
||||
item = await repo.ensure_default_agent()
|
||||
if not item or not user_can_access_agent(current_user, item):
|
||||
raise HTTPException(status_code=404, detail="默认智能体不可访问")
|
||||
return {"agent": await _serialize_agent(repo, item, current_user, include_configurable_items=True)}
|
||||
|
||||
|
||||
@agent_router.post("")
|
||||
async def create_agent(
|
||||
payload: AgentCreate, current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
if not agent_manager.get_agent(payload.backend_id):
|
||||
raise HTTPException(status_code=404, detail=f"智能体后端 {payload.backend_id} 不存在")
|
||||
if payload.set_default:
|
||||
raise HTTPException(status_code=422, detail="默认智能体已固定为内置智能助手")
|
||||
|
||||
repo = AgentRepository(db)
|
||||
try:
|
||||
item = await repo.create(
|
||||
name=payload.name,
|
||||
slug=payload.slug,
|
||||
backend_id=payload.backend_id,
|
||||
description=payload.description,
|
||||
icon=payload.icon,
|
||||
pics=payload.pics,
|
||||
config_json=_filter_agent_config_json(payload.backend_id, payload.config_json, current_user.role),
|
||||
share_config=payload.share_config,
|
||||
is_default=payload.set_default,
|
||||
created_by=str(current_user.uid),
|
||||
creator=current_user,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
return {"agent": await _serialize_agent(repo, item, current_user, include_configurable_items=True)}
|
||||
|
||||
|
||||
@agent_router.get("/{agent_id}")
|
||||
async def get_agent(agent_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)):
|
||||
repo = AgentRepository(db)
|
||||
item = await repo.get_visible_by_slug(slug=agent_id, user=current_user)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||||
return {"agent": await _serialize_agent(repo, item, current_user, include_configurable_items=True)}
|
||||
|
||||
|
||||
@agent_router.put("/{agent_id}")
|
||||
async def update_agent(
|
||||
agent_id: str,
|
||||
payload: AgentUpdate,
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = AgentRepository(db)
|
||||
item = await repo.get_visible_by_slug(slug=agent_id, user=current_user)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||||
if not user_can_manage_agent(current_user, item):
|
||||
raise HTTPException(status_code=403, detail="不能编辑非自己创建的智能体")
|
||||
|
||||
try:
|
||||
fields_set = getattr(payload, "model_fields_set", getattr(payload, "__fields_set__", set()))
|
||||
if "description" in fields_set and payload.description is None:
|
||||
item.description = None
|
||||
if "icon" in fields_set and payload.icon is None:
|
||||
item.icon = None
|
||||
|
||||
updated = await repo.update(
|
||||
item,
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
icon=payload.icon,
|
||||
pics=payload.pics,
|
||||
config_json=_filter_agent_config_json(item.backend_id, payload.config_json, current_user.role)
|
||||
if payload.config_json is not None
|
||||
else None,
|
||||
share_config=payload.share_config,
|
||||
updated_by=str(current_user.uid),
|
||||
updater=current_user,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
return {"agent": await _serialize_agent(repo, updated, current_user, include_configurable_items=True)}
|
||||
|
||||
|
||||
@agent_router.delete("/{agent_id}")
|
||||
async def delete_agent(
|
||||
agent_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
repo = AgentRepository(db)
|
||||
item = await repo.get_visible_by_slug(slug=agent_id, user=current_user)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||||
if not user_can_manage_agent(current_user, item):
|
||||
raise HTTPException(status_code=403, detail="不能删除非自己创建的智能体")
|
||||
if is_builtin_agent(item):
|
||||
raise HTTPException(status_code=409, detail="内置智能体不能删除")
|
||||
await repo.delete(agent=item)
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@agent_router.post("/{agent_id}/set_default")
|
||||
async def set_agent_default(
|
||||
agent_id: str,
|
||||
current_user: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = AgentRepository(db)
|
||||
item = await repo.get_by_slug(agent_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="智能体不存在")
|
||||
try:
|
||||
updated = await repo.set_default(agent=item, updated_by=str(current_user.uid))
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
return {"agent": await _serialize_agent(repo, updated, current_user, include_configurable_items=True)}
|
||||
|
||||
|
||||
@agent_router.post("/chat")
|
||||
async def chat_agent(
|
||||
payload: AgentChatRequest,
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
logger.info(f"query: {payload.query}, agent_id: {payload.agent_id}, meta: {payload.meta}")
|
||||
return StreamingResponse(
|
||||
stream_agent_chat(
|
||||
query=payload.query,
|
||||
agent_id=payload.agent_id,
|
||||
thread_id=payload.thread_id,
|
||||
meta=dict(payload.meta or {}),
|
||||
image_content=payload.image_content,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
),
|
||||
media_type="application/json",
|
||||
)
|
||||
|
||||
|
||||
@agent_router.post("/chat/sync")
|
||||
async def chat_agent_sync(
|
||||
payload: AgentChatRequest,
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await agent_chat(
|
||||
query=payload.query,
|
||||
agent_id=payload.agent_id,
|
||||
thread_id=payload.thread_id,
|
||||
meta=dict(payload.meta or {}),
|
||||
image_content=payload.image_content,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
|
||||
|
||||
@agent_router.post("/runs")
|
||||
async def create_agent_run(
|
||||
payload: AgentRunCreate,
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await create_agent_run_view(
|
||||
query=payload.query,
|
||||
agent_id=payload.agent_id,
|
||||
thread_id=payload.thread_id,
|
||||
meta=dict(payload.meta or {}),
|
||||
image_content=payload.image_content,
|
||||
current_uid=str(current_user.uid),
|
||||
db=db,
|
||||
)
|
||||
|
||||
|
||||
@agent_router.get("/runs/{run_id}")
|
||||
async def get_agent_run(
|
||||
run_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
return await get_agent_run_view(run_id=run_id, current_uid=str(current_user.uid), db=db)
|
||||
|
||||
|
||||
@agent_router.post("/runs/{run_id}/cancel")
|
||||
async def cancel_agent_run(
|
||||
run_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
return await cancel_agent_run_view(run_id=run_id, current_uid=str(current_user.uid), db=db)
|
||||
|
||||
|
||||
@agent_router.get("/runs/{run_id}/events")
|
||||
async def stream_run_events(run_id: str, after_seq: str = "0-0", current_user: User = Depends(get_required_user)):
|
||||
return StreamingResponse(
|
||||
stream_agent_run_events(run_id=run_id, after_seq=after_seq, current_uid=str(current_user.uid)),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
|
||||
@agent_router.get("/thread/{thread_id}/active_run")
|
||||
async def get_thread_active_run(
|
||||
thread_id: str,
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await get_active_run_by_thread(thread_id=thread_id, current_uid=str(current_user.uid), db=db)
|
||||
@ -9,20 +9,10 @@ from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
from server.routers.auth_router import get_admin_user
|
||||
from server.utils.auth_middleware import get_current_user, get_db, get_required_user
|
||||
from server.utils.auth_middleware import get_db, get_required_user
|
||||
from yuxi import config as conf
|
||||
from yuxi.agents.context import filter_config_by_role
|
||||
from yuxi.agents.buildin import agent_manager
|
||||
from yuxi.models import select_model
|
||||
from yuxi.services.chat_service import agent_chat, get_agent_state_view, stream_agent_chat, stream_agent_resume
|
||||
from yuxi.services.agent_run_service import (
|
||||
cancel_agent_run_view,
|
||||
create_agent_run_view,
|
||||
get_active_run_by_thread,
|
||||
get_agent_run_view,
|
||||
stream_agent_run_events,
|
||||
)
|
||||
from yuxi.services.chat_service import get_agent_state_view, stream_agent_resume
|
||||
from yuxi.repositories.conversation_repository import ConversationRepository
|
||||
from yuxi.services.conversation_service import (
|
||||
create_thread_view,
|
||||
@ -41,7 +31,6 @@ from yuxi.services.thread_files_service import (
|
||||
save_thread_artifact_to_workspace_view,
|
||||
)
|
||||
from yuxi.services.feedback_service import get_message_feedback_view, submit_message_feedback_view
|
||||
from yuxi.repositories.agent_config_repository import AgentConfigRepository
|
||||
from yuxi.utils.logging_config import logger
|
||||
from yuxi.utils.image_processor import process_uploaded_image
|
||||
from yuxi.utils.paths import VIRTUAL_PATH_PREFIX
|
||||
@ -63,111 +52,9 @@ class ImageUploadResponse(BaseModel):
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class AgentConfigCreate(BaseModel):
|
||||
name: str
|
||||
description: str | None = None
|
||||
icon: str | None = None
|
||||
pics: list[str] | None = None
|
||||
examples: list[str] | None = None
|
||||
config_json: dict | None = None
|
||||
set_default: bool = False
|
||||
|
||||
|
||||
class AgentConfigUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
icon: str | None = None
|
||||
pics: list[str] | None = None
|
||||
examples: list[str] | None = None
|
||||
config_json: dict | None = None
|
||||
|
||||
|
||||
class AgentRunCreate(BaseModel):
|
||||
query: str = Field(..., description="用户输入的问题")
|
||||
agent_config_id: int = Field(..., description="智能体配置 ID,后端将据此解析 agent_id 和运行时 context")
|
||||
thread_id: str = Field(..., description="会话线程 ID")
|
||||
meta: dict = Field(default_factory=dict, description="可选,请求追踪信息,例如 request_id")
|
||||
image_content: str | None = Field(None, description="可选,base64 图片内容")
|
||||
|
||||
|
||||
class AgentChatRequest(BaseModel):
|
||||
query: str = Field(..., description="用户输入的问题")
|
||||
agent_config_id: int = Field(..., description="智能体配置 ID,后端将据此解析 agent_id 和运行时 context")
|
||||
thread_id: str | None = Field(None, description="可选,会话线程 ID;不传则自动创建")
|
||||
meta: dict = Field(default_factory=dict, description="可选,请求追踪信息,例如 request_id")
|
||||
image_content: str | None = Field(None, description="可选,base64 图片内容")
|
||||
|
||||
|
||||
chat = APIRouter(prefix="/chat", tags=["chat"])
|
||||
|
||||
|
||||
async def get_config_user(user: User | None = Depends(get_current_user)) -> User:
|
||||
if user is None:
|
||||
raise HTTPException(status_code=401, detail="请登录后再访问", headers={"WWW-Authenticate": "Bearer"})
|
||||
return user
|
||||
|
||||
|
||||
def _filter_agent_config_json(agent_id: str, config_json: dict | None, role: str | None) -> dict:
|
||||
agent = agent_manager.get_agent(agent_id)
|
||||
context_schema = agent.context_schema if agent else None
|
||||
return filter_config_by_role(config_json or {}, role, context_schema=context_schema)
|
||||
|
||||
|
||||
def _serialize_agent_config(item, role: str | None) -> dict:
|
||||
data = item.to_dict()
|
||||
data["config_json"] = _filter_agent_config_json(item.agent_id, data.get("config_json"), role)
|
||||
return data
|
||||
|
||||
# =============================================================================
|
||||
# > === 智能体管理分组 ===
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@chat.get("/default_agent")
|
||||
async def get_default_agent(current_user: User = Depends(get_required_user)):
|
||||
"""获取默认智能体ID(需要登录)"""
|
||||
try:
|
||||
default_agent_id = conf.default_agent_id
|
||||
# 如果没有设置默认智能体,尝试获取第一个可用的智能体
|
||||
if not default_agent_id:
|
||||
agents = await agent_manager.get_agents_info(include_configurable_items=False)
|
||||
if agents:
|
||||
default_agent_id = agents[0].get("id", "")
|
||||
|
||||
return {"default_agent_id": default_agent_id}
|
||||
except Exception as e:
|
||||
logger.error(f"获取默认智能体出错: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"获取默认智能体出错: {str(e)}")
|
||||
|
||||
|
||||
@chat.post("/set_default_agent")
|
||||
async def set_default_agent(request_data: dict = Body(...), current_user=Depends(get_admin_user)):
|
||||
"""设置默认智能体ID (仅管理员)"""
|
||||
try:
|
||||
agent_id = request_data.get("agent_id")
|
||||
if not agent_id:
|
||||
raise HTTPException(status_code=422, detail="缺少必需的 agent_id 字段")
|
||||
|
||||
# 验证智能体是否存在
|
||||
agents = await agent_manager.get_agents_info(include_configurable_items=False)
|
||||
agent_ids = [agent.get("id", "") for agent in agents]
|
||||
|
||||
if agent_id not in agent_ids:
|
||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
||||
|
||||
# 设置默认智能体ID
|
||||
conf.default_agent_id = agent_id
|
||||
# 保存配置
|
||||
conf.save()
|
||||
|
||||
return {"success": True, "default_agent_id": agent_id}
|
||||
except HTTPException as he:
|
||||
raise he
|
||||
except Exception as e:
|
||||
logger.error(f"设置默认智能体出错: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"设置默认智能体出错: {str(e)}")
|
||||
|
||||
|
||||
@chat.post("/call")
|
||||
async def call(query: str = Body(...), meta: dict = Body(None), current_user: User = Depends(get_required_user)):
|
||||
"""调用模型进行简单问答(需要登录)"""
|
||||
@ -185,305 +72,11 @@ async def call(query: str = Body(...), meta: dict = Body(None), current_user: Us
|
||||
return {"response": response.content, "request_id": meta["request_id"]}
|
||||
|
||||
|
||||
@chat.get("/agent")
|
||||
async def get_agent(current_user: User = Depends(get_required_user)):
|
||||
"""获取所有可用智能体的基本信息(需要登录)"""
|
||||
agents_info = await agent_manager.get_agents_info(include_configurable_items=False)
|
||||
return {"agents": agents_info}
|
||||
|
||||
|
||||
@chat.get("/agent/{agent_id}")
|
||||
async def get_single_agent(
|
||||
agent_id: str,
|
||||
current_user: User = Depends(get_config_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取指定智能体的完整信息(包含配置选项)(需要登录)"""
|
||||
try:
|
||||
# 检查智能体是否存在
|
||||
if not (agent := agent_manager.get_agent(agent_id)):
|
||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
||||
|
||||
# 获取智能体的完整信息(包含 configurable_items)
|
||||
agent_info = await agent.get_info(user_role=current_user.role, db=db, user=current_user)
|
||||
|
||||
return agent_info
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取智能体 {agent_id} 信息出错: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"获取智能体信息出错: {str(e)}")
|
||||
|
||||
|
||||
@chat.get("/agent/{agent_id}/configs")
|
||||
async def list_agent_configs(
|
||||
agent_id: str,
|
||||
current_user: User = Depends(get_config_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if not agent_manager.get_agent(agent_id):
|
||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
||||
|
||||
repo = AgentConfigRepository(db)
|
||||
uid = str(current_user.uid)
|
||||
items = await repo.list_by_user_agent(uid=uid, agent_id=agent_id)
|
||||
if not items:
|
||||
await repo.get_or_create_default(
|
||||
uid=uid,
|
||||
agent_id=agent_id,
|
||||
created_by=uid,
|
||||
)
|
||||
items = await repo.list_by_user_agent(uid=uid, agent_id=agent_id)
|
||||
|
||||
configs = [
|
||||
{
|
||||
"id": item.id,
|
||||
"name": item.name,
|
||||
"description": item.description,
|
||||
"icon": item.icon,
|
||||
"pics": item.pics or [],
|
||||
"examples": item.examples or [],
|
||||
"is_default": bool(item.is_default),
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
return {"configs": configs}
|
||||
|
||||
|
||||
@chat.get("/agent/{agent_id}/configs/{config_id}")
|
||||
async def get_agent_config_profile(
|
||||
agent_id: str,
|
||||
config_id: int,
|
||||
current_user: User = Depends(get_config_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if not agent_manager.get_agent(agent_id):
|
||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
||||
|
||||
repo = AgentConfigRepository(db)
|
||||
item = await repo.get_by_id(config_id)
|
||||
if not item or item.agent_id != agent_id or item.uid != str(current_user.uid):
|
||||
raise HTTPException(status_code=404, detail="配置不存在")
|
||||
|
||||
return {"config": _serialize_agent_config(item, current_user.role)}
|
||||
|
||||
|
||||
@chat.post("/agent/{agent_id}/configs")
|
||||
async def create_agent_config_profile(
|
||||
agent_id: str,
|
||||
payload: AgentConfigCreate,
|
||||
current_user: User = Depends(get_config_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if not agent_manager.get_agent(agent_id):
|
||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
||||
|
||||
repo = AgentConfigRepository(db)
|
||||
uid = str(current_user.uid)
|
||||
item = await repo.create(
|
||||
uid=uid,
|
||||
agent_id=agent_id,
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
icon=payload.icon,
|
||||
pics=payload.pics,
|
||||
examples=payload.examples,
|
||||
config_json=_filter_agent_config_json(agent_id, payload.config_json, current_user.role),
|
||||
is_default=payload.set_default,
|
||||
created_by=uid,
|
||||
)
|
||||
|
||||
return {"config": _serialize_agent_config(item, current_user.role)}
|
||||
|
||||
|
||||
@chat.put("/agent/{agent_id}/configs/{config_id}")
|
||||
async def update_agent_config_profile(
|
||||
agent_id: str,
|
||||
config_id: int,
|
||||
payload: AgentConfigUpdate,
|
||||
current_user: User = Depends(get_config_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if not agent_manager.get_agent(agent_id):
|
||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
||||
|
||||
repo = AgentConfigRepository(db)
|
||||
item = await repo.get_by_id(config_id)
|
||||
if not item or item.agent_id != agent_id or item.uid != str(current_user.uid):
|
||||
raise HTTPException(status_code=404, detail="配置不存在")
|
||||
|
||||
updated = await repo.update(
|
||||
item,
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
icon=payload.icon,
|
||||
pics=payload.pics,
|
||||
examples=payload.examples,
|
||||
config_json=_filter_agent_config_json(agent_id, payload.config_json, current_user.role)
|
||||
if payload.config_json is not None
|
||||
else None,
|
||||
updated_by=str(current_user.uid),
|
||||
)
|
||||
return {"config": _serialize_agent_config(updated, current_user.role)}
|
||||
|
||||
|
||||
@chat.post("/agent/{agent_id}/configs/{config_id}/set_default")
|
||||
async def set_agent_config_default(
|
||||
agent_id: str,
|
||||
config_id: int,
|
||||
current_user: User = Depends(get_config_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if not agent_manager.get_agent(agent_id):
|
||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
||||
|
||||
repo = AgentConfigRepository(db)
|
||||
item = await repo.get_by_id(config_id)
|
||||
if not item or item.agent_id != agent_id or item.uid != str(current_user.uid):
|
||||
raise HTTPException(status_code=404, detail="配置不存在")
|
||||
|
||||
updated = await repo.set_default(config=item, updated_by=str(current_user.uid))
|
||||
return {"config": _serialize_agent_config(updated, current_user.role)}
|
||||
|
||||
|
||||
@chat.delete("/agent/{agent_id}/configs/{config_id}")
|
||||
async def delete_agent_config_profile(
|
||||
agent_id: str,
|
||||
config_id: int,
|
||||
current_user: User = Depends(get_config_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if not agent_manager.get_agent(agent_id):
|
||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在")
|
||||
|
||||
repo = AgentConfigRepository(db)
|
||||
item = await repo.get_by_id(config_id)
|
||||
if not item or item.agent_id != agent_id or item.uid != str(current_user.uid):
|
||||
raise HTTPException(status_code=404, detail="配置不存在")
|
||||
|
||||
await repo.delete(config=item, updated_by=str(current_user.uid))
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@chat.post("/agent")
|
||||
async def chat_agent(
|
||||
payload: AgentChatRequest,
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""使用特定智能体进行对话(需要登录)"""
|
||||
logger.info(f"query: {payload.query}, agent_config_id: {payload.agent_config_id}, meta: {payload.meta}")
|
||||
|
||||
# 查看图片内容
|
||||
logger.info(f"image_content present: {payload.image_content is not None}")
|
||||
if payload.image_content:
|
||||
logger.info(f"image_content length: {len(payload.image_content)}")
|
||||
logger.info(f"image_content preview: {payload.image_content[:50]}...")
|
||||
|
||||
return StreamingResponse(
|
||||
stream_agent_chat(
|
||||
query=payload.query,
|
||||
agent_config_id=payload.agent_config_id,
|
||||
thread_id=payload.thread_id,
|
||||
meta=dict(payload.meta or {}),
|
||||
image_content=payload.image_content,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
),
|
||||
media_type="application/json",
|
||||
)
|
||||
|
||||
|
||||
@chat.post("/agent/sync")
|
||||
async def chat_agent_sync(
|
||||
payload: AgentChatRequest,
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""使用特定智能体进行非流式对话(需要登录)"""
|
||||
logger.info(f"[sync] query: {payload.query}, agent_config_id: {payload.agent_config_id}, meta: {payload.meta}")
|
||||
logger.info(f"[sync] image_content present: {payload.image_content is not None}")
|
||||
if payload.image_content:
|
||||
logger.info(f"[sync] image_content length: {len(payload.image_content)}")
|
||||
|
||||
return await agent_chat(
|
||||
query=payload.query,
|
||||
agent_config_id=payload.agent_config_id,
|
||||
thread_id=payload.thread_id,
|
||||
meta=dict(payload.meta or {}),
|
||||
image_content=payload.image_content,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
)
|
||||
|
||||
|
||||
@chat.post("/runs")
|
||||
async def create_agent_run(
|
||||
payload: AgentRunCreate,
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""创建异步 run 任务并入队(需要登录)"""
|
||||
return await create_agent_run_view(
|
||||
query=payload.query,
|
||||
agent_config_id=payload.agent_config_id,
|
||||
thread_id=payload.thread_id,
|
||||
meta=dict(payload.meta or {}),
|
||||
image_content=payload.image_content,
|
||||
current_uid=str(current_user.uid),
|
||||
db=db,
|
||||
)
|
||||
|
||||
|
||||
@chat.get("/runs/{run_id}")
|
||||
async def get_agent_run(
|
||||
run_id: str,
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取 run 状态(需要登录)"""
|
||||
return await get_agent_run_view(run_id=run_id, current_uid=str(current_user.uid), db=db)
|
||||
|
||||
|
||||
@chat.post("/runs/{run_id}/cancel")
|
||||
async def cancel_agent_run(
|
||||
run_id: str,
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""取消 run(需要登录)"""
|
||||
return await cancel_agent_run_view(run_id=run_id, current_uid=str(current_user.uid), db=db)
|
||||
|
||||
|
||||
@chat.get("/runs/{run_id}/events")
|
||||
async def stream_run_events(
|
||||
run_id: str,
|
||||
after_seq: str = Query("0-0"),
|
||||
current_user: User = Depends(get_required_user),
|
||||
):
|
||||
"""SSE 拉取 run 事件(需要登录)"""
|
||||
return StreamingResponse(
|
||||
stream_agent_run_events(
|
||||
run_id=run_id,
|
||||
after_seq=after_seq,
|
||||
current_uid=str(current_user.uid),
|
||||
),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@chat.post("/thread/{thread_id}/resume")
|
||||
async def resume_thread_chat(
|
||||
thread_id: str,
|
||||
approved: bool | None = Body(None),
|
||||
answer: dict | None = Body(None),
|
||||
config: dict = Body({}),
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@ -566,11 +159,9 @@ async def resume_thread_chat(
|
||||
meta["request_id"] = str(uuid.uuid4())
|
||||
return StreamingResponse(
|
||||
stream_agent_resume(
|
||||
agent_id=agent_id,
|
||||
thread_id=thread_id,
|
||||
resume_input=resume_input,
|
||||
meta=meta,
|
||||
config=config,
|
||||
current_user=current_user,
|
||||
db=db,
|
||||
),
|
||||
@ -578,16 +169,6 @@ async def resume_thread_chat(
|
||||
)
|
||||
|
||||
|
||||
@chat.get("/thread/{thread_id}/active_run")
|
||||
async def get_thread_active_run(
|
||||
thread_id: str,
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取当前会话活跃 run(需要登录)"""
|
||||
return await get_active_run_by_thread(thread_id=thread_id, current_uid=str(current_user.uid), db=db)
|
||||
|
||||
|
||||
@chat.get("/thread/{thread_id}/history")
|
||||
async def get_thread_history(
|
||||
thread_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)
|
||||
@ -657,6 +238,7 @@ class AttachmentResponse(BaseModel):
|
||||
original_path: str | None = None
|
||||
original_artifact_url: str | None = None
|
||||
minio_url: str | None = None
|
||||
request_id: str | None = None
|
||||
|
||||
|
||||
class AttachmentLimits(BaseModel):
|
||||
|
||||
@ -34,6 +34,14 @@ async def lifespan(app: FastAPI):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to ensure builtin MCP servers during startup: {e}")
|
||||
|
||||
try:
|
||||
from yuxi.repositories.agent_repository import AgentRepository
|
||||
|
||||
async with pg_manager.get_async_session_context() as session:
|
||||
await AgentRepository(session).ensure_default_agent()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to ensure default agent during startup: {e}")
|
||||
|
||||
# 初始化内置模型供应商配置
|
||||
try:
|
||||
async with pg_manager.get_async_session_context() as session:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user