Skip to content

Commit a3e2121

Browse files
committed
add detect/camel
1 parent a54fbbf commit a3e2121

File tree

1 file changed

+313
-0
lines changed

1 file changed

+313
-0
lines changed

mcpify/detect/camel.py

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
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

Comments
 (0)