mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-02-17 16:38:37 +08:00
378 lines
14 KiB
Python
378 lines
14 KiB
Python
"""
|
|
Agent Bridge - Integrates Agent system with existing COW bridge
|
|
"""
|
|
|
|
from typing import Optional, List
|
|
|
|
from agent.protocol import Agent, LLMModel, LLMRequest
|
|
from bot.openai_compatible_bot import OpenAICompatibleBot
|
|
from bridge.bridge import Bridge
|
|
from bridge.context import Context
|
|
from bridge.reply import Reply, ReplyType
|
|
from common import const
|
|
from common.log import logger
|
|
|
|
|
|
def add_openai_compatible_support(bot_instance):
|
|
"""
|
|
Dynamically add OpenAI-compatible tool calling support to a bot instance.
|
|
|
|
This allows any bot to gain tool calling capability without modifying its code,
|
|
as long as it uses OpenAI-compatible API format.
|
|
"""
|
|
if hasattr(bot_instance, 'call_with_tools'):
|
|
# Bot already has tool calling support
|
|
return bot_instance
|
|
|
|
# Create a temporary mixin class that combines the bot with OpenAI compatibility
|
|
class EnhancedBot(bot_instance.__class__, OpenAICompatibleBot):
|
|
"""Dynamically enhanced bot with OpenAI-compatible tool calling"""
|
|
|
|
def get_api_config(self):
|
|
"""
|
|
Infer API config from common configuration patterns.
|
|
Most OpenAI-compatible bots use similar configuration.
|
|
"""
|
|
from config import conf
|
|
|
|
return {
|
|
'api_key': conf().get("open_ai_api_key"),
|
|
'api_base': conf().get("open_ai_api_base"),
|
|
'model': conf().get("model", "gpt-3.5-turbo"),
|
|
'default_temperature': conf().get("temperature", 0.9),
|
|
'default_top_p': conf().get("top_p", 1.0),
|
|
'default_frequency_penalty': conf().get("frequency_penalty", 0.0),
|
|
'default_presence_penalty': conf().get("presence_penalty", 0.0),
|
|
}
|
|
|
|
# Change the bot's class to the enhanced version
|
|
bot_instance.__class__ = EnhancedBot
|
|
logger.info(
|
|
f"[AgentBridge] Enhanced {bot_instance.__class__.__bases__[0].__name__} with OpenAI-compatible tool calling")
|
|
|
|
return bot_instance
|
|
|
|
|
|
class AgentLLMModel(LLMModel):
|
|
"""
|
|
LLM Model adapter that uses COW's existing bot infrastructure
|
|
"""
|
|
|
|
def __init__(self, bridge: Bridge, bot_type: str = "chat"):
|
|
# Get model name directly from config
|
|
from config import conf
|
|
model_name = conf().get("model", const.GPT_41)
|
|
super().__init__(model=model_name)
|
|
self.bridge = bridge
|
|
self.bot_type = bot_type
|
|
self._bot = None
|
|
|
|
@property
|
|
def bot(self):
|
|
"""Lazy load the bot and enhance it with tool calling if needed"""
|
|
if self._bot is None:
|
|
self._bot = self.bridge.get_bot(self.bot_type)
|
|
# Automatically add tool calling support if not present
|
|
self._bot = add_openai_compatible_support(self._bot)
|
|
return self._bot
|
|
|
|
def call(self, request: LLMRequest):
|
|
"""
|
|
Call the model using COW's bot infrastructure
|
|
"""
|
|
try:
|
|
# For non-streaming calls, we'll use the existing reply method
|
|
# This is a simplified implementation
|
|
if hasattr(self.bot, 'call_with_tools'):
|
|
# Use tool-enabled call if available
|
|
kwargs = {
|
|
'messages': request.messages,
|
|
'tools': getattr(request, 'tools', None),
|
|
'stream': False
|
|
}
|
|
# Only pass max_tokens if it's explicitly set
|
|
if request.max_tokens is not None:
|
|
kwargs['max_tokens'] = request.max_tokens
|
|
response = self.bot.call_with_tools(**kwargs)
|
|
return self._format_response(response)
|
|
else:
|
|
# Fallback to regular call
|
|
# This would need to be implemented based on your specific needs
|
|
raise NotImplementedError("Regular call not implemented yet")
|
|
|
|
except Exception as e:
|
|
logger.error(f"AgentLLMModel call error: {e}")
|
|
raise
|
|
|
|
def call_stream(self, request: LLMRequest):
|
|
"""
|
|
Call the model with streaming using COW's bot infrastructure
|
|
"""
|
|
try:
|
|
if hasattr(self.bot, 'call_with_tools'):
|
|
# Use tool-enabled streaming call if available
|
|
# Ensure max_tokens is an integer, use default if None
|
|
max_tokens = request.max_tokens if request.max_tokens is not None else 4096
|
|
|
|
# Extract system prompt if present
|
|
system_prompt = getattr(request, 'system', None)
|
|
|
|
# Build kwargs for call_with_tools
|
|
kwargs = {
|
|
'messages': request.messages,
|
|
'tools': getattr(request, 'tools', None),
|
|
'stream': True,
|
|
'max_tokens': max_tokens
|
|
}
|
|
|
|
# Add system prompt if present
|
|
if system_prompt:
|
|
kwargs['system'] = system_prompt
|
|
|
|
stream = self.bot.call_with_tools(**kwargs)
|
|
|
|
# Convert stream format to our expected format
|
|
for chunk in stream:
|
|
yield self._format_stream_chunk(chunk)
|
|
else:
|
|
bot_type = type(self.bot).__name__
|
|
raise NotImplementedError(f"Bot {bot_type} does not support call_with_tools. Please add the method.")
|
|
|
|
except Exception as e:
|
|
logger.error(f"AgentLLMModel call_stream error: {e}", exc_info=True)
|
|
raise
|
|
|
|
def _format_response(self, response):
|
|
"""Format Claude response to our expected format"""
|
|
# This would need to be implemented based on Claude's response format
|
|
return response
|
|
|
|
def _format_stream_chunk(self, chunk):
|
|
"""Format Claude stream chunk to our expected format"""
|
|
# This would need to be implemented based on Claude's stream format
|
|
return chunk
|
|
|
|
|
|
class AgentBridge:
|
|
"""
|
|
Bridge class that integrates single super Agent with COW
|
|
"""
|
|
|
|
def __init__(self, bridge: Bridge):
|
|
self.bridge = bridge
|
|
self.agent: Optional[Agent] = None
|
|
|
|
def create_agent(self, system_prompt: str, tools: List = None, **kwargs) -> Agent:
|
|
"""
|
|
Create the super agent with COW integration
|
|
|
|
Args:
|
|
system_prompt: System prompt
|
|
tools: List of tools (optional)
|
|
**kwargs: Additional agent parameters
|
|
|
|
Returns:
|
|
Agent instance
|
|
"""
|
|
# Create LLM model that uses COW's bot infrastructure
|
|
model = AgentLLMModel(self.bridge)
|
|
|
|
# Default tools if none provided
|
|
if tools is None:
|
|
# Use ToolManager to load all available tools
|
|
from agent.tools import ToolManager
|
|
tool_manager = ToolManager()
|
|
tool_manager.load_tools()
|
|
|
|
tools = []
|
|
for tool_name in tool_manager.tool_classes.keys():
|
|
try:
|
|
tool = tool_manager.create_tool(tool_name)
|
|
if tool:
|
|
tools.append(tool)
|
|
except Exception as e:
|
|
logger.warning(f"[AgentBridge] Failed to load tool {tool_name}: {e}")
|
|
|
|
# Create the single super agent
|
|
self.agent = Agent(
|
|
system_prompt=system_prompt,
|
|
description=kwargs.get("description", "AI Super Agent"),
|
|
model=model,
|
|
tools=tools,
|
|
max_steps=kwargs.get("max_steps", 15),
|
|
output_mode=kwargs.get("output_mode", "logger"),
|
|
workspace_dir=kwargs.get("workspace_dir"), # Pass workspace for skills loading
|
|
enable_skills=kwargs.get("enable_skills", True), # Enable skills by default
|
|
memory_manager=kwargs.get("memory_manager"), # Pass memory manager
|
|
max_context_tokens=kwargs.get("max_context_tokens"),
|
|
context_reserve_tokens=kwargs.get("context_reserve_tokens")
|
|
)
|
|
|
|
# Log skill loading details
|
|
if self.agent.skill_manager:
|
|
logger.info(f"[AgentBridge] SkillManager initialized:")
|
|
logger.info(f"[AgentBridge] - Managed dir: {self.agent.skill_manager.managed_skills_dir}")
|
|
logger.info(f"[AgentBridge] - Workspace dir: {self.agent.skill_manager.workspace_dir}")
|
|
logger.info(f"[AgentBridge] - Total skills: {len(self.agent.skill_manager.skills)}")
|
|
for skill_name in self.agent.skill_manager.skills.keys():
|
|
logger.info(f"[AgentBridge] * {skill_name}")
|
|
|
|
return self.agent
|
|
|
|
def get_agent(self) -> Optional[Agent]:
|
|
"""Get the super agent, create if not exists"""
|
|
if self.agent is None:
|
|
self._init_default_agent()
|
|
return self.agent
|
|
|
|
def _init_default_agent(self):
|
|
"""Initialize default super agent with new prompt system"""
|
|
from config import conf
|
|
import os
|
|
|
|
# Get workspace from config
|
|
workspace_root = os.path.expanduser(conf().get("agent_workspace", "~/cow"))
|
|
|
|
# Initialize workspace and create template files
|
|
from agent.prompt import ensure_workspace, load_context_files, PromptBuilder
|
|
|
|
workspace_files = ensure_workspace(workspace_root, create_templates=True)
|
|
logger.info(f"[AgentBridge] Workspace initialized at: {workspace_root}")
|
|
|
|
# Setup memory system
|
|
memory_manager = None
|
|
memory_tools = []
|
|
|
|
try:
|
|
# Try to initialize memory system
|
|
from agent.memory import MemoryManager, MemoryConfig
|
|
from agent.tools import MemorySearchTool, MemoryGetTool
|
|
|
|
memory_config = MemoryConfig(
|
|
workspace_root=workspace_root,
|
|
embedding_provider="local", # Use local embedding (no API key needed)
|
|
embedding_model="all-MiniLM-L6-v2"
|
|
)
|
|
|
|
# Create memory manager with the config
|
|
memory_manager = MemoryManager(memory_config)
|
|
|
|
# Create memory tools
|
|
memory_tools = [
|
|
MemorySearchTool(memory_manager),
|
|
MemoryGetTool(memory_manager)
|
|
]
|
|
|
|
logger.info(f"[AgentBridge] Memory system initialized")
|
|
|
|
except Exception as e:
|
|
logger.warning(f"[AgentBridge] Memory system not available: {e}")
|
|
logger.info("[AgentBridge] Continuing without memory features")
|
|
|
|
# Use ToolManager to dynamically load all available tools
|
|
from agent.tools import ToolManager
|
|
tool_manager = ToolManager()
|
|
tool_manager.load_tools()
|
|
|
|
# Create tool instances for all available tools
|
|
tools = []
|
|
file_config = {
|
|
"cwd": workspace_root,
|
|
"memory_manager": memory_manager
|
|
} if memory_manager else {"cwd": workspace_root}
|
|
|
|
for tool_name in tool_manager.tool_classes.keys():
|
|
try:
|
|
tool = tool_manager.create_tool(tool_name)
|
|
if tool:
|
|
# Apply workspace config to file operation tools
|
|
if tool_name in ['read', 'write', 'edit', 'bash', 'grep', 'find', 'ls']:
|
|
tool.config = file_config
|
|
tool.cwd = file_config.get("cwd", tool.cwd if hasattr(tool, 'cwd') else None)
|
|
if 'memory_manager' in file_config:
|
|
tool.memory_manager = file_config['memory_manager']
|
|
tools.append(tool)
|
|
logger.debug(f"[AgentBridge] Loaded tool: {tool_name}")
|
|
except Exception as e:
|
|
logger.warning(f"[AgentBridge] Failed to load tool {tool_name}: {e}")
|
|
|
|
# Add memory tools
|
|
if memory_tools:
|
|
tools.extend(memory_tools)
|
|
logger.info(f"[AgentBridge] Added {len(memory_tools)} memory tools")
|
|
|
|
logger.info(f"[AgentBridge] Loaded {len(tools)} tools: {[t.name for t in tools]}")
|
|
|
|
# Load context files (SOUL.md, USER.md, etc.)
|
|
context_files = load_context_files(workspace_root)
|
|
logger.info(f"[AgentBridge] Loaded {len(context_files)} context files: {[f.path for f in context_files]}")
|
|
|
|
# Build system prompt using new prompt builder
|
|
prompt_builder = PromptBuilder(
|
|
workspace_dir=workspace_root,
|
|
language="zh"
|
|
)
|
|
|
|
# Get runtime info
|
|
runtime_info = {
|
|
"model": conf().get("model", "unknown"),
|
|
"workspace": workspace_root,
|
|
"channel": "web" # TODO: get from actual channel, default to "web" to hide if not specified
|
|
}
|
|
|
|
system_prompt = prompt_builder.build(
|
|
tools=tools,
|
|
context_files=context_files,
|
|
memory_manager=memory_manager,
|
|
runtime_info=runtime_info
|
|
)
|
|
|
|
logger.info("[AgentBridge] System prompt built successfully")
|
|
|
|
# Create agent with configured tools and workspace
|
|
agent = self.create_agent(
|
|
system_prompt=system_prompt,
|
|
tools=tools,
|
|
max_steps=50,
|
|
output_mode="logger",
|
|
workspace_dir=workspace_root, # Pass workspace to agent for skills loading
|
|
enable_skills=True # Enable skills auto-loading
|
|
)
|
|
|
|
# Attach memory manager to agent if available
|
|
if memory_manager:
|
|
agent.memory_manager = memory_manager
|
|
logger.info(f"[AgentBridge] Memory manager attached to agent")
|
|
|
|
def agent_reply(self, query: str, context: Context = None,
|
|
on_event=None, clear_history: bool = False) -> Reply:
|
|
"""
|
|
Use super agent to reply to a query
|
|
|
|
Args:
|
|
query: User query
|
|
context: COW context (optional)
|
|
on_event: Event callback (optional)
|
|
clear_history: Whether to clear conversation history
|
|
|
|
Returns:
|
|
Reply object
|
|
"""
|
|
try:
|
|
# Get agent (will auto-initialize if needed)
|
|
agent = self.get_agent()
|
|
if not agent:
|
|
return Reply(ReplyType.ERROR, "Failed to initialize super agent")
|
|
|
|
# Use agent's run_stream method
|
|
response = agent.run_stream(
|
|
user_message=query,
|
|
on_event=on_event,
|
|
clear_history=clear_history
|
|
)
|
|
|
|
return Reply(ReplyType.TEXT, response)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Agent reply error: {e}")
|
|
return Reply(ReplyType.ERROR, f"Agent error: {str(e)}") |