chore: 删除旧版 agent/app 模块
移除已废弃的旧版 agent 实现,使用新的 core/agents 模块替代 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +0,0 @@
|
||||
# X-Agents Python Agent Engine
|
||||
@@ -1,3 +0,0 @@
|
||||
# Agent Core
|
||||
from app.agent.core.agent import AgentCore, AgentConfig, AgentResponse
|
||||
from app.agent.core.supervisor import Supervisor
|
||||
@@ -1,170 +0,0 @@
|
||||
"""
|
||||
Agent Core - 单智能体核心
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional, List, Dict, Any
|
||||
from pydantic import BaseModel
|
||||
from app.agent.memory.manager import MemoryManager
|
||||
from app.agent.skills.router import SkillRouter
|
||||
from app.agent.skills.executor import SkillExecutor
|
||||
from app.agent.llm.factory import LLMFactory
|
||||
|
||||
logger = logging.getLogger("agent.core")
|
||||
|
||||
|
||||
class AgentConfig(BaseModel):
|
||||
"""智能体配置"""
|
||||
id: int
|
||||
name: str
|
||||
role_description: str
|
||||
model_provider: str = "openai"
|
||||
model_name: str = "gpt-4"
|
||||
api_key: Optional[str] = None # API Key(可选,用于覆盖默认配置)
|
||||
base_url: Optional[str] = None # Base URL(可选,用于覆盖默认配置)
|
||||
skills: List[int] = [] # 技能 ID 列表
|
||||
knowledge_base_ids: List[int] = []
|
||||
timeout: int = 60
|
||||
memory_limit: int = 134217728 # 128MB
|
||||
|
||||
|
||||
class AgentResponse(BaseModel):
|
||||
"""智能体响应"""
|
||||
content: str
|
||||
tool_calls: List[Dict[str, Any]] = []
|
||||
tokens_used: int = 0
|
||||
duration_ms: int = 0
|
||||
session_id: Optional[str] = None
|
||||
|
||||
|
||||
class AgentCore:
|
||||
"""单智能体核心类"""
|
||||
|
||||
def __init__(self, config: AgentConfig):
|
||||
self.config = config
|
||||
|
||||
# 记录 LLM 初始化信息
|
||||
api_key_info = f"{config.api_key[:10]}..." if config.api_key else "None"
|
||||
logger.info(f"初始化 AgentCore: name={config.name}, provider={config.model_provider}, model={config.model_name}, api_key={api_key_info}, base_url={config.base_url}")
|
||||
|
||||
self.llm = LLMFactory.create(config.model_provider, config.model_name, config.api_key, config.base_url)
|
||||
self.memory = MemoryManager(config.id)
|
||||
self.skill_router = SkillRouter(config.skills)
|
||||
self.skill_executor = SkillExecutor()
|
||||
|
||||
async def run(self, user_input: str, user_id: int, session_id: str) -> AgentResponse:
|
||||
"""
|
||||
执行智能体对话
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
user_id: 用户 ID
|
||||
session_id: 会话 ID
|
||||
|
||||
Returns:
|
||||
AgentResponse: 智能体响应
|
||||
"""
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# 1. 加载记忆
|
||||
context = await self.memory.load_context(user_input, user_id, session_id)
|
||||
|
||||
# 2. 构建 Prompt
|
||||
prompt = self._build_prompt(user_input, context)
|
||||
|
||||
# 3. LLM 决策
|
||||
decision = await self.llm.decide(prompt)
|
||||
|
||||
# 4. 执行技能(如需)
|
||||
if decision.get('needs_skill'):
|
||||
skill_results = await self._execute_skills(decision.get('tool_calls', []))
|
||||
# 5. 基于结果生成回复
|
||||
final_response = await self.llm.generate(prompt, skill_results)
|
||||
else:
|
||||
final_response = decision.get('response', '')
|
||||
|
||||
# 6. 保存记忆
|
||||
await self.memory.save(user_input, final_response, user_id, session_id)
|
||||
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
return AgentResponse(
|
||||
content=final_response,
|
||||
tool_calls=decision.get('tool_calls', []),
|
||||
duration_ms=duration_ms,
|
||||
session_id=session_id
|
||||
)
|
||||
except Exception as e:
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
return AgentResponse(
|
||||
content=f"处理请求时发生错误: {str(e)}",
|
||||
duration_ms=duration_ms,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
def _build_prompt(self, user_input: str, context: dict) -> str:
|
||||
"""构建 Prompt"""
|
||||
system_prompt = f"""你是 {self.config.name}。
|
||||
{self.config.role_description}
|
||||
|
||||
相关记忆:
|
||||
{context.get('summary', '')}
|
||||
|
||||
知识库信息:
|
||||
{context.get('knowledge', '')}
|
||||
|
||||
请根据以上上下文回答用户问题,并使用 Markdown 格式输出。"""
|
||||
|
||||
return f"{system_prompt}\n\n用户: {user_input}"
|
||||
|
||||
async def _execute_skills(self, skill_decisions: List[Dict]) -> List[Dict]:
|
||||
"""执行技能"""
|
||||
if not skill_decisions:
|
||||
return []
|
||||
|
||||
results = []
|
||||
for decision in skill_decisions:
|
||||
result = await self.skill_executor.execute(
|
||||
skill_id=decision.get('skill_id'),
|
||||
params=decision.get('params', {})
|
||||
)
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
async def run_stream(self, user_input: str, user_id: int, session_id: str):
|
||||
"""
|
||||
执行智能体对话(流式输出)
|
||||
|
||||
优化:对于简单对话,直接流式生成,跳过 decide 步骤(省一次 LLM 调用)
|
||||
只有当需要工具时才先判断
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
user_id: 用户 ID
|
||||
session_id: 会话 ID
|
||||
|
||||
Yields:
|
||||
str: 流式回复片段
|
||||
"""
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# 1. 加载记忆
|
||||
context = await self.memory.load_context(user_input, user_id, session_id)
|
||||
|
||||
# 2. 构建 Prompt
|
||||
prompt = self._build_prompt(user_input, context)
|
||||
|
||||
# 3. 直接流式生成回复(跳过 decide,省一次 LLM 调用)
|
||||
# 如果将来需要工具能力,可以在这里添加判断逻辑
|
||||
async for chunk in self.llm.generate_stream(prompt, []):
|
||||
yield chunk
|
||||
|
||||
# 4. 保存记忆(完成后)
|
||||
final_response = ""
|
||||
await self.memory.save(user_input, final_response, user_id, session_id)
|
||||
|
||||
except Exception as e:
|
||||
yield f"处理请求时发生错误: {str(e)}"
|
||||
@@ -1,156 +0,0 @@
|
||||
"""
|
||||
Supervisor - 多智能体调度器
|
||||
"""
|
||||
import asyncio
|
||||
from typing import List, Dict, Any
|
||||
from app.agent.core.agent import AgentCore
|
||||
|
||||
|
||||
class Supervisor:
|
||||
"""多智能体调度器"""
|
||||
|
||||
def __init__(self, supervisor_agent: AgentCore, members: List[AgentCore], strategy: str = "parallel"):
|
||||
"""
|
||||
初始化调度器
|
||||
|
||||
Args:
|
||||
supervisor_agent: 主智能体
|
||||
members: 子智能体列表
|
||||
strategy: 调度策略 (parallel/sequential)
|
||||
"""
|
||||
self.supervisor = supervisor_agent
|
||||
self.members = members
|
||||
self.strategy = strategy
|
||||
|
||||
async def run(self, task: str, user_id: int, session_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
执行多智能体协作
|
||||
|
||||
Args:
|
||||
task: 用户任务
|
||||
user_id: 用户 ID
|
||||
session_id: 会话 ID
|
||||
|
||||
Returns:
|
||||
Dict: 包含主响应和子任务结果
|
||||
"""
|
||||
# 1. 任务分解
|
||||
subtasks = await self._decompose_task(task)
|
||||
|
||||
# 2. 分配任务
|
||||
if self.strategy == "parallel":
|
||||
results = await self._dispatch_parallel(subtasks, user_id, session_id)
|
||||
else:
|
||||
results = await self._dispatch_sequential(subtasks, user_id, session_id)
|
||||
|
||||
# 3. 汇总结果
|
||||
final_result = await self._aggregate(results)
|
||||
|
||||
return {
|
||||
"main_response": final_result,
|
||||
"subtask_results": results,
|
||||
"strategy": self.strategy
|
||||
}
|
||||
|
||||
async def _decompose_task(self, task: str) -> List[Dict[str, str]]:
|
||||
"""任务分解"""
|
||||
# 调用 LLM 分解任务
|
||||
prompt = f"""分解以下任务为子任务,返回 JSON 数组格式:
|
||||
任务: {task}
|
||||
|
||||
返回格式示例:
|
||||
[{{"task": "子任务1描述", "agent_type": "适合的智能体类型"}}, {{"task": "子任务2描述", "agent_type": "适合的智能体类型"}}]
|
||||
|
||||
请直接返回 JSON 数组,不要其他内容。"""
|
||||
|
||||
response = await self.supervisor.llm.generate(prompt, [])
|
||||
|
||||
try:
|
||||
import json
|
||||
# 尝试解析 JSON
|
||||
subtasks = json.loads(response)
|
||||
return subtasks
|
||||
except:
|
||||
# 解析失败,创建默认子任务
|
||||
return [{"task": task, "agent_type": "general"}]
|
||||
|
||||
async def _dispatch_parallel(self, subtasks: List[Dict], user_id: int, session_id: str) -> List[Dict]:
|
||||
"""并行分发任务"""
|
||||
tasks = []
|
||||
for i, subtask in enumerate(subtasks):
|
||||
if i < len(self.members):
|
||||
member = self.members[i]
|
||||
else:
|
||||
# 如果子任务多于成员,使用轮询
|
||||
member = self.members[i % len(self.members)]
|
||||
|
||||
tasks.append(member.run(subtask['task'], user_id, session_id))
|
||||
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# 处理结果
|
||||
formatted_results = []
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
formatted_results.append({
|
||||
"task": subtasks[i]['task'],
|
||||
"success": False,
|
||||
"error": str(result)
|
||||
})
|
||||
else:
|
||||
formatted_results.append({
|
||||
"task": subtasks[i]['task'],
|
||||
"success": True,
|
||||
"content": result.content,
|
||||
"tool_calls": result.tool_calls
|
||||
})
|
||||
|
||||
return formatted_results
|
||||
|
||||
async def _dispatch_sequential(self, subtasks: List[Dict], user_id: int, session_id: str) -> List[Dict]:
|
||||
"""顺序分发任务"""
|
||||
results = []
|
||||
context = ""
|
||||
|
||||
for i, subtask in enumerate(subtasks):
|
||||
# 选择子智能体
|
||||
if i < len(self.members):
|
||||
member = self.members[i]
|
||||
else:
|
||||
member = self.members[i % len(self.members)]
|
||||
|
||||
# 传递前一个结果作为上下文
|
||||
enhanced_task = f"{context}\n\n当前任务: {subtask['task']}" if context else subtask['task']
|
||||
|
||||
result = await member.run(enhanced_task, user_id, session_id)
|
||||
|
||||
results.append({
|
||||
"task": subtask['task'],
|
||||
"success": True,
|
||||
"content": result.content,
|
||||
"tool_calls": result.tool_calls
|
||||
})
|
||||
|
||||
# 累加上下文
|
||||
context += f"\n\n=== 任务: {subtask['task']} ===\n{result.content}"
|
||||
|
||||
return results
|
||||
|
||||
async def _aggregate(self, results: List[Dict]) -> str:
|
||||
"""汇总结果"""
|
||||
# 过滤成功的结果
|
||||
success_results = [r for r in results if r.get('success')]
|
||||
|
||||
if not success_results:
|
||||
return "所有子任务执行失败"
|
||||
|
||||
if len(success_results) == 1:
|
||||
return success_results[0].get('content', '')
|
||||
|
||||
# 调用 LLM 汇总
|
||||
summary_prompt = "请汇总以下所有任务的结果,生成一个完整的回复:\n\n"
|
||||
for i, result in enumerate(success_results, 1):
|
||||
summary_prompt += f"=== 任务 {i}: {result.get('task', '')} ===\n{result.get('content', '')}\n\n"
|
||||
|
||||
final_response = await self.supervisor.llm.generate(summary_prompt, [])
|
||||
return final_response
|
||||
@@ -1,4 +0,0 @@
|
||||
# LLM
|
||||
from app.agent.llm.factory import LLMFactory
|
||||
from app.agent.llm.openai import OpenAILLM
|
||||
from app.agent.llm.anthropic import AnthropicLLM
|
||||
@@ -1,136 +0,0 @@
|
||||
"""
|
||||
Anthropic LLM 实现
|
||||
"""
|
||||
import os
|
||||
from typing import Dict, Any, List, Optional
|
||||
from anthropic import AsyncAnthropic
|
||||
|
||||
|
||||
class AnthropicLLM:
|
||||
"""Anthropic Claude LLM"""
|
||||
|
||||
def __init__(self, model_name: str = "claude-3-sonnet-20240229", api_key: Optional[str] = None, base_url: Optional[str] = None):
|
||||
self.model_name = model_name
|
||||
# 支持自定义 base_url(如 OpenRouter)
|
||||
if base_url:
|
||||
self.client = AsyncAnthropic(
|
||||
api_key=api_key or os.getenv("ANTHROPIC_API_KEY", ""),
|
||||
base_url=base_url
|
||||
)
|
||||
else:
|
||||
self.client = AsyncAnthropic(
|
||||
api_key=api_key or os.getenv("ANTHROPIC_API_KEY", "")
|
||||
)
|
||||
|
||||
async def decide(self, prompt: str) -> Dict[str, Any]:
|
||||
"""
|
||||
LLM 决策 - 判断是否需要调用技能
|
||||
|
||||
Args:
|
||||
prompt: 完整的 Prompt
|
||||
|
||||
Returns:
|
||||
Dict: 包含 needs_skill, tool_calls, response 等
|
||||
"""
|
||||
system_prompt = """你是一个智能助手。请分析用户请求,判断是否需要调用工具来回答问题。
|
||||
|
||||
如果需要调用工具,请按以下格式返回 JSON:
|
||||
{"needs_skill": true, "tool_calls": [{"skill_id": "工具名称", "parameters": {"参数": "值"}, "reason": "调用原因"}]}
|
||||
|
||||
如果不需要调用工具,请返回:
|
||||
{"needs_skill": false, "response": "直接回答用户的内容"}
|
||||
|
||||
请只返回 JSON,不要其他内容。"""
|
||||
|
||||
try:
|
||||
response = await self.client.messages.create(
|
||||
model=self.model_name,
|
||||
max_tokens=2000,
|
||||
system=system_prompt,
|
||||
messages=[{"role": "user", "content": prompt}]
|
||||
)
|
||||
|
||||
content = response.content[0].text
|
||||
|
||||
# 尝试解析 JSON
|
||||
import json
|
||||
try:
|
||||
result = json.loads(content)
|
||||
return result
|
||||
except json.JSONDecodeError:
|
||||
return {
|
||||
"needs_skill": False,
|
||||
"response": content
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"needs_skill": False,
|
||||
"response": f"LLM 调用失败: {str(e)}"
|
||||
}
|
||||
|
||||
async def generate(self, prompt: str, tool_results: List[Dict]) -> str:
|
||||
"""
|
||||
生成回复
|
||||
|
||||
Args:
|
||||
prompt: 完整的 Prompt
|
||||
tool_results: 工具调用结果
|
||||
|
||||
Returns:
|
||||
str: 生成的回复
|
||||
"""
|
||||
user_message = prompt
|
||||
|
||||
# 添加工具结果作为上下文
|
||||
if tool_results:
|
||||
tool_context = "\n\n工具返回结果:\n"
|
||||
for result in tool_results:
|
||||
if result.get("success"):
|
||||
tool_context += f"- {result.get('skill_id')}: {result.get('result')}\n"
|
||||
user_message += tool_context
|
||||
|
||||
try:
|
||||
response = await self.client.messages.create(
|
||||
model=self.model_name,
|
||||
max_tokens=4000,
|
||||
messages=[{"role": "user", "content": user_message}]
|
||||
)
|
||||
|
||||
return response.content[0].text
|
||||
|
||||
except Exception as e:
|
||||
return f"生成回复失败: {str(e)}"
|
||||
|
||||
async def generate_stream(self, prompt: str, tool_results: List[Dict]):
|
||||
"""
|
||||
流式生成回复
|
||||
|
||||
Args:
|
||||
prompt: 完整的 Prompt
|
||||
tool_results: 工具调用结果
|
||||
|
||||
Yields:
|
||||
str: 生成的回复片段
|
||||
"""
|
||||
user_message = prompt
|
||||
|
||||
# 添加工具结果作为上下文
|
||||
if tool_results:
|
||||
tool_context = "\n\n工具返回结果:\n"
|
||||
for result in tool_results:
|
||||
if result.get("success"):
|
||||
tool_context += f"- {result.get('skill_id')}: {result.get('result')}\n"
|
||||
user_message += tool_context
|
||||
|
||||
try:
|
||||
async with self.client.messages.stream(
|
||||
model=self.model_name,
|
||||
max_tokens=4000,
|
||||
messages=[{"role": "user", "content": user_message}]
|
||||
) as stream:
|
||||
async for text in stream.text_stream:
|
||||
yield text
|
||||
|
||||
except Exception as e:
|
||||
yield f"生成回复失败: {str(e)}"
|
||||
@@ -1,32 +0,0 @@
|
||||
"""
|
||||
LLM Factory - LLM 工厂类
|
||||
"""
|
||||
from typing import Optional
|
||||
from app.agent.llm.openai import OpenAILLM
|
||||
from app.agent.llm.anthropic import AnthropicLLM
|
||||
|
||||
|
||||
class LLMFactory:
|
||||
"""LLM 工厂类"""
|
||||
|
||||
@staticmethod
|
||||
def create(provider: str, model_name: str, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
||||
"""
|
||||
创建 LLM 实例
|
||||
|
||||
Args:
|
||||
provider: 模型提供商 (openai/anthropic)
|
||||
model_name: 模型名称
|
||||
api_key: API Key(可选)
|
||||
base_url: Base URL(可选)
|
||||
|
||||
Returns:
|
||||
LLM 实例
|
||||
"""
|
||||
if provider.lower() == "openai":
|
||||
return OpenAILLM(model_name, api_key, base_url)
|
||||
elif provider.lower() == "anthropic":
|
||||
return AnthropicLLM(model_name, api_key, base_url)
|
||||
else:
|
||||
# 默认使用 OpenAI
|
||||
return OpenAILLM(model_name, api_key, base_url)
|
||||
@@ -1,167 +0,0 @@
|
||||
"""
|
||||
OpenAI LLM 实现
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from openai import AsyncOpenAI
|
||||
from openai._client import AsyncOpenAI
|
||||
|
||||
logger = logging.getLogger("llm.openai")
|
||||
|
||||
|
||||
class OpenAILLM:
|
||||
"""OpenAI LLM"""
|
||||
|
||||
def __init__(self, model_name: str = "gpt-4", api_key: Optional[str] = None, base_url: Optional[str] = None):
|
||||
self.model_name = model_name
|
||||
# 优先使用传入的参数,否则使用环境变量
|
||||
self.api_key = api_key or os.getenv("OPENAI_API_KEY", "")
|
||||
self.base_url = base_url or os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
||||
|
||||
api_key_info = f"{self.api_key[:10]}..." if self.api_key else "None"
|
||||
logger.info(f"初始化 OpenAI LLM: model={model_name}, api_key={api_key_info}, base_url={self.base_url}")
|
||||
|
||||
if not self.api_key:
|
||||
logger.warning("⚠️ WARNING: No API key provided!")
|
||||
|
||||
# 配置超时
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=self.api_key,
|
||||
base_url=self.base_url,
|
||||
timeout=60.0, # 60秒超时
|
||||
max_retries=1 # 减少重试次数
|
||||
)
|
||||
|
||||
async def decide(self, prompt: str) -> Dict[str, Any]:
|
||||
"""
|
||||
LLM 决策 - 判断是否需要调用技能
|
||||
|
||||
Args:
|
||||
prompt: 完整的 Prompt
|
||||
|
||||
Returns:
|
||||
Dict: 包含 needs_skill, tool_calls, response 等
|
||||
"""
|
||||
# 构建决策用的系统提示
|
||||
system_prompt = """你是一个智能助手。请分析用户请求,判断是否需要调用工具来回答问题。
|
||||
|
||||
如果需要调用工具,请按以下格式返回 JSON:
|
||||
{
|
||||
"needs_skill": true,
|
||||
"tool_calls": [
|
||||
{"skill_id": "工具名称", "parameters": {"参数": "值"}, "reason": "调用原因"}
|
||||
]
|
||||
}
|
||||
|
||||
如果不需要调用工具,请返回:
|
||||
{
|
||||
"needs_skill": false,
|
||||
"response": "直接回答用户的内容"
|
||||
}
|
||||
|
||||
请只返回 JSON,不要其他内容。"""
|
||||
|
||||
try:
|
||||
response = await self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=2000
|
||||
)
|
||||
|
||||
content = response.choices[0].message.content
|
||||
|
||||
# 尝试解析 JSON
|
||||
import json
|
||||
try:
|
||||
result = json.loads(content)
|
||||
return result
|
||||
except json.JSONDecodeError:
|
||||
# 解析失败,作为普通回复处理
|
||||
return {
|
||||
"needs_skill": False,
|
||||
"response": content
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"needs_skill": False,
|
||||
"response": f"LLM 调用失败: {str(e)}"
|
||||
}
|
||||
|
||||
async def generate(self, prompt: str, tool_results: List[Dict]) -> str:
|
||||
"""
|
||||
生成回复
|
||||
|
||||
Args:
|
||||
prompt: 完整的 Prompt
|
||||
tool_results: 工具调用结果
|
||||
|
||||
Returns:
|
||||
str: 生成的回复
|
||||
"""
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
# 添加工具结果作为上下文
|
||||
if tool_results:
|
||||
for result in tool_results:
|
||||
if result.get("success"):
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": f"工具 {result.get('skill_id')} 返回: {result.get('result')}"
|
||||
})
|
||||
|
||||
try:
|
||||
response = await self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
temperature=0.7,
|
||||
max_tokens=4000
|
||||
)
|
||||
|
||||
return response.choices[0].message.content
|
||||
|
||||
except Exception as e:
|
||||
return f"生成回复失败: {str(e)}"
|
||||
|
||||
async def generate_stream(self, prompt: str, tool_results: List[Dict]):
|
||||
"""
|
||||
流式生成回复
|
||||
|
||||
Args:
|
||||
prompt: 完整的 Prompt
|
||||
tool_results: 工具调用结果
|
||||
|
||||
Yields:
|
||||
str: 生成的回复片段
|
||||
"""
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
# 添加工具结果作为上下文
|
||||
if tool_results:
|
||||
for result in tool_results:
|
||||
if result.get("success"):
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": f"工具 {result.get('skill_id')} 返回: {result.get('result')}"
|
||||
})
|
||||
|
||||
try:
|
||||
response = await self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
temperature=0.7,
|
||||
max_tokens=4000,
|
||||
stream=True
|
||||
)
|
||||
|
||||
async for chunk in response:
|
||||
if chunk.choices[0].delta.content:
|
||||
yield chunk.choices[0].delta.content
|
||||
|
||||
except Exception as e:
|
||||
yield f"生成回复失败: {str(e)}"
|
||||
@@ -1,5 +0,0 @@
|
||||
# Memory
|
||||
from app.agent.memory.manager import MemoryManager
|
||||
from app.agent.memory.working import WorkingMemory
|
||||
from app.agent.memory.session import SessionMemory
|
||||
from app.agent.memory.persistent import PersistentMemory
|
||||
@@ -1,99 +0,0 @@
|
||||
"""
|
||||
Memory Manager - 记忆管理器
|
||||
"""
|
||||
from typing import Dict, List, Optional
|
||||
from app.agent.memory.working import WorkingMemory
|
||||
from app.agent.memory.session import SessionMemory
|
||||
from app.agent.memory.persistent import PersistentMemory
|
||||
|
||||
|
||||
class MemoryManager:
|
||||
"""记忆管理器 - 统一接口"""
|
||||
|
||||
def __init__(self, agent_id: int):
|
||||
self.agent_id = agent_id
|
||||
self.working = WorkingMemory()
|
||||
self.session = SessionMemory(agent_id)
|
||||
self.persistent = PersistentMemory(agent_id)
|
||||
|
||||
async def load_context(self, query: str, user_id: int, session_id: str) -> Dict[str, str]:
|
||||
"""
|
||||
加载上下文记忆
|
||||
|
||||
优化:跳过耗时的向量搜索,提升响应速度
|
||||
生产环境可以加回来
|
||||
|
||||
Args:
|
||||
query: 查询内容
|
||||
user_id: 用户 ID
|
||||
session_id: 会话 ID
|
||||
|
||||
Returns:
|
||||
Dict: 包含 summary, knowledge 等
|
||||
"""
|
||||
# 1. Working Memory (内存,最快)
|
||||
working_context = self.working.get()
|
||||
|
||||
# 2. Session Memory (Redis) - 暂时跳过,减少延迟
|
||||
# session_context = await self.session.get_summary(user_id, session_id)
|
||||
session_context = ""
|
||||
|
||||
# 3. Persistent Memory (向量库) - 暂时跳过,减少延迟
|
||||
# persistent_context = await self.persistent.search(query, user_id, top_k=3)
|
||||
persistent_context = []
|
||||
|
||||
return {
|
||||
'working': working_context.get('recent_messages', []),
|
||||
'session': session_context,
|
||||
'persistent': persistent_context,
|
||||
'summary': "", # 简化
|
||||
'knowledge': ""
|
||||
}
|
||||
|
||||
async def save(self, user_input: str, response: str, user_id: int, session_id: str):
|
||||
"""
|
||||
保存记忆
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
response: 智能体回复
|
||||
user_id: 用户 ID
|
||||
session_id: 会话 ID
|
||||
"""
|
||||
# 1. 写入 Working
|
||||
self.working.add(user_input, response)
|
||||
|
||||
# 2. 写入 Session (定期摘要)
|
||||
await self.session.add(user_input, response, user_id, session_id)
|
||||
|
||||
# 3. 提取关键信息写入 Persistent (定期)
|
||||
if self.working.size() >= 5:
|
||||
await self._extract_and_persist(user_input, response, user_id)
|
||||
|
||||
def _build_summary(self, session_context: str, persistent_context: List[str]) -> str:
|
||||
"""构建记忆摘要"""
|
||||
parts = []
|
||||
|
||||
if session_context:
|
||||
parts.append(f"会话记忆: {session_context}")
|
||||
|
||||
if persistent_context:
|
||||
parts.append(f"长期记忆: {'; '.join(persistent_context[:3])}")
|
||||
|
||||
return "\n".join(parts) if parts else "无相关记忆"
|
||||
|
||||
async def _extract_and_persist(self, user_input: str, response: str, user_id: int):
|
||||
"""提取并持久化关键信息"""
|
||||
# 提取关键信息(简化版:取前100字符作为摘要)
|
||||
key_points = []
|
||||
|
||||
# 简化:直接保存重要交互
|
||||
if len(response) > 50: # 只保存有意义的回复
|
||||
summary = response[:100] + "..."
|
||||
key_points.append(summary)
|
||||
|
||||
for point in key_points:
|
||||
await self.persistent.add(point, user_id, memory_type="conversation")
|
||||
|
||||
# 重置 Working Memory
|
||||
self.working.clear()
|
||||
@@ -1,109 +0,0 @@
|
||||
"""
|
||||
Persistent Memory - 长期记忆(向量存储)
|
||||
"""
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class PersistentMemory:
|
||||
"""长期记忆,向量存储"""
|
||||
|
||||
def __init__(self, agent_id: int, vector_store=None):
|
||||
"""
|
||||
初始化长期记忆
|
||||
|
||||
Args:
|
||||
agent_id: 智能体 ID
|
||||
vector_store: 向量存储客户端(可选)
|
||||
"""
|
||||
self.agent_id = agent_id
|
||||
self.vector_store = vector_store
|
||||
self.use_vector = vector_store is not None
|
||||
|
||||
async def add(self, content: str, user_id: int, memory_type: str = "experience"):
|
||||
"""
|
||||
添加长期记忆
|
||||
|
||||
Args:
|
||||
content: 记忆内容
|
||||
user_id: 用户 ID
|
||||
memory_type: 记忆类型 (experience/preference/conversation)
|
||||
"""
|
||||
if not self.use_vector:
|
||||
return self._add_memory(content, user_id, memory_type)
|
||||
|
||||
# 生成向量并存储
|
||||
embedding = await self._get_embedding(content)
|
||||
# TODO: 调用向量存储 API
|
||||
|
||||
async def search(self, query: str, user_id: int, top_k: int = 3) -> List[str]:
|
||||
"""
|
||||
搜索相关记忆
|
||||
|
||||
Args:
|
||||
query: 查询内容
|
||||
user_id: 用户 ID
|
||||
top_k: 返回数量
|
||||
|
||||
Returns:
|
||||
List[str]: 相关的记忆列表
|
||||
"""
|
||||
if not self.use_vector:
|
||||
return self._search_memory(query, user_id, top_k)
|
||||
|
||||
# 生成查询向量
|
||||
query_embedding = await self._get_embedding(query)
|
||||
|
||||
# TODO: 调用向量搜索 API
|
||||
# results = await self.vector_store.search(
|
||||
# agent_id=self.agent_id,
|
||||
# user_id=user_id,
|
||||
# embedding=query_embedding,
|
||||
# top_k=top_k
|
||||
# )
|
||||
|
||||
return []
|
||||
|
||||
async def _get_embedding(self, text: str) -> List[float]:
|
||||
"""
|
||||
获取文本向量
|
||||
|
||||
Args:
|
||||
text: 文本
|
||||
|
||||
Returns:
|
||||
List[float]: 向量
|
||||
"""
|
||||
# TODO: 实现向量生成
|
||||
# 可以使用 OpenAI Embedding API 或本地模型
|
||||
import hashlib
|
||||
# 简化:使用文本哈希模拟
|
||||
h = hashlib.md5(text.encode()).digest()
|
||||
return [float(b) / 255.0 for b in h[:16]] + [0.0] * 16
|
||||
|
||||
# === 内存模拟(无向量存储时使用)===
|
||||
_memory_store = {}
|
||||
|
||||
def _add_memory(self, content: str, user_id: int, memory_type: str):
|
||||
"""内存模拟 - 添加"""
|
||||
key = f"{self.agent_id}:{user_id}"
|
||||
if key not in self._memory_store:
|
||||
self._memory_store[key] = []
|
||||
|
||||
self._memory_store[key].append({
|
||||
"content": content,
|
||||
"type": memory_type
|
||||
})
|
||||
|
||||
def _search_memory(self, query: str, user_id: int, top_k: int) -> List[str]:
|
||||
"""内存模拟 - 搜索(简化版:关键词匹配)"""
|
||||
key = f"{self.agent_id}:{user_id}"
|
||||
if key not in self._memory_store:
|
||||
return []
|
||||
|
||||
# 简化:包含查询词的记忆
|
||||
results = []
|
||||
for mem in self._memory_store[key]:
|
||||
if query.lower() in mem["content"].lower():
|
||||
results.append(mem["content"])
|
||||
|
||||
return results[:top_k]
|
||||
@@ -1,125 +0,0 @@
|
||||
"""
|
||||
Session Memory - 会话级记忆(Redis 存储)
|
||||
"""
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class SessionMemory:
|
||||
"""会话级记忆,Redis 存储"""
|
||||
|
||||
def __init__(self, agent_id: int, redis_client=None):
|
||||
"""
|
||||
初始化会话记忆
|
||||
|
||||
Args:
|
||||
agent_id: 智能体 ID
|
||||
redis_client: Redis 客户端(可选)
|
||||
"""
|
||||
self.agent_id = agent_id
|
||||
self.redis = redis_client
|
||||
self.ttl = 3600 * 24 # 24 小时
|
||||
self.summary_threshold = 10 # 多少条消息后生成摘要
|
||||
|
||||
def _key(self, user_id: int, session_id: str) -> str:
|
||||
"""生成 Redis Key"""
|
||||
return f"agent:memory:session:{self.agent_id}:{user_id}:{session_id}"
|
||||
|
||||
async def add(self, user_input: str, response: str, user_id: int, session_id: str):
|
||||
"""
|
||||
添加对话到会话记忆
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
response: 智能体回复
|
||||
user_id: 用户 ID
|
||||
session_id: 会话 ID
|
||||
"""
|
||||
if not self.redis:
|
||||
# 如果没有 Redis,使用内存模拟
|
||||
return self._add_memory(user_input, response, user_id, session_id)
|
||||
|
||||
key = self._key(user_id, session_id)
|
||||
|
||||
# 获取现有数据
|
||||
data = await self.redis.get(key)
|
||||
messages = json.loads(data) if data else {"messages": [], "summary": ""}
|
||||
|
||||
# 添加新消息
|
||||
messages["messages"].append({"role": "user", "content": user_input})
|
||||
messages["messages"].append({"role": "assistant", "content": response})
|
||||
|
||||
# 定期生成摘要
|
||||
if len(messages["messages"]) >= self.summary_threshold:
|
||||
messages["summary"] = await self._generate_summary(messages["messages"])
|
||||
|
||||
# 保持消息数量
|
||||
if len(messages["messages"]) > 50:
|
||||
messages["messages"] = messages["messages"][-50:]
|
||||
|
||||
await self.redis.setex(key, self.ttl, json.dumps(messages))
|
||||
|
||||
async def get_summary(self, user_id: int, session_id: str) -> str:
|
||||
"""
|
||||
获取会话摘要
|
||||
|
||||
Args:
|
||||
user_id: 用户 ID
|
||||
session_id: 会话 ID
|
||||
|
||||
Returns:
|
||||
str: 会话摘要
|
||||
"""
|
||||
if not self.redis:
|
||||
return self._get_memory_summary(user_id, session_id)
|
||||
|
||||
key = self._key(user_id, session_id)
|
||||
data = await self.redis.get(key)
|
||||
|
||||
if data:
|
||||
messages = json.loads(data)
|
||||
return messages.get("summary", "")
|
||||
return ""
|
||||
|
||||
async def _generate_summary(self, messages: list) -> str:
|
||||
"""
|
||||
生成摘要(简化版)
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
|
||||
Returns:
|
||||
str: 摘要
|
||||
"""
|
||||
# 简化:取最后几条消息的要点
|
||||
if not messages:
|
||||
return ""
|
||||
|
||||
recent = messages[-6:] # 最近 3 轮
|
||||
summary = f"最近对话包含 {len(messages)//2} 轮交互"
|
||||
|
||||
# TODO: 后续可以使用 LLM 生成更好的摘要
|
||||
return summary
|
||||
|
||||
# === 内存模拟(无 Redis 时使用)===
|
||||
_memory_store = {}
|
||||
|
||||
def _add_memory(self, user_input: str, response: str, user_id: int, session_id: str):
|
||||
"""内存模拟 - 添加"""
|
||||
key = f"{self.agent_id}:{user_id}:{session_id}"
|
||||
if key not in self._memory_store:
|
||||
self._memory_store[key] = {"messages": [], "summary": ""}
|
||||
|
||||
messages = self._memory_store[key]["messages"]
|
||||
messages.append({"role": "user", "content": user_input})
|
||||
messages.append({"role": "assistant", "content": response})
|
||||
|
||||
if len(messages) >= self.summary_threshold:
|
||||
self._memory_store[key]["summary"] = self._generate_summary(messages)
|
||||
|
||||
def _get_memory_summary(self, user_id: int, session_id: str) -> str:
|
||||
"""内存模拟 - 获取摘要"""
|
||||
key = f"{self.agent_id}:{user_id}:{session_id}"
|
||||
if key in self._memory_store:
|
||||
return self._memory_store[key].get("summary", "")
|
||||
return ""
|
||||
@@ -1,47 +0,0 @@
|
||||
"""
|
||||
Working Memory - 当前任务上下文(内存级存储)
|
||||
"""
|
||||
|
||||
|
||||
class WorkingMemory:
|
||||
"""当前任务上下文,内存级存储"""
|
||||
|
||||
def __init__(self):
|
||||
self.current_task = None
|
||||
self.recent_messages = []
|
||||
self.max_size = 10 # 最大保留 10 轮对话
|
||||
|
||||
def get(self) -> dict:
|
||||
"""获取当前记忆"""
|
||||
return {
|
||||
'current_task': self.current_task,
|
||||
'recent_messages': self.recent_messages[-self.max_size:]
|
||||
}
|
||||
|
||||
def add(self, user_input: str, response: str):
|
||||
"""添加对话到记忆"""
|
||||
self.recent_messages.append({
|
||||
'role': 'user',
|
||||
'content': user_input
|
||||
})
|
||||
self.recent_messages.append({
|
||||
'role': 'assistant',
|
||||
'content': response
|
||||
})
|
||||
|
||||
# 保持固定大小
|
||||
if len(self.recent_messages) > self.max_size * 2:
|
||||
self.recent_messages = self.recent_messages[-self.max_size * 2:]
|
||||
|
||||
def set_current_task(self, task: str):
|
||||
"""设置当前任务"""
|
||||
self.current_task = task
|
||||
|
||||
def clear(self):
|
||||
"""清空记忆"""
|
||||
self.recent_messages = []
|
||||
self.current_task = None
|
||||
|
||||
def size(self) -> int:
|
||||
"""获取对话轮数"""
|
||||
return len(self.recent_messages) // 2
|
||||
@@ -1,3 +0,0 @@
|
||||
# Skills
|
||||
from app.agent.skills.router import SkillRouter
|
||||
from app.agent.skills.executor import SkillExecutor
|
||||
@@ -1,129 +0,0 @@
|
||||
"""
|
||||
Skill Executor - 技能执行器
|
||||
"""
|
||||
import asyncio
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class SkillExecutor:
|
||||
"""技能执行器,支持并发/串行执行"""
|
||||
|
||||
def __init__(self, skill_registry=None):
|
||||
"""
|
||||
初始化技能执行器
|
||||
|
||||
Args:
|
||||
skill_registry: 技能注册表(可选)
|
||||
"""
|
||||
self.skill_registry = skill_registry
|
||||
self._skill_handlers = self._init_default_handlers()
|
||||
|
||||
def _init_default_handlers(self) -> Dict[str, callable]:
|
||||
"""初始化默认技能处理器"""
|
||||
return {
|
||||
"query_database": self._handle_query_database,
|
||||
"data_analysis": self._handle_data_analysis,
|
||||
"search_knowledge": self._handle_search_knowledge,
|
||||
"web_search": self._handle_web_search,
|
||||
}
|
||||
|
||||
async def execute(self, skill_id: str, params: dict) -> Dict[str, Any]:
|
||||
"""
|
||||
执行单个技能
|
||||
|
||||
Args:
|
||||
skill_id: 技能 ID
|
||||
params: 技能参数
|
||||
|
||||
Returns:
|
||||
Dict: 执行结果
|
||||
"""
|
||||
handler = self._skill_handlers.get(skill_id)
|
||||
|
||||
if not handler:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Skill {skill_id} not found"
|
||||
}
|
||||
|
||||
try:
|
||||
result = await handler(params)
|
||||
return {
|
||||
"success": True,
|
||||
"skill_id": skill_id,
|
||||
"result": result
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": skill_id,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def execute_multiple(self, skills: List[Dict], strategy: str = "parallel") -> List[Dict]:
|
||||
"""
|
||||
批量执行技能
|
||||
|
||||
Args:
|
||||
skills: 技能列表
|
||||
strategy: 执行策略 (parallel/sequential)
|
||||
|
||||
Returns:
|
||||
List[Dict]: 执行结果列表
|
||||
"""
|
||||
if strategy == "parallel":
|
||||
tasks = [self.execute(s['skill_id'], s['params']) for s in skills]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# 处理异常结果
|
||||
formatted_results = []
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
formatted_results.append({
|
||||
"success": False,
|
||||
"skill_id": skills[i]['skill_id'],
|
||||
"error": str(result)
|
||||
})
|
||||
else:
|
||||
formatted_results.append(result)
|
||||
return formatted_results
|
||||
else:
|
||||
results = []
|
||||
for s in skills:
|
||||
result = await self.execute(s['skill_id'], s['params'])
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
# === 默认技能处理器 ===
|
||||
|
||||
async def _handle_query_database(self, params: dict) -> Dict[str, Any]:
|
||||
"""处理数据库查询"""
|
||||
# TODO: 调用现有的数据库查询功能
|
||||
return {
|
||||
"message": "数据库查询功能待实现",
|
||||
"sql": params.get("sql", "")
|
||||
}
|
||||
|
||||
async def _handle_data_analysis(self, params: dict) -> Dict[str, Any]:
|
||||
"""处理数据分析"""
|
||||
# TODO: 调用现有的数据分析功能
|
||||
return {
|
||||
"message": "数据分析功能待实现",
|
||||
"data": params.get("data", {})
|
||||
}
|
||||
|
||||
async def _handle_search_knowledge(self, params: dict) -> Dict[str, Any]:
|
||||
"""处理知识库搜索"""
|
||||
# TODO: 调用现有的知识库搜索功能
|
||||
return {
|
||||
"message": "知识库搜索功能待实现",
|
||||
"query": params.get("query", "")
|
||||
}
|
||||
|
||||
async def _handle_web_search(self, params: dict) -> Dict[str, Any]:
|
||||
"""处理网页搜索"""
|
||||
# TODO: 实现网页搜索
|
||||
return {
|
||||
"message": "网页搜索功能待实现",
|
||||
"query": params.get("query", "")
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
"""
|
||||
Skill Router - 技能路由器
|
||||
"""
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
class SkillRouter:
|
||||
"""根据 LLM 决策选择要调用的技能"""
|
||||
|
||||
def __init__(self, available_skills: List[int]):
|
||||
"""
|
||||
初始化技能路由器
|
||||
|
||||
Args:
|
||||
available_skills: 可用技能 ID 列表
|
||||
"""
|
||||
self.available_skills = available_skills
|
||||
|
||||
async def route(self, llm_decision: dict) -> List[dict]:
|
||||
"""
|
||||
解析 LLM 的技能调用决策
|
||||
|
||||
Args:
|
||||
llm_decision: LLM 决策结果
|
||||
|
||||
Returns:
|
||||
List[dict]: 要执行的技能列表
|
||||
"""
|
||||
if not llm_decision.get('tool_calls'):
|
||||
return []
|
||||
|
||||
routes = []
|
||||
for tool_call in llm_decision['tool_calls']:
|
||||
skill_id = tool_call.get('skill_id') or tool_call.get('tool_name')
|
||||
|
||||
# 检查技能是否可用
|
||||
if self.available_skills and skill_id not in self.available_skills:
|
||||
continue
|
||||
|
||||
routes.append({
|
||||
'skill_id': skill_id,
|
||||
'params': tool_call.get('parameters', {}),
|
||||
'reason': tool_call.get('reason', '')
|
||||
})
|
||||
|
||||
return routes
|
||||
@@ -1,549 +0,0 @@
|
||||
"""
|
||||
FastAPI Agent Engine Server
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import asyncio
|
||||
|
||||
from app.agent.core import AgentConfig
|
||||
from app.xbot import XBotAgent
|
||||
|
||||
|
||||
# 日志目录 - 放在 server/logs 下
|
||||
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "server", "logs", datetime.now().strftime("%Y-%m-%d"))
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
# 成功/失败日志文件
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
success_log_file = os.path.join(LOG_DIR, f"python_success.log")
|
||||
failure_log_file = os.path.join(LOG_DIR, f"python_failure.log")
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""配置日志系统"""
|
||||
log_level = os.getenv("LOG_LEVEL", "INFO")
|
||||
|
||||
# 创建格式化器
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
# 控制台处理器
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# 成功日志文件处理器
|
||||
success_handler = logging.FileHandler(success_log_file, encoding='utf-8')
|
||||
success_handler.setFormatter(formatter)
|
||||
success_handler.setLevel(logging.INFO)
|
||||
|
||||
# 失败日志文件处理器
|
||||
failure_handler = logging.FileHandler(failure_log_file, encoding='utf-8')
|
||||
failure_handler.setFormatter(formatter)
|
||||
failure_handler.setLevel(logging.WARNING)
|
||||
|
||||
# 根日志配置
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(getattr(logging, log_level))
|
||||
root_logger.addHandler(console_handler)
|
||||
root_logger.addHandler(success_handler)
|
||||
root_logger.addHandler(failure_handler)
|
||||
|
||||
return root_logger
|
||||
|
||||
|
||||
# 成功日志记录器(只记录 INFO 级别到成功日志)
|
||||
class SuccessLogger:
|
||||
"""成功日志记录器"""
|
||||
|
||||
@staticmethod
|
||||
def log(message: str):
|
||||
"""记录成功日志"""
|
||||
logger = logging.getLogger("success")
|
||||
logger.setLevel(logging.INFO)
|
||||
handler = logging.FileHandler(success_log_file, encoding='utf-8')
|
||||
handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S'))
|
||||
logger.addHandler(handler)
|
||||
logger.info(message)
|
||||
|
||||
# 同时输出到控制台
|
||||
print(f"✅ {message}")
|
||||
|
||||
|
||||
# 失败日志记录器
|
||||
class FailureLogger:
|
||||
"""失败日志记录器"""
|
||||
|
||||
@staticmethod
|
||||
def log(message: str, error: str = ""):
|
||||
"""记录失败日志"""
|
||||
logger = logging.getLogger("failure")
|
||||
logger.setLevel(logging.WARNING)
|
||||
handler = logging.FileHandler(failure_log_file, encoding='utf-8')
|
||||
handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s - %(error)s', datefmt='%Y-%m-%d %H:%M:%S'))
|
||||
logger.addHandler(handler)
|
||||
|
||||
full_message = f"{message} - Error: {error}" if error else message
|
||||
logger.warning(full_message)
|
||||
|
||||
# 同时输出到控制台
|
||||
print(f"❌ {full_message}")
|
||||
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
app = FastAPI(title="X-Agents Python Engine", version="1.0.0")
|
||||
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# === 请求/响应模型 ===
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
"""对话请求"""
|
||||
agent_id: int
|
||||
message: str
|
||||
user_id: int = 1
|
||||
session_id: Optional[str] = None
|
||||
# 模型参数(可选,如果传了就使用,否则用智能体配置的默认模型)
|
||||
model_id: Optional[str] = None
|
||||
model_name: Optional[str] = None
|
||||
model_provider: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
base_url: Optional[str] = None
|
||||
# Embedding 模型(可选)
|
||||
embedding_model: Optional[str] = None
|
||||
embedding_base_url: Optional[str] = None
|
||||
|
||||
|
||||
class TeamChatRequest(BaseModel):
|
||||
"""多智能体群聊请求"""
|
||||
supervisor_agent_id: int
|
||||
member_agent_ids: list[int]
|
||||
message: str
|
||||
user_id: int = 1
|
||||
session_id: Optional[str] = None
|
||||
strategy: str = "parallel"
|
||||
|
||||
|
||||
class CreateAgentRequest(BaseModel):
|
||||
"""创建智能体请求"""
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
avatar: str = "🤖"
|
||||
# 技能配置
|
||||
skills_mode: str = "all" # all / include / exclude
|
||||
skills: list[str] = [] # 技能ID列表
|
||||
# 知识库
|
||||
knowledge: str = "general" # general / codebase / docs / api
|
||||
# 自定义提示词
|
||||
prompt: Optional[str] = None
|
||||
# 模型配置
|
||||
model_provider: Optional[str] = None
|
||||
model_name: Optional[str] = None
|
||||
user_id: int = 1
|
||||
|
||||
|
||||
class CreateAgentResponse(BaseModel):
|
||||
"""创建智能体响应"""
|
||||
agent_id: int
|
||||
name: str
|
||||
message: str = "Agent created successfully"
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
"""对话响应"""
|
||||
agent_id: int
|
||||
response: str
|
||||
tool_calls: list = []
|
||||
tokens_used: int = 0
|
||||
duration_ms: int = 0
|
||||
session_id: Optional[str] = None
|
||||
|
||||
|
||||
# === 模拟数据存储 ===
|
||||
# TODO: 后续替换为从数据库加载
|
||||
_mock_agents = {
|
||||
1: {
|
||||
"id": 1,
|
||||
"name": "数据分析助手",
|
||||
"role_description": "你是一个专业的数据分析助手,擅长分析数据、生成报告。",
|
||||
"model_provider": "openai",
|
||||
"model_name": "gpt-4",
|
||||
"skills": [1, 2]
|
||||
},
|
||||
2: {
|
||||
"id": 2,
|
||||
"name": "代码审查助手",
|
||||
"role_description": "你是一个专业的代码审查助手,擅长审查代码、发现bug。",
|
||||
"model_provider": "openai",
|
||||
"model_name": "gpt-4",
|
||||
"skills": [3]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_agent_config(agent_id: int, api_key: str = None, base_url: str = None) -> AgentConfig:
|
||||
"""获取智能体配置"""
|
||||
agent_data = _mock_agents.get(agent_id)
|
||||
if not agent_data:
|
||||
raise HTTPException(status_code=404, detail="Agent not found")
|
||||
|
||||
return AgentConfig(
|
||||
id=agent_data["id"],
|
||||
name=agent_data["name"],
|
||||
role_description=agent_data["role_description"],
|
||||
model_provider=agent_data["model_provider"],
|
||||
model_name=agent_data["model_name"],
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
skills=agent_data.get("skills", [])
|
||||
)
|
||||
|
||||
|
||||
# === API 路由 ===
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "X-Agents Python Engine", "version": "1.0.0"}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
@app.post("/agent/chat", response_model=ChatResponse)
|
||||
async def chat(request: ChatRequest):
|
||||
"""
|
||||
单智能体对话
|
||||
"""
|
||||
chat_logger = logging.getLogger("agent.chat")
|
||||
|
||||
# 打印请求参数(隐藏 api_key 敏感信息)
|
||||
api_key_preview = f"{request.api_key[:10]}..." if request.api_key else "None"
|
||||
chat_logger.info(f"========== 收到聊天请求 ==========")
|
||||
chat_logger.info(f"agent_id: {request.agent_id}")
|
||||
chat_logger.info(f"model_id: {request.model_id}")
|
||||
chat_logger.info(f"model_provider: {request.model_provider}")
|
||||
chat_logger.info(f"model_name: {request.model_name}")
|
||||
chat_logger.info(f"api_key: {api_key_preview}")
|
||||
chat_logger.info(f"base_url: {request.base_url}")
|
||||
chat_logger.info(f"message: {request.message[:50]}...")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# 获取智能体配置
|
||||
try:
|
||||
config = get_agent_config(request.agent_id, request.api_key, request.base_url)
|
||||
chat_logger.info(f"Agent config loaded: provider={config.model_provider}, model={config.model_name}")
|
||||
except HTTPException as e:
|
||||
FailureLogger.log(f"Agent not found: agent_id={request.agent_id}", str(e))
|
||||
chat_logger.error(f"Agent not found: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
FailureLogger.log(f"Error loading config: agent_id={request.agent_id}", str(e))
|
||||
chat_logger.error(f"Error loading config: {e}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# 如果请求中指定了模型,覆盖智能体的默认配置
|
||||
if request.model_provider:
|
||||
config.model_provider = request.model_provider
|
||||
if request.model_name:
|
||||
config.model_name = request.model_name
|
||||
|
||||
chat_logger.info(f"Final LLM config: provider={config.model_provider}, model={config.model_name}, api_key={config.api_key[:10] if config.api_key else 'None'}..., base_url={config.base_url}")
|
||||
|
||||
# 生成 session_id
|
||||
session_id = request.session_id or f"session_{int(time.time())}"
|
||||
|
||||
# 执行对话 - 默认使用 XBot Agent (nanobot 核心)
|
||||
try:
|
||||
xbot = XBotAgent(
|
||||
name=config.name,
|
||||
role_description=config.role_description,
|
||||
provider=config.model_provider,
|
||||
model=config.model_name,
|
||||
api_key=request.api_key or config.api_key,
|
||||
base_url=request.base_url or config.base_url,
|
||||
embedding_model=request.embedding_model,
|
||||
embedding_base_url=request.embedding_base_url,
|
||||
)
|
||||
result = await xbot.run(request.message, session_id)
|
||||
response_content = result["content"]
|
||||
tool_calls = [{"name": tc} for tc in result.get("tool_calls", [])] if result.get("tool_calls") else []
|
||||
except Exception as e:
|
||||
FailureLogger.log(f"Agent execution failed: agent_id={request.agent_id}, message={request.message[:30]}", str(e))
|
||||
chat_logger.error(f"Agent execution error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
# 记录成功日志
|
||||
SuccessLogger.log(f"Chat success: agent_id={request.agent_id}, duration={duration_ms}ms, message={request.message[:30]}...")
|
||||
|
||||
return ChatResponse(
|
||||
agent_id=request.agent_id,
|
||||
response=response_content,
|
||||
tool_calls=tool_calls,
|
||||
tokens_used=0,
|
||||
duration_ms=duration_ms,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
|
||||
@app.post("/agent/chat/stream")
|
||||
async def chat_stream(request: ChatRequest):
|
||||
"""
|
||||
单智能体对话(流式输出)
|
||||
"""
|
||||
chat_logger = logging.getLogger("agent.chat.stream")
|
||||
|
||||
# 打印请求参数
|
||||
api_key_preview = f"{request.api_key[:10]}..." if request.api_key else "None"
|
||||
base_url_preview = request.base_url if request.base_url else "None"
|
||||
chat_logger.info(f"========== 收到流式聊天请求 ==========")
|
||||
chat_logger.info(f"agent_id: {request.agent_id}")
|
||||
chat_logger.info(f"model_provider: {request.model_provider}")
|
||||
chat_logger.info(f"model_name: {request.model_name}")
|
||||
chat_logger.info(f"api_key: {api_key_preview}")
|
||||
chat_logger.info(f"base_url: {base_url_preview}")
|
||||
|
||||
# 获取智能体配置
|
||||
try:
|
||||
config = get_agent_config(request.agent_id, request.api_key, request.base_url)
|
||||
except HTTPException as e:
|
||||
chat_logger.error(f"Agent not found: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
chat_logger.error(f"Error loading config: {e}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# 如果请求中指定了模型,覆盖智能体的默认配置
|
||||
if request.model_provider:
|
||||
config.model_provider = request.model_provider
|
||||
if request.model_name:
|
||||
config.model_name = request.model_name
|
||||
|
||||
chat_logger.info(f"最终配置 - provider: {config.model_provider}, model: {config.model_name}, base_url: {config.base_url}")
|
||||
|
||||
# 生成 session_id
|
||||
session_id = request.session_id or f"session_{int(time.time())}"
|
||||
|
||||
# Mock 模式测试流式
|
||||
if request.message.startswith("/mock "):
|
||||
mock_text = request.message[6:] # 去掉 "/mock " 前缀
|
||||
async def mock_stream():
|
||||
for char in mock_text:
|
||||
yield f"data: {char}\n\n"
|
||||
await asyncio.sleep(0.05) # 50ms 延迟模拟流式
|
||||
yield f"data: [DONE]\n\n"
|
||||
return StreamingResponse(mock_stream(), media_type="text/event-stream")
|
||||
|
||||
# 使用 XBot Agent (nanobot 核心)
|
||||
xbot = XBotAgent(
|
||||
name=config.name,
|
||||
role_description=config.role_description,
|
||||
provider=config.model_provider,
|
||||
model=config.model_name,
|
||||
api_key=request.api_key or config.api_key,
|
||||
base_url=request.base_url or config.base_url,
|
||||
embedding_model=request.embedding_model,
|
||||
embedding_base_url=request.embedding_base_url,
|
||||
)
|
||||
|
||||
async def event_generator():
|
||||
"""SSE 事件生成器"""
|
||||
try:
|
||||
# 执行流式对话
|
||||
async for chunk in xbot.run_stream(request.message, session_id):
|
||||
# 发送 SSE 格式的数据
|
||||
yield f"data: {chunk}\n\n"
|
||||
|
||||
# 发送结束信号
|
||||
yield f"data: [DONE]\n\n"
|
||||
|
||||
except Exception as e:
|
||||
chat_logger.error(f"Stream error: {e}")
|
||||
yield f"data: {{\"error\": \"{str(e)}\"}}\n\n"
|
||||
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
|
||||
|
||||
@app.post("/agent/team/chat")
|
||||
async def team_chat(request: TeamChatRequest):
|
||||
"""
|
||||
多智能体群聊
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
# 创建主智能体
|
||||
try:
|
||||
supervisor_config = get_agent_config(request.supervisor_agent_id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# 使用 XBot 作为主智能体
|
||||
supervisor_agent = XBotAgent(
|
||||
name=supervisor_config.name,
|
||||
role_description=supervisor_config.role_description,
|
||||
provider=supervisor_config.model_provider,
|
||||
model=supervisor_config.model_name,
|
||||
api_key=supervisor_config.api_key,
|
||||
base_url=supervisor_config.base_url,
|
||||
)
|
||||
|
||||
# 创建子智能体
|
||||
members = []
|
||||
for member_id in request.member_agent_ids:
|
||||
try:
|
||||
member_config = get_agent_config(member_id)
|
||||
members.append(XBotAgent(
|
||||
name=member_config.name,
|
||||
role_description=member_config.role_description,
|
||||
provider=member_config.model_provider,
|
||||
model=member_config.model_name,
|
||||
api_key=member_config.api_key,
|
||||
base_url=member_config.base_url,
|
||||
))
|
||||
except:
|
||||
continue
|
||||
|
||||
if not members:
|
||||
raise HTTPException(status_code=400, detail="No valid member agents")
|
||||
|
||||
# TODO: 群聊调度逻辑 - 目前简化为串行执行
|
||||
# 生成 session_id
|
||||
session_id = request.session_id or f"team_session_{int(time.time())}"
|
||||
|
||||
# 串行执行每个智能体
|
||||
subtask_results = []
|
||||
main_response = ""
|
||||
|
||||
try:
|
||||
# 主智能体先处理
|
||||
result = await supervisor_agent.run(request.message, session_id)
|
||||
main_response = result["content"]
|
||||
subtask_results.append({
|
||||
"agent_id": request.supervisor_agent_id,
|
||||
"response": main_response,
|
||||
})
|
||||
|
||||
# 子智能体并行处理
|
||||
# import asyncio
|
||||
# results = await asyncio.gather(*[m.run(request.message, session_id) for m in members])
|
||||
# for m, r in zip(members, results):
|
||||
# subtask_results.append({"agent_id": m.name, "response": r["content"]})
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
return {
|
||||
"supervisor_agent_id": request.supervisor_agent_id,
|
||||
"response": main_response,
|
||||
"subtask_results": subtask_results,
|
||||
"strategy": request.strategy or "parallel",
|
||||
"duration_ms": duration_ms,
|
||||
"session_id": session_id
|
||||
}
|
||||
|
||||
|
||||
@app.post("/agent/create", response_model=CreateAgentResponse)
|
||||
async def create_agent(request: CreateAgentRequest):
|
||||
"""
|
||||
创建新的智能体
|
||||
"""
|
||||
import json
|
||||
import uuid
|
||||
|
||||
# 生成唯一的 agent_id
|
||||
agent_id = int(datetime.now().timestamp() * 1000) % 100000
|
||||
|
||||
# 构建 Agent 配置
|
||||
agent_config = {
|
||||
"id": agent_id,
|
||||
"name": request.name,
|
||||
"description": request.description or "",
|
||||
"avatar": request.avatar,
|
||||
"skills_mode": request.skills_mode,
|
||||
"skills": request.skills,
|
||||
"knowledge": request.knowledge,
|
||||
"role_description": request.prompt or f"You are {request.name}. {request.description or ''}",
|
||||
"model_provider": request.model_provider or "anthropic",
|
||||
"model_name": request.model_name or "claude-sonnet-4-20250514",
|
||||
}
|
||||
|
||||
# 保存到 agents 目录
|
||||
agents_dir = os.path.join(os.path.dirname(__file__), "agents")
|
||||
os.makedirs(agents_dir, exist_ok=True)
|
||||
|
||||
config_file = os.path.join(agents_dir, f"agent_{agent_id}.json")
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(agent_config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"Agent created: {request.name} (ID: {agent_id})")
|
||||
|
||||
return CreateAgentResponse(
|
||||
agent_id=agent_id,
|
||||
name=request.name,
|
||||
message="Agent created successfully"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/agent/list")
|
||||
async def list_agents():
|
||||
"""
|
||||
获取智能体列表
|
||||
"""
|
||||
import json
|
||||
|
||||
agents_dir = os.path.join(os.path.dirname(__file__), "agents")
|
||||
if not os.path.exists(agents_dir):
|
||||
return {"agents": []}
|
||||
|
||||
agents = []
|
||||
for file in os.listdir(agents_dir):
|
||||
if file.endswith(".json"):
|
||||
config_file = os.path.join(agents_dir, file)
|
||||
try:
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
agent = json.load(f)
|
||||
agents.append(agent)
|
||||
except:
|
||||
continue
|
||||
|
||||
return {"agents": agents}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
port = int(os.getenv("AGENT_PORT", "8081"))
|
||||
uvicorn.run(
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=port,
|
||||
loop="asyncio",
|
||||
http="h11",
|
||||
access_log=False,
|
||||
timeout_keep_alive=5,
|
||||
)
|
||||
@@ -1,17 +0,0 @@
|
||||
"""XBot - 轻量级 Agent 框架(基于 nanobot 核心)"""
|
||||
|
||||
from .loop import AgentLoop
|
||||
from .memory import MemoryConsolidator, MemoryStore
|
||||
from .session import Session, SessionManager
|
||||
from .adapter import XBotLLMAdapter
|
||||
from .agent import XBotAgent
|
||||
|
||||
__all__ = [
|
||||
"AgentLoop",
|
||||
"MemoryConsolidator",
|
||||
"MemoryStore",
|
||||
"Session",
|
||||
"SessionManager",
|
||||
"XBotLLMAdapter",
|
||||
"XBotAgent",
|
||||
]
|
||||
@@ -1,186 +0,0 @@
|
||||
"""LLM Adapter - 将现有 LLM 适配到 XBot 接口"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.agent.llm.factory import LLMFactory
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCallRequest:
|
||||
"""A tool call request from the LLM."""
|
||||
id: str
|
||||
name: str
|
||||
arguments: dict[str, Any]
|
||||
|
||||
def to_openai_tool_call(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": self.name,
|
||||
"arguments": json.dumps(self.arguments, ensure_ascii=False),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMResponse:
|
||||
"""Response from an LLM provider."""
|
||||
content: str | None
|
||||
tool_calls: list[ToolCallRequest] = field(default_factory=list)
|
||||
finish_reason: str = "stop"
|
||||
usage: dict[str, int] = field(default_factory=dict)
|
||||
reasoning_content: str | None = None
|
||||
|
||||
@property
|
||||
def has_tool_calls(self) -> bool:
|
||||
return len(self.tool_calls) > 0
|
||||
|
||||
|
||||
class XBotLLMAdapter:
|
||||
"""
|
||||
适配器:将现有 LLM 适配到 XBot 的 LLMProvider 接口
|
||||
|
||||
封装 LLMFactory 创建的 LLM,使其符合 nanobot 风格的接口:
|
||||
- chat_with_retry(messages, tools, model) -> LLMResponse
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
provider: str,
|
||||
model_name: str,
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 4096,
|
||||
):
|
||||
self.provider_name = provider
|
||||
self.model = model_name
|
||||
self.temperature = temperature
|
||||
self.max_tokens = max_tokens
|
||||
|
||||
# 创建底层 LLM
|
||||
self._llm = LLMFactory.create(provider, model_name, api_key, base_url)
|
||||
|
||||
# 检查是否支持 tool calling
|
||||
self._supports_tools = self._check_tool_support()
|
||||
|
||||
def _check_tool_support(self) -> bool:
|
||||
"""检查模型是否支持 tool calling"""
|
||||
# GPT-4, Claude 支持 tool calling
|
||||
# 简单的判断逻辑
|
||||
model_lower = self.model.lower()
|
||||
if "gpt-4" in model_lower or "claude" in model_lower:
|
||||
return True
|
||||
return True # 默认支持
|
||||
|
||||
async def chat_with_retry(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: list[dict[str, Any]] | None = None,
|
||||
model: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
temperature: float | None = None,
|
||||
) -> LLMResponse:
|
||||
"""
|
||||
发送聊天请求(支持 tool calling)
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
tools: 工具定义列表
|
||||
model: 模型名称(可选)
|
||||
max_tokens: 最大 tokens(可选)
|
||||
temperature: 温度(可选)
|
||||
|
||||
Returns:
|
||||
LLMResponse: 包含内容和/或工具调用
|
||||
"""
|
||||
model = model or self.model
|
||||
max_tokens = max_tokens or self.max_tokens
|
||||
temperature = temperature or self.temperature
|
||||
|
||||
try:
|
||||
# 使用流式调用来获取完整响应
|
||||
response = await self._llm.client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
|
||||
message = response.choices[0].message
|
||||
|
||||
# 检查是否有 tool calls
|
||||
if message.tool_calls and tools:
|
||||
tool_calls = []
|
||||
for tc in message.tool_calls:
|
||||
tool_calls.append(ToolCallRequest(
|
||||
id=tc.id,
|
||||
name=tc.function.name,
|
||||
arguments=json.loads(tc.function.arguments) if isinstance(tc.function.arguments, str) else tc.function.arguments,
|
||||
))
|
||||
|
||||
return LLMResponse(
|
||||
content=message.content,
|
||||
tool_calls=tool_calls,
|
||||
finish_reason="tool_calls",
|
||||
)
|
||||
else:
|
||||
return LLMResponse(
|
||||
content=message.content or "",
|
||||
finish_reason="stop",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return LLMResponse(
|
||||
content=f"Error calling LLM: {str(e)}",
|
||||
finish_reason="error",
|
||||
)
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: list[dict[str, Any]] | None = None,
|
||||
model: str | None = None,
|
||||
max_tokens: int = 4096,
|
||||
temperature: float = 0.7,
|
||||
) -> LLMResponse:
|
||||
"""简化的 chat 方法"""
|
||||
return await self.chat_with_retry(
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
)
|
||||
|
||||
async def chat_stream(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: list[dict[str, Any]] | None = None,
|
||||
model: str | None = None,
|
||||
max_tokens: int = 4096,
|
||||
temperature: float = 0.7,
|
||||
):
|
||||
"""流式聊天"""
|
||||
model = model or self.model
|
||||
|
||||
try:
|
||||
response = await self._llm.client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
async for chunk in response:
|
||||
if chunk.choices[0].delta.content:
|
||||
yield chunk.choices[0].delta.content
|
||||
|
||||
except Exception as e:
|
||||
yield f"Error: {str(e)}"
|
||||
@@ -1,309 +0,0 @@
|
||||
"""XBot Agent - 封装 nanobot 核心能力的 Agent"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from .loop import AgentLoop
|
||||
from .session import SessionManager
|
||||
from .adapter import XBotLLMAdapter, LLMResponse
|
||||
from . import config
|
||||
|
||||
# 尝试导入 simplemem
|
||||
try:
|
||||
from simplemem import SimpleMemSystem
|
||||
HAS_SIMPLEMEM = True
|
||||
except ImportError:
|
||||
HAS_SIMPLEMEM = False
|
||||
|
||||
|
||||
class SimpleToolRegistry:
|
||||
"""简单的工具注册表"""
|
||||
|
||||
def __init__(self):
|
||||
self._tools: dict[str, Any] = {}
|
||||
|
||||
def register(self, name: str, func: Any, description: str = "") -> None:
|
||||
"""注册一个工具"""
|
||||
self._tools[name] = {
|
||||
"function": func,
|
||||
"description": description,
|
||||
}
|
||||
|
||||
def get_definitions(self) -> list[dict]:
|
||||
"""获取工具定义列表"""
|
||||
tools = []
|
||||
for name, tool in self._tools.items():
|
||||
tools.append({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name,
|
||||
"description": tool.get("description", ""),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
}
|
||||
}
|
||||
})
|
||||
return tools
|
||||
|
||||
def get(self, name: str) -> Optional[Any]:
|
||||
"""获取工具"""
|
||||
return self._tools.get(name)
|
||||
|
||||
async def execute(self, name: str, arguments: dict) -> Any:
|
||||
"""执行工具"""
|
||||
tool = self._tools.get(name)
|
||||
if not tool:
|
||||
return f"Tool {name} not found"
|
||||
|
||||
func = tool.get("function")
|
||||
if not func:
|
||||
return f"Tool {name} has no function"
|
||||
|
||||
try:
|
||||
if callable(func):
|
||||
return await func(**arguments) if hasattr(func, '__await__') else func(**arguments)
|
||||
return "Tool function is not callable"
|
||||
except Exception as e:
|
||||
return f"Tool execution error: {str(e)}"
|
||||
|
||||
|
||||
class XBotAgent:
|
||||
"""
|
||||
XBot Agent - 基于 nanobot 核心的 Agent 实现
|
||||
|
||||
特性:
|
||||
- 多轮 tool-calling 对话
|
||||
- 自动内存压缩
|
||||
- 会话历史持久化
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
role_description: str,
|
||||
provider: str = "openai",
|
||||
model: str = "gpt-4",
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
workspace: Optional[Path] = None,
|
||||
context_window_tokens: int = 200000,
|
||||
embedding_model: Optional[str] = None,
|
||||
embedding_base_url: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
初始化 XBot Agent
|
||||
|
||||
Args:
|
||||
name: Agent 名称
|
||||
role_description: Agent 角色描述
|
||||
provider: LLM 提供商
|
||||
model: 模型名称
|
||||
api_key: API Key
|
||||
base_url: Base URL
|
||||
workspace: 工作目录(用于存储会话和记忆)
|
||||
context_window_tokens: 上下文窗口大小
|
||||
"""
|
||||
self.name = name
|
||||
self.role_description = role_description
|
||||
|
||||
# 使用配置文件的默认值
|
||||
if api_key is None:
|
||||
api_key = config.API_KEY
|
||||
if base_url is None:
|
||||
base_url = config.BASE_URL
|
||||
if workspace is None:
|
||||
workspace = Path(config.WORKSPACE)
|
||||
|
||||
# 创建工作目录
|
||||
self.workspace = workspace
|
||||
self.workspace.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 创建 LLM 适配器
|
||||
self.provider = XBotLLMAdapter(
|
||||
provider=provider,
|
||||
model_name=model,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
# 创建工具注册表
|
||||
self.tools = SimpleToolRegistry()
|
||||
self._register_default_tools()
|
||||
|
||||
# 创建 Agent Loop
|
||||
self.agent_loop = AgentLoop(
|
||||
provider=self.provider,
|
||||
model=model,
|
||||
tools=self.tools,
|
||||
max_iterations=50,
|
||||
)
|
||||
|
||||
# 创建会话管理器
|
||||
self.sessions = SessionManager(self.workspace)
|
||||
|
||||
# 创建 SimpleMem 记忆系统
|
||||
if HAS_SIMPLEMEM and api_key and config.ENABLE_SIMPLEMEM:
|
||||
# 使用配置文件的 embedding 设置
|
||||
emb_model = embedding_model or config.EMBEDDING_MODEL
|
||||
emb_base = embedding_base_url or config.EMBEDDING_BASE_URL or base_url
|
||||
|
||||
self.memory = SimpleMemSystem(
|
||||
api_key=api_key,
|
||||
base_url=emb_base,
|
||||
model=model,
|
||||
embedding_model=emb_model,
|
||||
db_path=str(self.workspace / "memory_db"),
|
||||
clear_db=False,
|
||||
# 并行处理配置
|
||||
enable_parallel_processing=config.ENABLE_PARALLEL_PROCESSING,
|
||||
max_parallel_workers=config.MAX_PARALLEL_WORKERS,
|
||||
enable_parallel_retrieval=config.ENABLE_PARALLEL_RETRIEVAL,
|
||||
max_retrieval_workers=config.MAX_RETRIEVAL_WORKERS,
|
||||
enable_planning=config.ENABLE_PLANNING,
|
||||
enable_reflection=config.ENABLE_REFLECTION,
|
||||
max_reflection_rounds=config.MAX_REFLECTION_ROUNDS,
|
||||
)
|
||||
self._use_simplemem = True
|
||||
print(f"SimpleMem initialized with embedding: {emb_model}, base_url: {emb_base}")
|
||||
else:
|
||||
self.memory = None
|
||||
self._use_simplemem = False
|
||||
if not api_key:
|
||||
print("Warning: No API key provided, SimpleMem will be disabled")
|
||||
|
||||
def _register_default_tools(self) -> None:
|
||||
"""注册默认工具"""
|
||||
# 可以在这里添加默认工具
|
||||
pass
|
||||
|
||||
def register_tool(
|
||||
self,
|
||||
name: str,
|
||||
func: Any,
|
||||
description: str = "",
|
||||
parameters: Optional[dict] = None,
|
||||
) -> None:
|
||||
"""注册自定义工具"""
|
||||
tool_def = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"parameters": parameters or {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
}
|
||||
}
|
||||
}
|
||||
# 存储在 tools 中
|
||||
self.tools.register(name, func, description)
|
||||
|
||||
async def run(
|
||||
self,
|
||||
user_input: str,
|
||||
session_id: str = "default",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
运行 Agent 对话
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
session_id: 会话 ID
|
||||
|
||||
Returns:
|
||||
dict: 包含 content, tool_calls 等
|
||||
"""
|
||||
# 获取或创建会话
|
||||
session = self.sessions.get_or_create(session_id)
|
||||
|
||||
# 构建系统提示
|
||||
system_prompt = f"""你是 {self.name}。
|
||||
{self.role_description}
|
||||
|
||||
请根据用户的问题回答,并使用 Markdown 格式输出。"""
|
||||
|
||||
# 如果使用 SimpleMem,检索相关记忆
|
||||
memory_context = ""
|
||||
if self._use_simplemem and self.memory:
|
||||
try:
|
||||
memory_context = self.memory.ask(user_input)
|
||||
except Exception as e:
|
||||
print(f"Memory retrieval error: {e}")
|
||||
|
||||
if memory_context:
|
||||
system_prompt += f"\n\n相关记忆:\n{memory_context}"
|
||||
|
||||
# 获取历史消息
|
||||
history = session.get_history(max_messages=50)
|
||||
|
||||
# 构建初始消息
|
||||
initial_messages = history + [
|
||||
{"role": "user", "content": user_input}
|
||||
]
|
||||
|
||||
# 运行 agent loop
|
||||
final_content, tools_used, all_messages = await self.agent_loop.run_loop(
|
||||
initial_messages=initial_messages,
|
||||
system_prompt=system_prompt,
|
||||
)
|
||||
|
||||
# 保存到会话
|
||||
for m in all_messages[len(history):]:
|
||||
session.messages.append(m)
|
||||
self.sessions.save(session)
|
||||
|
||||
# 保存到 SimpleMem 记忆
|
||||
if self._use_simplemem and self.memory and final_content:
|
||||
try:
|
||||
self.memory.add_dialogue("User", user_input, datetime.now().isoformat())
|
||||
self.memory.add_dialogue(self.name, final_content, datetime.now().isoformat())
|
||||
self.memory.finalize()
|
||||
except Exception as e:
|
||||
print(f"Memory save error: {e}")
|
||||
|
||||
return {
|
||||
"content": final_content or "No response",
|
||||
"tool_calls": tools_used,
|
||||
"session_id": session_id,
|
||||
}
|
||||
|
||||
async def run_stream(
|
||||
self,
|
||||
user_input: str,
|
||||
session_id: str = "default",
|
||||
):
|
||||
"""
|
||||
运行 Agent 对话(流式输出)
|
||||
|
||||
先完整执行 agent loop,最后流式输出结果
|
||||
|
||||
Args:
|
||||
user_input: 用户输入
|
||||
session_id: 会话 ID
|
||||
|
||||
Yields:
|
||||
str: 流式回复片段
|
||||
"""
|
||||
# 先完整执行 agent loop(包含 tool-calling)
|
||||
result = await self.run(user_input, session_id)
|
||||
content = result["content"]
|
||||
|
||||
# 流式输出结果
|
||||
for char in content:
|
||||
yield char
|
||||
|
||||
def clear_session(self, session_id: str) -> None:
|
||||
"""清除会话"""
|
||||
session = self.sessions.get_or_create(session_id)
|
||||
session.clear()
|
||||
self.sessions.save(session)
|
||||
self.sessions.invalidate(session_id)
|
||||
|
||||
def list_sessions(self) -> list[dict]:
|
||||
"""列出所有会话"""
|
||||
return self.sessions.list_sessions()
|
||||
@@ -1,74 +0,0 @@
|
||||
"""
|
||||
XBot 配置文件
|
||||
"""
|
||||
|
||||
# ==================== LLM 配置 ====================
|
||||
|
||||
# 默认 LLM 提供商
|
||||
DEFAULT_PROVIDER = "openai"
|
||||
|
||||
# 默认模型
|
||||
DEFAULT_MODEL = "gpt-4"
|
||||
|
||||
# API Key(建议使用环境变量)
|
||||
import os
|
||||
API_KEY = os.getenv("OPENAI_API_KEY", "")
|
||||
|
||||
# Base URL
|
||||
BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
||||
|
||||
|
||||
# ==================== SimpleMem 记忆配置 ====================
|
||||
|
||||
# 是否启用 SimpleMem
|
||||
ENABLE_SIMPLEMEM = True
|
||||
|
||||
# Embedding 模型
|
||||
# 推荐: text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002
|
||||
# 或使用 Qwen: Qwen/Qwen3-Embedding-0.6B
|
||||
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small")
|
||||
|
||||
# Embedding 服务的 Base URL(可选,默认使用 BASE_URL)
|
||||
EMBEDDING_BASE_URL = os.getenv("EMBEDDING_BASE_URL", "")
|
||||
|
||||
|
||||
# ==================== 并行处理配置 ====================
|
||||
|
||||
# 是否启用并行处理
|
||||
ENABLE_PARALLEL_PROCESSING = True
|
||||
MAX_PARALLEL_WORKERS = 8
|
||||
|
||||
# 是否启用并行检索
|
||||
ENABLE_PARALLEL_RETRIEVAL = True
|
||||
MAX_RETRIEVAL_WORKERS = 4
|
||||
|
||||
# 是否启用规划
|
||||
ENABLE_PLANNING = True
|
||||
|
||||
# 是否启用反思
|
||||
ENABLE_REFLECTION = True
|
||||
MAX_REFLECTION_ROUNDS = 2
|
||||
|
||||
|
||||
# ==================== 工作目录 ====================
|
||||
|
||||
# 工作目录(用于存储会话和记忆)
|
||||
WORKSPACE = os.getenv("XAGENT_WORKSPACE", "./xbot_workspace")
|
||||
|
||||
# 上下文窗口大小
|
||||
CONTEXT_WINDOW_TOKENS = 200000
|
||||
|
||||
|
||||
# ==================== Agent 配置 ====================
|
||||
|
||||
# 默认 Agent 配置
|
||||
DEFAULT_AGENTS = {
|
||||
1: {
|
||||
"name": "数据分析助手",
|
||||
"role_description": "你是一个专业的数据分析助手,擅长分析数据、生成报告。",
|
||||
},
|
||||
2: {
|
||||
"name": "代码审查助手",
|
||||
"role_description": "你是一个专业的代码审查助手,擅长审查代码、发现bug。",
|
||||
},
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
"""Agent loop for tool-calling conversation."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class AgentLoop:
|
||||
"""
|
||||
Agent loop with tool-calling capability.
|
||||
|
||||
This is the core of the nanobot agent - it handles:
|
||||
- Multi-turn conversation with the LLM
|
||||
- Tool execution when the model requests it
|
||||
- Progress callbacks for streaming responses
|
||||
"""
|
||||
|
||||
_TOOL_RESULT_MAX_CHARS = 50000
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
provider: Any,
|
||||
model: str,
|
||||
tools: Any,
|
||||
max_iterations: int = 50,
|
||||
):
|
||||
"""
|
||||
Initialize the agent loop.
|
||||
|
||||
Args:
|
||||
provider: LLM provider (must implement chat_with_retry)
|
||||
model: Model name
|
||||
tools: Tool registry (must have get_definitions() and execute())
|
||||
max_iterations: Maximum tool call iterations
|
||||
"""
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.tools = tools
|
||||
self.max_iterations = max_iterations
|
||||
|
||||
@staticmethod
|
||||
def _strip_think(text: Optional[str]) -> Optional[str]:
|
||||
"""Strip model thinking blocks from content."""
|
||||
if not text:
|
||||
return None
|
||||
# Strip <thinking> tags commonly used by models like DeepSeek
|
||||
pattern = r"<thinking>[\s\S]*?</thinking>"
|
||||
text = re.sub(pattern, "", text)
|
||||
return text.strip() or None
|
||||
|
||||
@staticmethod
|
||||
def _tool_hint(tool_calls: list) -> str:
|
||||
"""Format tool calls as concise hint."""
|
||||
def _fmt(tc):
|
||||
args = tc.arguments or {}
|
||||
val = next(iter(args.values()), None) if isinstance(args, dict) else None
|
||||
if not isinstance(val, str):
|
||||
return tc.name
|
||||
return f'{tc.name}("{val[:40]}...")' if len(val) > 40 else f'{tc.name}("{val}")'
|
||||
return ", ".join(_fmt(tc) for tc in tool_calls)
|
||||
|
||||
async def run_loop(
|
||||
self,
|
||||
initial_messages: list[dict],
|
||||
system_prompt: str = "",
|
||||
on_progress: Optional[Callable[..., Any]] = None,
|
||||
) -> tuple[Optional[str], list[str], list[dict]]:
|
||||
"""
|
||||
Run the agent iteration loop.
|
||||
|
||||
Args:
|
||||
initial_messages: Starting message list
|
||||
system_prompt: System prompt to prepend
|
||||
on_progress: Optional callback for progress updates
|
||||
|
||||
Returns:
|
||||
Tuple of (final_content, tools_used, all_messages)
|
||||
"""
|
||||
# Prepend system prompt if provided
|
||||
if system_prompt:
|
||||
messages = [{"role": "system", "content": system_prompt}] + initial_messages
|
||||
else:
|
||||
messages = initial_messages
|
||||
|
||||
iteration = 0
|
||||
final_content = None
|
||||
tools_used: list[str] = []
|
||||
|
||||
while iteration < self.max_iterations:
|
||||
iteration += 1
|
||||
|
||||
tool_defs = self.tools.get_definitions() if self.tools else []
|
||||
|
||||
response = await self.provider.chat_with_retry(
|
||||
messages=messages,
|
||||
tools=tool_defs,
|
||||
model=self.model,
|
||||
)
|
||||
|
||||
if response.has_tool_calls:
|
||||
# Send progress update
|
||||
if on_progress:
|
||||
thought = self._strip_think(response.content)
|
||||
if thought:
|
||||
await on_progress(thought)
|
||||
await on_progress(self._tool_hint(response.tool_calls), tool_hint=True)
|
||||
|
||||
# Add assistant message with tool calls
|
||||
tool_call_dicts = [
|
||||
tc.to_openai_tool_call() if hasattr(tc, 'to_openai_tool_call') else tc
|
||||
for tc in response.tool_calls
|
||||
]
|
||||
|
||||
messages = self._add_assistant_message(
|
||||
messages, response.content, tool_call_dicts,
|
||||
reasoning_content=getattr(response, 'reasoning_content', None),
|
||||
)
|
||||
|
||||
# Execute tools
|
||||
for tool_call in response.tool_calls:
|
||||
tools_used.append(tool_call.name)
|
||||
args_str = json.dumps(tool_call.arguments, ensure_ascii=False)
|
||||
logger.info("Tool call: {}({})", tool_call.name, args_str[:200])
|
||||
|
||||
result = await self.tools.execute(tool_call.name, tool_call.arguments)
|
||||
messages = self._add_tool_result(messages, tool_call.id, tool_call.name, result)
|
||||
else:
|
||||
clean = self._strip_think(response.content)
|
||||
|
||||
# Handle error responses
|
||||
if response.finish_reason == "error":
|
||||
logger.error("LLM returned error: {}", (clean or "")[:200])
|
||||
final_content = clean or "Sorry, I encountered an error calling the AI model."
|
||||
break
|
||||
|
||||
messages = self._add_assistant_message(
|
||||
messages, clean,
|
||||
reasoning_content=getattr(response, 'reasoning_content', None),
|
||||
)
|
||||
final_content = clean
|
||||
break
|
||||
|
||||
if final_content is None and iteration >= self.max_iterations:
|
||||
logger.warning("Max iterations ({}) reached", self.max_iterations)
|
||||
final_content = (
|
||||
f"I reached the maximum number of tool call iterations ({self.max_iterations}) "
|
||||
"without completing the task."
|
||||
)
|
||||
|
||||
return final_content, tools_used, messages
|
||||
|
||||
def _add_assistant_message(
|
||||
self,
|
||||
messages: list[dict],
|
||||
content: Optional[str],
|
||||
tool_calls: Optional[list[dict]] = None,
|
||||
reasoning_content: Optional[str] = None,
|
||||
) -> list[dict]:
|
||||
"""Add an assistant message to the message list."""
|
||||
msg: dict[str, Any] = {"role": "assistant", "content": content}
|
||||
if tool_calls:
|
||||
msg["tool_calls"] = tool_calls
|
||||
if reasoning_content is not None:
|
||||
msg["reasoning_content"] = reasoning_content
|
||||
messages.append(msg)
|
||||
return messages
|
||||
|
||||
def _add_tool_result(
|
||||
self,
|
||||
messages: list[dict],
|
||||
tool_call_id: str,
|
||||
tool_name: str,
|
||||
result: Any,
|
||||
) -> list[dict]:
|
||||
"""Add a tool result message to the message list."""
|
||||
# Truncate large results
|
||||
content = str(result)
|
||||
if len(content) > self._TOOL_RESULT_MAX_CHARS:
|
||||
content = content[:self._TOOL_RESULT_MAX_CHARS] + "\n... (truncated)"
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"name": tool_name,
|
||||
"content": content,
|
||||
})
|
||||
return messages
|
||||
@@ -1,240 +0,0 @@
|
||||
"""Memory system for persistent agent memory."""
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import weakref
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
try:
|
||||
import tiktoken
|
||||
HAS_TIKTOKEN = True
|
||||
except ImportError:
|
||||
HAS_TIKTOKEN = False
|
||||
|
||||
|
||||
_SAVE_MEMORY_TOOL = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "save_memory",
|
||||
"description": "Save the memory consolidation result to persistent storage.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"history_entry": {
|
||||
"type": "string",
|
||||
"description": "A paragraph summarizing key events/decisions/topics.",
|
||||
},
|
||||
"memory_update": {
|
||||
"type": "string",
|
||||
"description": "Full updated long-term memory as markdown. Include all existing facts plus new ones.",
|
||||
},
|
||||
},
|
||||
"required": ["history_entry", "memory_update"],
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class MemoryStore:
|
||||
"""Two-layer memory: MEMORY.md (long-term facts) + HISTORY.md (grep-searchable log)."""
|
||||
|
||||
def __init__(self, workspace: Path):
|
||||
self.memory_dir = workspace / "memory"
|
||||
self.memory_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.memory_file = self.memory_dir / "MEMORY.md"
|
||||
self.history_file = self.memory_dir / "HISTORY.md"
|
||||
|
||||
def read_long_term(self) -> str:
|
||||
if self.memory_file.exists():
|
||||
return self.memory_file.read_text(encoding="utf-8")
|
||||
return ""
|
||||
|
||||
def write_long_term(self, content: str) -> None:
|
||||
self.memory_file.write_text(content, encoding="utf-8")
|
||||
|
||||
def append_history(self, entry: str) -> None:
|
||||
with open(self.history_file, "a", encoding="utf-8") as f:
|
||||
f.write(entry.rstrip() + "\n\n")
|
||||
|
||||
def get_memory_context(self) -> str:
|
||||
long_term = self.read_long_term()
|
||||
return f"## Long-term Memory\n{long_term}" if long_term else ""
|
||||
|
||||
|
||||
def _estimate_tokens(text: str) -> int:
|
||||
"""Estimate token count."""
|
||||
if HAS_TIKTOKEN:
|
||||
try:
|
||||
enc = tiktoken.get_encoding("cl100k_base")
|
||||
return len(enc.encode(text))
|
||||
except Exception:
|
||||
pass
|
||||
return max(1, len(text) // 4)
|
||||
|
||||
|
||||
def _estimate_message_tokens(message: dict[str, Any]) -> int:
|
||||
"""Estimate prompt tokens for a message."""
|
||||
content = message.get("content")
|
||||
parts = []
|
||||
|
||||
if isinstance(content, str):
|
||||
parts.append(content)
|
||||
elif isinstance(content, list):
|
||||
for part in content:
|
||||
if isinstance(part, dict) and part.get("type") == "text":
|
||||
text = part.get("text", "")
|
||||
if text:
|
||||
parts.append(text)
|
||||
else:
|
||||
parts.append(json.dumps(part, ensure_ascii=False))
|
||||
elif content is not None:
|
||||
parts.append(json.dumps(content, ensure_ascii=False))
|
||||
|
||||
for key in ("name", "tool_call_id"):
|
||||
value = message.get(key)
|
||||
if isinstance(value, str) and value:
|
||||
parts.append(value)
|
||||
if message.get("tool_calls"):
|
||||
parts.append(json.dumps(message["tool_calls"], ensure_ascii=False))
|
||||
|
||||
payload = "\n".join(parts)
|
||||
return max(1, _estimate_tokens(payload)) if payload else 1
|
||||
|
||||
|
||||
class MemoryConsolidator:
|
||||
"""Owns consolidation policy, locking, and session offset updates."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace: Path,
|
||||
provider: Any,
|
||||
model: str,
|
||||
sessions: Any,
|
||||
context_window_tokens: int = 200000,
|
||||
):
|
||||
self.store = MemoryStore(workspace)
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.sessions = sessions
|
||||
self.context_window_tokens = context_window_tokens
|
||||
self._locks: weakref.WeakValueDictionary[str, asyncio.Lock] = weakref.WeakValueDictionary()
|
||||
|
||||
def get_lock(self, session_key: str) -> asyncio.Lock:
|
||||
"""Return the shared consolidation lock for one session."""
|
||||
return self._locks.setdefault(session_key, asyncio.Lock())
|
||||
|
||||
async def consolidate_messages(self, messages: list[dict[str, object]]) -> bool:
|
||||
"""Archive a selected message chunk into persistent memory."""
|
||||
if not messages:
|
||||
return True
|
||||
|
||||
current_memory = self.store.read_long_term()
|
||||
prompt = f"""Process this conversation and call the save_memory tool.
|
||||
|
||||
## Current Long-term Memory
|
||||
{current_memory or "(empty)"}
|
||||
|
||||
## Conversation to Process
|
||||
{self._format_messages(messages)}"""
|
||||
|
||||
try:
|
||||
response = await self.provider.chat_with_retry(
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a memory consolidation agent."},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
tools=_SAVE_MEMORY_TOOL,
|
||||
model=self.model,
|
||||
)
|
||||
|
||||
if not response.has_tool_calls:
|
||||
return False
|
||||
|
||||
args = response.tool_calls[0].arguments
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
if isinstance(args, list):
|
||||
args = args[0] if args else {}
|
||||
|
||||
if entry := args.get("history_entry"):
|
||||
self.store.append_history(str(entry))
|
||||
if update := args.get("memory_update"):
|
||||
update = str(update)
|
||||
if update != current_memory:
|
||||
self.store.write_long_term(update)
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _format_messages(self, messages: list[dict]) -> str:
|
||||
lines = []
|
||||
for message in messages:
|
||||
if not message.get("content"):
|
||||
continue
|
||||
lines.append(
|
||||
f"[{message.get('timestamp', '?')[:16]}] {message['role'].upper()}: {message['content']}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
def pick_consolidation_boundary(
|
||||
self,
|
||||
session: Any,
|
||||
tokens_to_remove: int,
|
||||
) -> Optional[tuple[int, int]]:
|
||||
"""Pick a user-turn boundary that removes enough old prompt tokens."""
|
||||
start = session.last_consolidated
|
||||
if start >= len(session.messages) or tokens_to_remove <= 0:
|
||||
return None
|
||||
|
||||
removed_tokens = 0
|
||||
last_boundary: Optional[tuple[int, int]] = None
|
||||
for idx in range(start, len(session.messages)):
|
||||
message = session.messages[idx]
|
||||
if idx > start and message.get("role") == "user":
|
||||
last_boundary = (idx, removed_tokens)
|
||||
if removed_tokens >= tokens_to_remove:
|
||||
return last_boundary
|
||||
removed_tokens += _estimate_message_tokens(message)
|
||||
|
||||
return last_boundary
|
||||
|
||||
async def archive_unconsolidated(self, session: Any) -> bool:
|
||||
"""Archive the full unconsolidated tail for /new-style session rollover."""
|
||||
lock = self.get_lock(session.key)
|
||||
async with lock:
|
||||
snapshot = session.messages[session.last_consolidated:]
|
||||
if not snapshot:
|
||||
return True
|
||||
return await self.consolidate_messages(snapshot)
|
||||
|
||||
async def maybe_consolidate_by_tokens(self, session: Any) -> None:
|
||||
"""Loop: archive old messages until prompt fits within half the context window."""
|
||||
if not session.messages or self.context_window_tokens <= 0:
|
||||
return
|
||||
|
||||
lock = self.get_lock(session.key)
|
||||
async with lock:
|
||||
target = self.context_window_tokens // 2
|
||||
# Simple estimation without full prompt build
|
||||
estimated = sum(_estimate_message_tokens(m) for m in session.messages[session.last_consolidated:])
|
||||
|
||||
if estimated < self.context_window_tokens:
|
||||
return
|
||||
|
||||
# Find boundary and consolidate
|
||||
boundary = self.pick_consolidation_boundary(session, max(1, estimated - target))
|
||||
if boundary is None:
|
||||
return
|
||||
|
||||
end_idx = boundary[0]
|
||||
chunk = session.messages[session.last_consolidated:end_idx]
|
||||
if not chunk:
|
||||
return
|
||||
|
||||
if await self.consolidate_messages(chunk):
|
||||
session.last_consolidated = end_idx
|
||||
self.sessions.save(session)
|
||||
@@ -1,169 +0,0 @@
|
||||
"""Session management for conversation history."""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Session:
|
||||
"""
|
||||
A conversation session.
|
||||
|
||||
Stores messages in JSONL format for easy reading and persistence.
|
||||
"""
|
||||
|
||||
key: str # session_id
|
||||
messages: list[dict[str, Any]] = field(default_factory=list)
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
last_consolidated: int = 0 # Number of messages already consolidated to files
|
||||
|
||||
def add_message(self, role: str, content: str, **kwargs: Any) -> None:
|
||||
"""Add a message to the session."""
|
||||
msg = {
|
||||
"role": role,
|
||||
"content": content,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
**kwargs
|
||||
}
|
||||
self.messages.append(msg)
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
def get_history(self, max_messages: int = 500) -> list[dict[str, Any]]:
|
||||
"""Return unconsolidated messages for LLM input, aligned to a user turn."""
|
||||
unconsolidated = self.messages[self.last_consolidated:]
|
||||
sliced = unconsolidated[-max_messages:]
|
||||
|
||||
# Drop leading non-user messages to avoid orphaned tool_result blocks
|
||||
for i, m in enumerate(sliced):
|
||||
if m.get("role") == "user":
|
||||
sliced = sliced[i:]
|
||||
break
|
||||
|
||||
out: list[dict[str, Any]] = []
|
||||
for m in sliced:
|
||||
entry: dict[str, Any] = {"role": m["role"], "content": m.get("content", "")}
|
||||
for k in ("tool_calls", "tool_call_id", "name"):
|
||||
if k in m:
|
||||
entry[k] = m[k]
|
||||
out.append(entry)
|
||||
return out
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all messages and reset session to initial state."""
|
||||
self.messages = []
|
||||
self.last_consolidated = 0
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
|
||||
class SessionManager:
|
||||
"""Manages conversation sessions stored as JSONL files."""
|
||||
|
||||
def __init__(self, workspace: Path):
|
||||
self.workspace = workspace
|
||||
self.sessions_dir = workspace / "sessions"
|
||||
self.sessions_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._cache: dict[str, Session] = {}
|
||||
|
||||
def _get_session_path(self, key: str) -> Path:
|
||||
"""Get the file path for a session."""
|
||||
safe_key = key.replace(":", "_").replace("/", "_")
|
||||
return self.sessions_dir / f"{safe_key}.jsonl"
|
||||
|
||||
def get_or_create(self, key: str) -> Session:
|
||||
"""Get an existing session or create a new one."""
|
||||
if key in self._cache:
|
||||
return self._cache[key]
|
||||
|
||||
session = self._load(key)
|
||||
if session is None:
|
||||
session = Session(key=key)
|
||||
|
||||
self._cache[key] = session
|
||||
return session
|
||||
|
||||
def _load(self, key: str) -> Optional[Session]:
|
||||
"""Load a session from disk."""
|
||||
path = self._get_session_path(key)
|
||||
if not path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
messages = []
|
||||
metadata = {}
|
||||
created_at = None
|
||||
last_consolidated = 0
|
||||
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
data = json.loads(line)
|
||||
|
||||
if data.get("_type") == "metadata":
|
||||
metadata = data.get("metadata", {})
|
||||
created_at = datetime.fromisoformat(data["created_at"]) if data.get("created_at") else None
|
||||
last_consolidated = data.get("last_consolidated", 0)
|
||||
else:
|
||||
messages.append(data)
|
||||
|
||||
return Session(
|
||||
key=key,
|
||||
messages=messages,
|
||||
created_at=created_at or datetime.now(),
|
||||
metadata=metadata,
|
||||
last_consolidated=last_consolidated
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def save(self, session: Session) -> None:
|
||||
"""Save a session to disk."""
|
||||
path = self._get_session_path(session.key)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
metadata_line = {
|
||||
"_type": "metadata",
|
||||
"key": session.key,
|
||||
"created_at": session.created_at.isoformat(),
|
||||
"updated_at": session.updated_at.isoformat(),
|
||||
"metadata": session.metadata,
|
||||
"last_consolidated": session.last_consolidated
|
||||
}
|
||||
f.write(json.dumps(metadata_line, ensure_ascii=False) + "\n")
|
||||
for msg in session.messages:
|
||||
f.write(json.dumps(msg, ensure_ascii=False) + "\n")
|
||||
|
||||
self._cache[session.key] = session
|
||||
|
||||
def invalidate(self, key: str) -> None:
|
||||
"""Remove a session from the in-memory cache."""
|
||||
self._cache.pop(key, None)
|
||||
|
||||
def list_sessions(self) -> list[dict[str, Any]]:
|
||||
"""List all sessions."""
|
||||
sessions = []
|
||||
for path in self.sessions_dir.glob("*.jsonl"):
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
first_line = f.readline().strip()
|
||||
if first_line:
|
||||
data = json.loads(first_line)
|
||||
if data.get("_type") == "metadata":
|
||||
sessions.append({
|
||||
"key": data.get("key") or path.stem,
|
||||
"created_at": data.get("created_at"),
|
||||
"updated_at": data.get("updated_at"),
|
||||
"path": str(path)
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return sorted(sessions, key=lambda x: x.get("updated_at", ""), reverse=True)
|
||||
@@ -1,11 +0,0 @@
|
||||
fastapi>=0.100.0
|
||||
uvicorn[standard]>=0.23.0
|
||||
pydantic>=2.0.0
|
||||
openai>=1.0.0
|
||||
anthropic>=0.18.0
|
||||
python-dotenv>=1.0.0
|
||||
aiohttp>=3.8.0
|
||||
redis>=5.0.0
|
||||
loguru>=0.7.0
|
||||
tiktoken>=0.12.0
|
||||
simplemem>=0.1.0
|
||||
@@ -1,315 +0,0 @@
|
||||
---
|
||||
name: openakita/skills@xiaohongshu-creator
|
||||
description: Create engaging Xiaohongshu (RED/小红书) content including titles, body text, hashtags, and image style recommendations. Supports multiple content types such as product reviews, tutorials, lifestyle sharing, and shopping guides with platform-specific optimization.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: openakita
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# 小红书内容创作助手
|
||||
|
||||
专为小红书平台打造的内容创作技能,帮助你生成符合平台调性的高质量笔记,涵盖标题、正文、话题标签和配图建议。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 撰写种草笔记(好物推荐、购物分享)
|
||||
- 撰写产品测评笔记
|
||||
- 撰写教程类笔记(美妆、穿搭、美食、DIY)
|
||||
- 撰写生活分享笔记(旅行、日常、打卡)
|
||||
- 品牌合作内容创作
|
||||
- 小红书账号运营内容规划
|
||||
- 批量生成笔记框架
|
||||
|
||||
## 核心创作规范
|
||||
|
||||
### 一、标题规则
|
||||
|
||||
标题是笔记的第一印象,直接决定点击率。
|
||||
|
||||
**硬性要求:**
|
||||
- 字数限制:**不超过 20 个字符**
|
||||
- Emoji 数量:**1-2 个**,放在标题开头或结尾
|
||||
- 禁止使用:感叹号堆叠(`!!!`)、全大写字母
|
||||
|
||||
**钩子元素(至少使用 1 种):**
|
||||
|
||||
| 钩子类型 | 说明 | 示例 |
|
||||
|----------|------|------|
|
||||
| 数字法 | 用数字制造具体感 | `3步搞定通勤妆🌟` |
|
||||
| 反差法 | 制造意外感 | `月薪3K穿出3W的感觉✨` |
|
||||
| 痛点法 | 直击目标人群痛点 | `黄皮亲妈色号合集🎨` |
|
||||
| 悬念法 | 引发好奇心 | `这个习惯让我瘦了20斤💪` |
|
||||
| 对比法 | 前后/AB对比 | `早C晚A一个月的变化🔥` |
|
||||
| 权威法 | 借势专业背书 | `皮肤科医生推荐的面霜💊` |
|
||||
| 场景法 | 具体使用场景 | `约会前30分钟急救妆容💄` |
|
||||
| 共鸣法 | 引发情感共鸣 | `打工人的高效早餐方案☀️` |
|
||||
|
||||
**标题模板:**
|
||||
```
|
||||
[数字] + [核心关键词] + [利益点] + [emoji]
|
||||
[身份标签] + [动作] + [结果] + [emoji]
|
||||
[场景] + [解决方案] + [emoji]
|
||||
```
|
||||
|
||||
### 二、正文结构
|
||||
|
||||
正文长度控制在 **300-500 字符**,遵循四段式结构:
|
||||
|
||||
```
|
||||
🔥 Hook(开头钩子) —— 1-2 句,抓住注意力
|
||||
📝 Core(核心内容) —— 主体信息,有价值的干货
|
||||
📌 Summary(总结) —— 精炼要点
|
||||
👉 CTA(行动号召) —— 引导互动
|
||||
```
|
||||
|
||||
**Hook 写法:**
|
||||
- 提问式:`你们有没有这种烦恼?`
|
||||
- 共鸣式:`每个打工人都需要这个!`
|
||||
- 悬念式:`用了三年终于找到了最好用的...`
|
||||
- 成果式:`坚持30天,效果太惊人了`
|
||||
|
||||
**Core 写法要点:**
|
||||
- 使用 emoji 分点(📍🔸💡等)替代纯文字列表
|
||||
- 每个要点控制在 1-2 行
|
||||
- 穿插个人体验和感受(增加真实感)
|
||||
- 重要信息加【】或「」标注
|
||||
- 适当使用换行,避免大段文字
|
||||
|
||||
**CTA 常用句式:**
|
||||
- `觉得有用的话记得点赞收藏哦~`
|
||||
- `你们还想看什么类型的分享?评论区告诉我`
|
||||
- `有同款的姐妹举个手🙋♀️`
|
||||
- `关注我,持续分享[领域]干货`
|
||||
|
||||
### 三、话题标签规则
|
||||
|
||||
每篇笔记配 **8 个话题标签**,按以下比例分配:
|
||||
|
||||
| 类别 | 数量 | 说明 | 示例 |
|
||||
|------|------|------|------|
|
||||
| 核心词 | 2 个 | 笔记主题精准关键词 | `#面膜推荐` `#保湿面膜` |
|
||||
| 品类词 | 2 个 | 所属品类/领域 | `#护肤` `#美妆好物` |
|
||||
| 场景词 | 2 个 | 使用场景/人群 | `#学生党护肤` `#换季护肤` |
|
||||
| 热门词 | 2 个 | 平台热门话题 | `#好物分享` `#我的爱用物` |
|
||||
|
||||
**选择原则:**
|
||||
- 优先选择搜索量大但竞争适中的标签
|
||||
- 避免过于宽泛的标签(如 `#生活`)
|
||||
- 包含长尾关键词提升搜索曝光
|
||||
- 关注平台当前热门话题榜
|
||||
|
||||
### 四、配图风格建议
|
||||
|
||||
小红书是视觉驱动平台,封面决定 80% 的点击率。
|
||||
|
||||
**10 种推荐视觉风格:**
|
||||
|
||||
| 编号 | 风格 | 适用类型 | 要点 |
|
||||
|------|------|---------|------|
|
||||
| 1 | 对比拼图 | 测评/效果展示 | 左右或上下对比,标注差异 |
|
||||
| 2 | 清单图 | 好物合集/推荐 | 白底九宫格产品陈列 |
|
||||
| 3 | 教程步骤图 | 教程类 | 编号标注步骤,清晰易跟 |
|
||||
| 4 | 文字封面 | 干货分享 | 大字标题+简洁背景色 |
|
||||
| 5 | 场景氛围图 | 生活分享/穿搭 | 自然光,生活感强 |
|
||||
| 6 | 数据图表 | 测评/科普 | 简化数据可视化 |
|
||||
| 7 | 手绘/插画风 | 知识科普 | 可爱风格信息图 |
|
||||
| 8 | Vlog截图 | 日常分享 | 视频关键帧+文字标注 |
|
||||
| 9 | 实拍特写 | 产品种草 | 高清细节,突出质感 |
|
||||
| 10 | Ins风简约 | 穿搭/家居 | 低饱和度,高级感 |
|
||||
|
||||
**封面设计通用原则:**
|
||||
- 尺寸比例:**3:4**(1080×1440px)最佳
|
||||
- 文字不超过封面面积的 **20%**
|
||||
- 核心信息放在画面上半部分(feed 流裁剪安全区)
|
||||
- 色彩鲜明、对比度高
|
||||
- 避免过度 P 图,保持真实感
|
||||
|
||||
## 内容类型工作流
|
||||
|
||||
### 工作流一:种草笔记
|
||||
|
||||
```
|
||||
输入 → 产品名称、品类、价格、目标人群
|
||||
↓
|
||||
Step 1: 生成 3 个标题方案(痛点法/数字法/场景法各一)
|
||||
↓
|
||||
Step 2: 撰写正文
|
||||
- Hook:个人使用感受/发现契机
|
||||
- Core:产品亮点(3-5个)、使用方法、适合人群
|
||||
- Summary:一句话总结推荐理由
|
||||
- CTA:引导收藏和讨论
|
||||
↓
|
||||
Step 3: 生成 8 个话题标签
|
||||
↓
|
||||
Step 4: 封面建议(推荐风格 9 实拍特写 或 风格 2 清单图)
|
||||
↓
|
||||
输出 → 完整笔记(可直接发布)
|
||||
```
|
||||
|
||||
### 工作流二:测评笔记
|
||||
|
||||
```
|
||||
输入 → 产品列表(2-5 个)、测评维度
|
||||
↓
|
||||
Step 1: 标题使用对比法或数字法
|
||||
↓
|
||||
Step 2: 撰写正文
|
||||
- Hook:测评动机/痛点引入
|
||||
- Core:逐项对比(成分/价格/使用感/性价比)
|
||||
- Summary:各产品评分或排名
|
||||
- CTA:`你们用过哪个?评论区聊聊`
|
||||
↓
|
||||
Step 3: 话题标签(增加品牌词标签)
|
||||
↓
|
||||
Step 4: 封面建议(推荐风格 1 对比拼图 或 风格 6 数据图表)
|
||||
↓
|
||||
输出 → 完整测评笔记
|
||||
```
|
||||
|
||||
### 工作流三:教程笔记
|
||||
|
||||
```
|
||||
输入 → 教程主题、难度、目标人群
|
||||
↓
|
||||
Step 1: 标题使用数字法(`X步学会...`)
|
||||
↓
|
||||
Step 2: 撰写正文
|
||||
- Hook:学会后的效果/价值
|
||||
- Core:分步骤讲解(每步 1-2 句)
|
||||
- Summary:关键注意事项
|
||||
- CTA:`学会的打个✅`
|
||||
↓
|
||||
Step 3: 话题标签(增加 `#教程` `#手把手教学` 等)
|
||||
↓
|
||||
Step 4: 封面建议(推荐风格 3 步骤图 或 风格 4 文字封面)
|
||||
↓
|
||||
输出 → 完整教程笔记 + 多图建议(每步一张图)
|
||||
```
|
||||
|
||||
### 工作流四:生活分享笔记
|
||||
|
||||
```
|
||||
输入 → 分享主题、场景、情绪基调
|
||||
↓
|
||||
Step 1: 标题使用共鸣法或场景法
|
||||
↓
|
||||
Step 2: 撰写正文
|
||||
- Hook:故事开头/情感切入
|
||||
- Core:分享细节、个人感受、实用信息
|
||||
- Summary:感悟或建议
|
||||
- CTA:`你们有类似的经历吗?`
|
||||
↓
|
||||
Step 3: 话题标签(增加情感/生活方式标签)
|
||||
↓
|
||||
Step 4: 封面建议(推荐风格 5 场景氛围图 或 风格 8 Vlog截图)
|
||||
↓
|
||||
输出 → 完整分享笔记
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 示例:护肤种草笔记
|
||||
|
||||
**用户输入:** `帮我写一篇关于珂润面霜的种草笔记,目标人群是敏感肌学生党`
|
||||
|
||||
**输出:**
|
||||
|
||||
**标题方案:**
|
||||
1. `敏感肌救星!这罐面霜用了3年回购🧴`
|
||||
2. `学生党百元面霜天花板💰`
|
||||
3. `换季烂脸?皮肤科医生同款面霜🏥`
|
||||
|
||||
**正文(选用标题1):**
|
||||
|
||||
```
|
||||
姐妹们!!敏感肌的福音真的来了
|
||||
|
||||
作为一个烂脸三年的敏感肌 终于找到了真正好用的面霜
|
||||
|
||||
📍珂润面霜使用感受:
|
||||
🔸质地:乳白色霜状 不厚重 好推开
|
||||
🔸保湿:神经酰胺成分 修护屏障一绝
|
||||
🔸温和度:无香精无酒精 敏感期也能用
|
||||
🔸性价比:百元出头 学生党无压力
|
||||
|
||||
💡使用tips:
|
||||
1️⃣ 先乳后霜 吸收更好
|
||||
2️⃣ 换季加量使用 厚涂当睡眠面膜
|
||||
3️⃣ 搭配同系列化妆水效果翻倍
|
||||
|
||||
用了整整三年 每次回购都是囤两罐的节奏
|
||||
敏感肌姐妹真的可以闭眼入!
|
||||
|
||||
觉得有用记得收藏💕 还想看什么评论区告诉我~
|
||||
```
|
||||
|
||||
**话题标签:**
|
||||
`#珂润面霜` `#敏感肌护肤` `#学生党护肤` `#平价面霜推荐` `#神经酰胺` `#换季护肤` `#好物分享` `#护肤干货`
|
||||
|
||||
**封面建议:** 风格 9(实拍特写),自然光下拍摄面霜质地细节图,辅以手写标注关键成分。
|
||||
|
||||
## 高级技巧
|
||||
|
||||
### 发布时间建议
|
||||
|
||||
| 时段 | 说明 |
|
||||
|------|------|
|
||||
| 7:00-9:00 | 通勤高峰,碎片化浏览 |
|
||||
| 12:00-14:00 | 午休时间,浏览高峰 |
|
||||
| 18:00-20:00 | 下班后休闲浏览 |
|
||||
| 21:00-23:00 | 睡前黄金时段,互动率最高 |
|
||||
|
||||
### SEO 优化
|
||||
|
||||
- 标题和正文前 50 字包含核心关键词
|
||||
- 使用平台搜索下拉词作为参考
|
||||
- 正文自然融入 3-5 个相关关键词(避免堆砌)
|
||||
- 评论区补充关键词(自评增加曝光)
|
||||
|
||||
### 互动率提升
|
||||
|
||||
- 正文中设置互动问题(`你们觉得呢?`)
|
||||
- 结尾提供选择题(`A还是B?评论区投票`)
|
||||
- 24 小时内回复所有评论
|
||||
- 置顶评论放重要补充信息
|
||||
|
||||
## 常见误区
|
||||
|
||||
| 误区 | 正确做法 |
|
||||
|------|---------|
|
||||
| 标题过长(超20字) | 精炼到 20 字以内,信息密度优先 |
|
||||
| 正文大段不分行 | 每 2-3 行空一行,用 emoji 分隔 |
|
||||
| 标签太宽泛 | 组合使用泛词+精准词+长尾词 |
|
||||
| 封面文字太多 | 封面突出视觉冲击,详细信息放正文 |
|
||||
| 纯广告无真实感 | 加入个人体验和真实细节 |
|
||||
| 内容同质化严重 | 找到独特切入角度(身份/场景/反差) |
|
||||
| 忽略评论区运营 | 主动回复并引导二次互动 |
|
||||
|
||||
## 输出格式规范
|
||||
|
||||
每次生成内容时,严格按以下格式输出:
|
||||
|
||||
```markdown
|
||||
## 📝 小红书笔记
|
||||
|
||||
### 标题方案
|
||||
1. [方案一]
|
||||
2. [方案二]
|
||||
3. [方案三]
|
||||
|
||||
### 正文
|
||||
[完整正文内容]
|
||||
|
||||
### 话题标签
|
||||
[8个标签]
|
||||
|
||||
### 封面建议
|
||||
- 推荐风格:[编号+名称]
|
||||
- 具体建议:[详细说明]
|
||||
- 配色方案:[色系建议]
|
||||
|
||||
### 发布建议
|
||||
- 推荐时段:[具体时间]
|
||||
- 注意事项:[补充说明]
|
||||
```
|
||||
Reference in New Issue
Block a user