cursor-agent-tools 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
agent/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .base import BaseAgent
2
+ from .claude_agent import ClaudeAgent
3
+ from .openai_agent import OpenAIAgent
4
+
5
+ __all__ = ["BaseAgent", "ClaudeAgent", "OpenAIAgent"]
agent/base.py ADDED
@@ -0,0 +1,174 @@
1
+ import json
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any, Callable, Dict, List, Optional
4
+
5
+ from .permissions import PermissionManager, PermissionOptions, PermissionRequest, PermissionStatus
6
+
7
+
8
+ class BaseAgent(ABC):
9
+ """
10
+ Base abstract class for AI agents that use function calling capabilities.
11
+ This defines the common interface for all agents regardless of the underlying provider.
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ api_key: Optional[str] = None,
17
+ model: Optional[str] = None,
18
+ permission_options: Optional[PermissionOptions] = None,
19
+ permission_callback: Optional[Callable[[PermissionRequest], PermissionStatus]] = None,
20
+ ):
21
+ """
22
+ Initialize the agent.
23
+
24
+ Args:
25
+ api_key: API key for the model provider. If not provided, will attempt to load from environment.
26
+ model: Model to use. If not provided, will use the default model.
27
+ permission_options: Configuration options for permissions
28
+ permission_callback: Optional callback for handling permission requests
29
+ """
30
+ self.api_key: Optional[str] = api_key
31
+ self.model: Optional[str] = model
32
+ self.conversation_history: List[Dict[str, Any]] = []
33
+ self.available_tools: Dict[str, Dict[str, Any]] = {}
34
+ self.system_prompt: str = self._generate_system_prompt()
35
+
36
+ # Initialize permission manager with options and optional callback
37
+ self.permission_manager = PermissionManager(
38
+ options=permission_options or PermissionOptions(),
39
+ callback=permission_callback
40
+ )
41
+
42
+ @abstractmethod
43
+ def _generate_system_prompt(self) -> str:
44
+ """
45
+ Generate the system prompt that defines the agent's capabilities and behavior.
46
+
47
+ Returns:
48
+ The system prompt as a string
49
+ """
50
+ pass
51
+
52
+ @abstractmethod
53
+ async def chat(self, message: str, user_info: Optional[Dict[str, Any]] = None) -> str:
54
+ """
55
+ Send a message to the AI and get a response.
56
+
57
+ Args:
58
+ message: The user's message
59
+ user_info: Optional dict containing info about the user's current state
60
+
61
+ Returns:
62
+ The AI's response
63
+ """
64
+ pass
65
+
66
+ def register_tool(
67
+ self, name: str, function: Callable, description: str, parameters: Dict[str, Any]
68
+ ) -> None:
69
+ """
70
+ Register a function that can be called by the AI.
71
+
72
+ Args:
73
+ name: Name of the function
74
+ function: The actual function to call
75
+ description: Description of what the function does
76
+ parameters: Dict describing the parameters the function takes
77
+ """
78
+ self.available_tools[name] = {
79
+ "function": function,
80
+ "schema": {"name": name, "description": description, "parameters": parameters},
81
+ }
82
+
83
+ @abstractmethod
84
+ def _prepare_tools(self) -> Any:
85
+ """
86
+ Format the registered tools into the format expected by the model's API.
87
+
88
+ Returns:
89
+ Tools in the format expected by the model
90
+ """
91
+ pass
92
+
93
+ @abstractmethod
94
+ def _execute_tool_calls(self, tool_calls: Any) -> List[Dict[str, Any]]:
95
+ """
96
+ Execute the tool calls made by the AI.
97
+
98
+ Args:
99
+ tool_calls: Tool calls in the format provided by the specific model
100
+
101
+ Returns:
102
+ List of tool call results
103
+ """
104
+ pass
105
+
106
+ def request_permission(
107
+ self, operation_type: str, details: Dict[str, Any]
108
+ ) -> bool:
109
+ """
110
+ Request permission for an operation.
111
+
112
+ This method forwards the permission request to the permission manager.
113
+
114
+ Args:
115
+ operation_type: Type of operation ('create_file', 'edit_file', 'delete_file', 'run_terminal_command', etc.)
116
+ details: Dictionary containing operation details
117
+
118
+ Returns:
119
+ True if permission is granted, False otherwise
120
+ """
121
+ return self.permission_manager.request_permission(operation_type, details)
122
+
123
+ def _permission_request_callback(self, permission_request: PermissionRequest) -> PermissionStatus:
124
+ """
125
+ Default implementation of permission request callback.
126
+
127
+ This method can be overridden by subclasses to provide
128
+ appropriate user interaction for permission requests.
129
+
130
+ Args:
131
+ permission_request: The permission request object
132
+
133
+ Returns:
134
+ PermissionStatus indicating whether the request is granted, denied, or needs confirmation
135
+ """
136
+ # Default implementation prompts the user for confirmation
137
+ print(f"\n🔒 Permission Request: {permission_request.operation}")
138
+ print(f"Details: {json.dumps(permission_request.details, indent=2)}")
139
+
140
+ while True:
141
+ response = input("Allow this operation? (y/n): ").strip().lower()
142
+ if response in ("y", "yes"):
143
+ return PermissionStatus.GRANTED
144
+ elif response in ("n", "no"):
145
+ return PermissionStatus.DENIED
146
+ else:
147
+ print("Please enter 'y' or 'n'")
148
+
149
+ def format_user_message(self, message: str, user_info: Optional[Dict[str, Any]] = None) -> str:
150
+ """
151
+ Format the user message with user_info if provided.
152
+
153
+ Args:
154
+ message: The user's message
155
+ user_info: Optional dict containing info about the user's current state
156
+
157
+ Returns:
158
+ Formatted message
159
+ """
160
+ if user_info:
161
+ return f"<user_info>\n{json.dumps(user_info, indent=2)}\n</user_info>\n\n<user_query>\n{message}\n</user_query>"
162
+ else:
163
+ return f"<user_query>\n{message}\n</user_query>"
164
+
165
+ def register_default_tools(self) -> None:
166
+ """
167
+ Register the default set of tools with the agent.
168
+
169
+ This method imports and calls the register_default_tools function
170
+ from the tools module, passing self as the agent.
171
+ """
172
+ # Import here to avoid circular imports
173
+ from .tools.register_tools import register_default_tools
174
+ register_default_tools(self)
agent/claude_agent.py ADDED
@@ -0,0 +1,422 @@
1
+ # mypy: ignore-errors
2
+ import json
3
+ from typing import Any, Dict, List, Optional, Callable
4
+
5
+ from anthropic import APIError, AsyncAnthropic, AuthenticationError, BadRequestError, RateLimitError
6
+
7
+ from .base import BaseAgent
8
+ from .permissions import PermissionOptions, PermissionRequest, PermissionStatus
9
+ from .tools.register_tools import register_default_tools
10
+
11
+
12
+ class ClaudeAgent(BaseAgent):
13
+ """
14
+ Claude Agent that implements the BaseAgent interface using Anthropic's Claude models.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ api_key: str,
20
+ model: str = "claude-3-5-sonnet-latest",
21
+ temperature: float = 0.0,
22
+ timeout: int = 180,
23
+ permission_callback: Optional[Callable[[PermissionRequest], PermissionStatus]] = None,
24
+ permission_options: Optional[PermissionOptions] = None,
25
+ **kwargs
26
+ ):
27
+ """
28
+ Initialize a Claude agent.
29
+
30
+ Args:
31
+ api_key: Anthropic API key
32
+ model: Claude model to use, default is claude-3-opus
33
+ temperature: Temperature parameter for model (0.0 to 1.0)
34
+ timeout: Timeout in seconds for API requests
35
+ permission_callback: Optional callback for permission requests
36
+ permission_options: Permission configuration options
37
+ **kwargs: Additional parameters to pass to the model
38
+ """
39
+ super().__init__(
40
+ api_key=api_key,
41
+ model=model,
42
+ permission_options=permission_options,
43
+ permission_callback=permission_callback
44
+ )
45
+
46
+ self.temperature = temperature
47
+ self.timeout = timeout
48
+ self.extra_kwargs = kwargs
49
+
50
+ # Initialize Anthropic client
51
+ self.client = AsyncAnthropic(api_key=api_key)
52
+
53
+ self.conversation_history = []
54
+ self.available_tools = {}
55
+ self.system_prompt = self._generate_system_prompt()
56
+
57
+ def _is_valid_api_key(self, api_key: str) -> bool:
58
+ """
59
+ Validate the format of the Anthropic API key.
60
+
61
+ Args:
62
+ api_key: The API key to validate
63
+
64
+ Returns:
65
+ True if the key is a valid format, False otherwise
66
+ """
67
+ if not api_key or not isinstance(api_key, str):
68
+ return False
69
+
70
+ # Allow dummy keys in test environments
71
+ if api_key == "sk-ant-dummy" or "dummy" in api_key:
72
+ return True
73
+
74
+ # Anthropic keys should start with sk-ant- or sk-
75
+ valid_prefix = api_key.startswith(("sk-ant-", "sk-"))
76
+
77
+ # Keys should be fairly long and not contain spaces
78
+ valid_length = len(api_key) >= 20 and " " not in api_key
79
+
80
+ return valid_prefix and valid_length
81
+
82
+ def _generate_system_prompt(self) -> str:
83
+ """
84
+ Generate the system prompt that defines Claude's capabilities and behavior.
85
+ This is an extensive prompt that replicates Claude's behavior in Cursor.
86
+ """
87
+ return """
88
+ You are a powerful agentic AI coding assistant, powered by Claude 3.7 Sonnet. You operate exclusively in Cursor, the world's best IDE.
89
+
90
+ You are pair programming with a USER to solve their coding task.
91
+ The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.
92
+ Each time the USER sends a message, we may automatically attach some information about their current state, such as what files they have open, where their cursor is, recently viewed files, edit history in their session so far, linter errors, and more.
93
+ This information may or may not be relevant to the coding task, it is up for you to decide.
94
+ Your main goal is to follow the USER's instructions at each message, denoted by the <user_query> tag.
95
+
96
+ <tool_calling>
97
+ You have tools at your disposal to solve the coding task. Follow these rules regarding tool calls:
98
+ 1. ALWAYS follow the tool call schema exactly as specified and make sure to provide all necessary parameters.
99
+ 2. The conversation may reference tools that are no longer available. NEVER call tools that are not explicitly provided.
100
+ 3. **NEVER refer to tool names when speaking to the USER.** For example, instead of saying 'I need to use the edit_file tool to edit your file', just say 'I will edit your file'.
101
+ 4. Only calls tools when they are necessary. If the USER's task is general or you already know the answer, just respond without calling tools.
102
+ 5. Before calling each tool, first explain to the USER why you are calling it.
103
+ </tool_calling>
104
+
105
+ <making_code_changes>
106
+ When making code changes, NEVER output code to the USER, unless requested. Instead use one of the code edit tools to implement the change.
107
+ Use the code edit tools at most once per turn.
108
+ It is *EXTREMELY* important that your generated code can be run immediately by the USER. To ensure this, follow these instructions carefully:
109
+ 1. Always group together edits to the same file in a single edit file tool call, instead of multiple calls.
110
+ 2. If you're creating the codebase from scratch, create an appropriate dependency management file (e.g. requirements.txt) with package versions and a helpful README.
111
+ 3. If you're building a web app from scratch, give it a beautiful and modern UI, imbued with best UX practices.
112
+ 4. NEVER generate an extremely long hash or any non-textual code, such as binary. These are not helpful to the USER and are very expensive.
113
+ 5. Unless you are appending some small easy to apply edit to a file, or creating a new file, you MUST read the the contents or section of what you're editing before editing it.
114
+ 6. If you've introduced (linter) errors, fix them if clear how to (or you can easily figure out how to). Do not make uneducated guesses. And DO NOT loop more than 3 times on fixing linter errors on the same file. On the third time, you should stop and ask the user what to do next.
115
+ 7. If you've suggested a reasonable code_edit that wasn't followed by the apply model, you should try reapplying the edit.
116
+ </making_code_changes>
117
+
118
+ <searching_and_reading>
119
+ You have tools to search the codebase and read files. Follow these rules regarding tool calls:
120
+ 1. If available, heavily prefer the semantic search tool to grep search, file search, and list dir tools.
121
+ 2. If you need to read a file, prefer to read larger sections of the file at once over multiple smaller calls.
122
+ 3. If you have found a reasonable place to edit or answer, do not continue calling tools. Edit or answer from the information you have found.
123
+ </searching_and_reading>
124
+
125
+ Answer the user's request using the relevant tool(s), if they are available. Check that all the required parameters for each tool call are provided or can reasonably be inferred from context. IF there are no relevant tools or there are missing values for required parameters, ask the user to supply these values; otherwise proceed with the tool calls. If the user provides a specific value for a parameter (for example provided in quotes), make sure to use that value EXACTLY. DO NOT make up values for or ask about optional parameters. Carefully analyze descriptive terms in the request as they may indicate required parameter values that should be included even if not explicitly quoted.
126
+
127
+ You MUST use the following format when citing code regions or blocks:
128
+ ```12:15:app/components/Todo.tsx
129
+ // ... existing code ...
130
+ ```
131
+ This is the ONLY acceptable format for code citations. The format is ```startLine:endLine:filepath where startLine and endLine are line numbers.
132
+ """
133
+
134
+ def _prepare_tools(self) -> Optional[List[Dict[str, Any]]]:
135
+ """
136
+ Prepare the registered tools for Claude API.
137
+
138
+ Returns:
139
+ List of tools in the format expected by Claude API, or None if no tools are registered
140
+ """
141
+ if not self.available_tools:
142
+ return None
143
+
144
+ tools = []
145
+ for name, tool_data in self.available_tools.items():
146
+ # Claude tools format:
147
+ # {
148
+ # "name": "tool_name",
149
+ # "description": "Tool description",
150
+ # "input_schema": {
151
+ # "type": "object",
152
+ # "properties": {
153
+ # "property_name": {
154
+ # "type": "string",
155
+ # "description": "Property description"
156
+ # },
157
+ # ...
158
+ # },
159
+ # "required": ["property_name", ...]
160
+ # }
161
+ # }
162
+
163
+ tool = {
164
+ "name": name,
165
+ "description": tool_data["schema"]["description"],
166
+ "input_schema": {
167
+ "type": "object",
168
+ "properties": tool_data["schema"]["parameters"]["properties"],
169
+ "required": tool_data["schema"]["parameters"].get("required", []),
170
+ },
171
+ }
172
+ tools.append(tool)
173
+ return tools if tools else None
174
+
175
+ def _execute_tool_calls(self, tool_calls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
176
+ """
177
+ Execute the tool calls made by Claude.
178
+
179
+ Args:
180
+ tool_calls: List of tool calls to execute
181
+
182
+ Returns:
183
+ List of tool call results formatted for the Claude API
184
+ """
185
+ tool_results = []
186
+
187
+ for call in tool_calls:
188
+ tool_name = call["name"]
189
+ tool_id = call.get("id")
190
+ arguments = call.get("input", {})
191
+
192
+ # Format for user message with tool_result as required by the Claude API
193
+ result_message = {"role": "user", "content": []}
194
+
195
+ if tool_name not in self.available_tools:
196
+ # Add error result
197
+ error_msg = f"Tool '{tool_name}' not found. Error: Tool not available."
198
+ result_message["content"].append(
199
+ {
200
+ "type": "tool_result",
201
+ "tool_use_id": tool_id,
202
+ "is_error": True,
203
+ "content": error_msg,
204
+ }
205
+ )
206
+ else:
207
+ try:
208
+ function = self.available_tools[tool_name]["function"]
209
+ # Convert input to the expected format for the function
210
+ result = function(**arguments)
211
+
212
+ # Format the result based on whether it's a string or a JSON-serializable object
213
+ content = result if isinstance(result, str) else json.dumps(result)
214
+
215
+ # Add tool result
216
+ result_message["content"].append(
217
+ {"type": "tool_result", "tool_use_id": tool_id, "content": content}
218
+ )
219
+ except Exception as e:
220
+ error_msg = f"Error executing tool {tool_name}: {str(e)}"
221
+ result_message["content"].append(
222
+ {
223
+ "type": "tool_result",
224
+ "tool_use_id": tool_id,
225
+ "is_error": True,
226
+ "content": error_msg,
227
+ }
228
+ )
229
+
230
+ # Only add messages with non-empty content
231
+ if result_message["content"]:
232
+ tool_results.append(result_message)
233
+
234
+ return tool_results
235
+
236
+ async def chat(self, message: str, user_info: Optional[Dict[str, Any]] = None) -> str:
237
+ """
238
+ Send a message to Claude and get a response.
239
+
240
+ Args:
241
+ message: The user's message
242
+ user_info: Optional dict containing info about the user's current state
243
+
244
+ Returns:
245
+ Claude's response
246
+ """
247
+ # Format the user message with user_info if provided
248
+ formatted_message = self.format_user_message(message, user_info)
249
+
250
+ # Add the user message to the conversation history
251
+ self.conversation_history.append({"role": "user", "content": formatted_message})
252
+
253
+ # Prepare the messages for the API call - exclude system message from the conversation history
254
+ # because Anthropic API requires system prompt as a separate parameter
255
+ messages = []
256
+ for msg in self.conversation_history:
257
+ if msg["role"] != "system" and msg.get("content"): # Ensure content is not empty
258
+ messages.append(msg)
259
+
260
+ # Always enable tools regardless of message content
261
+ use_tools = True
262
+ # Note: Previous code disabled tools for file-related operations, but this was causing issues
263
+ # with the file_tools_demo and other demos that need to use file tools
264
+
265
+ # Prepare tools if needed
266
+ tools = None
267
+ if use_tools:
268
+ tools = self._prepare_tools()
269
+
270
+ try:
271
+ # Make the API call
272
+ api_params = {
273
+ "model": self.model if self.model else "claude-3-5-sonnet-latest",
274
+ "max_tokens": 4096,
275
+ "temperature": self.temperature,
276
+ "system": self.system_prompt, # System prompt as a separate parameter
277
+ }
278
+
279
+ # Add properly typed messages
280
+ typed_messages = []
281
+ for msg in messages:
282
+ if isinstance(msg["content"], str):
283
+ typed_messages.append({"role": msg["role"], "content": msg["content"]})
284
+ else:
285
+ # Handle content that's not a string (e.g., structured content)
286
+ typed_messages.append(msg)
287
+
288
+ api_params["messages"] = typed_messages
289
+
290
+ # Only include tools parameter if we have tools registered and are using tools
291
+ if tools and use_tools:
292
+ api_params["tools"] = tools
293
+
294
+ # Make the API call
295
+ response = await self.client.messages.create(**api_params) # type: ignore
296
+
297
+ # Process any tool calls
298
+ if response.content and any(block.type == "tool_use" for block in response.content):
299
+ # Add assistant message with tool calls to conversation history
300
+ assistant_content = []
301
+ for block in response.content:
302
+ if hasattr(block, "text") and block.text is not None:
303
+ assistant_content.append({"type": "text", "text": block.text}) # type: ignore
304
+ elif block.type == "tool_use":
305
+ assistant_content.append({ # type: ignore
306
+ "type": "tool_use",
307
+ "id": block.id,
308
+ "name": block.name,
309
+ "input": block.input
310
+ })
311
+
312
+ self.conversation_history.append({"role": "assistant", "content": assistant_content})
313
+
314
+ # Extract tool calls
315
+ tool_calls = []
316
+ for block in response.content:
317
+ if block.type == "tool_use":
318
+ tool_calls.append(
319
+ {"name": block.name, "id": block.id, "input": block.input}
320
+ )
321
+
322
+ # Execute tool calls
323
+ tool_results = self._execute_tool_calls(tool_calls)
324
+
325
+ # Add tool results to conversation history and prepare follow-up messages
326
+ if tool_results:
327
+ for result in tool_results:
328
+ self.conversation_history.append(result)
329
+
330
+ # Make a follow-up API call with the tool results
331
+ follow_up_messages = []
332
+ for msg in self.conversation_history:
333
+ if msg["role"] != "system" and msg.get(
334
+ "content"
335
+ ): # Ensure content is not empty
336
+ follow_up_messages.append(msg)
337
+
338
+ # Make a follow-up API call with the tool results
339
+ follow_up_response = await self.client.messages.create( # type: ignore
340
+ model=self.model if self.model else "claude-3-5-sonnet-latest",
341
+ system=self.system_prompt, # System prompt as a separate parameter
342
+ messages=follow_up_messages,
343
+ max_tokens=4096,
344
+ temperature=self.temperature,
345
+ )
346
+
347
+ # Add the assistant's follow-up response to the conversation history
348
+ self.conversation_history.append(
349
+ {"role": "assistant", "content": follow_up_response.content}
350
+ )
351
+
352
+ # Extract text from the response
353
+ response_text = "".join(
354
+ block.text for block in follow_up_response.content if block.type == "text"
355
+ )
356
+
357
+ return response_text
358
+ else:
359
+ # No valid tool results were generated
360
+ response_text = (
361
+ "Error: Failed to execute tool calls. Please try a different query."
362
+ )
363
+ return response_text
364
+ else:
365
+ # Extract text from the response
366
+ response_text = "".join(
367
+ block.text for block in response.content if block.type == "text"
368
+ )
369
+
370
+ # Add the assistant's response to the conversation history
371
+ self.conversation_history.append({"role": "assistant", "content": response.content})
372
+
373
+ return response_text
374
+
375
+ except AuthenticationError as e:
376
+ return f"Error: Authentication failed. Please check your Anthropic API key. Details: {str(e)}"
377
+ except BadRequestError as e:
378
+ # Provide more detailed information about the bad request
379
+ request_info = ""
380
+ if hasattr(e, "request"):
381
+ request_info = f"\nRequest information: {e.request}"
382
+ return f"Error: Bad request to the Anthropic API. Details: {str(e)}{request_info}"
383
+ except RateLimitError as e:
384
+ return f"Error: Rate limit exceeded. Please try again later. Details: {str(e)}"
385
+ except APIError as e:
386
+ return f"Error: Anthropic API error. Details: {str(e)}"
387
+ except Exception as e:
388
+ return f"Error: An unexpected error occurred. Details: {type(e).__name__}: {str(e)}"
389
+
390
+ def register_default_tools(self) -> None:
391
+ """
392
+ Register all the default tools available to the agent.
393
+ """
394
+ # Use the centralized tool registration function
395
+ register_default_tools(self)
396
+
397
+ def _permission_request_callback(self, permission_request: PermissionRequest) -> PermissionStatus:
398
+ """
399
+ Implementation of permission request callback for Claude agent.
400
+
401
+ In a real application, this would interact with the user to get permission.
402
+ For now, we'll default to a console-based interaction.
403
+
404
+ Args:
405
+ permission_request: The permission request object
406
+
407
+ Returns:
408
+ PermissionStatus indicating whether the request is granted or denied
409
+ """
410
+ # If yolo mode is enabled, check is already done in PermissionManager
411
+ if self.permission_manager.options.yolo_mode:
412
+ return PermissionStatus.GRANTED
413
+
414
+ # Default implementation asks on console
415
+ print(f"\n[PERMISSION REQUEST] {permission_request.operation}")
416
+ print(f"Details: {json.dumps(permission_request.details, indent=2)}")
417
+ response = input("Allow this operation? (y/n): ").strip().lower()
418
+
419
+ if response == 'y' or response == 'yes':
420
+ return PermissionStatus.GRANTED
421
+ else:
422
+ return PermissionStatus.DENIED