| | import json |
| | import re |
| | from ast import literal_eval |
| | from typing import Any, List, Union |
| |
|
| |
|
| | class ParseError(Exception): |
| | """Parsing exception class.""" |
| |
|
| | def __init__(self, err_msg: str): |
| | self.err_msg = err_msg |
| |
|
| |
|
| | class BaseParser: |
| | """Base parser to process inputs and outputs of actions. |
| | |
| | Args: |
| | action (:class:`BaseAction`): action to validate |
| | |
| | Attributes: |
| | PARAMETER_DESCRIPTION (:class:`str`): declare the input format which |
| | LLMs should follow when generating arguments for decided tools. |
| | """ |
| |
|
| | PARAMETER_DESCRIPTION: str = '' |
| |
|
| | def __init__(self, action): |
| | self.action = action |
| | self._api2param = {} |
| | self._api2required = {} |
| | |
| | if action.description: |
| | for api in action.description.get('api_list', |
| | [action.description]): |
| | name = (f'{action.name}.{api["name"]}' |
| | if self.action.is_toolkit else api['name']) |
| | required_parameters = set(api['required']) |
| | all_parameters = {j['name'] for j in api['parameters']} |
| | if not required_parameters.issubset(all_parameters): |
| | raise ValueError( |
| | f'unknown parameters for function "{name}": ' |
| | f'{required_parameters - all_parameters}') |
| | if self.PARAMETER_DESCRIPTION: |
| | api['parameter_description'] = self.PARAMETER_DESCRIPTION |
| | api_name = api['name'] if self.action.is_toolkit else 'run' |
| | self._api2param[api_name] = api['parameters'] |
| | self._api2required[api_name] = api['required'] |
| |
|
| | def parse_inputs(self, inputs: str, name: str = 'run') -> dict: |
| | """Parse inputs LLMs generate for the action. |
| | |
| | Args: |
| | inputs (:class:`str`): input string extracted from responses |
| | |
| | Returns: |
| | :class:`dict`: processed input |
| | """ |
| | inputs = {self._api2param[name][0]['name']: inputs} |
| | return inputs |
| |
|
| | def parse_outputs(self, outputs: Any) -> List[dict]: |
| | """Parser outputs returned by the action. |
| | |
| | Args: |
| | outputs (:class:`Any`): raw output of the action |
| | |
| | Returns: |
| | :class:`List[dict]`: processed output of which each member is a |
| | dictionary with two keys - 'type' and 'content'. |
| | """ |
| | if isinstance(outputs, dict): |
| | outputs = json.dumps(outputs, ensure_ascii=False) |
| | elif not isinstance(outputs, str): |
| | outputs = str(outputs) |
| | return [{ |
| | 'type': 'text', |
| | 'content': outputs.encode('gbk', 'ignore').decode('gbk') |
| | }] |
| |
|
| |
|
| | class JsonParser(BaseParser): |
| | """Json parser to convert input string into a dictionary. |
| | |
| | Args: |
| | action (:class:`BaseAction`): action to validate |
| | """ |
| |
|
| | PARAMETER_DESCRIPTION = ( |
| | 'If you call this tool, you must pass arguments in ' |
| | 'the JSON format {key: value}, where the key is the parameter name.') |
| |
|
| | def parse_inputs(self, |
| | inputs: Union[str, dict], |
| | name: str = 'run') -> dict: |
| | if not isinstance(inputs, dict): |
| | try: |
| | match = re.search(r'^\s*(```json\n)?(.*)\n```\s*$', inputs, |
| | re.S) |
| | if match: |
| | inputs = match.group(2).strip() |
| | inputs = json.loads(inputs) |
| | except json.JSONDecodeError as exc: |
| | raise ParseError(f'invalid json format: {inputs}') from exc |
| | input_keys = set(inputs) |
| | all_keys = {param['name'] for param in self._api2param[name]} |
| | if not input_keys.issubset(all_keys): |
| | raise ParseError(f'unknown arguments: {input_keys - all_keys}') |
| | required_keys = set(self._api2required[name]) |
| | if not input_keys.issuperset(required_keys): |
| | raise ParseError( |
| | f'missing required arguments: {required_keys - input_keys}') |
| | return inputs |
| |
|
| |
|
| | class TupleParser(BaseParser): |
| | """Tuple parser to convert input string into a tuple. |
| | |
| | Args: |
| | action (:class:`BaseAction`): action to validate |
| | """ |
| |
|
| | PARAMETER_DESCRIPTION = ( |
| | 'If you call this tool, you must pass arguments in the tuple format ' |
| | 'like (arg1, arg2, arg3), and the arguments are ordered.') |
| |
|
| | def parse_inputs(self, |
| | inputs: Union[str, tuple], |
| | name: str = 'run') -> dict: |
| | if not isinstance(inputs, tuple): |
| | try: |
| | inputs = literal_eval(inputs) |
| | except Exception as exc: |
| | raise ParseError(f'invalid tuple format: {inputs}') from exc |
| | if len(inputs) < len(self._api2required[name]): |
| | raise ParseError( |
| | f'API takes {len(self._api2required[name])} required positional ' |
| | f'arguments but {len(inputs)} were given') |
| | if len(inputs) > len(self._api2param[name]): |
| | raise ParseError( |
| | f'API takes {len(self._api2param[name])} positional arguments ' |
| | f'but {len(inputs)} were given') |
| | inputs = { |
| | self._api2param[name][i]['name']: item |
| | for i, item in enumerate(inputs) |
| | } |
| | return inputs |
| |
|