diff --git a/backend/package/yuxi/repositories/agent_config_repository.py b/backend/package/yuxi/repositories/agent_config_repository.py deleted file mode 100644 index fb1a8381..00000000 --- a/backend/package/yuxi/repositories/agent_config_repository.py +++ /dev/null @@ -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 diff --git a/backend/package/yuxi/repositories/agent_repository.py b/backend/package/yuxi/repositories/agent_repository.py new file mode 100644 index 00000000..d32f7849 --- /dev/null +++ b/backend/package/yuxi/repositories/agent_repository.py @@ -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 diff --git a/backend/package/yuxi/storage/postgres/migrations/versions/20260518_0001_agent_config_uid.py b/backend/package/yuxi/storage/postgres/migrations/versions/20260518_0001_agent_config_uid.py deleted file mode 100644 index cdaf894c..00000000 --- a/backend/package/yuxi/storage/postgres/migrations/versions/20260518_0001_agent_config_uid.py +++ /dev/null @@ -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"), - ) diff --git a/backend/package/yuxi/storage/postgres/models_business.py b/backend/package/yuxi/storage/postgres/models_business.py index 1a15c9ba..8555223d 100644 --- a/backend/package/yuxi/storage/postgres/models_business.py +++ b/backend/package/yuxi/storage/postgres/models_business.py @@ -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, diff --git a/backend/server/routers/__init__.py b/backend/server/routers/__init__.py index a9dacfef..23f80440 100644 --- a/backend/server/routers/__init__.py +++ b/backend/server/routers/__init__.py @@ -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/* 仪表盘聚合数据 diff --git a/backend/server/routers/agent_router.py b/backend/server/routers/agent_router.py new file mode 100644 index 00000000..58d3fc40 --- /dev/null +++ b/backend/server/routers/agent_router.py @@ -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) diff --git a/backend/server/routers/chat_router.py b/backend/server/routers/chat_router.py index d12d5576..9df8315c 100644 --- a/backend/server/routers/chat_router.py +++ b/backend/server/routers/chat_router.py @@ -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): diff --git a/backend/server/utils/lifespan.py b/backend/server/utils/lifespan.py index 52e2041a..3b1ef5fd 100644 --- a/backend/server/utils/lifespan.py +++ b/backend/server/utils/lifespan.py @@ -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: