diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 00000000..06b69cc3
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,41 @@
+# Xyzen Roadmap
+
+This roadmap outlines the development stages of the Xyzen AI Laboratory Server. It serves as a high-level guide for tracking major feature implementation and system architecture evolution.
+
+## Phase 1: Core Consolidation (Current)
+Focus: Cleaning up the legacy structure, unifying models, and establishing best practices.
+
+- [ ] **Unified Agent System**: Complete the migration to a single `Agent` model for both regular and graph-based agents.
+- [ ] **Idiomatic FastAPI Refactor**: Implement Dependency Injection (DI) for resource fetching and authorization across all API handlers.
+- [ ] **Frontend State Management**: Finalize the migration of all server-side state to TanStack Query and clean up Zustand slices.
+- [ ] **Error Handling**: Implement a global exception handler and unified error code system across backend and frontend.
+
+## Phase 2: Agent Intelligence & Workflows
+Focus: Expanding the capabilities of the agent engine.
+
+- [ ] **LangGraph Orchestration**: Full integration of LangGraph for complex, stateful multi-agent workflows.
+- [ ] **Advanced MCP Integration**: Dynamic discovery and management of Model Context Protocol (MCP) servers.
+- [ ] **Tool Confirmation UI**: A robust interface for users to inspect and approve agent tool calls before execution.
+- [ ] **Streaming Optimization**: Enhancing WebSocket performance for real-time agent thought process visualization.
+
+## Phase 3: Knowledge Base & RAG
+Focus: Providing agents with memory and specialized knowledge.
+
+- [ ] **Vector Database Support**: Integration with PostgreSQL (pgvector) or a dedicated vector DB for RAG capabilities.
+- [ ] **File Processing Pipeline**: Automated ingestion and chunking of documents (PDF, Markdown, Code).
+- [ ] **Knowledge Graphs**: Exploring graph-based retrieval to complement vector search.
+
+## Phase 4: Infrastructure & Scale
+Focus: Making Xyzen production-ready.
+
+- [ ] **Multi-Provider Support**: Seamless switching between OpenAI, Anthropic, Gemini, and local models (Ollama).
+- [ ] **User Usage Tracking**: Monitoring token consumption and execution costs.
+- [ ] **Deployment Templates**: Easy-to-use Docker Compose and Kubernetes configurations for various environments.
+
+---
+
+## Done ✅
+- [x] **Project Foundation**: Initial FastAPI + SQLModel backend setup.
+- [x] **Frontend Shell**: React + Tailwind + shadcn/ui dashboard layout.
+- [x] **Basic Agent Chat**: Functional WebSocket-based chat with regular agents.
+- [x] **Dockerized Environment**: Fully containerized development setup with PostgreSQL and MinIO.
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 00000000..e56ae538
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,30 @@
+# Xyzen Task Tracker (TODO)
+
+This file tracks tactical, short-term tasks and immediate technical debt. For high-level milestones, see `ROADMAP.md`.
+
+## 🛠️ Immediate Priorities
+- [ ] **Dependency Injection Refactor**: Move `auth_service` and `agent` fetching into FastAPI dependencies in `agents.py`.
+- [ ] **Agent Repository Cleanup**: Remove legacy methods in `AgentRepository` that supported the old unified agent service (e.g., `get_agent_with_mcp_servers`).
+- [ ] **Frontend Type Alignment**: Update `web/src/types/agents.ts` to match the simplified `AgentReadWithDetails` model from the backend.
+
+## 🚀 Backend Tasks
+- [ ] **Pydantic V2 Migration**: Verify all SQLModels and Schemas are utilizing Pydantic V2 features optimally.
+- [ ] **Logging Middleware**: Add request/response logging for better debugging in the Docker environment.
+- [ ] **Auth Error Mapping**: Finish mapping `ErrCodeError` to appropriate FastAPI `HTTPException` responses in `middleware/auth`.
+
+## 🎨 Frontend Tasks
+- [ ] **TanStack Query Refactor**: Move agent fetching from `agentSlice.ts` (Zustand) to a dedicated hook in `hooks/queries/useAgents.ts`.
+- [ ] **AddAgentModal UI**: Allow users to select a specific LLM Provider during the creation of a regular agent.
+- [ ] **Loading States**: Add skeleton loaders to the `AgentExplorer` sidebar.
+
+## 🧪 Testing & Quality
+- [ ] **Backend Unit Tests**: Add test cases for the newly unified `get_agent` endpoint.
+- [ ] **Frontend Linting**: Fix existing `yarn lint` warnings in `web/src/components/layouts/ChatToolbar.tsx`.
+- [ ] **API Documentation**: Update docstrings in `handler/api/v1/` to ensure Swagger UI is accurate.
+
+## ✅ Completed Tasks
+- [x] **Agent Unification**: Unified `get_agent` endpoint to return `AgentReadWithDetails` and removed `UnifiedAgentRead` dependencies.
+- [x] **Default Agent Cloning**: Implemented logic in `SystemAgentManager` to clone system agents as user-owned default agents.
+- [x] **Tag-based Identification**: Updated frontend (Chat, Agent List, Avatars) to identify default agents via tags (e.g., `default_chat`) rather than hardcoded UUIDs.
+- [x] **Workshop Removal**: Completely removed the legacy "Workshop" feature from both backend and frontend to simplify the core agent experience.
+- [x] **Policy Update**: Updated `AgentPolicy` to allow reading of system-scoped reference agents.
diff --git a/service/app/main.py b/service/app/main.py
index 0bd0686b..854e80de 100644
--- a/service/app/main.py
+++ b/service/app/main.py
@@ -26,7 +26,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
await initialize_providers_on_startup()
- # Initialize system agents (Chat and Workshop agents)
+ # Initialize system agents (Chat agent)
from core.system_agent import SystemAgentManager
from infra.database import AsyncSessionLocal
diff --git a/service/core/auth/policies/agent_policy.py b/service/core/auth/policies/agent_policy.py
index 7a2572b6..8f274bb2 100644
--- a/service/core/auth/policies/agent_policy.py
+++ b/service/core/auth/policies/agent_policy.py
@@ -3,7 +3,7 @@
from sqlmodel.ext.asyncio.session import AsyncSession
from common.code import ErrCode
-from models.agent import Agent
+from models.agent import Agent, AgentScope
from repos.agent import AgentRepository
from .resource_policy import ResourcePolicyBase
@@ -17,7 +17,9 @@ async def authorize_read(self, resource_id: UUID, user_id: str) -> Agent:
agent = await self.agent_repo.get_agent_by_id(resource_id)
if not agent:
raise ErrCode.AGENT_NOT_FOUND.with_messages(f"Agent {resource_id} not found")
- if agent.user_id == user_id:
+
+ # System agents are readable by everyone, user agents only by owner
+ if agent.scope == AgentScope.SYSTEM or agent.user_id == user_id:
return agent
raise ErrCode.AGENT_ACCESS_DENIED.with_messages(f"User {user_id} can not access agent {resource_id}")
diff --git a/service/core/chat/langchain.py b/service/core/chat/langchain.py
index 758a7ed0..08c1c157 100644
--- a/service/core/chat/langchain.py
+++ b/service/core/chat/langchain.py
@@ -140,8 +140,6 @@ async def _load_db_history(db: AsyncSession, topic: TopicModel) -> list[Any]:
for message in messages:
role = (message.role or "").lower()
content = message.content or ""
- if not content:
- continue
if role == "user":
# Check if message has file attachments
try:
@@ -153,10 +151,6 @@ async def _load_db_history(db: AsyncSession, topic: TopicModel) -> list[Any]:
# Multimodal message: combine text and file content
multimodal_content: list[dict[str, Any]] = [{"type": "text", "text": content}]
multimodal_content.extend(file_contents)
- # Debug: Log the exact content being sent
- logger.debug(
- f"Multimodal content types for message {message.id}: {[item.get('type') for item in multimodal_content]}"
- )
for idx, item in enumerate(file_contents):
item_type = item.get("type")
if item_type == "image_url":
@@ -183,6 +177,7 @@ async def _load_db_history(db: AsyncSession, topic: TopicModel) -> list[Any]:
try:
file_contents = await process_message_files(db, message.id)
if file_contents:
+ logger.debug("Successfully processed files for message")
# Multimodal assistant message
# Combine text content with file content
multimodal_content: list[dict[str, Any]] = []
@@ -234,6 +229,7 @@ async def _load_db_history(db: AsyncSession, topic: TopicModel) -> list[Any]:
else:
# Skip unknown/tool roles for now
continue
+ logger.info(f"Length of history: {len(history)}")
return history
except Exception as e:
logger.warning(f"Failed to load DB chat history for topic {getattr(topic, 'id', None)}: {e}")
@@ -311,7 +307,7 @@ async def get_ai_response_stream_langchain_legacy(
model_name = agent.model
# Get system prompt with MCP awareness
- system_prompt = await build_system_prompt(db, agent)
+ system_prompt = await build_system_prompt(db, agent, model_name)
yield {"type": ChatEventType.PROCESSING, "data": {"status": ProcessingStatus.PREPARING_REQUEST}}
@@ -366,10 +362,7 @@ async def get_ai_response_stream_langchain_legacy(
else:
history_messages.append(system_msg)
- async for chunk in langchain_agent.astream(
- {"messages": history_messages},
- stream_mode=["updates", "messages"],
- ):
+ async for chunk in langchain_agent.astream({"messages": history_messages}, stream_mode=["updates", "messages"]):
# chunk is a tuple: (stream_mode, data)
try:
mode, data = chunk
@@ -398,7 +391,7 @@ async def get_ai_response_stream_langchain_legacy(
continue
last_message = messages[-1]
- logger.debug("Last message in step '%s': %r", step_name, last_message)
+ # logger.debug("Last message in step '%s': %r", step_name, last_message)
# Check if this is a tool call request (from LLM node)
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
diff --git a/service/core/chat/messages.py b/service/core/chat/messages.py
index 8a1d984b..36202f5d 100644
--- a/service/core/chat/messages.py
+++ b/service/core/chat/messages.py
@@ -35,7 +35,7 @@ async def agent_has_dynamic_mcp(db: AsyncSession, agent: Optional[Agent]) -> boo
return any(s.name == "DynamicMCPServer" or "dynamic_mcp_server" in (s.url or "").lower() for s in mcp_servers)
-async def build_system_prompt(db: AsyncSession, agent: Optional[Agent]) -> str:
+async def build_system_prompt(db: AsyncSession, agent: Optional[Agent], model_name: str | None) -> str:
"""
Build system prompt for the agent.
@@ -54,11 +54,14 @@ async def build_system_prompt(db: AsyncSession, agent: Optional[Agent]) -> str:
if agent and agent.prompt:
base_prompt = agent.prompt
- formatting_instructions = """
-Please format your output using Markdown.
-When writing code, use triple backticks with the language identifier (e.g. ```python).
-If you generate HTML that should be previewed, use ```html.
-If you generate ECharts JSON options, use ```echart.
-"""
+ if model_name and "image" in model_name:
+ formatting_instructions = ""
+ else:
+ formatting_instructions = """
+ Please format your output using Markdown.
+ When writing code, use triple backticks with the language identifier (e.g. ```python).
+ If you generate HTML that should be previewed, use ```html.
+ If you generate ECharts JSON options, use ```echart.
+ """
return f"{base_prompt}\n{formatting_instructions}"
diff --git a/service/core/system_agent.py b/service/core/system_agent.py
index dc851882..526e0a47 100644
--- a/service/core/system_agent.py
+++ b/service/core/system_agent.py
@@ -2,13 +2,14 @@
System Agent Manager
Manages system-wide default agents that are available to all users.
-Creates and maintains the Chat Agent and Workshop Agent with distinct personalities.
+Creates and maintains the Chat Agent with distinct personalities.
"""
import logging
from typing import TypedDict
from uuid import UUID
+from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
from core.providers import SYSTEM_USER_ID
@@ -46,32 +47,9 @@ class AgentConfig(TypedDict):
你的目标是成为用户最可靠的AI助手,帮助他们解决问题并提供有价值的信息。""",
"personality": "friendly_assistant",
"capabilities": ["general_chat", "qa", "assistance", "tools"],
- "avatar": "https://avatars.githubusercontent.com/u/176685?v=4",
+ "avatar": "/defaults/agents/avatar1.png",
"tags": ["助手", "对话", "工具", "帮助"],
},
- "workshop": {
- "name": "创作工坊",
- "description": "专注于AI助手的设计、创建和优化的专业助手",
- "prompt": """你是一个专业的AI助手设计师和创作顾问。你专门帮助用户设计、创建和优化AI助手。
-
-专业能力:
-- 深入了解各种AI能力和工具集成方案
-- 熟悉对话设计模式和用户体验最佳实践
-- 能够分析需求并提供专业的架构建议
-- 指导用户进行提示词工程和角色设定
-
-工作方式:
-- 通过提问来深入了解用户的具体需求
-- 提供结构化的设计建议和实施步骤
-- 推荐合适的工具和能力组合
-- 帮助优化现有助手的表现
-
-你的目标是帮助用户创建出色的AI助手,提供专业指导和创意灵感。""",
- "personality": "creative_mentor",
- "capabilities": ["agent_design", "tool_selection", "prompt_engineering", "workflow_optimization"],
- "avatar": "https://cdn1.deepmd.net/static/img/affb038eChatGPT Image 2025年8月6日 10_33_07.png",
- "tags": ["设计", "创作", "优化", "专业"],
- },
}
@@ -88,92 +66,103 @@ def __init__(self, db: AsyncSession):
self.agent_repo = AgentRepository(db)
self.provider_repo = ProviderRepository(db)
- async def ensure_system_agents(self) -> dict[str, Agent]:
+ async def ensure_user_default_agents(self, user_id: str) -> list[Agent]:
"""
- Create or update both system agents on startup.
+ Check and ensure user has all default agents by verifying tags.
+ If a specific default agent is missing, it will be recreated.
+
+ Args:
+ user_id: The ID of the user to initialize defaults for.
Returns:
- Dictionary mapping agent keys to Agent instances
+ List of newly created default agents for the user.
"""
- logger.info("Ensuring system agents exist...")
-
- # Get system provider for both agents
+ # Fetch existing agents for the user to check tags
+ statement = select(Agent).where(Agent.user_id == user_id)
+ result = await self.db.exec(statement)
+ existing_agents = result.all()
+ existing_tags = set()
+ for agent in existing_agents:
+ if agent.tags:
+ existing_tags.update(agent.tags)
+
+ logger.info(f"Checking default agents for user: {user_id}")
+
+ # Get system provider to set as default if available
system_provider = await self.provider_repo.get_system_provider()
- if not system_provider:
- logger.warning("No system provider found - system agents will use user providers")
- created_agents: dict[str, Agent] = {}
-
- for agent_key, agent_config in SYSTEM_AGENTS.items():
- try:
- agent = await self._ensure_single_agent(agent_config, system_provider)
- created_agents[agent_key] = agent
- logger.info(f"System agent '{agent_key}' ready: {agent.name} (ID: {agent.id})")
- except Exception as e:
- logger.error(f"Failed to create system agent '{agent_key}': {e}")
+ created_agents = []
+ for agent_key, config in SYSTEM_AGENTS.items():
+ tag = f"default_{agent_key}"
+ if tag in existing_tags:
continue
- logger.info(f"System agent initialization complete: {len(created_agents)}/{len(SYSTEM_AGENTS)} agents ready")
- return created_agents
+ # Add a tag to identify which default agent this is
+ tags = config.get("tags", [])
+ if tag not in tags:
+ tags = tags + [tag]
+
+ agent_data = AgentCreate(
+ scope=AgentScope.USER,
+ name=config["name"],
+ description=config["description"],
+ prompt=config["prompt"],
+ avatar=config.get("avatar"),
+ tags=tags,
+ provider_id=system_provider.id if system_provider else None,
+ mcp_server_ids=[], # Defaults start clean
+ require_tool_confirmation=False,
+ model=None,
+ temperature=0.7,
+ )
+
+ agent = await self.agent_repo.create_agent(agent_data, user_id)
+ created_agents.append(agent)
+ logger.info(f"Created default {agent_key} agent for user {user_id}: {agent.id}")
- async def _ensure_single_agent(self, agent_config: AgentConfig, system_provider: "Provider | None") -> Agent:
- """
- Create or update a single system agent.
-
- Args:
- agent_config: Agent configuration dictionary
- system_provider: System provider instance or None
+ return created_agents
- Returns:
- Agent instance
+ async def ensure_system_agents(self) -> dict[str, Agent]:
"""
- # Check if agent already exists by name and scope
- existing = await self.agent_repo.get_agent_by_name_and_scope(agent_config["name"], AgentScope.SYSTEM)
-
- if existing:
- # Update existing agent if needed
- updated = await self._update_system_agent(existing, agent_config, system_provider)
- return updated
- else:
- # Create new system agent
- created = await self._create_system_agent(agent_config, system_provider)
- return created
-
- async def _create_system_agent(self, agent_config: AgentConfig, system_provider: "Provider | None") -> Agent:
+ Legacy: Create or update system agents.
+ Now primarily used for global/template reference if needed.
"""
- Create a new system agent.
+ logger.info("Ensuring system reference agents exist...")
- Args:
- agent_config: Agent configuration dictionary
- system_provider: System provider instance or None
+ system_provider = await self.provider_repo.get_system_provider()
+ created_agents: dict[str, Agent] = {}
- Returns:
- Newly created Agent instance
- """
- logger.info(f"Creating new system agent: {agent_config['name']}")
-
- # Get default MCP servers for this agent type
- mcp_server_ids = await self._get_default_mcp_servers(agent_config["personality"])
-
- agent_data = AgentCreate(
- scope=AgentScope.SYSTEM,
- name=agent_config["name"],
- description=agent_config["description"],
- prompt=agent_config["prompt"],
- avatar=agent_config.get("avatar"),
- tags=agent_config.get("tags"),
- provider_id=system_provider.id if system_provider else None,
- mcp_server_ids=mcp_server_ids,
- require_tool_confirmation=False,
- model=None, # Will use provider default
- temperature=0.7, # Balanced creativity
- )
+ for agent_key, agent_config in SYSTEM_AGENTS.items():
+ try:
+ # Check if agent already exists by name and scope
+ existing = await self.agent_repo.get_agent_by_name_and_scope(agent_config["name"], AgentScope.SYSTEM)
+
+ if existing:
+ agent = await self._update_system_agent(existing, agent_config, system_provider)
+ else:
+ logger.info(f"Creating new system reference agent: {agent_config['name']}")
+ mcp_server_ids = await self._get_default_mcp_servers(agent_config["personality"])
+ agent_data = AgentCreate(
+ scope=AgentScope.SYSTEM,
+ name=agent_config["name"],
+ description=agent_config["description"],
+ prompt=agent_config["prompt"],
+ avatar=agent_config.get("avatar"),
+ tags=agent_config.get("tags"),
+ provider_id=system_provider.id if system_provider else None,
+ mcp_server_ids=mcp_server_ids,
+ require_tool_confirmation=False,
+ model=None,
+ temperature=0.7,
+ )
+ agent = await self.agent_repo.create_agent(agent_data, SYSTEM_USER_ID)
- # Create agent
- created_agent = await self.agent_repo.create_agent(agent_data, SYSTEM_USER_ID)
+ created_agents[agent_key] = agent
+ except Exception as e:
+ logger.error(f"Failed to handle system agent '{agent_key}': {e}")
+ continue
- logger.info(f"Created system agent: {created_agent.name} (ID: {created_agent.id})")
- return created_agent
+ return created_agents
async def _update_system_agent(
self, existing: Agent, agent_config: AgentConfig, system_provider: "Provider | None"
@@ -226,7 +215,7 @@ async def _get_default_mcp_servers(self, agent_personality: str) -> list[UUID]:
Get default MCP servers for each agent personality.
Args:
- agent_personality: Personality type ('friendly_assistant' or 'creative_mentor')
+ agent_personality: Personality type ('friendly_assistant')
Returns:
List of MCP server UUIDs
@@ -237,9 +226,6 @@ async def _get_default_mcp_servers(self, agent_personality: str) -> list[UUID]:
if agent_personality == "friendly_assistant":
# Chat agent: basic utility tools
return await self._get_basic_mcp_servers()
- elif agent_personality == "creative_mentor":
- # Workshop agent: creation and design tools
- return await self._get_workshop_mcp_servers()
return []
@@ -254,23 +240,12 @@ async def _get_basic_mcp_servers(self) -> list[UUID]:
# For now, return empty list - can be enhanced with actual MCP servers
return []
- async def _get_workshop_mcp_servers(self) -> list[UUID]:
- """
- Get workshop-focused MCP servers for creation assistant.
-
- Returns:
- List of workshop MCP server UUIDs
- """
- # TODO: Query for workshop MCP servers (design tools, templates, etc.)
- # For now, return empty list - can be enhanced with actual MCP servers
- return []
-
async def get_system_agent(self, agent_type: str) -> Agent | None:
"""
Get a specific system agent by type.
Args:
- agent_type: Either 'chat' or 'workshop'
+ agent_type: 'chat'
Returns:
Agent instance or None if not found
diff --git a/service/handler/api/v1/agents.py b/service/handler/api/v1/agents.py
index 0b72b88a..6b9c0224 100644
--- a/service/handler/api/v1/agents.py
+++ b/service/handler/api/v1/agents.py
@@ -1,23 +1,27 @@
+"""
+Agent API Handlers.
+
+This module provides the following endpoints for agent management:
+- POST /: Create a new agent.
+- GET /: Get all agents for the current user.
+- GET /{agent_id}: Get details for a specific agent.
+- PATCH /{agent_id}: Update an existing agent.
+- DELETE /{agent_id}: Delete an agent.
+- GET /system/chat: Get the user's default chat agent.
+- GET /system/all: Get all user default agents.
+"""
+
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel.ext.asyncio.session import AsyncSession
from common.code import ErrCodeError, handle_auth_error
-from core.agent_service import AgentService, UnifiedAgentRead
from core.auth import AuthorizationService, get_auth_service
from core.system_agent import SystemAgentManager
from infra.database import get_session
from middleware.auth import get_current_user
from models.agent import AgentCreate, AgentRead, AgentReadWithDetails, AgentScope, AgentUpdate
-
-# Ensure forward references are resolved after importing both models
-# try:
-# AgentReadWithDetails.model_rebuild()
-# except Exception as e:
-# # If rebuild fails, log the error for debugging
-# import logging
-# logging.getLogger(__name__).warning(f"Failed to rebuild AgentReadWithDetails: {e}")
from repos import AgentRepository, ProviderRepository
router = APIRouter(tags=["agents"])
@@ -74,6 +78,7 @@ async def get_agents(
Returns all agents owned by the authenticated user, ordered by creation time.
Each agent includes its basic configuration, metadata, and associated MCP servers.
+ If the user has no agents, default agents will be initialized for them.
Args:
user: Authenticated user ID (injected by dependency)
@@ -83,8 +88,13 @@ async def get_agents(
list[AgentReadWithDetails]: list of agents owned by the user with MCP server details
Raises:
- HTTPException: None - this endpoint always succeeds, returning empty list if no agents
+ HTTPException: None - this endpoint always succeeds
"""
+ # Ensure user has default agents if they have none
+ system_manager = SystemAgentManager(db)
+ await system_manager.ensure_user_default_agents(user)
+ await db.commit()
+
agent_repo = AgentRepository(db)
agents = await agent_repo.get_agents_by_user(user)
@@ -102,87 +112,42 @@ async def get_agents(
return agents_with_details
-@router.get("/all/unified", response_model=list[UnifiedAgentRead])
-async def get_all_agents_unified(
- user: str = Depends(get_current_user),
- db: AsyncSession = Depends(get_session),
-) -> list[UnifiedAgentRead]:
- """
- Get all agents (both regular and graph) for the current authenticated user.
-
- Returns a unified list that includes both regular agents and graph agents,
- with consistent formatting and type indicators. This endpoint is designed
- for frontend consumption where both agent types should be displayed together.
-
- Args:
- user: Authenticated user ID (injected by dependency)
- db: Database session (injected by dependency)
-
- Returns:
- list[UnifiedAgentRead]: Unified list of all agents owned by the user
-
- Raises:
- HTTPException: None - this endpoint always succeeds, returning empty list if no agents
- """
- agent_service = AgentService(db)
- return await agent_service.get_all_agents_for_user(user)
-
-
-@router.get("/{agent_id}", response_model=UnifiedAgentRead)
+@router.get("/{agent_id}", response_model=AgentReadWithDetails)
async def get_agent(
- agent_id: str,
+ agent_id: UUID,
user_id: str = Depends(get_current_user),
auth_service: AuthorizationService = Depends(get_auth_service),
db: AsyncSession = Depends(get_session),
-) -> UnifiedAgentRead:
+) -> AgentReadWithDetails:
"""
- Get a single agent by ID (supports regular, graph, and builtin agents).
+ Get a single agent by ID.
- Returns the requested agent with full configuration details.
- For regular and graph agents, authorization ensures the user owns the agent.
- Builtin agents are available to all users.
+ Returns the requested agent with full configuration details including MCP servers.
+ Authorization ensures the user has access to the agent (owner or system agent).
Args:
- agent_id: Agent identifier (UUID string for regular/graph, or builtin agent ID)
- user: Authenticated user ID (injected by dependency)
+ agent_id: UUID of the agent to fetch
+ user_id: Authenticated user ID (injected by dependency)
db: Database session (injected by dependency)
Returns:
- UnifiedAgentRead: The requested agent with unified format
+ AgentReadWithDetails: The requested agent with MCP server details
Raises:
HTTPException: 404 if agent not found, 403 if access denied
"""
- agent_service = AgentService(db)
-
- # Handle builtin agents
- if agent_id.startswith("builtin_"):
- agent = await agent_service.get_agent_by_id(agent_id)
- if not agent:
- raise HTTPException(status_code=404, detail="Builtin agent not found")
- return agent
-
- # Handle regular/graph agents
try:
- agent_uuid = UUID(agent_id)
- agent = await agent_service.get_agent_by_id(agent_uuid, user_id)
- if not agent:
- raise HTTPException(status_code=404, detail="Agent not found")
-
- # For regular/graph agents, verify ownership
- if agent.agent_type in ["regular", "graph"]:
- # Get the actual agent model to check ownership
- from core.agent_type_detector import AgentTypeDetector
-
- detector = AgentTypeDetector(db)
+ agent = await auth_service.authorize_agent_read(agent_id, user_id)
- agent_with_type = await detector.get_agent_with_type(agent_uuid, user_id)
- if not agent_with_type:
- raise HTTPException(status_code=404, detail="Agent not found or access denied")
+ agent_repo = AgentRepository(db)
+ mcp_servers = await agent_repo.get_agent_mcp_servers(agent.id)
- return agent
- except ValueError:
- raise HTTPException(status_code=400, detail="Invalid agent ID format")
+ # Create agent dict with MCP servers
+ agent_dict = agent.model_dump()
+ agent_dict["mcp_servers"] = mcp_servers
+ return AgentReadWithDetails(**agent_dict)
+ except ErrCodeError as e:
+ raise handle_auth_error(e)
@router.patch("/{agent_id}", response_model=AgentReadWithDetails)
@@ -277,6 +242,10 @@ async def delete_agent(
if agent.scope == AgentScope.SYSTEM:
raise HTTPException(status_code=403, detail="Cannot delete system agents")
+ # Prevent deletion of default agents
+ if agent.tags and any(tag.startswith("default_") for tag in agent.tags):
+ raise HTTPException(status_code=403, detail="Cannot delete default agents")
+
agent_repo = AgentRepository(db)
await agent_repo.delete_agent(agent.id)
await db.commit()
@@ -290,70 +259,40 @@ async def get_system_chat_agent(
db: AsyncSession = Depends(get_session),
) -> AgentReadWithDetails:
"""
- Get the system chat agent available to all users.
+ Get the user's default chat agent.
- Returns the "随便聊聊" system agent with MCP server details.
- This agent is available to all authenticated users.
+ Returns the user's personal copy of the "随便聊聊" agent with MCP server details.
+ If it doesn't exist, it will be initialized.
Args:
user: Authenticated user ID (injected by dependency)
db: Database session (injected by dependency)
Returns:
- AgentReadWithDetails: The system chat agent with MCP server details
+ AgentReadWithDetails: The user's chat agent with MCP server details
Raises:
- HTTPException: 404 if system chat agent not found
+ HTTPException: 404 if chat agent not found
"""
- system_manager = SystemAgentManager(db)
- chat_agent = await system_manager.get_system_agent("chat")
-
- if not chat_agent:
- raise HTTPException(status_code=404, detail="System chat agent not found")
-
- # Get MCP servers for the system agent
agent_repo = AgentRepository(db)
- mcp_servers = await agent_repo.get_agent_mcp_servers(chat_agent.id)
-
- # Create agent dict with MCP servers
- agent_dict = chat_agent.model_dump()
- agent_dict["mcp_servers"] = mcp_servers
- return AgentReadWithDetails(**agent_dict)
-
-
-@router.get("/system/workshop", response_model=AgentReadWithDetails)
-async def get_system_workshop_agent(
- user: str = Depends(get_current_user),
- db: AsyncSession = Depends(get_session),
-) -> AgentReadWithDetails:
- """
- Get the system workshop agent available to all users.
-
- Returns the "创作工坊" system agent with MCP server details.
- This agent is available to all authenticated users.
-
- Args:
- user: Authenticated user ID (injected by dependency)
- db: Database session (injected by dependency)
+ agents = await agent_repo.get_agents_by_user(user)
- Returns:
- AgentReadWithDetails: The system workshop agent with MCP server details
+ chat_agent = next((a for a in agents if a.tags and "default_chat" in a.tags), None)
- Raises:
- HTTPException: 404 if system workshop agent not found
- """
- system_manager = SystemAgentManager(db)
- workshop_agent = await system_manager.get_system_agent("workshop")
+ if not chat_agent:
+ system_manager = SystemAgentManager(db)
+ new_agents = await system_manager.ensure_user_default_agents(user)
+ await db.commit()
+ chat_agent = next((a for a in new_agents if a.tags and "default_chat" in a.tags), None)
- if not workshop_agent:
- raise HTTPException(status_code=404, detail="System workshop agent not found")
+ if not chat_agent:
+ raise HTTPException(status_code=404, detail="Chat agent not found")
- # Get MCP servers for the system agent
- agent_repo = AgentRepository(db)
- mcp_servers = await agent_repo.get_agent_mcp_servers(workshop_agent.id)
+ # Get MCP servers for the agent
+ mcp_servers = await agent_repo.get_agent_mcp_servers(chat_agent.id)
# Create agent dict with MCP servers
- agent_dict = workshop_agent.model_dump()
+ agent_dict = chat_agent.model_dump()
agent_dict["mcp_servers"] = mcp_servers
return AgentReadWithDetails(**agent_dict)
@@ -364,26 +303,33 @@ async def get_all_system_agents(
db: AsyncSession = Depends(get_session),
) -> list[AgentReadWithDetails]:
"""
- Get all system agents available to all users.
+ Get all default agents for the user.
- Returns both the chat and workshop system agents with MCP server details.
- These agents are available to all authenticated users.
+ Returns the user's personal copies of system agents with MCP server details.
+ These are the agents tagged with 'default_'.
Args:
user: Authenticated user ID (injected by dependency)
db: Database session (injected by dependency)
Returns:
- list[AgentReadWithDetails]: list of all system agents with MCP server details
+ list[AgentReadWithDetails]: list of all user default agents with MCP server details
"""
- system_manager = SystemAgentManager(db)
- system_agents = await system_manager.get_all_system_agents()
+ agent_repo = AgentRepository(db)
+ agents = await agent_repo.get_agents_by_user(user)
+
+ # Filter for default agents
+ default_agents = [a for a in agents if a.tags and any(t.startswith("default_") for t in a.tags)]
+
+ if not default_agents:
+ system_manager = SystemAgentManager(db)
+ default_agents = await system_manager.ensure_user_default_agents(user)
+ await db.commit()
# Load MCP servers for each system agent
- agent_repo = AgentRepository(db)
agents_with_details = []
- for agent in system_agents:
+ for agent in default_agents:
# Get MCP servers for this agent
mcp_servers = await agent_repo.get_agent_mcp_servers(agent.id)
diff --git a/service/repos/agent.py b/service/repos/agent.py
index 197790bb..578242a3 100644
--- a/service/repos/agent.py
+++ b/service/repos/agent.py
@@ -5,7 +5,7 @@
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
-from models.agent import Agent, AgentCreate, AgentUpdate, AgentScope
+from models.agent import Agent, AgentCreate, AgentScope, AgentUpdate
from models.links import AgentMcpServerLink
from models.mcp import McpServer
diff --git a/web/src/app/App.tsx b/web/src/app/App.tsx
index d87527de..9a8d3855 100644
--- a/web/src/app/App.tsx
+++ b/web/src/app/App.tsx
@@ -42,7 +42,6 @@ export function Xyzen({
setBackendUrl,
toggleXyzen,
fetchAgents,
- fetchSystemAgents,
fetchMcpServers,
fetchChatHistory,
activateChannel,
@@ -131,7 +130,6 @@ export function Xyzen({
// 1. Fetch all necessary data in parallel
await Promise.all([
fetchAgents(),
- fetchSystemAgents(),
fetchMcpServers(),
fetchChatHistory(),
]);
@@ -160,7 +158,6 @@ export function Xyzen({
status,
initialLoadComplete,
fetchAgents,
- fetchSystemAgents,
fetchMcpServers,
fetchChatHistory,
activateChannel,
diff --git a/web/src/components/admin/CodesList.tsx b/web/src/components/admin/CodesList.tsx
index cd0b981a..b2a20f14 100644
--- a/web/src/components/admin/CodesList.tsx
+++ b/web/src/components/admin/CodesList.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useCallback } from "react";
interface GeneratedCode {
id: string;
@@ -28,7 +28,7 @@ export function CodesList({
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);
- const loadCodes = async () => {
+ const loadCodes = useCallback(async () => {
setIsLoading(true);
setError(null);
@@ -53,7 +53,7 @@ export function CodesList({
} finally {
setIsLoading(false);
}
- };
+ }, [adminSecret, backendUrl]);
const handleDeactivate = async (codeId: string) => {
if (!confirm("Are you sure you want to deactivate this code?")) return;
@@ -104,7 +104,7 @@ export function CodesList({
useEffect(() => {
loadCodes();
- }, []);
+ }, [loadCodes]);
useEffect(() => {
if (newCode) {
diff --git a/web/src/components/admin/SecretCodePage.tsx b/web/src/components/admin/SecretCodePage.tsx
index 0d68938e..8a09f7ab 100644
--- a/web/src/components/admin/SecretCodePage.tsx
+++ b/web/src/components/admin/SecretCodePage.tsx
@@ -58,7 +58,7 @@ export function SecretCodePage() {
} else {
setAuthError("Failed to verify admin secret key");
}
- } catch (err) {
+ } catch {
setAuthError("Network error: Failed to verify secret key");
} finally {
setIsVerifying(false);
diff --git a/web/src/components/layouts/AgentExplorer.tsx b/web/src/components/layouts/AgentExplorer.tsx
index 8ee3ad52..5aff8e69 100644
--- a/web/src/components/layouts/AgentExplorer.tsx
+++ b/web/src/components/layouts/AgentExplorer.tsx
@@ -314,8 +314,8 @@ export default function AgentExplorer() {
Official and community published graph agents will appear here.
- Create and publish your own graph agents in the Workshop to share
- with the community.
+ Create and publish your own graph agents to share with the
+ community.
diff --git a/web/src/components/layouts/Workshop.tsx b/web/src/components/layouts/Workshop.tsx
deleted file mode 100644
index 878f1329..00000000
--- a/web/src/components/layouts/Workshop.tsx
+++ /dev/null
@@ -1,260 +0,0 @@
-"use client";
-import { Badge } from "@/components/base/Badge";
-import { useXyzen } from "@/store";
-import {
- EyeIcon,
- EyeSlashIcon,
- PlayIcon,
- StopIcon,
-} from "@heroicons/react/24/outline";
-import { motion } from "framer-motion";
-import { useEffect } from "react";
-
-// Import the types we need
-import type { Agent } from "@/types/agents";
-import { isGraphAgent } from "@/types/agents";
-
-// Agent card component for workshop - moved outside the main component
-function GraphAgentCard({
- agent,
- onTogglePublish,
-}: {
- agent: Agent;
- onTogglePublish: (agentId: string) => void;
-}) {
- // Only render if it's a graph agent
- if (!isGraphAgent(agent)) {
- return null;
- }
-
- return (
-
-