Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion lf_toolkit/io/file_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from .base_server import BaseServer
from .handler import Handler
from .handler import InvalidSubmissionError


class FileHandler(Handler):
Expand All @@ -16,8 +17,13 @@ async def dispatch(self, req: str) -> str:
try:
result = await self.handle(command, request)
response = {"command": command, "result": result}
except InvalidSubmissionError as e:
response = {
"command": command,
"error": {"message": str(e), "code": "INVALID_SUBMISSION"},
}
except Exception as e:
response = {"command": command, "error": str(e)}
response = {"command": command, "error": {"message": str(e)}}
return ujson.dumps(response)


Expand Down
9 changes: 9 additions & 0 deletions lf_toolkit/io/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
from ..shared import Command
from ..shared import Params

# JSON-RPC error code for submissions the user handler cannot process.
INVALID_SUBMISSION_CODE = 422


class InvalidSubmissionError(Exception):
"""Raised when the user handler cannot process the submitted response."""


class Handler(ABC):

Expand All @@ -40,6 +47,8 @@ async def _call_user_handler(self, req: str, *args, **kwargs):
return await handler(*args, **kwargs)
else:
return await anyio.to_thread.run_sync(handler, *args, **kwargs)
except ValueError as e:
raise InvalidSubmissionError(str(e)) from e
except Exception as e:
raise ValueError(f"Error calling user handler for '{req}': {e}")

Expand Down
4 changes: 4 additions & 0 deletions lf_toolkit/io/rpc_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from jsonrpcserver import async_dispatch

from ..shared import Command
from .handler import INVALID_SUBMISSION_CODE
from .handler import Handler
from .handler import InvalidSubmissionError


class JsonRpcHandler(Handler):
Expand All @@ -31,6 +33,8 @@ async def wrapped(req: dict):
try:
result = await handler.handle(name, {"params": req})
return Success(result)
except InvalidSubmissionError as e:
return Error(INVALID_SUBMISSION_CODE, str(e))
except Exception as e:
# Pass only the message: the exception object is not JSON
# serializable, so sending it as `data` makes serializing the
Expand Down
38 changes: 36 additions & 2 deletions tests/io/file_server_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ async def test_dispatch_exception_handling(self, handler):

assert response["command"] == "bad_command"
assert "error" in response
assert response["error"] == "Invalid command"
assert response["error"] == {"message": "Invalid command"}
assert "result" not in response

@pytest.mark.asyncio
Expand Down Expand Up @@ -195,7 +195,7 @@ async def test_run_with_handler_error(self, temp_files, tmp_path):

assert response["command"] == "failing_command"
assert "error" in response
assert response["error"] == "Processing failed"
assert response["error"] == {"message": "Processing failed"}

@pytest.mark.asyncio
async def test_run_missing_request_file(self, temp_files):
Expand Down Expand Up @@ -273,3 +273,37 @@ async def test_run_with_complex_nested_data(self, temp_files, tmp_path):

assert response["command"] == "eval"
assert response["result"] == {"processed": True}


class TestFileHandlerUserErrors:
"""Tests for errors raised by registered user handlers"""

@pytest.mark.asyncio
async def test_value_error_returns_invalid_submission(self):
def preview_fn(response, params):
raise ValueError(f"Failed to parse SymPy expression: {response}")

handler = FileHandler()
handler.register("preview", preview_fn)

request = ujson.dumps({"command": "preview", "params": {"response": "A/(w*"}})
response = ujson.loads(await handler.dispatch(request))

assert response["error"] == {
"message": "Failed to parse SymPy expression: A/(w*",
"code": "INVALID_SUBMISSION",
}

@pytest.mark.asyncio
async def test_unexpected_error_has_no_code(self):
def preview_fn(response, params):
raise RuntimeError("boom")

handler = FileHandler()
handler.register("preview", preview_fn)

request = ujson.dumps({"command": "preview", "params": {"response": "x"}})
response = ujson.loads(await handler.dispatch(request))

assert "code" not in response["error"]
assert "boom" in response["error"]["message"]
40 changes: 40 additions & 0 deletions tests/io/rpc_handler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,43 @@ def chat_health_fn() -> ChatHealthResponse:

assert response["result"]["status"] == "OK"
assert response["result"]["capabilities"]["supportsChat"] is True


class TestJsonRpcHandlerErrors:

@pytest.fixture
def handler(self):
return JsonRpcHandler()

@staticmethod
def _eval_request(response):
return ujson.dumps({
"jsonrpc": "2.0",
"method": "eval",
"params": [{"response": response, "answer": "x", "params": {}}],
"id": 1,
})

@pytest.mark.asyncio
async def test_value_error_returns_invalid_submission_code(self, handler):
def eval_fn(response, answer, params):
raise ValueError(f"Failed to parse SymPy expression: {response}")

handler.register("eval", eval_fn)

response = ujson.loads(await handler.dispatch(self._eval_request("A/(w*")))

assert response["error"]["code"] == 422
assert response["error"]["message"] == "Failed to parse SymPy expression: A/(w*"

@pytest.mark.asyncio
async def test_unexpected_error_returns_generic_code(self, handler):
def eval_fn(response, answer, params):
raise RuntimeError("boom")

handler.register("eval", eval_fn)

response = ujson.loads(await handler.dispatch(self._eval_request("x+1")))

assert response["error"]["code"] == 0
assert "boom" in response["error"]["message"]
Loading