|
| 1 | +""" |
| 2 | +Camel-AI based project detector. |
| 3 | +
|
| 4 | +This module contains the CamelDetector class that uses camel-ai's ChatAgent |
| 5 | +framework for intelligent project analysis and tool detection. |
| 6 | +""" |
| 7 | + |
| 8 | +import json |
| 9 | +import os |
| 10 | +from pathlib import Path |
| 11 | +from typing import Any |
| 12 | + |
| 13 | +from .base import BaseDetector |
| 14 | +from .types import ProjectInfo, ToolSpec |
| 15 | + |
| 16 | +try: |
| 17 | + from camel.agents import ChatAgent |
| 18 | + from camel.configs import ChatGPTConfig |
| 19 | + from camel.messages import BaseMessage |
| 20 | + from camel.models import ModelFactory |
| 21 | + from camel.types import ModelPlatformType, ModelType |
| 22 | + |
| 23 | + CAMEL_AVAILABLE = True |
| 24 | +except ImportError: |
| 25 | + CAMEL_AVAILABLE = False |
| 26 | + |
| 27 | + |
| 28 | +class CamelDetector(BaseDetector): |
| 29 | + """Camel-AI based project detector using ChatAgent framework.""" |
| 30 | + |
| 31 | + def __init__(self, model_name: str = "gpt-4o-mini", **kwargs): |
| 32 | + """Initialize the detector with Camel-AI ChatAgent.""" |
| 33 | + super().__init__(**kwargs) |
| 34 | + |
| 35 | + if not CAMEL_AVAILABLE: |
| 36 | + raise ImportError( |
| 37 | + "camel-ai is required for CamelDetector. " |
| 38 | + "Install it with: pip install camel-ai" |
| 39 | + ) |
| 40 | + |
| 41 | + # Check for OpenAI API key |
| 42 | + if not os.getenv("OPENAI_API_KEY"): |
| 43 | + raise ValueError( |
| 44 | + "OpenAI API key is required for CamelDetector. " |
| 45 | + "Set OPENAI_API_KEY environment variable." |
| 46 | + ) |
| 47 | + |
| 48 | + self.model_name = model_name |
| 49 | + self.agent = None |
| 50 | + self._initialize_agent() |
| 51 | + |
| 52 | + def _initialize_agent(self): |
| 53 | + """Initialize the ChatAgent with correct API usage.""" |
| 54 | + # Create model using ModelFactory |
| 55 | + model = ModelFactory.create( |
| 56 | + model_platform=ModelPlatformType.OPENAI, |
| 57 | + model_type=ModelType.GPT_4O_MINI, |
| 58 | + model_config_dict=ChatGPTConfig(temperature=0.1).as_dict(), |
| 59 | + ) |
| 60 | + |
| 61 | + # Define the system prompt |
| 62 | + system_message = """You are an expert software engineer specializing in API analysis and tool detection. |
| 63 | +Your task is to analyze projects and identify all tools/APIs/commands that can be exposed |
| 64 | +as MCP (Model Context Protocol) tools. |
| 65 | +
|
| 66 | +You excel at: |
| 67 | +- Understanding code structure and patterns |
| 68 | +- Identifying CLI commands and their arguments |
| 69 | +- Recognizing web API endpoints |
| 70 | +- Finding reusable functions and methods |
| 71 | +- Extracting parameter information and types |
| 72 | +
|
| 73 | +Always provide detailed, accurate analysis in JSON format.""" |
| 74 | + |
| 75 | + # Create ChatAgent using the new API |
| 76 | + self.agent = ChatAgent( |
| 77 | + system_message=system_message, model=model, message_window_size=10 |
| 78 | + ) |
| 79 | + |
| 80 | + def _detect_tools( |
| 81 | + self, project_path: Path, project_info: ProjectInfo |
| 82 | + ) -> list[ToolSpec]: |
| 83 | + """Use Camel-AI ChatAgent to detect tools/APIs in the project.""" |
| 84 | + if not self.agent: |
| 85 | + raise ValueError("Camel-AI agent not initialized") |
| 86 | + |
| 87 | + # Gather project context |
| 88 | + context = self._gather_project_context(project_path, project_info) |
| 89 | + |
| 90 | + # Use ChatAgent to analyze and detect tools |
| 91 | + tools_data = self._agent_detect_tools(context, project_info) |
| 92 | + |
| 93 | + # Convert to ToolSpec objects |
| 94 | + tools = [] |
| 95 | + for tool_data in tools_data: |
| 96 | + tools.append( |
| 97 | + ToolSpec( |
| 98 | + name=tool_data["name"], |
| 99 | + description=tool_data["description"], |
| 100 | + args=tool_data.get("args", []), |
| 101 | + parameters=tool_data.get("parameters", []), |
| 102 | + ) |
| 103 | + ) |
| 104 | + |
| 105 | + return tools |
| 106 | + |
| 107 | + def _gather_project_context( |
| 108 | + self, project_path: Path, project_info: ProjectInfo |
| 109 | + ) -> str: |
| 110 | + """Gather comprehensive project context for agent analysis.""" |
| 111 | + context_parts = [] |
| 112 | + |
| 113 | + # Basic project info |
| 114 | + context_parts.append(f"Project Name: {project_info.name}") |
| 115 | + context_parts.append(f"Project Type: {project_info.project_type}") |
| 116 | + context_parts.append(f"Description: {project_info.description}") |
| 117 | + |
| 118 | + # Dependencies |
| 119 | + if project_info.dependencies: |
| 120 | + deps_str = ", ".join(project_info.dependencies) |
| 121 | + context_parts.append(f"Dependencies: {deps_str}") |
| 122 | + |
| 123 | + # README content (truncated) |
| 124 | + if project_info.readme_content: |
| 125 | + readme_excerpt = project_info.readme_content[:1500] |
| 126 | + context_parts.append(f"README:\n{readme_excerpt}") |
| 127 | + |
| 128 | + # Directory structure |
| 129 | + structure = self._get_directory_structure(project_path) |
| 130 | + context_parts.append(f"Directory Structure:\n{structure}") |
| 131 | + |
| 132 | + # Code samples from main files |
| 133 | + context_parts.append("\nKey Code Files:") |
| 134 | + for main_file in project_info.main_files[:5]: # Limit to first 5 files |
| 135 | + file_path = project_path / main_file |
| 136 | + try: |
| 137 | + with open(file_path, encoding="utf-8") as f: |
| 138 | + content = f.read()[:2500] # First 2500 chars |
| 139 | + context_parts.append(f"\n=== {main_file} ===\n{content}") |
| 140 | + except Exception as e: |
| 141 | + context_parts.append(f"\n=== {main_file} ===\nError: {e}") |
| 142 | + |
| 143 | + return "\n".join(context_parts) |
| 144 | + |
| 145 | + def _get_directory_structure(self, project_path: Path, max_depth: int = 2) -> str: |
| 146 | + """Get a simplified directory structure.""" |
| 147 | + structure_lines = [] |
| 148 | + |
| 149 | + def walk_dir(path: Path, depth: int = 0, prefix: str = ""): |
| 150 | + if depth > max_depth: |
| 151 | + return |
| 152 | + |
| 153 | + try: |
| 154 | + items = sorted(path.iterdir(), key=lambda x: (x.is_file(), x.name)) |
| 155 | + for i, item in enumerate(items): |
| 156 | + if item.name.startswith("."): |
| 157 | + continue |
| 158 | + |
| 159 | + is_last = i == len(items) - 1 |
| 160 | + current_prefix = "└── " if is_last else "├── " |
| 161 | + structure_lines.append(f"{prefix}{current_prefix}{item.name}") |
| 162 | + |
| 163 | + if item.is_dir() and depth < max_depth: |
| 164 | + next_prefix = prefix + (" " if is_last else "│ ") |
| 165 | + walk_dir(item, depth + 1, next_prefix) |
| 166 | + except PermissionError: |
| 167 | + pass |
| 168 | + |
| 169 | + walk_dir(project_path) |
| 170 | + return "\n".join(structure_lines[:20]) # Limit output |
| 171 | + |
| 172 | + def _agent_detect_tools( |
| 173 | + self, context: str, project_info: ProjectInfo |
| 174 | + ) -> list[dict[str, Any]]: |
| 175 | + """Use ChatAgent to analyze project and detect tools.""" |
| 176 | + |
| 177 | + user_prompt = f"""Analyze this project and identify all possible tools/APIs that can be exposed as MCP tools: |
| 178 | +
|
| 179 | +{context} |
| 180 | +
|
| 181 | +Based on the project type ({project_info.project_type}) and the code analysis, identify all tools that could be exposed. |
| 182 | +
|
| 183 | +Consider these aspects: |
| 184 | +1. CLI Commands: Look for argparse, click, typer patterns and their arguments |
| 185 | +2. Web APIs: Identify Flask/Django/FastAPI routes and endpoints |
| 186 | +3. Functions: Find public functions that could be called as tools |
| 187 | +4. Scripts: Identify executable scripts and their parameters |
| 188 | +5. Interactive Commands: Look for input/menu-driven functionality |
| 189 | +
|
| 190 | +For each tool, provide detailed information: |
| 191 | +- name: Clear, descriptive name (snake_case) |
| 192 | +- description: What the tool does and its purpose |
| 193 | +- args: Command structure or API call pattern |
| 194 | +- parameters: Detailed parameter specs with types and descriptions |
| 195 | +
|
| 196 | +Return your analysis as a JSON array with this EXACT format: |
| 197 | +[ |
| 198 | + {{ |
| 199 | + "name": "tool_name", |
| 200 | + "description": "Clear description of what this tool does", |
| 201 | + "args": ["command", "--flag", "{{parameter}}"], |
| 202 | + "parameters": [ |
| 203 | + {{ |
| 204 | + "name": "parameter", |
| 205 | + "type": "string|integer|number|boolean|array", |
| 206 | + "description": "Parameter description", |
| 207 | + "required": true |
| 208 | + }} |
| 209 | + ] |
| 210 | + }} |
| 211 | +] |
| 212 | +
|
| 213 | +Focus on practical, usable tools that provide real value.""" |
| 214 | + |
| 215 | + try: |
| 216 | + # Send message to agent using the correct API |
| 217 | + user_message = BaseMessage.make_user_message( |
| 218 | + role_name="User", content=user_prompt |
| 219 | + ) |
| 220 | + |
| 221 | + response = self.agent.step(user_message) |
| 222 | + content = response.msg.content.strip() |
| 223 | + |
| 224 | + # Extract JSON from response |
| 225 | + start_idx = content.find("[") |
| 226 | + end_idx = content.rfind("]") + 1 |
| 227 | + if start_idx != -1 and end_idx != 0: |
| 228 | + json_content = content[start_idx:end_idx] |
| 229 | + else: |
| 230 | + json_content = content |
| 231 | + |
| 232 | + tools_data = json.loads(json_content) |
| 233 | + |
| 234 | + # Validate the structure |
| 235 | + if not isinstance(tools_data, list): |
| 236 | + print("Warning: Agent returned non-list response, wrapping in list") |
| 237 | + tools_data = [tools_data] if tools_data else [] |
| 238 | + |
| 239 | + # Validate each tool has required fields |
| 240 | + validated_tools = [] |
| 241 | + for tool in tools_data: |
| 242 | + if isinstance(tool, dict) and "name" in tool and "description" in tool: |
| 243 | + # Set defaults for missing fields |
| 244 | + if "args" not in tool: |
| 245 | + tool["args"] = [] |
| 246 | + if "parameters" not in tool: |
| 247 | + tool["parameters"] = [] |
| 248 | + validated_tools.append(tool) |
| 249 | + else: |
| 250 | + print(f"Warning: Skipping invalid tool specification: {tool}") |
| 251 | + |
| 252 | + return validated_tools |
| 253 | + |
| 254 | + except json.JSONDecodeError as e: |
| 255 | + print(f"Error: Failed to parse agent response as JSON: {e}") |
| 256 | + print(f"Raw response: {content}") |
| 257 | + return [] |
| 258 | + except Exception as e: |
| 259 | + print(f"Error: Camel-AI tool detection failed: {e}") |
| 260 | + return [] |
| 261 | + |
| 262 | + def _enhance_tool_with_agent(self, tool: ToolSpec, context: str) -> ToolSpec: |
| 263 | + """Use ChatAgent to enhance a single tool's specification.""" |
| 264 | + try: |
| 265 | + prompt = f""" |
| 266 | + Enhance this tool specification with better descriptions and details: |
| 267 | +
|
| 268 | + Tool: {tool.name} |
| 269 | + Current Description: {tool.description} |
| 270 | + Args: {tool.args} |
| 271 | + Parameters: {tool.parameters} |
| 272 | +
|
| 273 | + Project Context (excerpt): |
| 274 | + {context[:800]} |
| 275 | +
|
| 276 | + Provide an enhanced version with: |
| 277 | + 1. A clear, professional description |
| 278 | + 2. Better parameter descriptions with proper types |
| 279 | + 3. Usage examples if helpful |
| 280 | +
|
| 281 | + Return as JSON in this exact format: |
| 282 | + {{ |
| 283 | + "name": "{tool.name}", |
| 284 | + "description": "Enhanced description", |
| 285 | + "args": {tool.args}, |
| 286 | + "parameters": [ |
| 287 | + {{ |
| 288 | + "name": "param_name", |
| 289 | + "type": "string|integer|number|boolean|array", |
| 290 | + "description": "Clear parameter description", |
| 291 | + "required": true |
| 292 | + }} |
| 293 | + ] |
| 294 | + }} |
| 295 | + """ |
| 296 | + |
| 297 | + user_message = BaseMessage.make_user_message( |
| 298 | + role_name="User", content=prompt |
| 299 | + ) |
| 300 | + |
| 301 | + response = self.agent.step(user_message) |
| 302 | + enhanced_data = json.loads(response.msg.content) |
| 303 | + |
| 304 | + return ToolSpec( |
| 305 | + name=enhanced_data["name"], |
| 306 | + description=enhanced_data["description"], |
| 307 | + args=enhanced_data.get("args", tool.args), |
| 308 | + parameters=enhanced_data.get("parameters", tool.parameters), |
| 309 | + ) |
| 310 | + |
| 311 | + except Exception as e: |
| 312 | + print(f"Warning: Failed to enhance tool {tool.name}: {e}") |
| 313 | + return tool |
0 commit comments