improve: generalize transformations and scripts of runner and preloads into TemplateTransformer (#4487)
parent
c255a20d7c
commit
5f4df34829
@ -1,58 +1,25 @@
|
|||||||
import json
|
from textwrap import dedent
|
||||||
import re
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from core.helper.code_executor.entities import CodeDependency
|
|
||||||
from core.helper.code_executor.template_transformer import TemplateTransformer
|
from core.helper.code_executor.template_transformer import TemplateTransformer
|
||||||
|
|
||||||
NODEJS_RUNNER = """// declare main function here
|
|
||||||
{{code}}
|
|
||||||
|
|
||||||
// execute main function, and return the result
|
|
||||||
// inputs is a dict, unstructured inputs
|
|
||||||
output = main({{inputs}})
|
|
||||||
|
|
||||||
// convert output to json and print
|
|
||||||
output = JSON.stringify(output)
|
|
||||||
|
|
||||||
result = `<<RESULT>>${output}<<RESULT>>`
|
|
||||||
|
|
||||||
console.log(result)
|
|
||||||
"""
|
|
||||||
|
|
||||||
NODEJS_PRELOAD = """"""
|
|
||||||
|
|
||||||
|
|
||||||
class NodeJsTemplateTransformer(TemplateTransformer):
|
class NodeJsTemplateTransformer(TemplateTransformer):
|
||||||
@classmethod
|
@classmethod
|
||||||
def transform_caller(cls, code: str, inputs: dict,
|
def get_runner_script(cls) -> str:
|
||||||
dependencies: Optional[list[CodeDependency]] = None) -> tuple[str, str, list[CodeDependency]]:
|
runner_script = dedent(
|
||||||
"""
|
f"""
|
||||||
Transform code to python runner
|
// declare main function
|
||||||
:param code: code
|
{cls._code_placeholder}
|
||||||
:param inputs: inputs
|
|
||||||
:return:
|
// decode and prepare input object
|
||||||
"""
|
var inputs_obj = JSON.parse(atob('{cls._inputs_placeholder}'))
|
||||||
|
|
||||||
# transform inputs to json string
|
// execute main function
|
||||||
inputs_str = json.dumps(inputs, indent=4, ensure_ascii=False)
|
var output_obj = main(inputs_obj)
|
||||||
|
|
||||||
# replace code and inputs
|
// convert output to json and print
|
||||||
runner = NODEJS_RUNNER.replace('{{code}}', code)
|
var output_json = JSON.stringify(output_obj)
|
||||||
runner = runner.replace('{{inputs}}', inputs_str)
|
var result = `<<RESULT>>${{output_json}}<<RESULT>>`
|
||||||
|
console.log(result)
|
||||||
return runner, NODEJS_PRELOAD, []
|
""")
|
||||||
|
return runner_script
|
||||||
@classmethod
|
|
||||||
def transform_response(cls, response: str) -> dict:
|
|
||||||
"""
|
|
||||||
Transform response to dict
|
|
||||||
:param response: response
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
# extract result
|
|
||||||
result = re.search(r'<<RESULT>>(.*)<<RESULT>>', response, re.DOTALL)
|
|
||||||
if not result:
|
|
||||||
raise ValueError('Failed to parse result')
|
|
||||||
result = result.group(1)
|
|
||||||
return json.loads(result)
|
|
||||||
|
|||||||
@ -1,108 +1,64 @@
|
|||||||
import json
|
from textwrap import dedent
|
||||||
import re
|
|
||||||
from base64 import b64encode
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from core.helper.code_executor.entities import CodeDependency
|
from core.helper.code_executor.python3.python3_transformer import Python3TemplateTransformer
|
||||||
from core.helper.code_executor.python3.python3_transformer import PYTHON_STANDARD_PACKAGES
|
|
||||||
from core.helper.code_executor.template_transformer import TemplateTransformer
|
from core.helper.code_executor.template_transformer import TemplateTransformer
|
||||||
|
|
||||||
PYTHON_RUNNER = """
|
|
||||||
import jinja2
|
|
||||||
from json import loads
|
|
||||||
from base64 import b64decode
|
|
||||||
|
|
||||||
template = jinja2.Template('''{{code}}''')
|
|
||||||
|
|
||||||
def main(**inputs):
|
|
||||||
return template.render(**inputs)
|
|
||||||
|
|
||||||
# execute main function, and return the result
|
|
||||||
inputs = b64decode('{{inputs}}').decode('utf-8')
|
|
||||||
output = main(**loads(inputs))
|
|
||||||
|
|
||||||
result = f'''<<RESULT>>{output}<<RESULT>>'''
|
|
||||||
|
|
||||||
print(result)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
JINJA2_PRELOAD_TEMPLATE = """{% set fruits = ['Apple'] %}
|
|
||||||
{{ 'a' }}
|
|
||||||
{% for fruit in fruits %}
|
|
||||||
<li>{{ fruit }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
{% if fruits|length > 1 %}
|
|
||||||
1
|
|
||||||
{% endif %}
|
|
||||||
{% for i in range(5) %}
|
|
||||||
{% if i == 3 %}{{ i }}{% else %}{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% for i in range(3) %}
|
|
||||||
{{ i + 1 }}
|
|
||||||
{% endfor %}
|
|
||||||
{% macro say_hello() %}a{{ 'b' }}{% endmacro %}
|
|
||||||
{{ s }}{{ say_hello() }}"""
|
|
||||||
|
|
||||||
JINJA2_PRELOAD = f"""
|
|
||||||
import jinja2
|
|
||||||
from base64 import b64decode
|
|
||||||
|
|
||||||
def _jinja2_preload_():
|
|
||||||
# prepare jinja2 environment, load template and render before to avoid sandbox issue
|
|
||||||
template = jinja2.Template('''{JINJA2_PRELOAD_TEMPLATE}''')
|
|
||||||
template.render(s='a')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
_jinja2_preload_()
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Jinja2TemplateTransformer(TemplateTransformer):
|
class Jinja2TemplateTransformer(TemplateTransformer):
|
||||||
@classmethod
|
@classmethod
|
||||||
def transform_caller(cls, code: str, inputs: dict,
|
def get_standard_packages(cls) -> set[str]:
|
||||||
dependencies: Optional[list[CodeDependency]] = None) -> tuple[str, str, list[CodeDependency]]:
|
return {'jinja2'} | Python3TemplateTransformer.get_standard_packages()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def transform_response(cls, response: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Transform code to python runner
|
Transform response to dict
|
||||||
:param code: code
|
:param response: response
|
||||||
:param inputs: inputs
|
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
return {
|
||||||
|
'result': cls.extract_result_str_from_response(response)
|
||||||
|
}
|
||||||
|
|
||||||
inputs_str = b64encode(json.dumps(inputs, ensure_ascii=False).encode()).decode('utf-8')
|
@classmethod
|
||||||
|
def get_runner_script(cls) -> str:
|
||||||
|
runner_script = dedent(f"""
|
||||||
|
# declare main function
|
||||||
|
def main(**inputs):
|
||||||
|
import jinja2
|
||||||
|
template = jinja2.Template('''{cls._code_placeholder}''')
|
||||||
|
return template.render(**inputs)
|
||||||
|
|
||||||
# transform jinja2 template to python code
|
import json
|
||||||
runner = PYTHON_RUNNER.replace('{{code}}', code)
|
from base64 import b64decode
|
||||||
runner = runner.replace('{{inputs}}', inputs_str)
|
|
||||||
|
|
||||||
if not dependencies:
|
# decode and prepare input dict
|
||||||
dependencies = []
|
inputs_obj = json.loads(b64decode('{cls._inputs_placeholder}').decode('utf-8'))
|
||||||
|
|
||||||
# add native packages and jinja2
|
# execute main function
|
||||||
for package in PYTHON_STANDARD_PACKAGES.union(['jinja2']):
|
output = main(**inputs_obj)
|
||||||
dependencies.append(CodeDependency(name=package, version=''))
|
|
||||||
|
|
||||||
# deduplicate
|
# convert output and print
|
||||||
dependencies = list({
|
result = f'''<<RESULT>>{{output}}<<RESULT>>'''
|
||||||
dep.name: dep for dep in dependencies if dep.name
|
print(result)
|
||||||
}.values())
|
|
||||||
|
|
||||||
return runner, JINJA2_PRELOAD, dependencies
|
""")
|
||||||
|
return runner_script
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def transform_response(cls, response: str) -> dict:
|
def get_preload_script(cls) -> str:
|
||||||
"""
|
preload_script = dedent("""
|
||||||
Transform response to dict
|
import jinja2
|
||||||
:param response: response
|
from base64 import b64decode
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
# extract result
|
|
||||||
result = re.search(r'<<RESULT>>(.*)<<RESULT>>', response, re.DOTALL)
|
|
||||||
if not result:
|
|
||||||
raise ValueError('Failed to parse result')
|
|
||||||
result = result.group(1)
|
|
||||||
|
|
||||||
return {
|
def _jinja2_preload_():
|
||||||
'result': result
|
# prepare jinja2 environment, load template and render before to avoid sandbox issue
|
||||||
}
|
template = jinja2.Template('{{s}}')
|
||||||
|
template.render(s='a')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
_jinja2_preload_()
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
return preload_script
|
||||||
|
|||||||
@ -1,83 +1,51 @@
|
|||||||
import json
|
|
||||||
import re
|
|
||||||
from base64 import b64encode
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from core.helper.code_executor.entities import CodeDependency
|
|
||||||
from core.helper.code_executor.template_transformer import TemplateTransformer
|
from core.helper.code_executor.template_transformer import TemplateTransformer
|
||||||
|
|
||||||
PYTHON_RUNNER = dedent("""
|
|
||||||
# declare main function here
|
|
||||||
{{code}}
|
|
||||||
|
|
||||||
from json import loads, dumps
|
|
||||||
from base64 import b64decode
|
|
||||||
|
|
||||||
# execute main function, and return the result
|
|
||||||
# inputs is a dict, and it
|
|
||||||
inputs = b64decode('{{inputs}}').decode('utf-8')
|
|
||||||
output = main(**json.loads(inputs))
|
|
||||||
|
|
||||||
# convert output to json and print
|
|
||||||
output = dumps(output, indent=4)
|
|
||||||
|
|
||||||
result = f'''<<RESULT>>
|
|
||||||
{output}
|
|
||||||
<<RESULT>>'''
|
|
||||||
|
|
||||||
print(result)
|
|
||||||
""")
|
|
||||||
|
|
||||||
PYTHON_PRELOAD = """"""
|
|
||||||
|
|
||||||
PYTHON_STANDARD_PACKAGES = {
|
|
||||||
'json', 'datetime', 'math', 'random', 're', 'string', 'sys', 'time', 'traceback', 'uuid', 'os', 'base64',
|
|
||||||
'hashlib', 'hmac', 'binascii', 'collections', 'functools', 'operator', 'itertools', 'uuid',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Python3TemplateTransformer(TemplateTransformer):
|
class Python3TemplateTransformer(TemplateTransformer):
|
||||||
@classmethod
|
@classmethod
|
||||||
def transform_caller(cls, code: str, inputs: dict,
|
def get_standard_packages(cls) -> set[str]:
|
||||||
dependencies: Optional[list[CodeDependency]] = None) -> tuple[str, str, list[CodeDependency]]:
|
return {
|
||||||
"""
|
'base64',
|
||||||
Transform code to python runner
|
'binascii',
|
||||||
:param code: code
|
'collections',
|
||||||
:param inputs: inputs
|
'datetime',
|
||||||
:return:
|
'functools',
|
||||||
"""
|
'hashlib',
|
||||||
|
'hmac',
|
||||||
# transform inputs to json string
|
'itertools',
|
||||||
inputs_str = b64encode(json.dumps(inputs, ensure_ascii=False).encode()).decode('utf-8')
|
'json',
|
||||||
|
'math',
|
||||||
# replace code and inputs
|
'operator',
|
||||||
runner = PYTHON_RUNNER.replace('{{code}}', code)
|
'os',
|
||||||
runner = runner.replace('{{inputs}}', inputs_str)
|
'random',
|
||||||
|
're',
|
||||||
# add standard packages
|
'string',
|
||||||
if dependencies is None:
|
'sys',
|
||||||
dependencies = []
|
'time',
|
||||||
|
'traceback',
|
||||||
for package in PYTHON_STANDARD_PACKAGES:
|
'uuid',
|
||||||
if package not in dependencies:
|
}
|
||||||
dependencies.append(CodeDependency(name=package, version=''))
|
|
||||||
|
|
||||||
# deduplicate
|
|
||||||
dependencies = list({dep.name: dep for dep in dependencies if dep.name}.values())
|
|
||||||
|
|
||||||
return runner, PYTHON_PRELOAD, dependencies
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def transform_response(cls, response: str) -> dict:
|
def get_runner_script(cls) -> str:
|
||||||
"""
|
runner_script = dedent(f"""
|
||||||
Transform response to dict
|
# declare main function
|
||||||
:param response: response
|
{cls._code_placeholder}
|
||||||
:return:
|
|
||||||
"""
|
import json
|
||||||
# extract result
|
from base64 import b64decode
|
||||||
result = re.search(r'<<RESULT>>(.*?)<<RESULT>>', response, re.DOTALL)
|
|
||||||
if not result:
|
# decode and prepare input dict
|
||||||
raise ValueError('Failed to parse result')
|
inputs_obj = json.loads(b64decode('{cls._inputs_placeholder}').decode('utf-8'))
|
||||||
result = result.group(1)
|
|
||||||
return json.loads(result)
|
# execute main function
|
||||||
|
output_obj = main(**inputs_obj)
|
||||||
|
|
||||||
|
# convert output to json and print
|
||||||
|
output_json = json.dumps(output_obj, indent=4)
|
||||||
|
result = f'''<<RESULT>>{{output_json}}<<RESULT>>'''
|
||||||
|
print(result)
|
||||||
|
""")
|
||||||
|
return runner_script
|
||||||
|
|||||||
Loading…
Reference in New Issue