Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better error handling for RunSkill and REPLs #458

Merged
merged 2 commits into from
Feb 24, 2024
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
7 changes: 7 additions & 0 deletions ix/skills/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@
description="Skill to run",
input_type="IX:skill",
),
NodeTypeField(
name="raise_error",
label="Halt on error",
type="boolean",
description="Halt on error by raising exception",
default=False,
),
],
)

Expand Down
68 changes: 62 additions & 6 deletions ix/skills/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
import json
import os
import subprocess
import sys
import textwrap
import traceback
from typing import Dict, Any, Optional

from langchain_experimental.utilities import PythonREPL
from pydantic import BaseModel


class MissingTypeHintError(Exception):
Expand Down Expand Up @@ -102,24 +105,77 @@ def parse_skill(
return func_name, input_schema, description


class ErrorResponse(BaseModel):
message: str
traceback: str
line: int


def execute_function(function, raise_errors: bool = False):
try:
result = function()
return result
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
line_number = exc_tb.tb_lineno
error_response = ErrorResponse(
message=str(e),
traceback="".join(traceback.format_tb(exc_tb)),
line=line_number,
)
if raise_errors:
raise e
else:
return error_response


def run_code_with_repl(
code: str, function: str, input: Dict[str, Any], timeout: Optional[int] = None
) -> str:
code: str,
function: str,
input: Dict[str, Any],
timeout: Optional[int] = None,
raise_errors: bool = False,
) -> Any:
# HAX: use globals for I/O with the REPL. Hacky way to avoid serialization.
func_output = []
repl = PythonREPL(
_globals={"func_input": input, "func_output": func_output, "json": json}
_globals={
"func_input": input,
"func_output": func_output,
"json": json,
"ErrorResponse": ErrorResponse,
}
)

# Prepare the command to run in the REPL
command = textwrap.dedent(
f"""
import traceback
import sys

{code}
output = {function}(**func_input)
func_output.append(output)

try:
output = {function}(**func_input)
func_output.append(output)
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
error_response = ErrorResponse(
message=str(e),
traceback=''.join(traceback.format_tb(exc_tb)),
line=exc_tb.tb_lineno
)
func_output.append(error_response)
if raise_errors:
raise e
"""
)

# Run the command in the PythonREPL
response = repl.run(command, timeout)
return func_output[0] if func_output else response
output = func_output[0] if func_output else response

if isinstance(output, ErrorResponse) and raise_errors:
raise Exception(output.message)

return output
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
"config_schema": {
"display_groups": null,
"properties": {
"raise_error": {
"default": false,
"description": "Halt on error by raising exception",
"label": "Halt on error",
"type": "boolean"
},
"skill_id": {
"description": "Skill to run",
"input_type": "IX:skill",
Expand Down Expand Up @@ -35,6 +41,23 @@
"step": null,
"style": null,
"type": "string"
},
{
"choices": null,
"default": false,
"description": "Halt on error by raising exception",
"init_type": "init",
"input_type": null,
"label": "Halt on error",
"max": null,
"min": null,
"name": "raise_error",
"parent": null,
"required": false,
"secret_key": null,
"step": null,
"style": null,
"type": "boolean"
}
],
"name": "Run Skill",
Expand Down
Loading