Spaces:
Running
Running
File size: 13,068 Bytes
841b545 2915f7a 841b545 ac005e8 841b545 f0ca218 841b545 32abcae a648108 841b545 36e65eb 3458c60 36e65eb 3458c60 36e65eb 3458c60 36e65eb 841b545 2915f7a 841b545 a648108 841b545 a648108 7c71035 a648108 7c71035 a648108 7c71035 a648108 7c71035 a648108 7c71035 a648108 4725f58 2915f7a a648108 2915f7a a648108 2915f7a 841b545 8f63e88 841b545 8f63e88 841b545 4725f58 841b545 8f63e88 2915f7a 8f63e88 2915f7a 841b545 4725f58 8f63e88 4725f58 8f63e88 4725f58 2915f7a 4725f58 8f63e88 2915f7a 841b545 4725f58 841b545 eeb84e8 841b545 2915f7a a648108 2915f7a 841b545 eeb84e8 841b545 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 |
import os
import re
import subprocess
from pathlib import Path
from smolagents import LiteLLMModel, ToolCallingAgent, tool
from .settings import settings
PROMPT_TEMPLATE = """You are an expert software developer for Gradio.
Usually you are asked to devlop Gradio apps but sometimes you might just be asked a question.
In this case you can just answer it without using any tools, or use tools if you deem them helpful.
You are given a task to develop a Gradio application and can but don't have to use all the tools \
at your disposal to do so.
You always try to do everything inside of the app.py file. Only in rare cases \
you might need to edit or create other files.
The overarching goal is always to create a Gradio application.
**CRITICAL REQUIREMENT - ABSOLUTELY MANDATORY:**
Every app.py file you create MUST end with EXACTLY this code block:
```python
# Launch the app
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run the Gradio App")
parser.add_argument(
"--server-port", type=int, default=7860, help="Port to run the server on"
)
parser.add_argument(
"--server-name", type=str, default="0.0.0.0", help="Server name to bind to"
)
parser.add_argument("--root-path", type=str, help="Root path for the app")
args = parser.parse_args()
demo.launch(
server_name=args.server_name,
server_port=args.server_port,
root_path=args.root_path,
)
```
**THIS IS NOT OPTIONAL. THE APP WILL NOT WORK WITHOUT THIS EXACT CODE.**
**NEVER, UNDER ANY CIRCUMSTANCES, OMIT OR MODIFY THIS LAUNCH CODE.**
**IF YOU CREATE AN APP.PY WITHOUT THIS EXACT ENDING, THE APPLICATION WILL FAIL.**
Make sure to:
1. Import argparse at the top of the file
2. Name your Gradio interface `demo`
3. Include the EXACT launch code shown above at the very end
4. Adjust the description in argparse if needed for your specific app
Here is the user's request:
{task}
Always test the app.py file after you have made changes to it!
"""
@tool
def create_new_file(whole_edit: str) -> str:
"""
Create python files using aider's whole edit format. You can use this tool
to overwrite any python file or create a new one.
Your input should be a string in the following format:
[filename]
```python
[complete file content]
```
For example:
app.py
```python
import gradio as gr
...
```
Args:
whole_edit: The new complete content in whole edit format
Returns:
Status message indicating success or failure
"""
try:
# Split into lines and find first non-empty line as filename
lines = whole_edit.strip().split("\n")
if not lines:
return "Error: Empty input provided"
filename = next((line.strip() for line in lines if line.strip()), None)
if not filename:
return "Error: No filename found in input"
# Find code block content between ```
content = whole_edit.strip()
start_marker = content.find("```")
if start_marker == -1:
return "Error: No code block found"
# Find the end of the first line after ```
start_content = content.find("\n", start_marker) + 1
end_marker = content.find("```", start_content)
if end_marker == -1:
# No closing ```, take everything after the opening ```
file_content = content[start_content:]
else:
file_content = content[start_content:end_marker]
# Create the file path and write content
file_path = Path("./sandbox") / filename
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
f.write(file_content.rstrip()) # Remove trailing whitespace
line_count = (
len(file_content.strip().split("\n")) if file_content.strip() else 0
)
return f"Successfully wrote {line_count} lines to {filename}"
except Exception as e:
return f"Error applying whole edit: {str(e)}"
@tool
def install_package(package_name: str) -> str:
"""
Install a Python package using pip when encountering ModuleNotFoundError.
This tool installs the package to the user's local directory.
Args:
package_name: Name of the package to install (e.g., 'requests', 'pandas==2.0.0')
Returns:
Status message indicating success or failure
"""
try:
# Run pip install with --user flag to install to user's local directory
# This matches the Docker container setup with /home/user/.local/bin in PATH
result = subprocess.run(
["pip", "install", "--user", package_name],
capture_output=True,
text=True,
timeout=60, # 60 second timeout for package installation
)
if result.returncode == 0:
return f"β
Successfully installed {package_name} using pip"
else:
error_msg = result.stderr or result.stdout
return f"β Failed to install {package_name}:\n{error_msg}"
except subprocess.TimeoutExpired:
return f"β Installation of {package_name} timed out after 60 seconds"
except FileNotFoundError:
return "β Error: pip command not found. Please make sure pip is installed."
except Exception as e:
return f"β Error installing {package_name}: {str(e)}"
@tool
def python_editor(diff_content: str, filename: str = "app.py") -> str:
"""
This tool allows you to edit the code in a python file by applying a diff
edit to app.py using aider's diff edit format.
The input should be in the format:
[filename]
```
<<<<<<< SEARCH
[text to search for]
=======
[text to replace with]
>>>>>>> REPLACE
```
For multiple changes in the same file, include multiple search/replace blocks:
Example with multiple changes:
app.py
```
<<<<<<< SEARCH
def old_function():
return "old"
=======
def new_function():
return "new and improved"
>>>>>>> REPLACE
<<<<<<< SEARCH
title="Old Title"
=======
title="New Amazing Title"
>>>>>>> REPLACE
<<<<<<< SEARCH
# TODO: Add error handling
result = process_data()
=======
# Error handling implemented
try:
result = process_data()
except Exception as e:
result = f"Error: {e}"
>>>>>>> REPLACE
```
Important notes:
- Each search block must contain EXACT text that exists in the file
- Search text is case-sensitive and whitespace-sensitive
- You can have as many search/replace blocks as needed
- All changes are applied sequentially in the order they appear
- If any search text is not found, the entire operation fails
Args:
diff_content: The diff content in aider's diff format
filename: Name of the file to edit
Returns:
Status message indicating success or failure
"""
try:
app_path = Path("sandbox") / filename
if not app_path.exists():
return "Error: app.py does not exist. Use setup_project_structure first."
# Read current content
with open(app_path) as f:
current_content = f.read()
# Parse the diff format
lines = diff_content.strip().split("\n")
# Look for filename
filename_found = False
for i, line in enumerate(lines):
if line.strip() == "app.py":
filename_found = True
lines = lines[i + 1 :] # Remove filename line
break
if not filename_found:
return "Error: Expected filename 'app.py' not found in diff format"
# Remove code block markers if present
if lines and lines[0].strip().startswith("```"):
lines = lines[1:]
if lines and lines[-1].strip().startswith("```"):
lines = lines[:-1]
# Parse search/replace blocks
content_lines = "\n".join(lines)
# Find all search/replace blocks
search_replace_pattern = (
r"<<<<<<< SEARCH\n(.*?)\n=======\n(.*?)\n>>>>>>> REPLACE"
)
matches = re.findall(search_replace_pattern, content_lines, re.DOTALL)
if not matches:
return "Error: No valid search/replace blocks found in diff format"
# Apply each search/replace
modified_content = current_content
replacements_made = 0
for search_text, replace_text in matches:
# Clean up the search and replace text
search_text = search_text.strip()
replace_text = replace_text.strip()
if search_text in modified_content:
modified_content = modified_content.replace(search_text, replace_text)
replacements_made += 1
else:
return f"Error: Search text not found in app.py:\n{search_text}"
# Write the modified content back
with open(app_path, "w") as f:
f.write(modified_content)
return f"Successfully applied {replacements_made} diff replacements to app.py"
except Exception as e:
return f"Error applying diff edit: {str(e)}"
@tool
def file_explorer() -> str:
"""This tool shows you the file structure of your working directory.
Returns:
str: file structure of your working directory
"""
return "File structure of your working directory:\n" + "\n".join(
[f"- {file}" for file in os.listdir("sandbox")]
)
@tool
def file_viewer(filename: str) -> str:
"""This tool shows you the content of a file.
Args:
filename: Name of the file to view
Returns:
str: content of the file
"""
with open(Path("sandbox") / filename) as f:
return f.read()
@tool
def test_app_py() -> str:
"""
Test the app.py file by running it as a subprocess.
This test uses a hardcoded port (7865) to avoid conflicts.
A successful test means the app launches and runs without crashing.
"""
TEST_PORT = 7865
try:
app_path = Path("sandbox") / "app.py"
if not app_path.exists():
return "Error: app.py not found in sandbox directory."
print(f"--- Starting test on port {TEST_PORT} ---")
process = subprocess.Popen(
["python", str(app_path), "--server-port", str(TEST_PORT)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# The key is to see if the process crashes quickly.
# If it runs for a few seconds, it's considered a success.
try:
# Wait for 5 seconds. If it exits, it's a failure.
stdout, stderr = process.communicate(timeout=5)
error_message = (
f"β Test failed: App exited unexpectedly before timeout.\n"
f"---EXIT CODE---\n{process.returncode}\n"
f"---STDERR---\n{stderr}\n---STDOUT---\n{stdout}"
)
return error_message
except subprocess.TimeoutExpired:
# This is the SUCCESS case! The app ran for 5s without crashing.
print("β
Test successful: App process is stable.")
process.terminate() # Clean up the process
try:
process.wait(timeout=2) # Wait for graceful shutdown
except subprocess.TimeoutExpired:
process.kill() # Force kill if it doesn't respond
return "β
Test passed: App launched successfully."
except Exception as e:
return f"β An unexpected error occurred during testing: {str(e)}"
class KISSAgent(ToolCallingAgent):
def __init__(
self,
model_id: str | None = None,
api_base_url: str | None = None,
api_key: str | None = None,
prompt_template: str | None = None,
**kwargs,
):
model_id = model_id or settings.model_id
api_base_url = api_base_url or settings.api_base_url
api_key = api_key or settings.api_key
self.prompt_template = prompt_template or PROMPT_TEMPLATE
# Initialize the language model
model = LiteLLMModel(
model_id=model_id,
api_base=api_base_url,
api_key=api_key,
)
# Initialize the parent CodeAgent
super().__init__(
tools=[
python_editor,
test_app_py,
file_explorer,
file_viewer,
create_new_file,
install_package,
],
model=model,
add_base_tools=False,
**kwargs,
)
# TODO: add callback to manage memory to limit context window
def run(self, task: str, **kwargs) -> str:
"""Override run method to format prompt with task before calling parent run."""
formatted_prompt = self.prompt_template.format(task=task)
return super().run(formatted_prompt, **kwargs)
|