Skip to content

Commit 6bf1e64

Browse files
Merge pull request #98 from MarcSkovMadsen/enhancement/error-handling
Enhancement/error handling
2 parents 68c1383 + d6deb00 commit 6bf1e64

6 files changed

Lines changed: 75 additions & 37 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ pydata = [
9292
"polars",
9393
"scikit-learn",
9494
"seaborn",
95+
"yfinance",
9596
]
9697
[tool.ruff]
9798
exclude = [

src/holoviz_mcp/display_mcp/database.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424

2525
from holoviz_mcp.config import get_config
2626
from holoviz_mcp.config import logger
27+
from holoviz_mcp.display_mcp.utils import find_extensions
28+
from holoviz_mcp.display_mcp.utils import find_requirements
29+
from holoviz_mcp.display_mcp.utils import validate_code
2730

2831

2932
class Snippet(BaseModel):
@@ -435,7 +438,7 @@ def create_visualization(
435438
description: str = "",
436439
readme: str = "",
437440
method: Literal["jupyter", "panel"] = "jupyter",
438-
) -> dict[str, str]:
441+
) -> Snippet:
439442
"""Create a visualization request.
440443
441444
This is the core business logic for creating visualizations,
@@ -456,8 +459,8 @@ def create_visualization(
456459
457460
Returns
458461
-------
459-
dict[str, str]
460-
Dictionary with 'id', 'url', and 'created_at' keys
462+
Snippet
463+
The snippet created for the visualization request.
461464
462465
Raises
463466
------
@@ -468,10 +471,6 @@ def create_visualization(
468471
Exception
469472
If database operation or other errors occur
470473
"""
471-
# Import here to avoid circular dependency
472-
from holoviz_mcp.display_mcp.utils import find_extensions
473-
from holoviz_mcp.display_mcp.utils import find_requirements
474-
475474
# Validate app is not empty
476475
if not app:
477476
raise ValueError("App code is required")
@@ -481,6 +480,8 @@ def create_visualization(
481480
# Validate syntax
482481
ast.parse(app) # Raises SyntaxError if invalid
483482

483+
validation_result = validate_code(app)
484+
484485
# Infer requirements and extensions
485486
requirements = find_requirements(app)
486487
extensions = find_extensions(app) if method == "jupyter" else []
@@ -494,16 +495,14 @@ def create_visualization(
494495
method=method,
495496
requirements=requirements,
496497
extensions=extensions,
497-
status="pending",
498+
status="success" if not validation_result else "error",
499+
error_message=validation_result if validation_result else None,
498500
)
499501

500-
self.create_snippet(snippet_obj)
502+
snippet_saved = self.create_snippet(snippet_obj)
501503

502504
# Return result
503-
return {
504-
"id": snippet_obj.id,
505-
"created_at": snippet_obj.created_at.isoformat(),
506-
}
505+
return snippet_saved
507506

508507
@staticmethod
509508
def _row_to_snippet(row: dict) -> Snippet:

src/holoviz_mcp/display_mcp/endpoints.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,27 @@ def post(self):
3737
method = request_body.get("method", "jupyter")
3838

3939
# Call shared business logic
40-
result = db.create_visualization(
40+
snippet = db.create_visualization(
4141
app=code,
4242
name=name,
4343
description=description,
4444
method=method,
4545
)
46+
4647
if jupyter_base := os.getenv("JUPYTER_SERVER_PROXY_URL"):
4748
port = self.request.host.split(":")[-1]
4849
base_url = f"{jupyter_base.rstrip('/')}/{port}"
49-
url = f"{base_url}/view?id={result['id']}"
50+
url = f"{base_url}/view?id={snippet.id}"
5051
else:
5152
full_url = self.request.full_url()
52-
url = full_url.replace("/api/snippet", "/view?id=" + result["id"])
53+
url = full_url.replace("/api/snippet", "/view?id=" + snippet.id)
5354

54-
result["url"] = url
55+
result = {
56+
"id": snippet.id,
57+
"url": url,
58+
}
59+
if snippet.error_message:
60+
result["error_message"] = snippet.error_message
5561

5662
# Return success response
5763
self.set_status(200)

src/holoviz_mcp/display_mcp/utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import ast
44
import importlib.util
5+
import traceback
56
from typing import Any
67

78
# Check for pandas availability once at module level
@@ -144,3 +145,31 @@ def get_relative_view_url(id: str) -> str:
144145
Relative URL to view the visualization
145146
"""
146147
return f"./view?id={id}"
148+
149+
150+
def validate_code(code: str) -> str:
151+
"""
152+
Validate Python code by attempting to execute it.
153+
154+
Parameters
155+
----------
156+
code : str
157+
Python code to validate as a string.
158+
159+
Returns
160+
-------
161+
str
162+
An empty string if the code is valid, otherwise the traceback of the error.
163+
"""
164+
try:
165+
exec(code, {}, {})
166+
except Exception as e:
167+
# Get the traceback but skip the outermost frame (the exec call itself)
168+
if e.__traceback__ is not None:
169+
tb = e.__traceback__.tb_next # Skip the exec() frame
170+
else:
171+
tb = None
172+
traceback_str = "".join(traceback.format_exception(type(e), e, tb))
173+
traceback_str = traceback_str.strip()
174+
return traceback_str
175+
return ""

src/holoviz_mcp/holoviz_mcp/server.py

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -433,30 +433,17 @@ async def display(
433433
description=description,
434434
method=method,
435435
)
436+
url = response.get("url", "")
436437

437438
# Check for errors in response
438-
if "error" in response:
439-
error_type = response.get("error", "Unknown")
440-
message = response.get("message", "Unknown error")
441-
442-
if ctx:
443-
await ctx.error(f"Code execution failed: {error_type}: {message}")
444-
445-
# Return detailed error
446-
error_msg = f"Error: {error_type}\n\n{message}"
447-
448-
if "traceback" in response:
449-
error_msg += f"\n\nTraceback:\n{response['traceback']}"
439+
if error_message := response.get("error_message", None):
440+
return f"""
441+
Visualization created with errors. View [here]({url})
450442
451-
return error_msg
452-
453-
# Success - return URL
454-
url = response.get("url", "")
455-
456-
if ctx:
457-
await ctx.info(f"Created visualization: {url}")
443+
{error_message}
444+
"""
458445

459-
return f"Visualization created successfully!\n\nView at: {url}"
446+
return f"Visualization created successfully!\n\nView [here]({url})"
460447

461448
except Exception as e:
462449
logger.exception(f"Error creating visualization: {e}")

tests/display_mcp/test_utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,19 @@ def test_extract_last_expression_syntax_error(self):
9191

9292
with pytest.raises(ValueError, match="Syntax error"):
9393
extract_last_expression(code)
94+
95+
def test_validate_code_valid(self):
96+
"""Test validate_code with valid Python code."""
97+
from holoviz_mcp.display_mcp.utils import validate_code
98+
99+
code = "x = 1\ny = 2\nz = x + y"
100+
result = validate_code(code)
101+
assert result == ""
102+
103+
def test_validate_code_invalid(self):
104+
"""Test validate_code with invalid Python code."""
105+
from holoviz_mcp.display_mcp.utils import validate_code
106+
107+
code = "x = 1\ny = 2\nz = x + undefined_var"
108+
result = validate_code(code)
109+
assert "NameError" in result

0 commit comments

Comments
 (0)