Spaces:
Running
Running
FIX: Modify test_app_py tool to use free port and fix linters
Browse files- app.py +30 -6
- kiss_agent.py +43 -92
app.py
CHANGED
@@ -39,6 +39,19 @@ signal.signal(signal.SIGINT, signal_handler)
|
|
39 |
atexit.register(cleanup_preview_on_exit)
|
40 |
|
41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
def get_preview_url():
|
43 |
"""Get the appropriate preview URL with a cache-busting timestamp."""
|
44 |
# Append a timestamp as a query parameter to break browser cache
|
@@ -277,10 +290,12 @@ def create_iframe_preview():
|
|
277 |
<div style="color: #d32f2f; padding: 20px; text-align: center;
|
278 |
border: 1px solid #d32f2f; border-radius: 8px;
|
279 |
background: #ffebee;">
|
280 |
-
<h3>🚧 Preview App Temporarily Unavailable</h3>
|
281 |
-
<p><strong>Status:</strong> {message}</p>
|
282 |
-
<p
|
283 |
-
|
|
|
|
|
284 |
<button onclick="location.reload()" style="
|
285 |
background: #1976d2; color: white; border: none;
|
286 |
padding: 8px 16px; border-radius: 4px; cursor: pointer;">
|
@@ -893,19 +908,28 @@ if __name__ == "__main__":
|
|
893 |
print(f"❌ Failed to start preview app: {message}")
|
894 |
|
895 |
# Parse command line arguments for server configuration
|
896 |
-
|
897 |
server_name = "127.0.0.1" # default
|
898 |
|
899 |
if "--server-port" in sys.argv:
|
900 |
port_idx = sys.argv.index("--server-port")
|
901 |
if port_idx + 1 < len(sys.argv):
|
902 |
-
|
903 |
|
904 |
if "--server-name" in sys.argv:
|
905 |
name_idx = sys.argv.index("--server-name")
|
906 |
if name_idx + 1 < len(sys.argv):
|
907 |
server_name = sys.argv[name_idx + 1]
|
908 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
909 |
GradioUI(agent).launch(
|
910 |
share=False, server_port=server_port, server_name=server_name
|
911 |
)
|
|
|
39 |
atexit.register(cleanup_preview_on_exit)
|
40 |
|
41 |
|
42 |
+
def find_free_port(start_port=7860, max_ports=100):
|
43 |
+
"""Find an available TCP port, starting from a given port."""
|
44 |
+
for port in range(start_port, start_port + max_ports):
|
45 |
+
try:
|
46 |
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
47 |
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
48 |
+
sock.bind(("127.0.0.1", port))
|
49 |
+
return port
|
50 |
+
except OSError:
|
51 |
+
print(f" Port {port} is in use, trying next...")
|
52 |
+
return None
|
53 |
+
|
54 |
+
|
55 |
def get_preview_url():
|
56 |
"""Get the appropriate preview URL with a cache-busting timestamp."""
|
57 |
# Append a timestamp as a query parameter to break browser cache
|
|
|
290 |
<div style="color: #d32f2f; padding: 20px; text-align: center;
|
291 |
border: 1px solid #d32f2f; border-radius: 8px;
|
292 |
background: #ffebee;">
|
293 |
+
<h3 style="color: #d32f2f;">🚧 Preview App Temporarily Unavailable</h3>
|
294 |
+
<p style="color: #333333;"><strong>Status:</strong> {message}</p>
|
295 |
+
<p style="color: #333333;">
|
296 |
+
The preview app is starting up. Please wait a few seconds
|
297 |
+
and try refreshing.
|
298 |
+
</p>
|
299 |
<button onclick="location.reload()" style="
|
300 |
background: #1976d2; color: white; border: none;
|
301 |
padding: 8px 16px; border-radius: 4px; cursor: pointer;">
|
|
|
908 |
print(f"❌ Failed to start preview app: {message}")
|
909 |
|
910 |
# Parse command line arguments for server configuration
|
911 |
+
server_port_arg = 7860 # default
|
912 |
server_name = "127.0.0.1" # default
|
913 |
|
914 |
if "--server-port" in sys.argv:
|
915 |
port_idx = sys.argv.index("--server-port")
|
916 |
if port_idx + 1 < len(sys.argv):
|
917 |
+
server_port_arg = int(sys.argv[port_idx + 1])
|
918 |
|
919 |
if "--server-name" in sys.argv:
|
920 |
name_idx = sys.argv.index("--server-name")
|
921 |
if name_idx + 1 < len(sys.argv):
|
922 |
server_name = sys.argv[name_idx + 1]
|
923 |
|
924 |
+
# Find an available port for the main app, starting with the desired one
|
925 |
+
server_port = find_free_port(server_port_arg)
|
926 |
+
if server_port is None:
|
927 |
+
print(f"❌ Could not find any available ports starting from {server_port_arg}")
|
928 |
+
sys.exit(1)
|
929 |
+
|
930 |
+
if server_port != server_port_arg:
|
931 |
+
print(f"⚠️ Port {server_port_arg} was busy. Running on free port: {server_port}")
|
932 |
+
|
933 |
GradioUI(agent).launch(
|
934 |
share=False, server_port=server_port, server_name=server_name
|
935 |
)
|
kiss_agent.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
import os
|
2 |
import re
|
|
|
3 |
import subprocess
|
4 |
from pathlib import Path
|
5 |
|
@@ -164,6 +165,20 @@ def install_package(package_name: str) -> str:
|
|
164 |
pass
|
165 |
|
166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
167 |
@tool
|
168 |
def python_editor(diff_content: str, filename: str = "app.py") -> str:
|
169 |
"""
|
@@ -321,113 +336,49 @@ def file_viewer(filename: str) -> str:
|
|
321 |
@tool
|
322 |
def test_app_py() -> str:
|
323 |
"""
|
324 |
-
Test
|
325 |
-
|
326 |
-
|
327 |
-
Returns:
|
328 |
-
Test result message
|
329 |
"""
|
330 |
try:
|
331 |
app_path = Path("sandbox") / "app.py"
|
332 |
-
|
333 |
if not app_path.exists():
|
334 |
-
return "Error: app.py
|
335 |
|
336 |
-
#
|
337 |
-
|
|
|
|
|
338 |
|
339 |
-
|
340 |
-
os.chdir(app_path.parent)
|
341 |
|
342 |
-
#
|
343 |
process = subprocess.Popen(
|
344 |
-
["python", "
|
345 |
stdout=subprocess.PIPE,
|
346 |
stderr=subprocess.PIPE,
|
347 |
text=True,
|
348 |
)
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
error_indicators = [
|
358 |
-
"error",
|
359 |
-
"exception",
|
360 |
-
"traceback",
|
361 |
-
"attributeerror",
|
362 |
-
"importerror",
|
363 |
-
"modulenotfounderror",
|
364 |
-
]
|
365 |
-
|
366 |
-
# Combine stdout and stderr for error checking
|
367 |
-
all_output = (stdout or "") + (stderr or "")
|
368 |
-
all_output_lower = all_output.lower()
|
369 |
-
|
370 |
-
# Check if any error indicators are present
|
371 |
-
has_error = any(
|
372 |
-
indicator in all_output_lower for indicator in error_indicators
|
373 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
374 |
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
# Check specifically for ModuleNotFoundError to
|
379 |
-
# suggest package installation
|
380 |
-
if (
|
381 |
-
"modulenotfounderror" in all_output_lower
|
382 |
-
or "no module named" in all_output_lower
|
383 |
-
):
|
384 |
-
# Try to extract the missing module name
|
385 |
-
import_pattern = (
|
386 |
-
r"(?:ModuleNotFoundError:|No module named) '?([^'\s]+)'?"
|
387 |
-
)
|
388 |
-
match = re.search(import_pattern, all_output, re.IGNORECASE)
|
389 |
-
missing_module = match.group(1) if match else "unknown"
|
390 |
-
return (
|
391 |
-
f"❌ ModuleNotFoundError: Missing module '{missing_module}'\n"
|
392 |
-
f"Consider using the install_package tool to install it.\n"
|
393 |
-
f"Full error:\n{error_output}"
|
394 |
-
)
|
395 |
-
|
396 |
-
return f"❌ Error running app.py:\n{error_output}"
|
397 |
-
else:
|
398 |
-
return "✅ app.py executed successfully"
|
399 |
-
|
400 |
-
except subprocess.TimeoutExpired:
|
401 |
-
# Process is still running after 3 seconds - likely a server
|
402 |
-
# This is actually good news for server apps like Gradio
|
403 |
-
process.terminate()
|
404 |
-
|
405 |
-
# Wait a bit for graceful termination
|
406 |
-
try:
|
407 |
-
process.wait(timeout=2)
|
408 |
-
except subprocess.TimeoutExpired:
|
409 |
-
# Force kill if it doesn't terminate gracefully
|
410 |
-
process.kill()
|
411 |
-
process.wait()
|
412 |
-
|
413 |
-
# Check if there were any immediate errors in stderr
|
414 |
-
try:
|
415 |
-
_, stderr = process.communicate(timeout=1)
|
416 |
-
if stderr and "error" in stderr.lower():
|
417 |
-
return f"❌ Error detected in app.py:\n{stderr}"
|
418 |
-
except Exception:
|
419 |
-
pass
|
420 |
-
|
421 |
-
return "✅ app.py started successfully (server detected and stopped)"
|
422 |
-
|
423 |
except Exception as e:
|
424 |
-
return f"
|
425 |
-
finally:
|
426 |
-
# Change back to original directory
|
427 |
-
try:
|
428 |
-
os.chdir(original_cwd)
|
429 |
-
except NameError:
|
430 |
-
pass
|
431 |
|
432 |
|
433 |
class KISSAgent(ToolCallingAgent):
|
|
|
1 |
import os
|
2 |
import re
|
3 |
+
import socket
|
4 |
import subprocess
|
5 |
from pathlib import Path
|
6 |
|
|
|
165 |
pass
|
166 |
|
167 |
|
168 |
+
def _find_free_port(start_port=7860, max_ports=100):
|
169 |
+
"""Find an available TCP port, starting from a given port."""
|
170 |
+
for port in range(start_port, start_port + max_ports):
|
171 |
+
try:
|
172 |
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
173 |
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
174 |
+
sock.bind(("127.0.0.1", port))
|
175 |
+
return port
|
176 |
+
except OSError:
|
177 |
+
# This port is in use, try the next one
|
178 |
+
continue
|
179 |
+
return None
|
180 |
+
|
181 |
+
|
182 |
@tool
|
183 |
def python_editor(diff_content: str, filename: str = "app.py") -> str:
|
184 |
"""
|
|
|
336 |
@tool
|
337 |
def test_app_py() -> str:
|
338 |
"""
|
339 |
+
Test the app.py file by running it as a subprocess and checking its output.
|
340 |
+
This tool will automatically find a free port to run the app on.
|
|
|
|
|
|
|
341 |
"""
|
342 |
try:
|
343 |
app_path = Path("sandbox") / "app.py"
|
|
|
344 |
if not app_path.exists():
|
345 |
+
return "Error: app.py not found in sandbox directory."
|
346 |
|
347 |
+
# Find a free port to avoid conflicts
|
348 |
+
free_port = _find_free_port()
|
349 |
+
if free_port is None:
|
350 |
+
return "Error: Could not find any free ports to run the test."
|
351 |
|
352 |
+
print(f"Found free port {free_port}, running test...")
|
|
|
353 |
|
354 |
+
# Run the app as a subprocess with a timeout
|
355 |
process = subprocess.Popen(
|
356 |
+
["python", str(app_path), "--server-port", str(free_port)],
|
357 |
stdout=subprocess.PIPE,
|
358 |
stderr=subprocess.PIPE,
|
359 |
text=True,
|
360 |
)
|
361 |
+
stdout, stderr = process.communicate(timeout=15)
|
362 |
+
|
363 |
+
if "Running on local URL" in stdout:
|
364 |
+
return "✅ Test passed: App launched successfully."
|
365 |
+
elif process.returncode != 0:
|
366 |
+
error_message = (
|
367 |
+
f"❌ Test failed: App exited with code {process.returncode}\n"
|
368 |
+
f"---STDERR---\n{stderr}\n---STDOUT---\n{stdout}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
369 |
)
|
370 |
+
return error_message
|
371 |
+
else:
|
372 |
+
inconclusive_message = (
|
373 |
+
f"⚠️ Test inconclusive: App ran without clear success message.\n"
|
374 |
+
f"---STDERR---\n{stderr}\n---STDOUT---\n{stdout}"
|
375 |
+
)
|
376 |
+
return inconclusive_message
|
377 |
|
378 |
+
except subprocess.TimeoutExpired:
|
379 |
+
return "❌ Test failed: App ran for over 15 seconds and was terminated."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
380 |
except Exception as e:
|
381 |
+
return f"❌ An unexpected error occurred during testing: {str(e)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
382 |
|
383 |
|
384 |
class KISSAgent(ToolCallingAgent):
|