code-puppy 0.0.2__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.
- code_agent/__init__.py +0 -0
- code_agent/agent.py +19 -0
- code_agent/agent_prompts.py +52 -0
- code_agent/main.py +234 -0
- code_agent/models/__init__.py +4 -0
- code_agent/models/codesnippet.py +20 -0
- code_agent/tools/__init__.py +4 -0
- code_agent/tools/command_runner.py +187 -0
- code_agent/tools/common.py +3 -0
- code_agent/tools/file_modifications.py +264 -0
- code_agent/tools/file_operations.py +350 -0
- code_agent/tools/web_search.py +41 -0
- code_puppy-0.0.2.dist-info/METADATA +133 -0
- code_puppy-0.0.2.dist-info/RECORD +16 -0
- code_puppy-0.0.2.dist-info/WHEEL +4 -0
- code_puppy-0.0.2.dist-info/entry_points.txt +2 -0
code_agent/__init__.py
ADDED
|
File without changes
|
code_agent/agent.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pydantic
|
|
3
|
+
from pydantic_ai import Agent
|
|
4
|
+
from code_agent.agent_prompts import SYSTEM_PROMPT
|
|
5
|
+
|
|
6
|
+
# Check if we have a valid API key
|
|
7
|
+
api_key = os.environ.get("OPENAI_API_KEY", "")
|
|
8
|
+
|
|
9
|
+
class AgentResponse(pydantic.BaseModel):
|
|
10
|
+
"""Represents a response from the agent."""
|
|
11
|
+
output_message: str = pydantic.Field(..., description="The final output message to display to the user")
|
|
12
|
+
awaiting_user_input: bool = pydantic.Field(False, description="True if user input is needed to continue the task")
|
|
13
|
+
|
|
14
|
+
# Create agent with tool usage explicitly enabled
|
|
15
|
+
code_generation_agent = Agent(
|
|
16
|
+
model='openai:gpt-4.1-mini',
|
|
17
|
+
system_prompt=SYSTEM_PROMPT,
|
|
18
|
+
output_type=AgentResponse,
|
|
19
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
SYSTEM_PROMPT = """
|
|
2
|
+
You are a code-agent assistant with the ability to use tools to help users complete coding tasks. You MUST use the provided tools to write, modify, and execute code rather than just describing what to do.
|
|
3
|
+
|
|
4
|
+
Be super informal - we're here to have fun. Writing software is super fun. Don't be scared of being a little bit sarcastic too.
|
|
5
|
+
Be very pedantic about code principles like DRY, YAGNI, and SOLID.
|
|
6
|
+
Be super pedantic about code quality and best practices.
|
|
7
|
+
Be fun and playful. Don't be too serious.
|
|
8
|
+
|
|
9
|
+
Individual files should be very short and concise, at most around 250 lines if possible. If they get longer,
|
|
10
|
+
consider refactoring the code and splitting it into multiple files.
|
|
11
|
+
|
|
12
|
+
Always obey the Zen of Python, even if you are not writing Python code.
|
|
13
|
+
|
|
14
|
+
When given a coding task:
|
|
15
|
+
1. Analyze the requirements carefully
|
|
16
|
+
2. Execute the plan by using appropriate tools
|
|
17
|
+
3. Provide clear explanations for your implementation choices
|
|
18
|
+
4. Continue autonomously whenever possible to achieve the task.
|
|
19
|
+
|
|
20
|
+
YOU MUST USE THESE TOOLS to complete tasks (do not just describe what should be done - actually do it):
|
|
21
|
+
|
|
22
|
+
File Operations:
|
|
23
|
+
- list_files(directory=".", recursive=True): ALWAYS use this to explore directories before trying to read/modify files
|
|
24
|
+
- read_file(file_path, start_line=0, end_line=None): ALWAYS use this to read existing files before modifying them. Don't read less than 500 lines at a time.
|
|
25
|
+
- create_file(file_path, content=""): Use this to create new files with content
|
|
26
|
+
- modify_file(file_path, proposed_changes, replace_content): Use this to replace specific content in files
|
|
27
|
+
- delete_snippet_from_file(file_path, snippet): Use this to remove specific code snippets from files
|
|
28
|
+
- delete_file(file_path): Use this to remove files when needed
|
|
29
|
+
|
|
30
|
+
System Operations:
|
|
31
|
+
- run_shell_command(command, cwd=None, timeout=60): Use this to execute commands, run tests, or start services
|
|
32
|
+
- web_search(query): Use this to search the web for information
|
|
33
|
+
- web_crawl(url): Use this to crawl a website for information
|
|
34
|
+
|
|
35
|
+
Reasoning & Explanation:
|
|
36
|
+
- share_your_reasoning(reasoning, next_steps=None): Use this to explicitly share your thought process and planned next steps
|
|
37
|
+
|
|
38
|
+
Important rules:
|
|
39
|
+
- You MUST use tools to accomplish tasks - DO NOT just output code or descriptions
|
|
40
|
+
- Before every other tool use, you must use "share_your_reasoning" to explain your thought process and planned next steps
|
|
41
|
+
- Check if files exist before trying to modify or delete them
|
|
42
|
+
- After using system operations tools, always explain the results
|
|
43
|
+
- You're encouraged to loop between share_your_reasoning, file tools, and run_shell_command to test output in order to write programs
|
|
44
|
+
- Aim to continue operations independently unless user input is definitively required.
|
|
45
|
+
|
|
46
|
+
Your solutions should be production-ready, maintainable, and follow best practices for the chosen language.
|
|
47
|
+
|
|
48
|
+
Return your final response as a structured output having the following fields:
|
|
49
|
+
* output_message: The final output message to display to the user
|
|
50
|
+
* awaiting_user_input: True if user input is needed to continue the task. If you get an error, you might consider asking the user for help.
|
|
51
|
+
|
|
52
|
+
"""
|
code_agent/main.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import readline
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.markdown import Markdown
|
|
8
|
+
from rich.console import ConsoleOptions, RenderResult
|
|
9
|
+
from rich.markdown import CodeBlock
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
from rich.syntax import Syntax
|
|
12
|
+
|
|
13
|
+
# Initialize rich console for pretty output
|
|
14
|
+
from code_agent.tools.common import console
|
|
15
|
+
from code_agent.agent import code_generation_agent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Define a function to get the secret file path
|
|
19
|
+
def get_secret_file_path():
|
|
20
|
+
hidden_directory = os.path.join(os.path.expanduser("~"), ".agent_secret")
|
|
21
|
+
if not os.path.exists(hidden_directory):
|
|
22
|
+
os.makedirs(hidden_directory)
|
|
23
|
+
return os.path.join(hidden_directory, "history.txt")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def main():
|
|
27
|
+
global shutdown_flag
|
|
28
|
+
|
|
29
|
+
# Load environment variables from .env file
|
|
30
|
+
load_dotenv()
|
|
31
|
+
|
|
32
|
+
# Set up argument parser
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
description="Code Generation Agent - Similar to Windsurf or Cursor"
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--interactive", "-i", action="store_true", help="Run in interactive mode"
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument("command", nargs="*", help="Run a single command")
|
|
40
|
+
args = parser.parse_args()
|
|
41
|
+
|
|
42
|
+
history_file_path = get_secret_file_path()
|
|
43
|
+
|
|
44
|
+
if args.command:
|
|
45
|
+
# Join the list of command arguments into a single string command
|
|
46
|
+
command = " ".join(args.command)
|
|
47
|
+
try:
|
|
48
|
+
while not shutdown_flag:
|
|
49
|
+
response = await code_generation_agent.run(command)
|
|
50
|
+
console.print(response.output_message)
|
|
51
|
+
if response.awaiting_user_input:
|
|
52
|
+
console.print(
|
|
53
|
+
"[bold red]The agent requires further input. Interactive mode is recommended for such tasks."
|
|
54
|
+
)
|
|
55
|
+
except AttributeError as e:
|
|
56
|
+
console.print(f"[bold red]AttributeError:[/bold red] {str(e)}")
|
|
57
|
+
console.print(
|
|
58
|
+
"[bold yellow]\u26a0 The response might not be in the expected format, missing attributes like 'output_message'."
|
|
59
|
+
)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
console.print(f"[bold red]Unexpected Error:[/bold red] {str(e)}")
|
|
62
|
+
elif args.interactive:
|
|
63
|
+
await interactive_mode(history_file_path)
|
|
64
|
+
else:
|
|
65
|
+
parser.print_help()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Add the file handling functionality for interactive mode
|
|
69
|
+
async def interactive_mode(history_file_path: str) -> None:
|
|
70
|
+
"""Run the agent in interactive mode."""
|
|
71
|
+
console.print("[bold green]Code Generation Agent[/bold green] - Interactive Mode")
|
|
72
|
+
console.print("Type 'exit' or 'quit' to exit the interactive mode.")
|
|
73
|
+
console.print("Type 'clear' to reset the conversation history.")
|
|
74
|
+
|
|
75
|
+
message_history = []
|
|
76
|
+
|
|
77
|
+
# Set up readline history file in home directory
|
|
78
|
+
history_file = os.path.expanduser("~/.code_agent_history.txt")
|
|
79
|
+
history_dir = os.path.dirname(history_file)
|
|
80
|
+
|
|
81
|
+
# Ensure history directory exists
|
|
82
|
+
if history_dir and not os.path.exists(history_dir):
|
|
83
|
+
try:
|
|
84
|
+
os.makedirs(history_dir, exist_ok=True)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
console.print(
|
|
87
|
+
f"[yellow]Warning: Could not create history directory: {e}[/yellow]"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Try to read history file
|
|
91
|
+
try:
|
|
92
|
+
if os.path.exists(history_file):
|
|
93
|
+
readline.read_history_file(history_file)
|
|
94
|
+
except (FileNotFoundError, OSError) as e:
|
|
95
|
+
console.print(f"[yellow]Warning: Could not read history file: {e}[/yellow]")
|
|
96
|
+
|
|
97
|
+
readline.set_history_length(100)
|
|
98
|
+
|
|
99
|
+
while True:
|
|
100
|
+
console.print("[bold blue]Enter your coding task:[/bold blue]")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
# Simple single-line input
|
|
104
|
+
task = input(">>> ")
|
|
105
|
+
|
|
106
|
+
# Add to readline history if not empty
|
|
107
|
+
if task.strip():
|
|
108
|
+
readline.add_history(task)
|
|
109
|
+
|
|
110
|
+
# Save history
|
|
111
|
+
try:
|
|
112
|
+
readline.write_history_file(history_file)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
console.print(
|
|
115
|
+
f"[yellow]Warning: Could not write history file: {e}[/yellow]"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
except (KeyboardInterrupt, EOFError):
|
|
119
|
+
# Handle Ctrl+C or Ctrl+D
|
|
120
|
+
console.print("\n[yellow]Input cancelled[/yellow]")
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
# Check for exit commands
|
|
124
|
+
if task.strip().lower() in ["exit", "quit"]:
|
|
125
|
+
console.print("[bold green]Goodbye![/bold green]")
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
# Check for clear command
|
|
129
|
+
if task.strip().lower() == "clear":
|
|
130
|
+
message_history = []
|
|
131
|
+
console.print("[bold yellow]Conversation history cleared![/bold yellow]")
|
|
132
|
+
console.print(
|
|
133
|
+
"[dim]The agent will not remember previous interactions.[/dim]\n"
|
|
134
|
+
)
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
if task.strip():
|
|
138
|
+
console.print(f"\n[bold blue]Processing task:[/bold blue] {task}\n")
|
|
139
|
+
|
|
140
|
+
# Write to the secret file for permanent history
|
|
141
|
+
with open(history_file_path, "a") as history_file:
|
|
142
|
+
history_file.write(f"{task}\n")
|
|
143
|
+
|
|
144
|
+
# Counter for consecutive auto-continue invocations
|
|
145
|
+
auto_continue_count = 0
|
|
146
|
+
max_auto_continues = 10
|
|
147
|
+
is_done = False
|
|
148
|
+
|
|
149
|
+
while not is_done and auto_continue_count <= max_auto_continues:
|
|
150
|
+
try:
|
|
151
|
+
prettier_code_blocks()
|
|
152
|
+
|
|
153
|
+
# Only show "asking" message for initial query or if not auto-continuing
|
|
154
|
+
if auto_continue_count == 0:
|
|
155
|
+
console.log(f"Asking: {task}...", style="cyan")
|
|
156
|
+
else:
|
|
157
|
+
console.log(
|
|
158
|
+
f"Auto-continuing ({auto_continue_count}/{max_auto_continues})...",
|
|
159
|
+
style="cyan",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Store agent's full response
|
|
163
|
+
agent_response = None
|
|
164
|
+
|
|
165
|
+
result = await code_generation_agent.run(
|
|
166
|
+
task, message_history=message_history
|
|
167
|
+
)
|
|
168
|
+
# Get the structured response
|
|
169
|
+
agent_response = result.output
|
|
170
|
+
console.print(agent_response.output_message)
|
|
171
|
+
|
|
172
|
+
# Update message history with all messages from this interaction
|
|
173
|
+
message_history = result.new_messages()
|
|
174
|
+
if agent_response:
|
|
175
|
+
# Check if the agent needs user input
|
|
176
|
+
if agent_response.awaiting_user_input:
|
|
177
|
+
console.print(
|
|
178
|
+
"\n[bold yellow]\u26a0 Agent needs your input to continue.[/bold yellow]"
|
|
179
|
+
)
|
|
180
|
+
is_done = True # Exit the loop to get user input
|
|
181
|
+
# Otherwise, auto-continue if we haven't reached the limit
|
|
182
|
+
elif auto_continue_count < max_auto_continues:
|
|
183
|
+
auto_continue_count += 1
|
|
184
|
+
task = "please continue"
|
|
185
|
+
console.print(
|
|
186
|
+
"\n[yellow]Agent continuing automatically...[/yellow]"
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
# Reached max auto-continues
|
|
190
|
+
console.print(
|
|
191
|
+
f"\n[bold yellow]\u26a0 Reached maximum of {max_auto_continues} automatic continuations.[/bold yellow]"
|
|
192
|
+
)
|
|
193
|
+
console.print(
|
|
194
|
+
"[dim]You can enter a new request or type 'please continue' to resume.[/dim]"
|
|
195
|
+
)
|
|
196
|
+
is_done = True
|
|
197
|
+
|
|
198
|
+
# Show context status
|
|
199
|
+
console.print(
|
|
200
|
+
f"[dim]Context: {len(message_history)} messages in history[/dim]\n"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
except Exception:
|
|
204
|
+
console.print_exception(show_locals=True)
|
|
205
|
+
is_done = True
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def prettier_code_blocks():
|
|
209
|
+
class SimpleCodeBlock(CodeBlock):
|
|
210
|
+
def __rich_console__(
|
|
211
|
+
self, console: Console, options: ConsoleOptions
|
|
212
|
+
) -> RenderResult:
|
|
213
|
+
code = str(self.text).rstrip()
|
|
214
|
+
yield Text(self.lexer_name, style="dim")
|
|
215
|
+
syntax = Syntax(
|
|
216
|
+
code,
|
|
217
|
+
self.lexer_name,
|
|
218
|
+
theme=self.theme,
|
|
219
|
+
background_color="default",
|
|
220
|
+
line_numbers=True,
|
|
221
|
+
)
|
|
222
|
+
yield syntax
|
|
223
|
+
yield Text(f"/{self.lexer_name}", style="dim")
|
|
224
|
+
|
|
225
|
+
Markdown.elements["fence"] = SimpleCodeBlock
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def main_entry():
|
|
229
|
+
"""Entry point for the installed CLI tool."""
|
|
230
|
+
asyncio.run(main())
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
if __name__ == "__main__":
|
|
234
|
+
main_entry()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CodeSnippet(BaseModel):
|
|
6
|
+
"""Model representing a code snippet with explanation."""
|
|
7
|
+
|
|
8
|
+
language: str
|
|
9
|
+
code: str
|
|
10
|
+
explanation: Optional[str] = None
|
|
11
|
+
imports: Optional[List[str]] = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CodeResponse(BaseModel):
|
|
15
|
+
"""Model representing a response with code snippets and explanation."""
|
|
16
|
+
|
|
17
|
+
snippets: List[CodeSnippet]
|
|
18
|
+
overall_explanation: Optional[str] = None
|
|
19
|
+
success: bool = True
|
|
20
|
+
error_message: Optional[str] = None
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# command_runner.py
|
|
2
|
+
import subprocess
|
|
3
|
+
import time
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
from code_agent.tools.common import console
|
|
6
|
+
from code_agent.agent import code_generation_agent
|
|
7
|
+
from pydantic_ai import RunContext
|
|
8
|
+
from rich.markdown import Markdown
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@code_generation_agent.tool
|
|
13
|
+
def run_shell_command(
|
|
14
|
+
context: RunContext, command: str, cwd: str = None, timeout: int = 60
|
|
15
|
+
) -> Dict[str, Any]:
|
|
16
|
+
"""Run a shell command and return its output.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
command: The shell command to execute.
|
|
20
|
+
cwd: The current working directory to run the command in. Defaults to None (current directory).
|
|
21
|
+
timeout: Maximum time in seconds to wait for the command to complete. Defaults to 60.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
A dictionary with the command result, including stdout, stderr, and exit code.
|
|
25
|
+
"""
|
|
26
|
+
if not command or not command.strip():
|
|
27
|
+
console.print("[bold red]Error:[/bold red] Command cannot be empty")
|
|
28
|
+
return {"error": "Command cannot be empty"}
|
|
29
|
+
|
|
30
|
+
# Display command execution in a visually distinct way
|
|
31
|
+
console.print("\n[bold white on blue] SHELL COMMAND [/bold white on blue]")
|
|
32
|
+
console.print(f"[bold green]$ {command}[/bold green]")
|
|
33
|
+
if cwd:
|
|
34
|
+
console.print(f"[dim]Working directory: {cwd}[/dim]")
|
|
35
|
+
console.print("[dim]" + "-" * 60 + "[/dim]")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
start_time = time.time()
|
|
39
|
+
|
|
40
|
+
# Execute the command with timeout
|
|
41
|
+
process = subprocess.Popen(
|
|
42
|
+
command,
|
|
43
|
+
shell=True,
|
|
44
|
+
stdout=subprocess.PIPE,
|
|
45
|
+
stderr=subprocess.PIPE,
|
|
46
|
+
text=True,
|
|
47
|
+
cwd=cwd,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
stdout, stderr = process.communicate(timeout=timeout)
|
|
52
|
+
exit_code = process.returncode
|
|
53
|
+
execution_time = time.time() - start_time
|
|
54
|
+
|
|
55
|
+
# Display command output
|
|
56
|
+
if stdout.strip():
|
|
57
|
+
console.print("[bold white]STDOUT:[/bold white]")
|
|
58
|
+
console.print(
|
|
59
|
+
Syntax(
|
|
60
|
+
stdout.strip(),
|
|
61
|
+
"bash",
|
|
62
|
+
theme="monokai",
|
|
63
|
+
background_color="default",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if stderr.strip():
|
|
68
|
+
console.print("[bold yellow]STDERR:[/bold yellow]")
|
|
69
|
+
console.print(
|
|
70
|
+
Syntax(
|
|
71
|
+
stderr.strip(),
|
|
72
|
+
"bash",
|
|
73
|
+
theme="monokai",
|
|
74
|
+
background_color="default",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Show execution summary
|
|
79
|
+
if exit_code == 0:
|
|
80
|
+
console.print(
|
|
81
|
+
f"[bold green]✓ Command completed successfully[/bold green] [dim](took {execution_time:.2f}s)[/dim]"
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
console.print(
|
|
85
|
+
f"[bold red]✗ Command failed with exit code {exit_code}[/bold red] [dim](took {execution_time:.2f}s)[/dim]"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"success": exit_code == 0,
|
|
92
|
+
"command": command,
|
|
93
|
+
"stdout": stdout,
|
|
94
|
+
"stderr": stderr,
|
|
95
|
+
"exit_code": exit_code,
|
|
96
|
+
"execution_time": execution_time,
|
|
97
|
+
"timeout": False,
|
|
98
|
+
}
|
|
99
|
+
except subprocess.TimeoutExpired:
|
|
100
|
+
# Kill the process if it times out
|
|
101
|
+
process.kill()
|
|
102
|
+
stdout, stderr = process.communicate()
|
|
103
|
+
execution_time = time.time() - start_time
|
|
104
|
+
|
|
105
|
+
# Display timeout information
|
|
106
|
+
if stdout.strip():
|
|
107
|
+
console.print(
|
|
108
|
+
"[bold white]STDOUT (incomplete due to timeout):[/bold white]"
|
|
109
|
+
)
|
|
110
|
+
console.print(
|
|
111
|
+
Syntax(
|
|
112
|
+
stdout.strip(),
|
|
113
|
+
"bash",
|
|
114
|
+
theme="monokai",
|
|
115
|
+
background_color="default",
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if stderr.strip():
|
|
120
|
+
console.print("[bold yellow]STDERR:[/bold yellow]")
|
|
121
|
+
console.print(
|
|
122
|
+
Syntax(
|
|
123
|
+
stderr.strip(),
|
|
124
|
+
"bash",
|
|
125
|
+
theme="monokai",
|
|
126
|
+
background_color="default",
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
console.print(
|
|
131
|
+
f"[bold red]⏱ Command timed out after {timeout} seconds[/bold red] [dim](ran for {execution_time:.2f}s)[/dim]"
|
|
132
|
+
)
|
|
133
|
+
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
"success": False,
|
|
137
|
+
"command": command,
|
|
138
|
+
"stdout": stdout,
|
|
139
|
+
"stderr": stderr,
|
|
140
|
+
"exit_code": None, # No exit code since the process was killed
|
|
141
|
+
"execution_time": execution_time,
|
|
142
|
+
"timeout": True,
|
|
143
|
+
"error": f"Command timed out after {timeout} seconds",
|
|
144
|
+
}
|
|
145
|
+
except Exception as e:
|
|
146
|
+
# Display error information
|
|
147
|
+
console.print_exception(show_locals=True)
|
|
148
|
+
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
"success": False,
|
|
152
|
+
"command": command,
|
|
153
|
+
"error": f"Error executing command: {str(e)}",
|
|
154
|
+
"stdout": "",
|
|
155
|
+
"stderr": "",
|
|
156
|
+
"exit_code": -1,
|
|
157
|
+
"timeout": False,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@code_generation_agent.tool
|
|
162
|
+
def share_your_reasoning(
|
|
163
|
+
context: RunContext, reasoning: str, next_steps: str = None
|
|
164
|
+
) -> Dict[str, Any]:
|
|
165
|
+
"""Share the agent's current reasoning and planned next steps with the user.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
reasoning: The agent's current reasoning or thought process.
|
|
169
|
+
next_steps: Optional description of what the agent plans to do next.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
A dictionary with the reasoning information.
|
|
173
|
+
"""
|
|
174
|
+
console.print("\n[bold white on purple] AGENT REASONING [/bold white on purple]")
|
|
175
|
+
|
|
176
|
+
# Display the reasoning with markdown formatting
|
|
177
|
+
console.print("[bold cyan]Current reasoning:[/bold cyan]")
|
|
178
|
+
console.print(Markdown(reasoning))
|
|
179
|
+
|
|
180
|
+
# Display next steps if provided
|
|
181
|
+
if next_steps and next_steps.strip():
|
|
182
|
+
console.print("\n[bold cyan]Planned next steps:[/bold cyan]")
|
|
183
|
+
console.print(Markdown(next_steps))
|
|
184
|
+
|
|
185
|
+
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
186
|
+
|
|
187
|
+
return {"success": True, "reasoning": reasoning, "next_steps": next_steps}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# file_modifications.py
|
|
2
|
+
import os
|
|
3
|
+
import difflib
|
|
4
|
+
from code_agent.tools.common import console
|
|
5
|
+
from typing import Dict, Any, Optional
|
|
6
|
+
from code_agent.agent import code_generation_agent
|
|
7
|
+
from pydantic_ai import RunContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import difflib
|
|
12
|
+
from code_agent.tools.common import console
|
|
13
|
+
from typing import Dict, Any, Optional
|
|
14
|
+
from code_agent.agent import code_generation_agent
|
|
15
|
+
from pydantic_ai import RunContext
|
|
16
|
+
|
|
17
|
+
@code_generation_agent.tool
|
|
18
|
+
def modify_file(
|
|
19
|
+
context: RunContext,
|
|
20
|
+
file_path: str,
|
|
21
|
+
proposed_changes: str,
|
|
22
|
+
replace_content: str,
|
|
23
|
+
overwrite_entire_file: bool = False
|
|
24
|
+
) -> Dict[str, Any]:
|
|
25
|
+
"""Modify a file with proposed changes, generating a diff and applying the changes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
file_path: Path of the file to modify.
|
|
29
|
+
proposed_changes: The new content to replace the targeted section or entire file content.
|
|
30
|
+
replace_content: The content to replace. If blank or not present in the file, the whole file will be replaced ONLY if overwrite_entire_file is True.
|
|
31
|
+
overwrite_entire_file: Explicitly allow replacing the entire file content (default False). You MUST supply True to allow this.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A dictionary with the operation result, including success status, message, and diff.
|
|
35
|
+
"""
|
|
36
|
+
file_path = os.path.abspath(file_path)
|
|
37
|
+
|
|
38
|
+
console.print("\n[bold white on yellow] FILE MODIFICATION [/bold white on yellow]")
|
|
39
|
+
console.print(f"[bold yellow]Modifying:[/bold yellow] {file_path}")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Check if the file exists
|
|
43
|
+
if not os.path.exists(file_path):
|
|
44
|
+
console.print(f"[bold red]Error:[/bold red] File '{file_path}' does not exist")
|
|
45
|
+
return {"error": f"File '{file_path}' does not exist"}
|
|
46
|
+
|
|
47
|
+
if not os.path.isfile(file_path):
|
|
48
|
+
console.print(f"[bold red]Error:[/bold red] '{file_path}' is not a file")
|
|
49
|
+
return {"error": f"'{file_path}' is not a file."}
|
|
50
|
+
|
|
51
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
52
|
+
current_content = f.read()
|
|
53
|
+
|
|
54
|
+
# Decide how to modify
|
|
55
|
+
targeted_replacement = bool(replace_content) and (replace_content in current_content)
|
|
56
|
+
replace_content_provided = bool(replace_content)
|
|
57
|
+
|
|
58
|
+
if targeted_replacement:
|
|
59
|
+
modified_content = current_content.replace(replace_content, proposed_changes)
|
|
60
|
+
console.print(f"[cyan]Replacing targeted content in '{file_path}'[/cyan]")
|
|
61
|
+
elif not targeted_replacement:
|
|
62
|
+
# Only allow full replacement if explicitly authorized
|
|
63
|
+
if overwrite_entire_file:
|
|
64
|
+
modified_content = proposed_changes
|
|
65
|
+
if replace_content_provided:
|
|
66
|
+
console.print(f"[bold yellow]Target content not found—replacing the entire file by explicit request (overwrite_entire_file=True).[/bold yellow]")
|
|
67
|
+
else:
|
|
68
|
+
console.print(f"[bold yellow]No target provided—replacing the entire file by explicit request (overwrite_entire_file=True).[/bold yellow]")
|
|
69
|
+
else:
|
|
70
|
+
if not replace_content_provided:
|
|
71
|
+
msg = "Refusing to replace the entire file: No replace_content provided and overwrite_entire_file=False."
|
|
72
|
+
else:
|
|
73
|
+
msg = "Refusing to replace the entire file: Target content not found in file and overwrite_entire_file=False."
|
|
74
|
+
console.print(f"[bold red]Error:[/bold red] {msg}")
|
|
75
|
+
return {
|
|
76
|
+
"success": False,
|
|
77
|
+
"path": file_path,
|
|
78
|
+
"message": msg,
|
|
79
|
+
"diff": "",
|
|
80
|
+
"changed": False,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Generate a diff for display
|
|
84
|
+
diff_lines = list(difflib.unified_diff(
|
|
85
|
+
current_content.splitlines(keepends=True),
|
|
86
|
+
modified_content.splitlines(keepends=True),
|
|
87
|
+
fromfile=f"a/{os.path.basename(file_path)}",
|
|
88
|
+
tofile=f"b/{os.path.basename(file_path)}",
|
|
89
|
+
n=3,
|
|
90
|
+
))
|
|
91
|
+
diff_text = "".join(diff_lines)
|
|
92
|
+
console.print("[bold cyan]Changes to be applied:[/bold cyan]")
|
|
93
|
+
if diff_text.strip():
|
|
94
|
+
formatted_diff = ""
|
|
95
|
+
for line in diff_lines:
|
|
96
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
97
|
+
formatted_diff += f"[bold green]{line}[/bold green]"
|
|
98
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
99
|
+
formatted_diff += f"[bold red]{line}[/bold red]"
|
|
100
|
+
elif line.startswith("@"):
|
|
101
|
+
formatted_diff += f"[bold cyan]{line}[/bold cyan]"
|
|
102
|
+
else:
|
|
103
|
+
formatted_diff += line
|
|
104
|
+
console.print(formatted_diff)
|
|
105
|
+
else:
|
|
106
|
+
console.print("[dim]No changes detected - file content is identical[/dim]")
|
|
107
|
+
return {
|
|
108
|
+
"success": False,
|
|
109
|
+
"path": file_path,
|
|
110
|
+
"message": "No changes to apply.",
|
|
111
|
+
"diff": diff_text,
|
|
112
|
+
"changed": False,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Write the modified content to the file
|
|
116
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
117
|
+
f.write(modified_content)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"success": True,
|
|
121
|
+
"path": file_path,
|
|
122
|
+
"message": f"File modified at '{file_path}'",
|
|
123
|
+
"diff": diff_text,
|
|
124
|
+
"changed": True,
|
|
125
|
+
}
|
|
126
|
+
except Exception as e:
|
|
127
|
+
return {"error": f"Error modifying file '{file_path}': {str(e)}"}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@code_generation_agent.tool
|
|
131
|
+
def delete_snippet_from_file(context: RunContext, file_path: str, snippet: str) -> Dict[str, Any]:
|
|
132
|
+
console.log(f"🗑️ Deleting snippet from file [bold red]{file_path}[/bold red]")
|
|
133
|
+
"""Delete a snippet from a file at the given file path.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
file_path: Path to the file to delete.
|
|
137
|
+
snippet: The snippet to delete.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
A dictionary with status and message about the operation.
|
|
141
|
+
"""
|
|
142
|
+
file_path = os.path.abspath(file_path)
|
|
143
|
+
|
|
144
|
+
console.print("\n[bold white on red] SNIPPET DELETION [/bold white on red]")
|
|
145
|
+
console.print(f"[bold yellow]From file:[/bold yellow] {file_path}")
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
# Check if the file exists
|
|
149
|
+
if not os.path.exists(file_path):
|
|
150
|
+
console.print(f"[bold red]Error:[/bold red] File '{file_path}' does not exist")
|
|
151
|
+
return {"error": f"File '{file_path}' does not exist."}
|
|
152
|
+
|
|
153
|
+
# Check if it's a file (not a directory)
|
|
154
|
+
if not os.path.isfile(file_path):
|
|
155
|
+
console.print(f"[bold red]Error:[/bold red] '{file_path}' is not a file")
|
|
156
|
+
return {"error": f"'{file_path}' is not a file. Use rmdir for directories."}
|
|
157
|
+
|
|
158
|
+
# Read the file content
|
|
159
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
160
|
+
content = f.read()
|
|
161
|
+
|
|
162
|
+
# Check if the snippet exists in the file
|
|
163
|
+
if snippet not in content:
|
|
164
|
+
console.print(f"[bold red]Error:[/bold red] Snippet not found in file '{file_path}'")
|
|
165
|
+
return {"error": f"Snippet not found in file '{file_path}'."}
|
|
166
|
+
|
|
167
|
+
# Remove the snippet from the file content
|
|
168
|
+
modified_content = content.replace(snippet, "")
|
|
169
|
+
|
|
170
|
+
# Generate a diff
|
|
171
|
+
diff_lines = list(
|
|
172
|
+
difflib.unified_diff(
|
|
173
|
+
content.splitlines(keepends=True),
|
|
174
|
+
modified_content.splitlines(keepends=True),
|
|
175
|
+
fromfile=f"a/{os.path.basename(file_path)}",
|
|
176
|
+
tofile=f"b/{os.path.basename(file_path)}",
|
|
177
|
+
n=3, # Context lines
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
diff_text = "".join(diff_lines)
|
|
182
|
+
|
|
183
|
+
# Display the diff
|
|
184
|
+
console.print("[bold cyan]Changes to be applied:[/bold cyan]")
|
|
185
|
+
|
|
186
|
+
if diff_text.strip():
|
|
187
|
+
# Format the diff for display with colorization
|
|
188
|
+
formatted_diff = ""
|
|
189
|
+
for line in diff_lines:
|
|
190
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
191
|
+
formatted_diff += f"[bold green]{line}[/bold green]"
|
|
192
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
193
|
+
formatted_diff += f"[bold red]{line}[/bold red]"
|
|
194
|
+
elif line.startswith("@"):
|
|
195
|
+
formatted_diff += f"[bold cyan]{line}[/bold cyan]"
|
|
196
|
+
else:
|
|
197
|
+
formatted_diff += line
|
|
198
|
+
|
|
199
|
+
console.print(formatted_diff)
|
|
200
|
+
else:
|
|
201
|
+
console.print("[dim]No changes detected[/dim]")
|
|
202
|
+
return {
|
|
203
|
+
"success": False,
|
|
204
|
+
"path": file_path,
|
|
205
|
+
"message": "No changes needed.",
|
|
206
|
+
"diff": "",
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# Write the modified content back to the file
|
|
210
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
211
|
+
f.write(modified_content)
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
"success": True,
|
|
215
|
+
"path": file_path,
|
|
216
|
+
"message": f"Snippet deleted from file '{file_path}'.",
|
|
217
|
+
"diff": diff_text,
|
|
218
|
+
}
|
|
219
|
+
except PermissionError:
|
|
220
|
+
return {"error": f"Permission denied to delete '{file_path}'."}
|
|
221
|
+
except FileNotFoundError:
|
|
222
|
+
# This should be caught by the initial check, but just in case
|
|
223
|
+
return {"error": f"File '{file_path}' does not exist."}
|
|
224
|
+
except Exception as e:
|
|
225
|
+
return {"error": f"Error deleting file '{file_path}': {str(e)}"}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@code_generation_agent.tool
|
|
229
|
+
def delete_file(context: RunContext, file_path: str) -> Dict[str, Any]:
|
|
230
|
+
console.log(f"🗑️ Deleting file [bold red]{file_path}[/bold red]")
|
|
231
|
+
"""Delete a file at the given file path.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
file_path: Path to the file to delete.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
A dictionary with status and message about the operation.
|
|
238
|
+
"""
|
|
239
|
+
file_path = os.path.abspath(file_path)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
# Check if the file exists
|
|
243
|
+
if not os.path.exists(file_path):
|
|
244
|
+
return {"error": f"File '{file_path}' does not exist."}
|
|
245
|
+
|
|
246
|
+
# Check if it's a file (not a directory)
|
|
247
|
+
if not os.path.isfile(file_path):
|
|
248
|
+
return {"error": f"'{file_path}' is not a file. Use rmdir for directories."}
|
|
249
|
+
|
|
250
|
+
# Attempt to delete the file
|
|
251
|
+
os.remove(file_path)
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"success": True,
|
|
255
|
+
"path": file_path,
|
|
256
|
+
"message": f"File '{file_path}' deleted successfully.",
|
|
257
|
+
}
|
|
258
|
+
except PermissionError:
|
|
259
|
+
return {"error": f"Permission denied to delete '{file_path}'."}
|
|
260
|
+
except FileNotFoundError:
|
|
261
|
+
# This should be caught by the initial check, but just in case
|
|
262
|
+
return {"error": f"File '{file_path}' does not exist."}
|
|
263
|
+
except Exception as e:
|
|
264
|
+
return {"error": f"Error deleting file '{file_path}': {str(e)}"}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# file_operations.py
|
|
2
|
+
import os
|
|
3
|
+
import fnmatch
|
|
4
|
+
from typing import List, Dict, Any
|
|
5
|
+
from code_agent.tools.common import console
|
|
6
|
+
from pydantic_ai import RunContext
|
|
7
|
+
from code_agent.agent import code_generation_agent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Constants for file operations
|
|
11
|
+
IGNORE_PATTERNS = [
|
|
12
|
+
"**/node_modules/**",
|
|
13
|
+
"**/.git/**",
|
|
14
|
+
"**/__pycache__/**",
|
|
15
|
+
"**/.DS_Store",
|
|
16
|
+
"**/.env",
|
|
17
|
+
"**/.venv/**",
|
|
18
|
+
"**/venv/**",
|
|
19
|
+
"**/.idea/**",
|
|
20
|
+
"**/.vscode/**",
|
|
21
|
+
"**/dist/**",
|
|
22
|
+
"**/build/**",
|
|
23
|
+
"**/*.pyc",
|
|
24
|
+
"**/*.pyo",
|
|
25
|
+
"**/*.pyd",
|
|
26
|
+
"**/*.so",
|
|
27
|
+
"**/*.dll",
|
|
28
|
+
"**/*.exe",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def should_ignore_path(path: str) -> bool:
|
|
33
|
+
"""Check if the path should be ignored based on patterns."""
|
|
34
|
+
for pattern in IGNORE_PATTERNS:
|
|
35
|
+
if fnmatch.fnmatch(path, pattern):
|
|
36
|
+
return True
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@code_generation_agent.tool
|
|
41
|
+
def list_files(
|
|
42
|
+
context: RunContext, directory: str = ".", recursive: bool = True
|
|
43
|
+
) -> List[Dict[str, Any]]:
|
|
44
|
+
"""Recursively list all files in a directory, ignoring common patterns.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
directory: The directory to list files from. Defaults to current directory.
|
|
48
|
+
recursive: Whether to search recursively. Defaults to True.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A list of dictionaries with file information including path, size, and type.
|
|
52
|
+
"""
|
|
53
|
+
results = []
|
|
54
|
+
directory = os.path.abspath(directory)
|
|
55
|
+
|
|
56
|
+
# Display directory listing header
|
|
57
|
+
console.print("\n[bold white on blue] DIRECTORY LISTING [/bold white on blue]")
|
|
58
|
+
console.print(
|
|
59
|
+
f"📂 [bold cyan]{directory}[/bold cyan] [dim](recursive={recursive})[/dim]"
|
|
60
|
+
)
|
|
61
|
+
console.print("[dim]" + "-" * 60 + "[/dim]")
|
|
62
|
+
|
|
63
|
+
if not os.path.exists(directory):
|
|
64
|
+
console.print(
|
|
65
|
+
f"[bold red]Error:[/bold red] Directory '{directory}' does not exist"
|
|
66
|
+
)
|
|
67
|
+
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
68
|
+
return [{"error": f"Directory '{directory}' does not exist"}]
|
|
69
|
+
|
|
70
|
+
if not os.path.isdir(directory):
|
|
71
|
+
console.print(f"[bold red]Error:[/bold red] '{directory}' is not a directory")
|
|
72
|
+
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
73
|
+
return [{"error": f"'{directory}' is not a directory"}]
|
|
74
|
+
|
|
75
|
+
# Track folders and files at each level for tree display
|
|
76
|
+
folder_structure = {}
|
|
77
|
+
file_list = []
|
|
78
|
+
|
|
79
|
+
for root, dirs, files in os.walk(directory):
|
|
80
|
+
# Skip ignored directories
|
|
81
|
+
dirs[:] = [d for d in dirs if not should_ignore_path(os.path.join(root, d))]
|
|
82
|
+
|
|
83
|
+
rel_path = os.path.relpath(root, directory)
|
|
84
|
+
depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
|
|
85
|
+
|
|
86
|
+
if rel_path == ".":
|
|
87
|
+
rel_path = ""
|
|
88
|
+
|
|
89
|
+
# Add directory entry to results
|
|
90
|
+
if rel_path:
|
|
91
|
+
dir_path = os.path.join(directory, rel_path)
|
|
92
|
+
results.append(
|
|
93
|
+
{
|
|
94
|
+
"path": rel_path,
|
|
95
|
+
"type": "directory",
|
|
96
|
+
"size": 0,
|
|
97
|
+
"full_path": dir_path,
|
|
98
|
+
"depth": depth,
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Add to folder structure for display
|
|
103
|
+
folder_structure[rel_path] = {
|
|
104
|
+
"path": rel_path,
|
|
105
|
+
"depth": depth,
|
|
106
|
+
"full_path": dir_path,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Add file entries
|
|
110
|
+
for file in files:
|
|
111
|
+
file_path = os.path.join(root, file)
|
|
112
|
+
if should_ignore_path(file_path):
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
rel_file_path = os.path.join(rel_path, file) if rel_path else file
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
size = os.path.getsize(file_path)
|
|
119
|
+
file_info = {
|
|
120
|
+
"path": rel_file_path,
|
|
121
|
+
"type": "file",
|
|
122
|
+
"size": size,
|
|
123
|
+
"full_path": file_path,
|
|
124
|
+
"depth": depth,
|
|
125
|
+
}
|
|
126
|
+
results.append(file_info)
|
|
127
|
+
file_list.append(file_info)
|
|
128
|
+
except (FileNotFoundError, PermissionError):
|
|
129
|
+
# Skip files we can't access
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
if not recursive:
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
# Helper function to format file size
|
|
136
|
+
def format_size(size_bytes):
|
|
137
|
+
if size_bytes < 1024:
|
|
138
|
+
return f"{size_bytes} B"
|
|
139
|
+
elif size_bytes < 1024 * 1024:
|
|
140
|
+
return f"{size_bytes / 1024:.1f} KB"
|
|
141
|
+
elif size_bytes < 1024 * 1024 * 1024:
|
|
142
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
143
|
+
else:
|
|
144
|
+
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
|
|
145
|
+
|
|
146
|
+
# Helper function to get file icon based on extension
|
|
147
|
+
def get_file_icon(file_path):
|
|
148
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
149
|
+
if ext in [".py", ".pyw"]:
|
|
150
|
+
return "🐍" # Python
|
|
151
|
+
elif ext in [".js", ".jsx", ".ts", ".tsx"]:
|
|
152
|
+
return "📜" # JavaScript/TypeScript
|
|
153
|
+
elif ext in [".html", ".htm", ".xml"]:
|
|
154
|
+
return "🌐" # HTML/XML
|
|
155
|
+
elif ext in [".css", ".scss", ".sass"]:
|
|
156
|
+
return "🎨" # CSS
|
|
157
|
+
elif ext in [".md", ".markdown", ".rst"]:
|
|
158
|
+
return "📝" # Markdown/docs
|
|
159
|
+
elif ext in [".json", ".yaml", ".yml", ".toml"]:
|
|
160
|
+
return "⚙️" # Config files
|
|
161
|
+
elif ext in [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp"]:
|
|
162
|
+
return "🖼️" # Images
|
|
163
|
+
elif ext in [".mp3", ".wav", ".ogg", ".flac"]:
|
|
164
|
+
return "🎵" # Audio
|
|
165
|
+
elif ext in [".mp4", ".avi", ".mov", ".webm"]:
|
|
166
|
+
return "🎬" # Video
|
|
167
|
+
elif ext in [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"]:
|
|
168
|
+
return "📄" # Documents
|
|
169
|
+
elif ext in [".zip", ".tar", ".gz", ".rar", ".7z"]:
|
|
170
|
+
return "📦" # Archives
|
|
171
|
+
elif ext in [".exe", ".dll", ".so", ".dylib"]:
|
|
172
|
+
return "⚡" # Executables
|
|
173
|
+
else:
|
|
174
|
+
return "📄" # Default file icon
|
|
175
|
+
|
|
176
|
+
# Display tree structure
|
|
177
|
+
if results:
|
|
178
|
+
# Sort directories and files
|
|
179
|
+
directories = sorted(
|
|
180
|
+
[d for d in results if d["type"] == "directory"], key=lambda x: x["path"]
|
|
181
|
+
)
|
|
182
|
+
files = sorted(
|
|
183
|
+
[f for f in results if f["type"] == "file"], key=lambda x: x["path"]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# First show directory itself
|
|
187
|
+
console.print(
|
|
188
|
+
f"📁 [bold blue]{os.path.basename(directory) or directory}[/bold blue]"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# After gathering all results
|
|
192
|
+
# Combine both directories and files, then sort
|
|
193
|
+
all_items = sorted(results, key=lambda x: x['path'])
|
|
194
|
+
|
|
195
|
+
current_depth = 0
|
|
196
|
+
parent_dirs_with_content = set()
|
|
197
|
+
|
|
198
|
+
for i, item in enumerate(all_items):
|
|
199
|
+
# Skip root directory
|
|
200
|
+
if item["type"] == "directory" and not item["path"]:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
# Get parent directories to track which ones have content
|
|
204
|
+
if os.sep in item["path"]:
|
|
205
|
+
parent_path = os.path.dirname(item["path"])
|
|
206
|
+
parent_dirs_with_content.add(parent_path)
|
|
207
|
+
|
|
208
|
+
# Calculate depth from path
|
|
209
|
+
depth = item["path"].count(os.sep) + 1 if item["path"] else 0
|
|
210
|
+
|
|
211
|
+
# Calculate prefix for tree structure
|
|
212
|
+
prefix = ""
|
|
213
|
+
for d in range(depth):
|
|
214
|
+
if d == depth - 1:
|
|
215
|
+
prefix += "└── "
|
|
216
|
+
else:
|
|
217
|
+
prefix += " "
|
|
218
|
+
|
|
219
|
+
# Display item with appropriate icon and color
|
|
220
|
+
name = os.path.basename(item["path"]) or item["path"]
|
|
221
|
+
|
|
222
|
+
if item["type"] == "directory":
|
|
223
|
+
console.print(f"{prefix}📁 [bold blue]{name}/[/bold blue]")
|
|
224
|
+
else: # file
|
|
225
|
+
icon = get_file_icon(item["path"])
|
|
226
|
+
size_str = format_size(item["size"])
|
|
227
|
+
console.print(
|
|
228
|
+
f"{prefix}{icon} [green]{name}[/green] [dim]({size_str})[/dim]"
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
console.print("[yellow]Directory is empty[/yellow]")
|
|
232
|
+
|
|
233
|
+
# Display summary
|
|
234
|
+
dir_count = sum(1 for item in results if item["type"] == "directory")
|
|
235
|
+
file_count = sum(1 for item in results if item["type"] == "file")
|
|
236
|
+
total_size = sum(item["size"] for item in results if item["type"] == "file")
|
|
237
|
+
|
|
238
|
+
console.print("\n[bold cyan]Summary:[/bold cyan]")
|
|
239
|
+
console.print(
|
|
240
|
+
f"📁 [blue]{dir_count} directories[/blue], 📄 [green]{file_count} files[/green] [dim]({format_size(total_size)} total)[/dim]"
|
|
241
|
+
)
|
|
242
|
+
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
243
|
+
|
|
244
|
+
return results
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@code_generation_agent.tool
|
|
248
|
+
def create_file(
|
|
249
|
+
context: RunContext, file_path: str, content: str = ""
|
|
250
|
+
) -> Dict[str, Any]:
|
|
251
|
+
console.log(f"✨ Creating new file [bold green]{file_path}[/bold green]")
|
|
252
|
+
"""Create a new file with optional content.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
file_path: Path where the file should be created
|
|
256
|
+
content: Optional content to write to the file
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
A dictionary with the result of the operation
|
|
260
|
+
"""
|
|
261
|
+
file_path = os.path.abspath(file_path)
|
|
262
|
+
|
|
263
|
+
# Check if file already exists
|
|
264
|
+
if os.path.exists(file_path):
|
|
265
|
+
return {
|
|
266
|
+
"error": f"File '{file_path}' already exists. Use modify_file to edit it."
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# Create parent directories if they don't exist
|
|
270
|
+
directory = os.path.dirname(file_path)
|
|
271
|
+
if directory and not os.path.exists(directory):
|
|
272
|
+
try:
|
|
273
|
+
os.makedirs(directory)
|
|
274
|
+
except Exception as e:
|
|
275
|
+
return {"error": f"Error creating directory '{directory}': {str(e)}"}
|
|
276
|
+
|
|
277
|
+
# Create the file
|
|
278
|
+
try:
|
|
279
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
280
|
+
console.print("[yellow]Writing to file:[/yellow]")
|
|
281
|
+
console.print(content)
|
|
282
|
+
f.write(content)
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
"success": True,
|
|
286
|
+
"path": file_path,
|
|
287
|
+
"message": f"File created at '{file_path}'",
|
|
288
|
+
"content_length": len(content),
|
|
289
|
+
}
|
|
290
|
+
except Exception as e:
|
|
291
|
+
return {"error": f"Error creating file '{file_path}': {str(e)}"}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@code_generation_agent.tool
|
|
295
|
+
def read_file(
|
|
296
|
+
context: RunContext, file_path: str, start_line: int = 0, end_line: int = None
|
|
297
|
+
) -> Dict[str, Any]:
|
|
298
|
+
console.log(
|
|
299
|
+
f"📄 Reading [bold cyan]{file_path}[/bold cyan] (lines {start_line} to {end_line or 'end'})"
|
|
300
|
+
)
|
|
301
|
+
"""Read the contents of a file, optionally within a line range.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
file_path: Path to the file to read
|
|
305
|
+
start_line: Starting line number (0-indexed). Defaults to 0.
|
|
306
|
+
end_line: Ending line number (inclusive, 0-indexed). Defaults to None (read to end).
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
A dictionary with the file contents and metadata.
|
|
310
|
+
"""
|
|
311
|
+
file_path = os.path.abspath(file_path)
|
|
312
|
+
|
|
313
|
+
if not os.path.exists(file_path):
|
|
314
|
+
return {"error": f"File '{file_path}' does not exist"}
|
|
315
|
+
|
|
316
|
+
if not os.path.isfile(file_path):
|
|
317
|
+
return {"error": f"'{file_path}' is not a file"}
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
321
|
+
lines = f.readlines()
|
|
322
|
+
|
|
323
|
+
# Handle line range
|
|
324
|
+
if end_line is None:
|
|
325
|
+
end_line = len(lines) - 1
|
|
326
|
+
|
|
327
|
+
# Ensure valid range
|
|
328
|
+
start_line = max(0, min(start_line, len(lines) - 1))
|
|
329
|
+
end_line = max(start_line, min(end_line, len(lines) - 1))
|
|
330
|
+
|
|
331
|
+
selected_lines = lines[start_line : end_line + 1]
|
|
332
|
+
content = "".join(selected_lines)
|
|
333
|
+
|
|
334
|
+
# Get file extension
|
|
335
|
+
_, ext = os.path.splitext(file_path)
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
"content": content,
|
|
339
|
+
"path": file_path,
|
|
340
|
+
"extension": ext.lstrip("."),
|
|
341
|
+
"total_lines": len(lines),
|
|
342
|
+
"read_lines": end_line - start_line + 1,
|
|
343
|
+
"start_line": start_line,
|
|
344
|
+
"end_line": end_line,
|
|
345
|
+
}
|
|
346
|
+
except UnicodeDecodeError:
|
|
347
|
+
# For binary files, return an error
|
|
348
|
+
return {"error": f"Cannot read '{file_path}' as text - it may be a binary file"}
|
|
349
|
+
except Exception as e:
|
|
350
|
+
return {"error": f"Error reading file '{file_path}': {str(e)}"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from code_agent.agent import code_generation_agent
|
|
2
|
+
from typing import List, Dict
|
|
3
|
+
import requests
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
from pydantic_ai import RunContext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@code_generation_agent.tool
|
|
9
|
+
def web_search(
|
|
10
|
+
context: RunContext, query: str, num_results: int = 5
|
|
11
|
+
) -> List[Dict[str, str]]:
|
|
12
|
+
"""Perform a web search and return a list of results with titles and URLs.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
query: The search query.
|
|
16
|
+
num_results: Number of results to return. Defaults to 5.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
A list of dictionaries, each containing 'title' and 'url' for a search result.
|
|
20
|
+
"""
|
|
21
|
+
search_url = "https://www.google.com/search"
|
|
22
|
+
headers = {
|
|
23
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
|
|
24
|
+
}
|
|
25
|
+
params = {"q": query}
|
|
26
|
+
|
|
27
|
+
response = requests.get(search_url, headers=headers, params=params)
|
|
28
|
+
response.raise_for_status()
|
|
29
|
+
|
|
30
|
+
soup = BeautifulSoup(response.text, "html.parser")
|
|
31
|
+
results = []
|
|
32
|
+
|
|
33
|
+
for g in soup.find_all("div", class_="tF2Cxc")[:num_results]:
|
|
34
|
+
title_element = g.find("h3")
|
|
35
|
+
link_element = g.find("a")
|
|
36
|
+
if title_element and link_element:
|
|
37
|
+
title = title_element.get_text()
|
|
38
|
+
url = link_element["href"]
|
|
39
|
+
results.append({"title": title, "url": url})
|
|
40
|
+
|
|
41
|
+
return results
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: code-puppy
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Code generation agent similar to Windsurf or Cursor
|
|
5
|
+
Author: Windsurf Engineering Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Requires-Dist: bs4>=0.0.2
|
|
16
|
+
Requires-Dist: httpx>=0.24.1
|
|
17
|
+
Requires-Dist: logfire>=0.7.1
|
|
18
|
+
Requires-Dist: pydantic-ai>=0.1.0
|
|
19
|
+
Requires-Dist: pydantic>=2.4.0
|
|
20
|
+
Requires-Dist: pytest-cov>=6.1.1
|
|
21
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
22
|
+
Requires-Dist: rich>=13.4.2
|
|
23
|
+
Requires-Dist: ruff>=0.11.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Code Generation Agent
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
29
|
+
|
|
30
|
+
This project is a sophisticated AI-powered code generation agent, designed to understand programming tasks, generate high-quality code, and explain its reasoning similar to tools like Windsurf and Cursor.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Multi-language support**: Capable of generating code in various programming languages.
|
|
35
|
+
- **Interactive CLI**: A command-line interface for interactive use.
|
|
36
|
+
- **Detailed explanations**: Provides insights into generated code to understand its logic and structure.
|
|
37
|
+
- **Easy Integration**: Embed it seamlessly into Python projects.
|
|
38
|
+
|
|
39
|
+
## New Feature
|
|
40
|
+
- **Real-time collaboration**: Allows multiple users to collaboratively edit and review code generation tasks in real-time.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
> **NOTE:** This project uses [astral-sh/uv](https://github.com/astral-sh/uv) for all dependency management and builds. Please install [uv](https://github.com/astral-sh/uv) before continuing.
|
|
45
|
+
|
|
46
|
+
1. **Clone the repository**:
|
|
47
|
+
```bash
|
|
48
|
+
git clone <repository_url>
|
|
49
|
+
cd <repository_name>
|
|
50
|
+
```
|
|
51
|
+
2. **Install dependencies**:
|
|
52
|
+
```bash
|
|
53
|
+
uv pip install -e .
|
|
54
|
+
```
|
|
55
|
+
3. **(optional)** If contributing, install additional development dependencies:
|
|
56
|
+
```bash
|
|
57
|
+
uv pip install -r dev-requirements.txt # If present
|
|
58
|
+
```
|
|
59
|
+
4. **Configure environment variables**:
|
|
60
|
+
- Create an `.env` file in the root, using `.env.example` as a template, to store required API keys.
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
### Command Line Interface
|
|
65
|
+
|
|
66
|
+
Run specific tasks or engage in interactive mode:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Execute a task directly
|
|
70
|
+
uv run python main.py "write me a C++ hello world program in /tmp/main.cpp then compile it and run it"
|
|
71
|
+
|
|
72
|
+
# Enter interactive mode
|
|
73
|
+
uv run python main.py --interactive
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Python API
|
|
77
|
+
|
|
78
|
+
Utilize the agent programmatically within your Python scripts:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import asyncio
|
|
82
|
+
from code_agent.agent_tools import generate_code
|
|
83
|
+
|
|
84
|
+
async def main():
|
|
85
|
+
task = "Your task description"
|
|
86
|
+
response = await generate_code(None, task)
|
|
87
|
+
|
|
88
|
+
if response.success:
|
|
89
|
+
for snippet in response.snippets:
|
|
90
|
+
print(f"Language: {snippet.language}")
|
|
91
|
+
print(snippet.code)
|
|
92
|
+
print(snippet.explanation)
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
asyncio.run(main())
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Explore the `examples` directory for elaborated utilization samples.
|
|
99
|
+
|
|
100
|
+
## Project Structure
|
|
101
|
+
|
|
102
|
+
- **`code_agent/agent.py`** - Core functionalities of the agent.
|
|
103
|
+
- **`code_agent/agent_tools.py`** - Tools and utilities for code generation.
|
|
104
|
+
- **`code_agent/agent_prompts.py`** - Templates and prompts used by the system.
|
|
105
|
+
- **`code_agent/models/`** - Data models for defining code and responses.
|
|
106
|
+
- **`examples/`** - Example scripts showcasing agent capabilities.
|
|
107
|
+
- **`main.py`** - Entry point for command-line interactions.
|
|
108
|
+
|
|
109
|
+
## Contributing
|
|
110
|
+
|
|
111
|
+
Contributions are welcome! Please follow these steps:
|
|
112
|
+
|
|
113
|
+
1. Fork the repository.
|
|
114
|
+
2. Create a new branch (`git checkout -b feature/xyz`).
|
|
115
|
+
3. Commit your changes (`git commit -m 'Add feature'`).
|
|
116
|
+
4. Push to the branch (`git push origin feature/xyz`).
|
|
117
|
+
5. Open a Pull Request.
|
|
118
|
+
|
|
119
|
+
## Requirements
|
|
120
|
+
|
|
121
|
+
- Python 3.9+
|
|
122
|
+
- [uv](https://github.com/astral-sh/uv) (for dependency management & builds)
|
|
123
|
+
- OpenAI API key (for GPT models)
|
|
124
|
+
- Optionally: Gemini API key (for Google's Gemini models)
|
|
125
|
+
|
|
126
|
+
## Troubleshooting
|
|
127
|
+
|
|
128
|
+
- Ensure all dependencies are installed correctly via uv and the environment is properly configured.
|
|
129
|
+
- Check that API keys are valid and not expired.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
code_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
code_agent/agent.py,sha256=kA_goXZ9zT87yPkfpyBIjzz0N3rfUn-C88wAV8iK7pA,689
|
|
3
|
+
code_agent/agent_prompts.py,sha256=6DCCsFIxG8pCSa_uING_mITkQ30N1gh7FBITCl18rJY,3171
|
|
4
|
+
code_agent/main.py,sha256=QuhbNK6bDsu5yRmut2pI174feZPN6WB-2mQsmWZ0yrI,8806
|
|
5
|
+
code_agent/models/__init__.py,sha256=mczyauLuqkIeujryzootps3Zpu5ch2ZrQRAYpurXAE8,132
|
|
6
|
+
code_agent/models/codesnippet.py,sha256=5fgH5HoxE3YZX7DsVwywsZ2-XXotnUfHdvEuzHk0oX8,523
|
|
7
|
+
code_agent/tools/__init__.py,sha256=VOubgURQJYsL5Xp-44rOX5MVwO58PLQ8tMhxNo6ZTWs,157
|
|
8
|
+
code_agent/tools/command_runner.py,sha256=Bz8tDEL7_9MGARX2ARto9xQxa1GtUVkSOgEFCJzCcFs,6514
|
|
9
|
+
code_agent/tools/common.py,sha256=UpMqeJ0C8i0pkue1AHnnyyX0bFJ9zZeJ7HBR6yhuA8A,54
|
|
10
|
+
code_agent/tools/file_modifications.py,sha256=FeDVi-pcKp0q5QJnZsnXaiBn0VDT2Z5pD2cl-fnVLLU,10672
|
|
11
|
+
code_agent/tools/file_operations.py,sha256=NUHsHeO38ahO9sdOAjVES1UHj7-PyAVnyqnbpepRcTA,11747
|
|
12
|
+
code_agent/tools/web_search.py,sha256=vMgfg3g5lEVP2Kbqc1zUWKGwUnNmKJ0CY_zIUGKE7aw,1347
|
|
13
|
+
code_puppy-0.0.2.dist-info/METADATA,sha256=zfX-1rCFsYrCITbEW7E2wqvXBKFS1IQfBk-k4RtITBQ,4354
|
|
14
|
+
code_puppy-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
15
|
+
code_puppy-0.0.2.dist-info/entry_points.txt,sha256=qzzQH24Qc3KtiZ-FQMBU9ogKmzY_OKm4OtXQv2_IUTw,58
|
|
16
|
+
code_puppy-0.0.2.dist-info/RECORD,,
|