Spaces:
Running
Running
| """Module for quick LLM evaluations""" | |
| from deepdiff import DeepDiff | |
| from chemgraph.schemas.ase_input import ASEInputSchema | |
| def remove_ignored_fields(obj, ignored_keys=("cell", "pbc")): | |
| """Remove ignored fields from object | |
| Args: | |
| obj (_type_): _description_ | |
| ignored_keys (tuple, optional): _description_. Defaults to ("cell", "pbc"). | |
| Returns: | |
| _type_: _description_ | |
| """ | |
| if isinstance(obj, dict): | |
| return { | |
| k: remove_ignored_fields(v, ignored_keys) | |
| for k, v in obj.items() | |
| if k not in ignored_keys | |
| } | |
| elif isinstance(obj, list): | |
| return [remove_ignored_fields(item, ignored_keys) for item in obj] | |
| else: | |
| return obj | |
| def apply_defaults(args: dict, schema: dict) -> dict: | |
| """Fill missing fields with default values from a JSON-like schema. | |
| Handles nested objects and anyOf/default combinations. | |
| Parameters | |
| ---------- | |
| args : dict | |
| Tool-call arguments to augment. | |
| schema : dict | |
| JSON schema containing properties and defaults. | |
| Returns | |
| ------- | |
| dict | |
| Arguments with applicable default values filled in. | |
| """ | |
| if not isinstance(args, dict): | |
| return args # Only process dicts | |
| args_with_defaults = dict(args) # shallow copy | |
| properties = schema.get("properties", {}) | |
| for key, prop_schema in properties.items(): | |
| # Skip if already set | |
| if key in args_with_defaults: | |
| # Recurse into nested object | |
| if isinstance(args_with_defaults[key], dict) and prop_schema.get("type") == "object": | |
| args_with_defaults[key] = apply_defaults(args_with_defaults[key], prop_schema) | |
| continue | |
| # Handle default at top level | |
| if "default" in prop_schema: | |
| args_with_defaults[key] = prop_schema["default"] | |
| continue | |
| # Handle nested default inside anyOf (take first subschema with default) | |
| if "anyOf" in prop_schema: | |
| for option in prop_schema["anyOf"]: | |
| if isinstance(option, dict) and "default" in option: | |
| args_with_defaults[key] = option["default"] | |
| break | |
| # Handle nested object with defaults even if not explicitly present | |
| if prop_schema.get("type") == "object" and "properties" in prop_schema: | |
| args_with_defaults[key] = apply_defaults({}, prop_schema) | |
| return args_with_defaults | |
| def lowercase_dict(obj): | |
| """Recursively lowercase string keys and string values. | |
| Parameters | |
| ---------- | |
| obj : Any | |
| Dictionary, list, string, or scalar value to normalize. | |
| Returns | |
| ------- | |
| Any | |
| Normalized object with lowercased string keys/values. | |
| """ | |
| if isinstance(obj, dict): | |
| return {(k.lower() if isinstance(k, str) else k): lowercase_dict(v) for k, v in obj.items()} | |
| elif isinstance(obj, list): | |
| return [lowercase_dict(i) for i in obj] | |
| elif isinstance(obj, str): | |
| return obj.lower() | |
| else: | |
| return obj | |
| def single_function_checker( | |
| func_description: dict, | |
| model_output: dict, | |
| answer: dict, | |
| ignore_fields=None, | |
| ) -> dict: | |
| """ | |
| Compare model tool call output with expected answer, validating with function schema. | |
| Args: | |
| func_description (dict): Tool function schema (JSON schema style) | |
| model_output (dict): LLM's tool call output (with "arguments" and optionally "result") | |
| answer (dict): Reference tool call output | |
| Returns: | |
| """ | |
| if ignore_fields is None: | |
| ignore_fields = ["cell", "pbc"] | |
| # Extract schema and values | |
| schema = func_description.get("parameters", {}) | |
| result = {"valid": False, "error": ""} | |
| tool_name_model, model_args_raw = next(iter(model_output.items())) | |
| tool_name_answer, answer_args_raw = next(iter(answer.items())) | |
| if tool_name_model != tool_name_answer: | |
| error = "different tool_name" | |
| result = {"valid": False, "error": error} | |
| return result | |
| # Have a special case for run_ase due to complex input schema | |
| if tool_name_model == "run_ase": | |
| try: | |
| model_args = ASEInputSchema(**model_args_raw["params"]).model_dump() | |
| answer_args = ASEInputSchema(**answer_args_raw["params"]).model_dump() | |
| except Exception as e: | |
| result = {"valid": False, "error": e} | |
| return result | |
| # Apply lower case to both sides | |
| model_args = lowercase_dict(model_args) | |
| answer_args = lowercase_dict(answer_args) | |
| # Remove ignored fields | |
| model_args = remove_ignored_fields(model_args, ignore_fields) | |
| answer_args = remove_ignored_fields(answer_args, ignore_fields) | |
| else: | |
| # Apply lower case to both sides | |
| model_args_lower = lowercase_dict(model_args_raw) | |
| answer_args_lower = lowercase_dict(answer_args_raw) | |
| # Apply defaults to both sides | |
| model_args_full = apply_defaults(model_args_lower, schema) | |
| answer_args_full = apply_defaults(answer_args_lower, schema) | |
| # Remove ignored fields | |
| model_args = remove_ignored_fields(model_args_full, ignore_fields) | |
| answer_args = remove_ignored_fields(answer_args_full, ignore_fields) | |
| diff = DeepDiff( | |
| model_args, | |
| answer_args, | |
| significant_digits=3, # Controls float tolerance | |
| ignore_order=True, # Ignores order in dicts and lists | |
| ) | |
| if not diff: | |
| result = {"valid": True, "error": ""} | |
| else: | |
| result = {"valid": False, "error": diff.to_dict()} | |
| return result | |
| def find_description(func_descriptions: list, func_name: str) -> dict: | |
| """Find the function description by name | |
| Args: | |
| func_descriptions (list): list of function descriptions | |
| func_name (str): function name | |
| Returns: | |
| dict: dictionary of the | |
| """ | |
| if isinstance(func_descriptions, list): | |
| for func_description in func_descriptions: | |
| if func_description["name"] == func_name: | |
| return func_description | |
| return None | |
| else: | |
| return func_descriptions | |
| def multi_function_checker_with_order( | |
| func_descriptions: dict, | |
| model_outputs: list, | |
| answers: list, | |
| ignore_fields=None, | |
| ) -> dict: | |
| """Evaluate multiple function calls. | |
| Args: | |
| func_description (dict): _description_ | |
| model_output (list): _description_ | |
| answer (list): _description_ | |
| ignore_fields (_type_, optional): _description_. Defaults to None. | |
| Returns: | |
| dict: _description_ | |
| """ | |
| if ignore_fields is None: | |
| ignore_fields = ["cell", "pbc"] | |
| # Initialize result | |
| result = { | |
| "valid": True, | |
| "error": "", | |
| "n_true_toolcalls": len(answers), | |
| "n_llm_tool_calls": len(model_outputs), | |
| "acc_n_toolcalls": 0, | |
| "args_differences": {}, | |
| } | |
| if len(model_outputs) != len(answers): | |
| result['error'] = "Different number of tool calls" | |
| result['valid'] = False | |
| return result | |
| for model_output, answer in zip(model_outputs, answers): | |
| tool_name_model, model_args_raw = next(iter(model_output.items())) | |
| # Get function description | |
| func_description = find_description( | |
| func_descriptions=func_descriptions, | |
| func_name=tool_name_model, | |
| ) | |
| if func_description is None: | |
| result["error"] += f"Function {tool_name_model} is not in the given functions.\n" | |
| continue | |
| else: | |
| result_single = single_function_checker( | |
| func_description=func_description, | |
| model_output=model_output, | |
| answer=answer, | |
| ) | |
| if result_single["valid"] is True: | |
| result["acc_n_toolcalls"] += 1 | |
| else: | |
| result["args_differences"][tool_name_model] = result_single["error"] | |
| return result | |
| def multi_function_checker_without_order( | |
| func_descriptions: dict, | |
| model_outputs: list, | |
| answers: list, | |
| ignore_fields=None, | |
| ) -> dict: | |
| """Evaluate multiple function calls. | |
| Args: | |
| func_description (dict): _description_ | |
| model_output (list): _description_ | |
| answer (list): _description_ | |
| ignore_fields (_type_, optional): _description_. Defaults to None. | |
| Returns: | |
| dict: _description_ | |
| """ | |
| if ignore_fields is None: | |
| ignore_fields = ["cell", "pbc"] | |
| # Initialize result | |
| result = { | |
| "valid": True, | |
| "error": "", | |
| "n_true_toolcalls": len(answers), | |
| "n_llm_tool_calls": len(model_outputs), | |
| "acc_n_toolcalls": 0, | |
| "answers_without_match": [], | |
| } | |
| for model_id, model_output in enumerate(model_outputs): | |
| for answer_id, answer in enumerate(answers): | |
| tool_name_model, model_args_raw = next(iter(model_output.items())) | |
| # Get function description | |
| func_description = find_description( | |
| func_descriptions=func_descriptions, | |
| func_name=tool_name_model, | |
| ) | |
| if func_description is None: | |
| result["error"] += f"Function {tool_name_model} is not in the given functions.\n" | |
| continue | |
| else: | |
| result_single = single_function_checker( | |
| func_description=func_description, | |
| model_output=model_output, | |
| answer=answer, | |
| ) | |
| if result_single["valid"] is True: | |
| result["acc_n_toolcalls"] += 1 | |
| # Remove accurate answer from future comparison after a match. | |
| answers.remove(answer) | |
| else: | |
| continue | |
| if len(answers) != 0: | |
| for answer in answers: | |
| result["answers_without_match"].append(answer) | |
| return result | |