Compare commits
12 Commits
37958291ae
...
f9ba3c8e51
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9ba3c8e51 | ||
|
|
fa3e30cc07 | ||
|
|
de3d5f6bf1 | ||
|
|
20d5e96986 | ||
|
|
723b249e42 | ||
|
|
90117b41fe | ||
|
|
174a5e2059 | ||
|
|
b93c40d5a5 | ||
|
|
d45079ca55 | ||
|
|
5a4aee91ab | ||
|
|
43ca06f591 | ||
|
|
c1bf679166 |
@ -85,6 +85,8 @@ class AgentConfig:
|
||||
robot_type = "deep_agent"
|
||||
|
||||
preamble_text, system_prompt = get_preamble_text(request.language, request.system_prompt)
|
||||
enable_thinking = request.enable_thinking and "<guidelines>" in request.system_prompt
|
||||
|
||||
config = cls(
|
||||
bot_id=request.bot_id,
|
||||
api_key=api_key,
|
||||
@ -96,7 +98,7 @@ class AgentConfig:
|
||||
robot_type=robot_type,
|
||||
user_identifier=request.user_identifier,
|
||||
session_id=request.session_id,
|
||||
enable_thinking=request.enable_thinking,
|
||||
enable_thinking=enable_thinking,
|
||||
project_dir=project_dir,
|
||||
stream=request.stream,
|
||||
tool_response=request.tool_response,
|
||||
@ -121,10 +123,11 @@ class AgentConfig:
|
||||
messages = []
|
||||
language = request.language or bot_config.get("language", "zh")
|
||||
preamble_text, system_prompt = get_preamble_text(language, bot_config.get("system_prompt"))
|
||||
|
||||
robot_type = bot_config.get("robot_type", "general_agent")
|
||||
if robot_type == "catalog_agent":
|
||||
robot_type = "deep_agent"
|
||||
enable_thinking = request.enable_thinking and "<guidelines>" in bot_config.get("system_prompt")
|
||||
|
||||
|
||||
config = cls(
|
||||
bot_id=request.bot_id,
|
||||
@ -137,7 +140,7 @@ class AgentConfig:
|
||||
robot_type=robot_type,
|
||||
user_identifier=request.user_identifier,
|
||||
session_id=request.session_id,
|
||||
enable_thinking=request.enable_thinking,
|
||||
enable_thinking=enable_thinking,
|
||||
project_dir=project_dir,
|
||||
stream=request.stream,
|
||||
tool_response=request.tool_response,
|
||||
|
||||
290
agent/chat_history_manager.py
Normal file
290
agent/chat_history_manager.py
Normal file
@ -0,0 +1,290 @@
|
||||
"""
|
||||
聊天历史记录管理器
|
||||
直接保存完整的原始聊天消息到数据库,不受 checkpoint summary 影响
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
logger = logging.getLogger('app')
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChatMessage:
|
||||
"""聊天消息"""
|
||||
id: str
|
||||
session_id: str
|
||||
role: str
|
||||
content: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ChatHistoryManager:
|
||||
"""
|
||||
聊天历史管理器
|
||||
|
||||
使用独立的数据库表存储完整的聊天历史记录
|
||||
复用 checkpoint_manager 的 PostgreSQL 连接池
|
||||
"""
|
||||
|
||||
def __init__(self, pool: AsyncConnectionPool):
|
||||
"""
|
||||
初始化聊天历史管理器
|
||||
|
||||
Args:
|
||||
pool: PostgreSQL 连接池
|
||||
"""
|
||||
self._pool = pool
|
||||
|
||||
async def create_table(self) -> None:
|
||||
"""创建 chat_messages 表"""
|
||||
async with self._pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS chat_messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
session_id VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
bot_id VARCHAR(255),
|
||||
user_identifier VARCHAR(255)
|
||||
)
|
||||
""")
|
||||
# 创建索引以加速查询
|
||||
await cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_session_created
|
||||
ON chat_messages (session_id, created_at DESC)
|
||||
""")
|
||||
await conn.commit()
|
||||
logger.info("chat_messages table created successfully")
|
||||
|
||||
async def save_message(
|
||||
self,
|
||||
session_id: str,
|
||||
role: str,
|
||||
content: str,
|
||||
bot_id: Optional[str] = None,
|
||||
user_identifier: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
保存一条聊天消息
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
role: 消息角色 ('user' 或 'assistant')
|
||||
content: 消息内容
|
||||
bot_id: 机器人ID
|
||||
user_identifier: 用户标识
|
||||
|
||||
Returns:
|
||||
str: 消息ID
|
||||
"""
|
||||
async with self._pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("""
|
||||
INSERT INTO chat_messages (session_id, role, content, bot_id, user_identifier)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (session_id, role, content, bot_id, user_identifier))
|
||||
result = await cursor.fetchone()
|
||||
await conn.commit()
|
||||
message_id = str(result[0]) if result else None
|
||||
logger.debug(f"Saved message: session_id={session_id}, role={role}, id={message_id}")
|
||||
return message_id
|
||||
|
||||
async def save_messages(
|
||||
self,
|
||||
session_id: str,
|
||||
messages: List[Dict[str, str]],
|
||||
bot_id: Optional[str] = None,
|
||||
user_identifier: Optional[str] = None
|
||||
) -> List[str]:
|
||||
"""
|
||||
批量保存聊天消息
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
messages: 消息列表,每条消息包含 role 和 content
|
||||
bot_id: 机器人ID
|
||||
user_identifier: 用户标识
|
||||
|
||||
Returns:
|
||||
List[str]: 消息ID列表
|
||||
"""
|
||||
message_ids = []
|
||||
async with self._pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
for msg in messages:
|
||||
role = msg.get('role')
|
||||
content = msg.get('content', '')
|
||||
if not role or not content:
|
||||
continue
|
||||
await cursor.execute("""
|
||||
INSERT INTO chat_messages (session_id, role, content, bot_id, user_identifier)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (session_id, role, content, bot_id, user_identifier))
|
||||
result = await cursor.fetchone()
|
||||
message_id = str(result[0]) if result else None
|
||||
message_ids.append(message_id)
|
||||
await conn.commit()
|
||||
logger.info(f"Saved {len(message_ids)} messages for session_id={session_id}")
|
||||
return message_ids
|
||||
|
||||
async def get_history(
|
||||
self,
|
||||
session_id: str,
|
||||
limit: int = 20,
|
||||
before_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取聊天历史记录(倒序,最新在前)
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
limit: 返回的消息数量
|
||||
before_id: 获取此消息ID之前的消息(用于分页)
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
"messages": [...],
|
||||
"has_more": bool
|
||||
}
|
||||
"""
|
||||
async with self._pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
if before_id:
|
||||
# 查询 before_id 的 created_at,然后获取更早的消息
|
||||
await cursor.execute("""
|
||||
SELECT created_at FROM chat_messages
|
||||
WHERE id = %s
|
||||
""", (before_id,))
|
||||
result = await cursor.fetchone()
|
||||
if not result:
|
||||
# 如果找不到指定的 ID,从头开始
|
||||
query = """
|
||||
SELECT id, session_id, role, content, created_at
|
||||
FROM chat_messages
|
||||
WHERE session_id = %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s + 1
|
||||
"""
|
||||
await cursor.execute(query, (session_id, limit))
|
||||
else:
|
||||
before_time = result[0]
|
||||
query = """
|
||||
SELECT id, session_id, role, content, created_at
|
||||
FROM chat_messages
|
||||
WHERE session_id = %s AND created_at < %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s + 1
|
||||
"""
|
||||
await cursor.execute(query, (session_id, before_time, limit))
|
||||
else:
|
||||
query = """
|
||||
SELECT id, session_id, role, content, created_at
|
||||
FROM chat_messages
|
||||
WHERE session_id = %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s + 1
|
||||
"""
|
||||
await cursor.execute(query, (session_id, limit))
|
||||
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
# 判断是否有更多(多取一条用于判断)
|
||||
has_more = len(rows) > limit
|
||||
if has_more:
|
||||
rows = rows[:limit]
|
||||
|
||||
messages = []
|
||||
for row in rows:
|
||||
messages.append({
|
||||
"id": str(row[0]),
|
||||
"role": row[2],
|
||||
"content": row[3],
|
||||
"timestamp": row[4].isoformat() if row[4] else None
|
||||
})
|
||||
|
||||
return {
|
||||
"messages": messages,
|
||||
"has_more": has_more
|
||||
}
|
||||
|
||||
async def get_history_by_message_id(
|
||||
self,
|
||||
session_id: str,
|
||||
last_message_id: Optional[str] = None,
|
||||
limit: int = 20
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
根据 last_message_id 获取更早的历史记录
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
last_message_id: 上一页最后一条消息的ID
|
||||
limit: 返回的消息数量
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
"messages": [...],
|
||||
"has_more": bool
|
||||
}
|
||||
"""
|
||||
return await self.get_history(
|
||||
session_id=session_id,
|
||||
limit=limit,
|
||||
before_id=last_message_id
|
||||
)
|
||||
|
||||
|
||||
# 全局单例
|
||||
_global_manager: Optional['ChatHistoryManagerWithPool'] = None
|
||||
|
||||
|
||||
class ChatHistoryManagerWithPool:
|
||||
"""
|
||||
带连接池的聊天历史管理器单例
|
||||
复用 checkpoint_manager 的连接池
|
||||
"""
|
||||
def __init__(self):
|
||||
self._pool: Optional[AsyncConnectionPool] = None
|
||||
self._manager: Optional[ChatHistoryManager] = None
|
||||
self._initialized = False
|
||||
|
||||
async def initialize(self, pool: AsyncConnectionPool) -> None:
|
||||
"""初始化管理器"""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self._pool = pool
|
||||
self._manager = ChatHistoryManager(pool)
|
||||
await self._manager.create_table()
|
||||
self._initialized = True
|
||||
logger.info("ChatHistoryManager initialized successfully")
|
||||
|
||||
@property
|
||||
def manager(self) -> ChatHistoryManager:
|
||||
"""获取 ChatHistoryManager 实例"""
|
||||
if not self._initialized or not self._manager:
|
||||
raise RuntimeError("ChatHistoryManager not initialized")
|
||||
return self._manager
|
||||
|
||||
|
||||
def get_chat_history_manager() -> ChatHistoryManagerWithPool:
|
||||
"""获取全局 ChatHistoryManager 单例"""
|
||||
global _global_manager
|
||||
if _global_manager is None:
|
||||
_global_manager = ChatHistoryManagerWithPool()
|
||||
return _global_manager
|
||||
|
||||
|
||||
async def init_chat_history_manager(pool: AsyncConnectionPool) -> None:
|
||||
"""初始化全局聊天历史管理器"""
|
||||
manager = get_chat_history_manager()
|
||||
await manager.initialize(pool)
|
||||
@ -68,6 +68,10 @@ class CheckpointerManager:
|
||||
checkpointer = AsyncPostgresSaver(conn=conn)
|
||||
await checkpointer.setup()
|
||||
|
||||
# 初始化 ChatHistoryManager(复用同一个连接池)
|
||||
from .chat_history_manager import init_chat_history_manager
|
||||
await init_chat_history_manager(self._pool)
|
||||
|
||||
self._initialized = True
|
||||
logger.info("PostgreSQL checkpointer pool initialized successfully")
|
||||
except Exception as e:
|
||||
|
||||
@ -30,6 +30,8 @@ from langchain_core.language_models import BaseChatModel
|
||||
from langgraph.pregel import Pregel
|
||||
from deepagents_cli.shell import ShellMiddleware
|
||||
from deepagents_cli.agent_memory import AgentMemoryMiddleware
|
||||
from langchain.agents.middleware import AgentMiddleware
|
||||
from langgraph.types import Checkpointer
|
||||
from deepagents_cli.skills import SkillsMiddleware
|
||||
from deepagents_cli.config import settings, get_default_coding_instructions
|
||||
import os
|
||||
@ -161,6 +163,30 @@ async def init_agent(config: AgentConfig):
|
||||
|
||||
checkpointer = None
|
||||
create_start = time.time()
|
||||
|
||||
# 构建中间件列表
|
||||
middleware = []
|
||||
# 首先添加 ToolUseCleanupMiddleware 来清理孤立的 tool_use
|
||||
middleware.append(ToolUseCleanupMiddleware())
|
||||
# 添加工具输出长度控制中间件
|
||||
tool_output_middleware = ToolOutputLengthMiddleware(
|
||||
max_length=getattr(config.generate_cfg, 'tool_output_max_length', None) if config.generate_cfg else None or TOOL_OUTPUT_MAX_LENGTH,
|
||||
truncation_strategy=getattr(config.generate_cfg, 'tool_output_truncation_strategy', 'smart') if config.generate_cfg else 'smart',
|
||||
tool_filters=getattr(config.generate_cfg, 'tool_output_filters', None) if config.generate_cfg else None,
|
||||
exclude_tools=getattr(config.generate_cfg, 'tool_output_exclude', []) if config.generate_cfg else [],
|
||||
preserve_code_blocks=getattr(config.generate_cfg, 'preserve_code_blocks', True) if config.generate_cfg else True,
|
||||
preserve_json=getattr(config.generate_cfg, 'preserve_json', True) if config.generate_cfg else True
|
||||
)
|
||||
middleware.append(tool_output_middleware)
|
||||
|
||||
# 从连接池获取 checkpointer
|
||||
if config.session_id:
|
||||
from .checkpoint_manager import get_checkpointer_manager
|
||||
manager = get_checkpointer_manager()
|
||||
checkpointer = manager.checkpointer
|
||||
await prepare_checkpoint_message(config, checkpointer)
|
||||
|
||||
|
||||
if config.robot_type == "deep_agent":
|
||||
# 使用 DeepAgentX 创建 agent,自定义 workspace_root
|
||||
workspace_root = f"projects/robot/{config.bot_id}"
|
||||
@ -172,34 +198,16 @@ async def init_agent(config: AgentConfig):
|
||||
tools=mcp_tools,
|
||||
auto_approve=True,
|
||||
enable_memory=False,
|
||||
workspace_root=workspace_root
|
||||
workspace_root=workspace_root,
|
||||
middleware=middleware,
|
||||
checkpointer=checkpointer
|
||||
)
|
||||
else:
|
||||
# 构建中间件列表
|
||||
middleware = []
|
||||
# 首先添加 ToolUseCleanupMiddleware 来清理孤立的 tool_use
|
||||
middleware.append(ToolUseCleanupMiddleware())
|
||||
# 只有在 enable_thinking 为 True 时才添加 GuidelineMiddleware
|
||||
if config.enable_thinking:
|
||||
middleware.append(GuidelineMiddleware(llm_instance, config, system_prompt))
|
||||
|
||||
# 添加工具输出长度控制中间件
|
||||
tool_output_middleware = ToolOutputLengthMiddleware(
|
||||
max_length=getattr(config.generate_cfg, 'tool_output_max_length', None) if config.generate_cfg else None or TOOL_OUTPUT_MAX_LENGTH,
|
||||
truncation_strategy=getattr(config.generate_cfg, 'tool_output_truncation_strategy', 'smart') if config.generate_cfg else 'smart',
|
||||
tool_filters=getattr(config.generate_cfg, 'tool_output_filters', None) if config.generate_cfg else None,
|
||||
exclude_tools=getattr(config.generate_cfg, 'tool_output_exclude', []) if config.generate_cfg else [],
|
||||
preserve_code_blocks=getattr(config.generate_cfg, 'preserve_code_blocks', True) if config.generate_cfg else True,
|
||||
preserve_json=getattr(config.generate_cfg, 'preserve_json', True) if config.generate_cfg else True
|
||||
)
|
||||
middleware.append(tool_output_middleware)
|
||||
|
||||
# 从连接池获取 checkpointer
|
||||
if config.session_id:
|
||||
from .checkpoint_manager import get_checkpointer_manager
|
||||
manager = get_checkpointer_manager()
|
||||
checkpointer = manager.checkpointer
|
||||
await prepare_checkpoint_message(config, checkpointer)
|
||||
summarization_middleware = SummarizationMiddleware(
|
||||
model=llm_instance,
|
||||
max_tokens_before_summary=SUMMARIZATION_MAX_TOKENS,
|
||||
@ -317,7 +325,9 @@ def create_custom_cli_agent(
|
||||
enable_memory: bool = True,
|
||||
enable_skills: bool = True,
|
||||
enable_shell: bool = True,
|
||||
middleware: list[AgentMiddleware] = [],
|
||||
workspace_root: str | None = None,
|
||||
checkpointer: Checkpointer | None = None,
|
||||
) -> tuple[Pregel, CompositeBackend]:
|
||||
"""Create a CLI-configured agent with custom workspace_root for shell commands.
|
||||
|
||||
@ -358,7 +368,7 @@ def create_custom_cli_agent(
|
||||
agent_md.write_text(source_content)
|
||||
|
||||
# Build middleware stack based on enabled features
|
||||
agent_middleware = []
|
||||
agent_middleware = middleware
|
||||
|
||||
# CONDITIONAL SETUP: Local vs Remote Sandbox
|
||||
if sandbox is None:
|
||||
@ -453,6 +463,6 @@ def create_custom_cli_agent(
|
||||
backend=composite_backend,
|
||||
middleware=agent_middleware,
|
||||
interrupt_on=interrupt_on,
|
||||
checkpointer=InMemorySaver(),
|
||||
checkpointer=checkpointer,
|
||||
).with_config(config)
|
||||
return agent, composite_backend
|
||||
51
docker-compose-with-pgsql.yml
Normal file
51
docker-compose-with-pgsql.yml
Normal file
@ -0,0 +1,51 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: qwen-agent-postgres
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=E5ACJo6zJub4QS
|
||||
- POSTGRES_DB=agent_db
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
qwen-agent:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.modelscope
|
||||
container_name: qwen-agent-api
|
||||
ports:
|
||||
- "8001:8001"
|
||||
environment:
|
||||
# 应用配置
|
||||
- BACKEND_HOST=http://api-dev.gbase.ai
|
||||
- MAX_CONTEXT_TOKENS=262144
|
||||
- DEFAULT_THINKING_ENABLE=true
|
||||
# PostgreSQL 配置
|
||||
- CHECKPOINT_DB_URL=postgresql://postgres:E5ACJo6zJub4QS@postgres:5432/agent_db
|
||||
volumes:
|
||||
# 挂载项目数据目录
|
||||
- ./projects:/app/projects
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8001/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
volumes:
|
||||
postgres_data:
|
||||
@ -1,8 +1,4 @@
|
||||
<env>
|
||||
Working directory: {agent_dir_path}
|
||||
Current User: {user_identifier}
|
||||
Current Time: {datetime}
|
||||
</env>
|
||||
{extra_prompt}
|
||||
|
||||
### Current Working Directory
|
||||
|
||||
@ -39,10 +35,44 @@ When using the write_todos tool:
|
||||
2. Only create todos for complex, multi-step tasks that truly need tracking
|
||||
3. Break down work into clear, actionable items without over-fragmenting
|
||||
4. For simple tasks (1-2 steps), just do them directly without creating todos
|
||||
5. When first creating a todo list for a task, ALWAYS ask the user if the plan looks good before starting work
|
||||
- Create the todos, let them render, then ask: "Does this plan look good?" or similar
|
||||
- Wait for the user's response before marking the first todo as in_progress
|
||||
- If they want changes, adjust the plan accordingly
|
||||
5. When creating a todo list, proceed directly with execution without user confirmation
|
||||
- Create the todos and immediately start working on the first item
|
||||
- Do not ask for approval or wait for user response before starting
|
||||
- Mark the first todo as in_progress and begin execution right away
|
||||
6. Update todo status promptly as you complete each item
|
||||
|
||||
The todo list is a planning tool - use it judiciously to avoid overwhelming the user with excessive task tracking.
|
||||
|
||||
### Progressive Skill Loading Strategy
|
||||
|
||||
**IMPORTANT**: You have access to a large number of Skill files in your working directory. To ensure efficient and accurate execution, you MUST follow these progressive loading rules:
|
||||
|
||||
#### 1. Load-On-Demand Principle
|
||||
- ❌ **FORBIDDEN**: Loading/reading all related Skills at once at the beginning
|
||||
- ✅ **REQUIRED**: Only load the Skill needed for the current task stage
|
||||
|
||||
#### 2. Phased Loading Process
|
||||
|
||||
Break down complex tasks into stages. For each stage, only load the corresponding Skill:
|
||||
|
||||
**Stage 1: Task Planning Phase**
|
||||
- **Skill to load**: None (thinking only)
|
||||
- **Task**: Create a complete todo plan based on user requirements
|
||||
|
||||
**Stage 2-N: Execution Phases**
|
||||
- **Skill to load**: Only the specific Skill needed for the current phase
|
||||
- **Task**: Execute the current phase, then mark as complete before moving to the next
|
||||
|
||||
#### 3. Prohibited Behaviors
|
||||
|
||||
1. ❌ **Loading all Skills at once** - Must use progressive, phased loading
|
||||
2. ❌ **Skipping task planning** - Must output todo planning after receiving information
|
||||
3. ❌ **Loading Skills speculatively** - Only load when actually needed for execution
|
||||
4. ❌ **Loading multiple Skills simultaneously** - Only load one Skill at a time for current phase
|
||||
|
||||
## System Information
|
||||
<env>
|
||||
Working directory: {agent_dir_path}
|
||||
Current User: {user_identifier}
|
||||
Current Time: {datetime}
|
||||
</env>
|
||||
|
||||
@ -13,5 +13,7 @@
|
||||
**Image Handling**: The content returned by the `rag_retrieve` tool may include images. Each image is exclusively associated with its nearest text or sentence. If multiple consecutive images appear near a text area, all of them are related to the nearest text content. Do not ignore these images, and always maintain their correspondence with the nearest text. Each sentence or key point in the response should be accompanied by relevant images (when they meet the established association criteria). Avoid placing all images at the end of the response.
|
||||
|
||||
## System Information
|
||||
- **Current User**: {user_identifier}
|
||||
- **Current Time**: {datetime}
|
||||
<env>
|
||||
Current User: {user_identifier}
|
||||
Current Time: {datetime}
|
||||
</env>
|
||||
|
||||
909
public/bot-manager.html
Normal file
909
public/bot-manager.html
Normal file
@ -0,0 +1,909 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bot Manager</title>
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<!-- Lucide Icons -->
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary: #2563EB;
|
||||
--primary-hover: #1D4ED8;
|
||||
--secondary: #3B82F6;
|
||||
--background: #F8FAFC;
|
||||
--surface: #FFFFFF;
|
||||
--text: #1E293B;
|
||||
--text-muted: #64748B;
|
||||
--border: #E2E8F0;
|
||||
--success: #10B981;
|
||||
--warning: #F59E0B;
|
||||
--error: #EF4444;
|
||||
--glass-bg: rgba(255, 255, 255, 0.8);
|
||||
--glass-blur: 12px;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--primary: #3B82F6;
|
||||
--primary-hover: #60A5FA;
|
||||
--secondary: #60A5FA;
|
||||
--background: #0F172A;
|
||||
--surface: #1E293B;
|
||||
--text: #F1F5F9;
|
||||
--text-muted: #94A3B8;
|
||||
--border: #334155;
|
||||
--glass-bg: rgba(30, 41, 59, 0.8);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
/* ===== Header ===== */
|
||||
.header {
|
||||
height: 64px;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.header-btn:hover {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.header-btn.primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: 0 16px;
|
||||
width: auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-btn.primary:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
/* ===== Main Content ===== */
|
||||
.main-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ===== Bot Grid ===== */
|
||||
.bot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.bot-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bot-card:hover {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 4px 20px rgba(37, 99, 235, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.bot-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.bot-card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bot-card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bot-card-name {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.bot-card-id {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
background: var(--background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bot-card-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.bot-card-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bot-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.bot-card-action {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.bot-card-action:hover {
|
||||
background: var(--background);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.bot-card-action.delete:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: var(--error);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
/* ===== Empty State ===== */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 20px;
|
||||
border-radius: 20px;
|
||||
background: var(--background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-state-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* ===== Modal ===== */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--surface);
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 480px;
|
||||
max-height: 85vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--background);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
/* ===== Delete Confirmation Modal ===== */
|
||||
.delete-confirm-content {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.delete-confirm-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: 0 auto 16px;
|
||||
border-radius: 50%;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.delete-confirm-title {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.delete-confirm-text {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ===== Scrollbar ===== */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ===== Responsive ===== */
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.bot-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-btn.primary span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<div class="header-logo">
|
||||
<i data-lucide="bot" style="width: 20px; height: 20px;"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="header-title">Bot Manager</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="header-btn" id="theme-toggle" title="切换主题">
|
||||
<i data-lucide="moon" style="width: 18px; height: 18px;"></i>
|
||||
</button>
|
||||
<button class="header-btn primary" id="add-bot-btn">
|
||||
<i data-lucide="plus" style="width: 16px; height: 16px;"></i>
|
||||
<span>新建 Bot</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">我的 Bots</h1>
|
||||
<p class="page-subtitle">管理您的 AI 聊天机器人配置</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bot Grid -->
|
||||
<div class="bot-grid" id="bot-grid">
|
||||
<!-- 动态生成 -->
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Add/Edit Bot Modal -->
|
||||
<div class="modal-overlay" id="bot-modal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="bot-modal-title">新建 Bot</h3>
|
||||
<button class="modal-close" id="bot-modal-close">
|
||||
<i data-lucide="x" style="width: 18px; height: 18px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="bot-name">Bot 名称 *</label>
|
||||
<input type="text" id="bot-name" class="form-input" placeholder="例如:客服助手、销售顾问">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="bot-id-input">Bot ID *</label>
|
||||
<input type="text" id="bot-id-input" class="form-input" placeholder="例如:test 或 UUID">
|
||||
<p class="form-hint">这是用于 API 调用的唯一标识符</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="bot-modal-cancel">取消</button>
|
||||
<button class="btn btn-primary" id="bot-modal-save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal-overlay" id="delete-modal">
|
||||
<div class="modal" style="max-width: 400px;">
|
||||
<div class="modal-body">
|
||||
<div class="delete-confirm-content">
|
||||
<div class="delete-confirm-icon">
|
||||
<i data-lucide="alert-triangle" style="width: 28px; height: 28px;"></i>
|
||||
</div>
|
||||
<h3 class="delete-confirm-title">确认删除</h3>
|
||||
<p class="delete-confirm-text">确定要删除这个 Bot 吗?此操作无法撤销。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="delete-modal-cancel">取消</button>
|
||||
<button class="btn btn-primary" id="delete-modal-confirm" style="background: var(--error);">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
|
||||
class BotManager {
|
||||
constructor() {
|
||||
this.elements = {
|
||||
themeToggle: document.getElementById('theme-toggle'),
|
||||
addBotBtn: document.getElementById('add-bot-btn'),
|
||||
botGrid: document.getElementById('bot-grid'),
|
||||
// Bot Modal
|
||||
botModal: document.getElementById('bot-modal'),
|
||||
botModalTitle: document.getElementById('bot-modal-title'),
|
||||
botModalClose: document.getElementById('bot-modal-close'),
|
||||
botModalCancel: document.getElementById('bot-modal-cancel'),
|
||||
botModalSave: document.getElementById('bot-modal-save'),
|
||||
botNameInput: document.getElementById('bot-name'),
|
||||
botIdInput: document.getElementById('bot-id-input'),
|
||||
// Delete Modal
|
||||
deleteModal: document.getElementById('delete-modal'),
|
||||
deleteModalCancel: document.getElementById('delete-modal-cancel'),
|
||||
deleteModalConfirm: document.getElementById('delete-modal-confirm')
|
||||
};
|
||||
|
||||
this.bots = [];
|
||||
this.editingBotId = null;
|
||||
this.deletingBotId = null;
|
||||
|
||||
this.initializeEventListeners();
|
||||
this.loadTheme();
|
||||
this.loadBots();
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
// Theme toggle
|
||||
this.elements.themeToggle.addEventListener('click', () => this.toggleTheme());
|
||||
|
||||
// Add bot
|
||||
this.elements.addBotBtn.addEventListener('click', () => this.openBotModal());
|
||||
|
||||
// Bot modal
|
||||
this.elements.botModalClose.addEventListener('click', () => this.closeBotModal());
|
||||
this.elements.botModalCancel.addEventListener('click', () => this.closeBotModal());
|
||||
this.elements.botModalSave.addEventListener('click', () => this.saveBot());
|
||||
this.elements.botModal.addEventListener('click', (e) => {
|
||||
if (e.target === this.elements.botModal) this.closeBotModal();
|
||||
});
|
||||
|
||||
// Delete modal
|
||||
this.elements.deleteModalCancel.addEventListener('click', () => this.closeDeleteModal());
|
||||
this.elements.deleteModalConfirm.addEventListener('click', () => this.confirmDelete());
|
||||
}
|
||||
|
||||
loadTheme() {
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
this.updateThemeIcon(theme === 'dark');
|
||||
}
|
||||
|
||||
toggleTheme() {
|
||||
document.documentElement.classList.toggle('dark');
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||
this.updateThemeIcon(isDark);
|
||||
}
|
||||
|
||||
updateThemeIcon(isDark) {
|
||||
const icon = this.elements.themeToggle.querySelector('i');
|
||||
if (!icon) return;
|
||||
icon.setAttribute('data-lucide', isDark ? 'sun' : 'moon');
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
loadBots() {
|
||||
const stored = localStorage.getItem('bot-list');
|
||||
if (stored) {
|
||||
try {
|
||||
this.bots = JSON.parse(stored);
|
||||
} catch (e) {
|
||||
this.bots = [];
|
||||
}
|
||||
}
|
||||
this.renderBots();
|
||||
}
|
||||
|
||||
saveBots() {
|
||||
localStorage.setItem('bot-list', JSON.stringify(this.bots));
|
||||
}
|
||||
|
||||
renderBots() {
|
||||
const grid = this.elements.botGrid;
|
||||
|
||||
if (this.bots.length === 0) {
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state" style="grid-column: 1 / -1;">
|
||||
<div class="empty-state-icon">
|
||||
<i data-lucide="bot" style="width: 40px; height: 40px;"></i>
|
||||
</div>
|
||||
<h2 class="empty-state-title">暂无 Bots</h2>
|
||||
<p class="empty-state-subtitle">点击上方按钮创建您的第一个 Bot</p>
|
||||
</div>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = '';
|
||||
this.bots.forEach(bot => {
|
||||
const card = this.createBotCard(bot);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
createBotCard(bot) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'bot-card';
|
||||
|
||||
const formatDate = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
};
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="bot-card-header">
|
||||
<div class="bot-card-icon">
|
||||
<i data-lucide="bot" style="width: 24px; height: 24px;"></i>
|
||||
</div>
|
||||
<div class="bot-card-info">
|
||||
<div class="bot-card-name">${this.escapeHtml(bot.name)}</div>
|
||||
<span class="bot-card-id">${this.escapeHtml(bot.botId)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bot-card-meta">
|
||||
<span class="bot-card-meta-item">
|
||||
<i data-lucide="calendar" style="width: 12px; height: 12px;"></i>
|
||||
创建于 ${formatDate(bot.createdAt)}
|
||||
</span>
|
||||
<span class="bot-card-meta-item">
|
||||
<i data-lucide="clock" style="width: 12px; height: 12px;"></i>
|
||||
更新于 ${formatDate(bot.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="bot-card-actions">
|
||||
<button class="bot-card-action" data-action="open" data-bot-id="${bot.id}">
|
||||
<i data-lucide="external-link" style="width: 14px; height: 14px;"></i>
|
||||
打开
|
||||
</button>
|
||||
<button class="bot-card-action" data-action="edit" data-bot-id="${bot.id}">
|
||||
<i data-lucide="pencil" style="width: 14px; height: 14px;"></i>
|
||||
编辑
|
||||
</button>
|
||||
<button class="bot-card-action delete" data-action="delete" data-bot-id="${bot.id}">
|
||||
<i data-lucide="trash-2" style="width: 14px; height: 14px;"></i>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event listeners
|
||||
card.addEventListener('click', (e) => {
|
||||
const action = e.target.closest('[data-action]')?.dataset.action;
|
||||
const botId = e.target.closest('[data-bot-id]')?.dataset.botId;
|
||||
|
||||
if (action === 'open' && botId) {
|
||||
this.openBotChat(botId);
|
||||
} else if (action === 'edit' && botId) {
|
||||
e.stopPropagation();
|
||||
this.openEditModal(botId);
|
||||
} else if (action === 'delete' && botId) {
|
||||
e.stopPropagation();
|
||||
this.openDeleteModal(botId);
|
||||
}
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
openBotChat(botInternalId) {
|
||||
const bot = this.bots.find(b => b.id === botInternalId);
|
||||
if (bot) {
|
||||
// 保存当前选中的 bot ID
|
||||
sessionStorage.setItem('current-bot-id', bot.id);
|
||||
// 跳转到聊天页面
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
}
|
||||
|
||||
openBotModal() {
|
||||
this.editingBotId = null;
|
||||
this.elements.botModalTitle.textContent = '新建 Bot';
|
||||
this.elements.botNameInput.value = '';
|
||||
this.elements.botIdInput.value = '';
|
||||
this.elements.botModal.classList.add('active');
|
||||
this.elements.botNameInput.focus();
|
||||
}
|
||||
|
||||
openEditModal(botInternalId) {
|
||||
const bot = this.bots.find(b => b.id === botInternalId);
|
||||
if (!bot) return;
|
||||
|
||||
this.editingBotId = botInternalId;
|
||||
this.elements.botModalTitle.textContent = '编辑 Bot';
|
||||
this.elements.botNameInput.value = bot.name;
|
||||
this.elements.botIdInput.value = bot.botId;
|
||||
this.elements.botModal.classList.add('active');
|
||||
this.elements.botNameInput.focus();
|
||||
}
|
||||
|
||||
closeBotModal() {
|
||||
this.elements.botModal.classList.remove('active');
|
||||
this.editingBotId = null;
|
||||
}
|
||||
|
||||
saveBot() {
|
||||
const name = this.elements.botNameInput.value.trim();
|
||||
const botId = this.elements.botIdInput.value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('请输入 Bot 名称');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!botId) {
|
||||
alert('请输入 Bot ID');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 Bot ID 是否重复
|
||||
const existingBot = this.bots.find(b => b.botId === botId && b.id !== this.editingBotId);
|
||||
if (existingBot) {
|
||||
alert('Bot ID 已存在,请使用其他 ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (this.editingBotId) {
|
||||
// 编辑现有 bot
|
||||
const bot = this.bots.find(b => b.id === this.editingBotId);
|
||||
if (bot) {
|
||||
bot.name = name;
|
||||
bot.botId = botId;
|
||||
bot.updatedAt = now;
|
||||
}
|
||||
} else {
|
||||
// 新建 bot
|
||||
const newBot = {
|
||||
id: this.generateUUID(),
|
||||
name: name,
|
||||
botId: botId,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
this.bots.unshift(newBot);
|
||||
}
|
||||
|
||||
this.saveBots();
|
||||
this.renderBots();
|
||||
this.closeBotModal();
|
||||
}
|
||||
|
||||
openDeleteModal(botInternalId) {
|
||||
this.deletingBotId = botInternalId;
|
||||
this.elements.deleteModal.classList.add('active');
|
||||
}
|
||||
|
||||
closeDeleteModal() {
|
||||
this.elements.deleteModal.classList.remove('active');
|
||||
this.deletingBotId = null;
|
||||
}
|
||||
|
||||
confirmDelete() {
|
||||
if (!this.deletingBotId) return;
|
||||
|
||||
const bot = this.bots.find(b => b.id === this.deletingBotId);
|
||||
if (bot) {
|
||||
// 删除 bot 的设置数据
|
||||
localStorage.removeItem(`bot-settings-${bot.botId}`);
|
||||
}
|
||||
|
||||
this.bots = this.bots.filter(b => b.id !== this.deletingBotId);
|
||||
this.saveBots();
|
||||
this.renderBots();
|
||||
this.closeDeleteModal();
|
||||
}
|
||||
|
||||
generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize app
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new BotManager();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
4083
public/index.html
4083
public/index.html
File diff suppressed because it is too large
Load Diff
1070
public/model-manager.html
Normal file
1070
public/model-manager.html
Normal file
File diff suppressed because it is too large
Load Diff
176
routes/chat.py
176
routes/chat.py
@ -1,7 +1,7 @@
|
||||
import json
|
||||
import os
|
||||
import asyncio
|
||||
from typing import Union, Optional
|
||||
from typing import Union, Optional, Any, List, Dict
|
||||
from fastapi import APIRouter, HTTPException, Header
|
||||
from fastapi.responses import StreamingResponse
|
||||
import logging
|
||||
@ -17,7 +17,7 @@ from utils.fastapi_utils import (
|
||||
call_preamble_llm,
|
||||
create_stream_chunk
|
||||
)
|
||||
from langchain_core.messages import AIMessageChunk, ToolMessage, AIMessage
|
||||
from langchain_core.messages import AIMessageChunk, ToolMessage, AIMessage, HumanMessage
|
||||
from utils.settings import MAX_OUTPUT_TOKENS
|
||||
from agent.agent_config import AgentConfig
|
||||
from agent.deep_assistant import init_agent
|
||||
@ -34,11 +34,18 @@ async def enhanced_generate_stream_response(
|
||||
agent: LangChain agent 对象
|
||||
config: AgentConfig 对象,包含所有参数
|
||||
"""
|
||||
# 用于收集完整的响应内容,用于保存到数据库
|
||||
full_response_content = []
|
||||
|
||||
try:
|
||||
# 创建输出队列和控制事件
|
||||
output_queue = asyncio.Queue()
|
||||
preamble_completed = asyncio.Event()
|
||||
|
||||
# 在流式开始前保存用户消息
|
||||
if config.session_id:
|
||||
asyncio.create_task(_save_user_messages(config))
|
||||
|
||||
# Preamble 任务
|
||||
async def preamble_task():
|
||||
try:
|
||||
@ -76,7 +83,7 @@ async def enhanced_generate_stream_response(
|
||||
|
||||
if isinstance(msg, AIMessageChunk):
|
||||
# 处理工具调用
|
||||
if msg.tool_call_chunks:
|
||||
if msg.tool_call_chunks and config.tool_response:
|
||||
message_tag = "TOOL_CALL"
|
||||
for tool_call_chunk in msg.tool_call_chunks:
|
||||
if tool_call_chunk["name"]:
|
||||
@ -100,8 +107,11 @@ async def enhanced_generate_stream_response(
|
||||
message_tag = "TOOL_RESPONSE"
|
||||
new_content = f"[{message_tag}] {msg.name}\n{msg.text}\n"
|
||||
|
||||
# 发送内容块
|
||||
# 收集完整内容
|
||||
if new_content:
|
||||
full_response_content.append(new_content)
|
||||
|
||||
# 发送内容块
|
||||
if chunk_id == 0:
|
||||
logger.info(f"Agent首个Token已生成, 开始流式输出")
|
||||
chunk_id += 1
|
||||
@ -176,6 +186,10 @@ async def enhanced_generate_stream_response(
|
||||
yield "data: [DONE]\n\n"
|
||||
logger.info(f"Enhanced stream response completed")
|
||||
|
||||
# 流式结束后保存 AI 响应
|
||||
if full_response_content and config.session_id:
|
||||
asyncio.create_task(_save_assistant_response(config, "".join(full_response_content)))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in enhanced_generate_stream_response: {e}")
|
||||
yield f'data: {{"error": "{str(e)}"}}\n\n'
|
||||
@ -197,11 +211,24 @@ async def create_agent_and_generate_response(
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}
|
||||
)
|
||||
|
||||
|
||||
agent, checkpointer = await init_agent(config)
|
||||
# 使用更新后的 messages
|
||||
agent_responses = await agent.ainvoke({"messages": config.messages}, config=config.invoke_config(), max_tokens=MAX_OUTPUT_TOKENS)
|
||||
append_messages = agent_responses["messages"][len(config.messages):]
|
||||
|
||||
# 从后往前找第一个 HumanMessage,之后的内容都给 append_messages
|
||||
all_messages = agent_responses["messages"]
|
||||
first_human_idx = None
|
||||
for i in range(len(all_messages) - 1, -1, -1):
|
||||
if isinstance(all_messages[i], HumanMessage):
|
||||
first_human_idx = i
|
||||
break
|
||||
|
||||
if first_human_idx is not None:
|
||||
append_messages = all_messages[first_human_idx + 1:]
|
||||
else:
|
||||
# 如果没找到 HumanMessage,取所有消息
|
||||
append_messages = all_messages
|
||||
response_text = ""
|
||||
for msg in append_messages:
|
||||
if isinstance(msg,AIMessage):
|
||||
@ -209,7 +236,7 @@ async def create_agent_and_generate_response(
|
||||
meta_message_tag = msg.additional_kwargs.get("message_tag", "ANSWER")
|
||||
output_text = msg.text.replace("````","").replace("````","") if meta_message_tag == "THINK" else msg.text
|
||||
response_text += f"[{meta_message_tag}]\n"+output_text+ "\n"
|
||||
if len(msg.tool_calls)>0:
|
||||
if len(msg.tool_calls)>0 and config.tool_response:
|
||||
response_text += "".join([f"[TOOL_CALL] {tool['name']}\n{json.dumps(tool["args"]) if isinstance(tool["args"],dict) else tool["args"]}\n" for tool in msg.tool_calls])
|
||||
elif isinstance(msg,ToolMessage) and config.tool_response:
|
||||
response_text += f"[TOOL_RESPONSE] {msg.name}\n{msg.text}\n"
|
||||
@ -231,11 +258,86 @@ async def create_agent_and_generate_response(
|
||||
"total_tokens": sum(len(msg.get("content", "")) for msg in config.messages) + len(response_text)
|
||||
}
|
||||
)
|
||||
|
||||
# 保存聊天历史到数据库(与流式接口保持一致的逻辑)
|
||||
await _save_user_messages(config)
|
||||
await _save_assistant_response(config, response_text)
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="No response from agent")
|
||||
|
||||
return result
|
||||
|
||||
async def _save_user_messages(config: AgentConfig) -> None:
|
||||
"""
|
||||
保存最后一条用户消息(用于流式和非流式接口)
|
||||
|
||||
Args:
|
||||
config: AgentConfig 对象
|
||||
"""
|
||||
# 只有在 session_id 存在时才保存
|
||||
if not config.session_id:
|
||||
return
|
||||
|
||||
try:
|
||||
from agent.chat_history_manager import get_chat_history_manager
|
||||
|
||||
manager = get_chat_history_manager()
|
||||
|
||||
# 只保存最后一条 user 消息
|
||||
for msg in reversed(config.messages):
|
||||
if isinstance(msg, dict):
|
||||
role = msg.get("role", "")
|
||||
content = msg.get("content", "")
|
||||
if role == "user" and content:
|
||||
await manager.manager.save_message(
|
||||
session_id=config.session_id,
|
||||
role=role,
|
||||
content=content,
|
||||
bot_id=config.bot_id,
|
||||
user_identifier=config.user_identifier
|
||||
)
|
||||
break # 只保存最后一条,然后退出
|
||||
|
||||
logger.debug(f"Saved last user message for session_id={config.session_id}")
|
||||
except Exception as e:
|
||||
# 保存失败不影响主流程
|
||||
logger.error(f"Failed to save user messages: {e}")
|
||||
|
||||
|
||||
async def _save_assistant_response(config: AgentConfig, assistant_response: str) -> None:
|
||||
"""
|
||||
保存 AI 助手的响应(用于流式和非流式接口)
|
||||
|
||||
Args:
|
||||
config: AgentConfig 对象
|
||||
assistant_response: AI 助手的响应内容
|
||||
"""
|
||||
# 只有在 session_id 存在时才保存
|
||||
if not config.session_id:
|
||||
return
|
||||
|
||||
if not assistant_response:
|
||||
return
|
||||
|
||||
try:
|
||||
from agent.chat_history_manager import get_chat_history_manager
|
||||
|
||||
manager = get_chat_history_manager()
|
||||
|
||||
# 保存 AI 助手的响应
|
||||
await manager.manager.save_message(
|
||||
session_id=config.session_id,
|
||||
role="assistant",
|
||||
content=assistant_response,
|
||||
bot_id=config.bot_id,
|
||||
user_identifier=config.user_identifier
|
||||
)
|
||||
|
||||
logger.debug(f"Saved assistant response for session_id={config.session_id}")
|
||||
except Exception as e:
|
||||
# 保存失败不影响主流程
|
||||
logger.error(f"Failed to save assistant response: {e}")
|
||||
|
||||
|
||||
@router.post("/api/v1/chat/completions")
|
||||
async def chat_completions(request: ChatRequest, authorization: Optional[str] = Header(None)):
|
||||
@ -550,3 +652,63 @@ async def chat_completions_v2(request: ChatRequestV2, authorization: Optional[st
|
||||
logger.error(f"Error in chat_completions_v2: {str(e)}")
|
||||
logger.error(f"Full traceback: {error_details}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 聊天历史查询接口
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/api/v1/chat/history", response_model=dict)
|
||||
async def get_chat_history(
|
||||
session_id: str,
|
||||
last_message_id: Optional[str] = None,
|
||||
limit: int = 20
|
||||
):
|
||||
"""
|
||||
获取聊天历史记录
|
||||
|
||||
从独立的聊天历史表查询,返回完整的原始消息(不受 checkpoint summary 影响)
|
||||
|
||||
参数:
|
||||
session_id: 会话ID
|
||||
last_message_id: 上一页最后一条消息的ID,用于获取更早的消息
|
||||
limit: 每次返回的消息数量,默认 20,最大 100
|
||||
|
||||
返回:
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"id": "唯一消息ID",
|
||||
"role": "user 或 assistant",
|
||||
"content": "消息内容",
|
||||
"timestamp": "ISO 8601 格式的时间戳"
|
||||
},
|
||||
...
|
||||
],
|
||||
"has_more": true/false // 是否还有更多历史消息
|
||||
}
|
||||
"""
|
||||
try:
|
||||
from agent.chat_history_manager import get_chat_history_manager
|
||||
|
||||
# 参数验证
|
||||
limit = min(max(1, limit), 100)
|
||||
|
||||
manager = get_chat_history_manager()
|
||||
result = await manager.manager.get_history_by_message_id(
|
||||
session_id=session_id,
|
||||
last_message_id=last_message_id,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
return {
|
||||
"messages": result["messages"],
|
||||
"has_more": result["has_more"]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
logger.error(f"Error in get_chat_history: {str(e)}")
|
||||
logger.error(f"Full traceback: {error_details}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
@ -83,6 +83,45 @@ async def validate_upload_file_size(file: UploadFile) -> int:
|
||||
return file_size
|
||||
|
||||
|
||||
def detect_zip_has_top_level_dirs(zip_path: str) -> bool:
|
||||
"""检测 zip 文件是否包含顶级目录(而非直接包含文件)
|
||||
|
||||
Args:
|
||||
zip_path: zip 文件路径
|
||||
|
||||
Returns:
|
||||
bool: 如果 zip 包含顶级目录则返回 True
|
||||
"""
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
# 获取所有顶级路径(第一层目录/文件)
|
||||
top_level_paths = set()
|
||||
for name in zip_ref.namelist():
|
||||
# 跳过空目录项(以 / 结尾的空路径)
|
||||
if not name or name == '/':
|
||||
continue
|
||||
# 提取顶级路径(第一层)
|
||||
parts = name.split('/')
|
||||
if parts[0]: # 忽略空字符串
|
||||
top_level_paths.add(parts[0])
|
||||
|
||||
logger.info(f"Zip top-level paths: {top_level_paths}")
|
||||
|
||||
# 检查是否有目录(目录项以 / 结尾,或路径中包含 /)
|
||||
for path in top_level_paths:
|
||||
# 如果路径中包含 /,说明是目录
|
||||
# 或者检查 namelist 中是否有以该路径/ 开头的项
|
||||
for full_name in zip_ref.namelist():
|
||||
if full_name.startswith(f"{path}/"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error detecting zip structure: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def safe_extract_zip(zip_path: str, extract_dir: str) -> None:
|
||||
"""安全地解压 zip 文件,防止 ZipSlip 和 zip 炸弹攻击
|
||||
|
||||
@ -140,8 +179,15 @@ async def safe_extract_zip(zip_path: str, extract_dir: str) -> None:
|
||||
f"文件将被解压到目标目录之外: {zip_info.filename}"
|
||||
)
|
||||
|
||||
# 检查符号链接
|
||||
if zip_info.is_symlink():
|
||||
# 检查符号链接(兼容 Python 3.8+)
|
||||
# is_symlink() 方法在 Python 3.9+ 才有,使用 hasattr 兼容旧版本
|
||||
is_symlink = (
|
||||
hasattr(zip_info, 'is_symlink') and zip_info.is_symlink()
|
||||
) or (
|
||||
# 通过 external_attr 检查(兼容所有版本)
|
||||
(zip_info.external_attr >> 16) & 0o170000 == 0o120000
|
||||
)
|
||||
if is_symlink:
|
||||
raise zipfile.BadZipFile(
|
||||
f"不允许符号链接: {zip_info.filename}"
|
||||
)
|
||||
@ -373,12 +419,8 @@ async def upload_skill(file: UploadFile = File(...), bot_id: Optional[str] = For
|
||||
|
||||
folder_name = name_without_ext
|
||||
|
||||
# 创建上传目录
|
||||
# 创建上传目录(先保存 zip 文件)
|
||||
upload_dir = os.path.join("projects", "uploads", bot_id, "skill_zip")
|
||||
extract_target = os.path.join("projects", "uploads", bot_id, "skills", folder_name)
|
||||
|
||||
# 使用线程池避免阻塞
|
||||
await asyncio.to_thread(os.makedirs, extract_target, exist_ok=True)
|
||||
await asyncio.to_thread(os.makedirs, upload_dir, exist_ok=True)
|
||||
|
||||
# 保存zip文件路径
|
||||
@ -388,6 +430,25 @@ async def upload_skill(file: UploadFile = File(...), bot_id: Optional[str] = For
|
||||
await save_upload_file_async(file, file_path)
|
||||
logger.info(f"Saved zip file: {file_path}")
|
||||
|
||||
# 检测 zip 文件结构:是否包含顶级目录
|
||||
has_top_level_dirs = await asyncio.to_thread(
|
||||
detect_zip_has_top_level_dirs, file_path
|
||||
)
|
||||
logger.info(f"Zip contains top-level directories: {has_top_level_dirs}")
|
||||
|
||||
# 根据检测结果决定解压目标目录
|
||||
if has_top_level_dirs:
|
||||
# zip 包含目录(如 a-skill/, b-skill/),解压到 skills/ 目录
|
||||
extract_target = os.path.join("projects", "uploads", bot_id, "skills")
|
||||
logger.info(f"Detected directories in zip, extracting to: {extract_target}")
|
||||
else:
|
||||
# zip 直接包含文件,解压到 skills/{folder_name}/ 目录
|
||||
extract_target = os.path.join("projects", "uploads", bot_id, "skills", folder_name)
|
||||
logger.info(f"No directories in zip, extracting to: {extract_target}")
|
||||
|
||||
# 使用线程池避免阻塞
|
||||
await asyncio.to_thread(os.makedirs, extract_target, exist_ok=True)
|
||||
|
||||
# P1-001, P1-005: 安全解压(防止 ZipSlip 和 zip 炸弹)
|
||||
await safe_extract_zip(file_path, extract_target)
|
||||
logger.info(f"Extracted to: {extract_target}")
|
||||
|
||||
247
skills/excel-analysis/SKILL.md
Normal file
247
skills/excel-analysis/SKILL.md
Normal file
@ -0,0 +1,247 @@
|
||||
---
|
||||
name: excel-analysis
|
||||
description: Analyze Excel spreadsheets, create pivot tables, generate charts, and perform data analysis. Use when analyzing Excel files, spreadsheets, tabular data, or .xlsx files.
|
||||
---
|
||||
|
||||
# Excel Analysis
|
||||
|
||||
## Quick start
|
||||
|
||||
Read Excel files with pandas:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
# Read Excel file
|
||||
df = pd.read_excel("data.xlsx", sheet_name="Sheet1")
|
||||
|
||||
# Display first few rows
|
||||
print(df.head())
|
||||
|
||||
# Basic statistics
|
||||
print(df.describe())
|
||||
```
|
||||
|
||||
## Reading multiple sheets
|
||||
|
||||
Process all sheets in a workbook:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
# Read all sheets
|
||||
excel_file = pd.ExcelFile("workbook.xlsx")
|
||||
|
||||
for sheet_name in excel_file.sheet_names:
|
||||
df = pd.read_excel(excel_file, sheet_name=sheet_name)
|
||||
print(f"\n{sheet_name}:")
|
||||
print(df.head())
|
||||
```
|
||||
|
||||
## Data analysis
|
||||
|
||||
Perform common analysis tasks:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
df = pd.read_excel("sales.xlsx")
|
||||
|
||||
# Group by and aggregate
|
||||
sales_by_region = df.groupby("region")["sales"].sum()
|
||||
print(sales_by_region)
|
||||
|
||||
# Filter data
|
||||
high_sales = df[df["sales"] > 10000]
|
||||
|
||||
# Calculate metrics
|
||||
df["profit_margin"] = (df["revenue"] - df["cost"]) / df["revenue"]
|
||||
|
||||
# Sort by column
|
||||
df_sorted = df.sort_values("sales", ascending=False)
|
||||
```
|
||||
|
||||
## Creating Excel files
|
||||
|
||||
Write data to Excel with formatting:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
df = pd.DataFrame({
|
||||
"Product": ["A", "B", "C"],
|
||||
"Sales": [100, 200, 150],
|
||||
"Profit": [20, 40, 30]
|
||||
})
|
||||
|
||||
# Write to Excel
|
||||
writer = pd.ExcelWriter("output.xlsx", engine="openpyxl")
|
||||
df.to_excel(writer, sheet_name="Sales", index=False)
|
||||
|
||||
# Get worksheet for formatting
|
||||
worksheet = writer.sheets["Sales"]
|
||||
|
||||
# Auto-adjust column widths
|
||||
for column in worksheet.columns:
|
||||
max_length = 0
|
||||
column_letter = column[0].column_letter
|
||||
for cell in column:
|
||||
if len(str(cell.value)) > max_length:
|
||||
max_length = len(str(cell.value))
|
||||
worksheet.column_dimensions[column_letter].width = max_length + 2
|
||||
|
||||
writer.close()
|
||||
```
|
||||
|
||||
## Pivot tables
|
||||
|
||||
Create pivot tables programmatically:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
df = pd.read_excel("sales_data.xlsx")
|
||||
|
||||
# Create pivot table
|
||||
pivot = pd.pivot_table(
|
||||
df,
|
||||
values="sales",
|
||||
index="region",
|
||||
columns="product",
|
||||
aggfunc="sum",
|
||||
fill_value=0
|
||||
)
|
||||
|
||||
print(pivot)
|
||||
|
||||
# Save pivot table
|
||||
pivot.to_excel("pivot_report.xlsx")
|
||||
```
|
||||
|
||||
## Charts and visualization
|
||||
|
||||
Generate charts from Excel data:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
df = pd.read_excel("data.xlsx")
|
||||
|
||||
# Create bar chart
|
||||
df.plot(x="category", y="value", kind="bar")
|
||||
plt.title("Sales by Category")
|
||||
plt.xlabel("Category")
|
||||
plt.ylabel("Sales")
|
||||
plt.tight_layout()
|
||||
plt.savefig("chart.png")
|
||||
|
||||
# Create pie chart
|
||||
df.set_index("category")["value"].plot(kind="pie", autopct="%1.1f%%")
|
||||
plt.title("Market Share")
|
||||
plt.ylabel("")
|
||||
plt.savefig("pie_chart.png")
|
||||
```
|
||||
|
||||
## Data cleaning
|
||||
|
||||
Clean and prepare Excel data:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
df = pd.read_excel("messy_data.xlsx")
|
||||
|
||||
# Remove duplicates
|
||||
df = df.drop_duplicates()
|
||||
|
||||
# Handle missing values
|
||||
df = df.fillna(0) # or df.dropna()
|
||||
|
||||
# Remove whitespace
|
||||
df["name"] = df["name"].str.strip()
|
||||
|
||||
# Convert data types
|
||||
df["date"] = pd.to_datetime(df["date"])
|
||||
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
|
||||
|
||||
# Save cleaned data
|
||||
df.to_excel("cleaned_data.xlsx", index=False)
|
||||
```
|
||||
|
||||
## Merging and joining
|
||||
|
||||
Combine multiple Excel files:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
# Read multiple files
|
||||
df1 = pd.read_excel("sales_q1.xlsx")
|
||||
df2 = pd.read_excel("sales_q2.xlsx")
|
||||
|
||||
# Concatenate vertically
|
||||
combined = pd.concat([df1, df2], ignore_index=True)
|
||||
|
||||
# Merge on common column
|
||||
customers = pd.read_excel("customers.xlsx")
|
||||
sales = pd.read_excel("sales.xlsx")
|
||||
|
||||
merged = pd.merge(sales, customers, on="customer_id", how="left")
|
||||
|
||||
merged.to_excel("merged_data.xlsx", index=False)
|
||||
```
|
||||
|
||||
## Advanced formatting
|
||||
|
||||
Apply conditional formatting and styles:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
from openpyxl import load_workbook
|
||||
from openpyxl.styles import PatternFill, Font
|
||||
|
||||
# Create Excel file
|
||||
df = pd.DataFrame({
|
||||
"Product": ["A", "B", "C"],
|
||||
"Sales": [100, 200, 150]
|
||||
})
|
||||
|
||||
df.to_excel("formatted.xlsx", index=False)
|
||||
|
||||
# Load workbook for formatting
|
||||
wb = load_workbook("formatted.xlsx")
|
||||
ws = wb.active
|
||||
|
||||
# Apply conditional formatting
|
||||
red_fill = PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid")
|
||||
green_fill = PatternFill(start_color="00FF00", end_color="00FF00", fill_type="solid")
|
||||
|
||||
for row in range(2, len(df) + 2):
|
||||
cell = ws[f"B{row}"]
|
||||
if cell.value < 150:
|
||||
cell.fill = red_fill
|
||||
else:
|
||||
cell.fill = green_fill
|
||||
|
||||
# Bold headers
|
||||
for cell in ws[1]:
|
||||
cell.font = Font(bold=True)
|
||||
|
||||
wb.save("formatted.xlsx")
|
||||
```
|
||||
|
||||
## Performance tips
|
||||
|
||||
- Use `read_excel` with `usecols` to read specific columns only
|
||||
- Use `chunksize` for very large files
|
||||
- Consider using `engine='openpyxl'` or `engine='xlrd'` based on file type
|
||||
- Use `dtype` parameter to specify column types for faster reading
|
||||
|
||||
## Available packages
|
||||
|
||||
- **pandas** - Data analysis and manipulation (primary)
|
||||
- **openpyxl** - Excel file creation and formatting
|
||||
- **xlrd** - Reading older .xls files
|
||||
- **xlsxwriter** - Advanced Excel writing capabilities
|
||||
- **matplotlib** - Chart generation
|
||||
231
skills/managing-scripts/SKILL.md
Normal file
231
skills/managing-scripts/SKILL.md
Normal file
@ -0,0 +1,231 @@
|
||||
---
|
||||
name: managing-scripts
|
||||
description: Manages shared scripts repository for reusable data analysis tools. Check scripts/README.md before writing, design generalized scripts with parameters, and keep documentation in sync.
|
||||
---
|
||||
|
||||
# Managing Scripts
|
||||
|
||||
管理可复用的数据分析脚本资源库,通过通用化设计最大化脚本复用价值。
|
||||
|
||||
## Quick Start
|
||||
|
||||
编写数据分析脚本时的通用化流程:
|
||||
1. 读取 `./scripts/README.md` 检查是否有可复用的现成脚本
|
||||
2. 如有合适脚本,优先复用
|
||||
3. 如需编写新脚本,**设计通用化方案**而非解决单一问题
|
||||
4. 保存到 `./scripts/` 并更新 README
|
||||
|
||||
## Instructions
|
||||
|
||||
### 使用前检查
|
||||
|
||||
当用户请求任何数据处理/分析任务时:
|
||||
|
||||
1. 检查 `./scripts/README.md` 是否存在
|
||||
2. 查找可处理**此类问题**的脚本(非完全匹配即可)
|
||||
3. 现有脚本可通过参数调整满足需求时,优先复用
|
||||
4. 告知用户使用的是现成脚本
|
||||
|
||||
### 编写通用化脚本
|
||||
|
||||
核心原则:**解决一类问题,而非单一问题**
|
||||
|
||||
#### 1. 识别问题模式
|
||||
|
||||
在编写脚本前,分析当前请求属于哪类通用模式:
|
||||
|
||||
| 问题类型 | 通用模式 | 可参数化项 |
|
||||
|---------|---------|-----------|
|
||||
| 数据转换 | 格式A → 格式B | 输入文件、输出格式、字段映射 |
|
||||
| 数据分析 | 统计/聚合/可视化 | 数据源、分析维度、输出类型 |
|
||||
| 数据清洗 | 去重/填充/过滤 | 规则配置、阈值参数 |
|
||||
| 文件操作 | 批量处理文件 | 文件路径、匹配模式、操作类型 |
|
||||
|
||||
#### 2. 参数化设计
|
||||
|
||||
将硬编码值改为可配置参数:
|
||||
|
||||
```python
|
||||
# ❌ 不通用:硬编码特定字段
|
||||
def analyze_sales():
|
||||
df = pd.read_excel("sales_data.xlsx")
|
||||
result = df.groupby("region")["amount"].sum()
|
||||
|
||||
# ✅ 通用化:参数化输入
|
||||
def analyze_data(input_file, group_by_column, aggregate_column, method="sum"):
|
||||
"""
|
||||
通用数据聚合分析
|
||||
:param input_file: 输入文件路径
|
||||
:param group_by_column: 分组列名
|
||||
:param aggregate_column: 聚合列名
|
||||
:param method: 聚合方法 (sum/mean/count/etc)
|
||||
"""
|
||||
df = pd.read_excel(input_file)
|
||||
return df.groupby(group_by_column)[aggregate_column].agg(method)
|
||||
```
|
||||
|
||||
#### 3. 使用命令行参数
|
||||
|
||||
```python
|
||||
import argparse
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="通用数据聚合工具")
|
||||
parser.add_argument("--input", required=True, help="输入文件路径")
|
||||
parser.add_argument("--output", help="输出文件路径")
|
||||
parser.add_argument("--group-by", required=True, help="分组列名")
|
||||
parser.add_argument("--agg-column", required=True, help="聚合列名")
|
||||
parser.add_argument("--method", default="sum", help="聚合方法")
|
||||
args = parser.parse_args()
|
||||
# ... 处理逻辑
|
||||
```
|
||||
|
||||
#### 4. 配置文件支持
|
||||
|
||||
复杂逻辑使用配置文件:
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
transformations:
|
||||
- column: "date"
|
||||
action: "parse_format"
|
||||
params: {"format": "%Y-%m-%d"}
|
||||
- column: "amount"
|
||||
action: "fill_na"
|
||||
params: {"value": 0}
|
||||
```
|
||||
|
||||
### 保存新脚本
|
||||
|
||||
脚本验证成功后:
|
||||
|
||||
1. 使用**通用性强的命名**:
|
||||
- ✅ `aggregate_data.py`、`convert_format.py`、`clean_dataset.py`
|
||||
- ❌ `analyze_sales_2024.py`、`fix_import_error.py`
|
||||
|
||||
2. 保存到 `./scripts/` 文件夹
|
||||
|
||||
3. 在 `./scripts/README.md` 添加说明,包含:
|
||||
- **通用功能描述**(描述解决的问题类型,非具体业务)
|
||||
- 使用方法(含所有参数说明)
|
||||
- 输入/输出格式
|
||||
- 使用示例
|
||||
- 依赖要求
|
||||
|
||||
### 修改现有脚本
|
||||
|
||||
修改 `./scripts/` 下的脚本时:
|
||||
|
||||
1. 保持/增强通用性,避免收缩为特定用途
|
||||
2. 同步更新 README.md 文档
|
||||
3. 在变更日志中记录修改内容
|
||||
|
||||
## Examples
|
||||
|
||||
**场景:用户请求分析销售数据**
|
||||
|
||||
```
|
||||
用户:帮我分析这个 Excel 文件,按地区统计销售额
|
||||
|
||||
思考过程:
|
||||
1. 这是一个"数据聚合"问题,属于通用模式
|
||||
2. 检查 scripts/README.md 是否有聚合工具
|
||||
3. 如没有,创建通用聚合脚本 aggregate_data.py
|
||||
4. 支持参数:--input、--group-by、--agg-column、--method
|
||||
5. 用户调用:python scripts/aggregate_data.py \
|
||||
--input data.xlsx --group-by region --agg-column amount
|
||||
```
|
||||
|
||||
**通用脚本模板示例:**
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
通用数据聚合工具
|
||||
支持按任意列分组,对任意列进行聚合统计
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import pandas as pd
|
||||
|
||||
def aggregate_data(input_file, group_by, agg_column, method="sum", output=None):
|
||||
"""通用聚合函数"""
|
||||
# 根据文件扩展名选择读取方法
|
||||
ext = input_file.split(".")[-1]
|
||||
read_func = getattr(pd, f"read_{ext}", pd.read_csv)
|
||||
df = read_func(input_file)
|
||||
|
||||
# 执行聚合
|
||||
result = df.groupby(group_by)[agg_column].agg(method)
|
||||
|
||||
# 输出
|
||||
if output:
|
||||
result.to_csv(output)
|
||||
return result
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="通用数据聚合工具")
|
||||
parser.add_argument("--input", "-i", required=True, help="输入文件路径")
|
||||
parser.add_argument("--group-by", "-g", required=True, help="分组列名")
|
||||
parser.add_argument("--agg-column", "-a", required=True, help="聚合列名")
|
||||
parser.add_argument("--method", "-m", default="sum",
|
||||
choices=["sum", "mean", "count", "min", "max"],
|
||||
help="聚合方法")
|
||||
parser.add_argument("--output", "-o", help="输出文件路径")
|
||||
args = parser.parse_args()
|
||||
|
||||
aggregate_data(args.input, args.group_by, args.agg_column,
|
||||
args.method, args.output)
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
|
||||
### 通用化设计原则
|
||||
|
||||
- **抽象思维**:识别问题的本质模式,而非表面细节
|
||||
- **参数化一切**:任何可能变化的值都应可配置
|
||||
- **避免业务术语**:使用通用技术术语(如 "group_by" 而非 "region")
|
||||
- **支持扩展**:预留扩展点,便于未来增加新功能
|
||||
- **提供默认值**:合理默认值降低使用门槛
|
||||
|
||||
### 命名规范
|
||||
|
||||
| 类型 | 推荐 | 避免 |
|
||||
|-----|------|------|
|
||||
| 脚本名 | `aggregate_data.py` | `sales_analysis.py` |
|
||||
| 参数名 | `--group-by` | `--region` |
|
||||
| 函数名 | `transform_data()` | `fix_sales_format()` |
|
||||
|
||||
### 文档规范
|
||||
|
||||
README 条目应说明:
|
||||
- **解决哪类问题**(非具体业务场景)
|
||||
- **所有参数及默认值**
|
||||
- **支持的输入格式**
|
||||
- **使用示例**(至少2个不同场景)
|
||||
|
||||
### 何时创建新脚本
|
||||
|
||||
创建新脚本当:
|
||||
- 现有脚本无法通过参数调整满足需求
|
||||
- 问题属于新的通用模式
|
||||
- 预计该场景会重复出现
|
||||
|
||||
### 何时修改现有脚本
|
||||
|
||||
修改现有脚本当:
|
||||
- 增强通用性(添加新参数/配置)
|
||||
- 修复 bug 但不破坏现有接口
|
||||
- 扩展功能范围
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
./scripts/
|
||||
├── README.md # 脚本目录和使用说明(必须存在)
|
||||
├── aggregate_data.py # 通用聚合工具
|
||||
├── convert_format.py # 格式转换工具
|
||||
├── clean_dataset.py # 数据清洗工具
|
||||
└── config/ # 可选:配置文件目录
|
||||
└── templates.yaml
|
||||
```
|
||||
@ -356,7 +356,7 @@ def create_chat_response(
|
||||
"""Create a chat completion response"""
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
return {
|
||||
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
|
||||
"object": "chat.completion",
|
||||
@ -378,3 +378,28 @@ def create_chat_response(
|
||||
"total_tokens": 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 聊天历史查询相关模型
|
||||
# ============================================================================
|
||||
|
||||
class ChatHistoryRequest(BaseModel):
|
||||
"""聊天历史查询请求"""
|
||||
session_id: str = Field(..., description="会话ID (thread_id)")
|
||||
last_message_id: Optional[str] = Field(None, description="上一条消息的ID,用于分页查询更早的消息")
|
||||
limit: int = Field(20, ge=1, le=100, description="每次查询的消息数量上限")
|
||||
|
||||
|
||||
class ChatHistoryMessage(BaseModel):
|
||||
"""聊天历史消息"""
|
||||
id: str = Field(..., description="消息唯一ID")
|
||||
role: str = Field(..., description="消息角色: user 或 assistant")
|
||||
content: str = Field(..., description="消息内容")
|
||||
timestamp: Optional[str] = Field(None, description="消息时间戳 (ISO 8601)")
|
||||
|
||||
|
||||
class ChatHistoryResponse(BaseModel):
|
||||
"""聊天历史查询响应"""
|
||||
messages: List[ChatHistoryMessage] = Field(..., description="消息列表,按时间倒序排列")
|
||||
has_more: bool = Field(..., description="是否还有更多历史消息")
|
||||
|
||||
@ -371,6 +371,10 @@ def create_project_directory(dataset_ids: Optional[List[str]], bot_id: str, robo
|
||||
if robot_type == "general_agent":
|
||||
return None
|
||||
|
||||
# 如果 dataset_ids 为空,不创建目录
|
||||
if not dataset_ids:
|
||||
dataset_ids = []
|
||||
|
||||
try:
|
||||
from utils.multi_project_manager import create_robot_project
|
||||
from pathlib import Path
|
||||
@ -493,15 +497,15 @@ def get_language_text(language: str):
|
||||
return language_map.get(language.lower(), '')
|
||||
|
||||
def get_preamble_text(language: str, system_prompt: str):
|
||||
# 首先检查system_prompt中是否有preamble代码块
|
||||
# 首先检查system_prompt中是否有preamble标签
|
||||
if system_prompt:
|
||||
preamble_pattern = r'```preamble\s*\n(.*?)\n```'
|
||||
preamble_pattern = r'<preamble>\s*(.*?)\s*</preamble>'
|
||||
preamble_matches = re.findall(preamble_pattern, system_prompt, re.DOTALL)
|
||||
if preamble_matches:
|
||||
# 提取preamble内容
|
||||
preamble_content = preamble_matches[0].strip()
|
||||
if preamble_content:
|
||||
# 从system_prompt中删除preamble代码块
|
||||
# 从system_prompt中删除preamble标签
|
||||
cleaned_system_prompt = re.sub(preamble_pattern, '', system_prompt, flags=re.DOTALL)
|
||||
return preamble_content, cleaned_system_prompt
|
||||
|
||||
@ -697,27 +701,40 @@ def extract_block_from_system_prompt(system_prompt: str) -> tuple[str, str, str,
|
||||
|
||||
terms_list = []
|
||||
|
||||
# 首先分割所有的代码块
|
||||
block_pattern = r'```(\w+)\s*\n(.*?)\n```'
|
||||
# 使用XML标签格式解析块
|
||||
blocks_to_remove = []
|
||||
|
||||
for match in re.finditer(block_pattern, system_prompt, re.DOTALL):
|
||||
block_type, content = match.groups()
|
||||
# 解析 <guidelines>
|
||||
guidelines_pattern = r'<guidelines>\s*(.*?)\s*</guidelines>'
|
||||
match = re.search(guidelines_pattern, system_prompt, re.DOTALL)
|
||||
if match:
|
||||
guidelines = match.group(1).strip()
|
||||
blocks_to_remove.append(match.group(0))
|
||||
|
||||
if block_type == 'guideline' or block_type == 'guidelines':
|
||||
guidelines = content.strip()
|
||||
# 解析 <tools>
|
||||
tools_pattern = r'<tools>\s*(.*?)\s*</tools>'
|
||||
match = re.search(tools_pattern, system_prompt, re.DOTALL)
|
||||
if match:
|
||||
tools = match.group(1).strip()
|
||||
blocks_to_remove.append(match.group(0))
|
||||
|
||||
# 解析 <scenarios>
|
||||
scenarios_pattern = r'<scenarios>\s*(.*?)\s*</scenarios>'
|
||||
match = re.search(scenarios_pattern, system_prompt, re.DOTALL)
|
||||
if match:
|
||||
scenarios = match.group(1).strip()
|
||||
blocks_to_remove.append(match.group(0))
|
||||
|
||||
# 解析 <terms>
|
||||
terms_pattern = r'<terms>\s*(.*?)\s*</terms>'
|
||||
match = re.search(terms_pattern, system_prompt, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
terms = parse_terms_text(match.group(1).strip())
|
||||
terms_list.extend(terms)
|
||||
blocks_to_remove.append(match.group(0))
|
||||
elif block_type == 'tools':
|
||||
tools = content.strip()
|
||||
elif block_type == 'scenarios':
|
||||
scenarios = content.strip()
|
||||
elif block_type == 'terms':
|
||||
try:
|
||||
terms = parse_terms_text(content.strip())
|
||||
terms_list.extend(terms)
|
||||
blocks_to_remove.append(match.group(0))
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing terms: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing terms: {e}")
|
||||
|
||||
# 从system_prompt中移除这些已解析的块
|
||||
cleaned_prompt = system_prompt
|
||||
@ -729,55 +746,6 @@ def extract_block_from_system_prompt(system_prompt: str) -> tuple[str, str, str,
|
||||
return cleaned_prompt, guidelines, tools, scenarios, terms_list
|
||||
|
||||
|
||||
def parse_guidelines_text(text: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
解析guidelines文本,支持多种格式
|
||||
|
||||
Args:
|
||||
text: guidelines文本内容
|
||||
|
||||
Returns:
|
||||
List[Dict]: guidelines列表
|
||||
"""
|
||||
guidelines = []
|
||||
|
||||
# 尝试解析JSON格式
|
||||
if text.strip().startswith('[') or text.strip().startswith('{'):
|
||||
try:
|
||||
data = json.loads(text)
|
||||
if isinstance(data, list):
|
||||
for item in data:
|
||||
if isinstance(item, dict):
|
||||
guidelines.append(item)
|
||||
elif isinstance(data, dict):
|
||||
guidelines.append(data)
|
||||
return guidelines
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 解析行格式,支持多种分隔符
|
||||
lines = [line.strip() for line in text.split('\n') if line.strip()]
|
||||
|
||||
for line in lines:
|
||||
# 跳过注释行
|
||||
if line.startswith('#') or line.startswith('//'):
|
||||
continue
|
||||
|
||||
# 尝试解析 "id) Condition: ... Action: ..." 格式
|
||||
id_condition_action_pattern = r'(\d+)\)\s*Condition:\s*(.*?)\s*Action:\s*(.*?)(?:\s*Priority:\s*(\d+))?$'
|
||||
match = re.match(id_condition_action_pattern, line, re.IGNORECASE)
|
||||
if match:
|
||||
guidelines.append({
|
||||
'guideline_id': int(match.group(1)),
|
||||
'condition': match.group(2).strip(),
|
||||
'action': match.group(3).strip(),
|
||||
'priority': int(match.group(4)) if match.group(4) else 1
|
||||
})
|
||||
continue
|
||||
|
||||
return guidelines
|
||||
|
||||
|
||||
def parse_terms_text(text: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
解析terms文本,支持多种格式
|
||||
|
||||
@ -48,8 +48,8 @@ MCP_SSE_READ_TIMEOUT = int(os.getenv("MCP_SSE_READ_TIMEOUT", 300)) # SSE 读取
|
||||
|
||||
# PostgreSQL 连接字符串
|
||||
# 格式: postgresql://user:password@host:port/database
|
||||
CHECKPOINT_DB_URL = os.getenv("CHECKPOINT_DB_URL", "postgresql://postgres:AeEGDB0b7Z5GK0E2tblt@dev-circleo-pg.celp3nik7oaq.ap-northeast-1.rds.amazonaws.com:5432/gptbase")
|
||||
#CHECKPOINT_DB_URL = os.getenv("CHECKPOINT_DB_URL", "postgresql://moshui:@localhost:5432/moshui")
|
||||
#CHECKPOINT_DB_URL = os.getenv("CHECKPOINT_DB_URL", "postgresql://postgres:AeEGDB0b7Z5GK0E2tblt@dev-circleo-pg.celp3nik7oaq.ap-northeast-1.rds.amazonaws.com:5432/gptbase")
|
||||
CHECKPOINT_DB_URL = os.getenv("CHECKPOINT_DB_URL", "postgresql://moshui:@localhost:5432/moshui")
|
||||
|
||||
# 连接池大小
|
||||
# 同时可以持有的最大连接数
|
||||
|
||||
Loading…
Reference in New Issue
Block a user