Spaces:
Running
Running
| """This module contains functions to fix JSON strings generated by LLM models, such as ChatGPT, using the assistance | |
| of the ChatGPT API or LLM models.""" | |
| from __future__ import annotations | |
| import contextlib | |
| import json | |
| from typing import Any, Dict | |
| from colorama import Fore | |
| from regex import regex | |
| from autogpt.config import Config | |
| from autogpt.json_utils.json_fix_general import correct_json | |
| from autogpt.llm_utils import call_ai_function | |
| from autogpt.logs import logger | |
| from autogpt.speech import say_text | |
| JSON_SCHEMA = """ | |
| { | |
| "command": { | |
| "name": "command name", | |
| "args": { | |
| "arg name": "value" | |
| } | |
| }, | |
| "thoughts": | |
| { | |
| "text": "thought", | |
| "reasoning": "reasoning", | |
| "plan": "- short bulleted\n- list that conveys\n- long-term plan", | |
| "criticism": "constructive self-criticism", | |
| "speak": "thoughts summary to say to user" | |
| } | |
| } | |
| """ | |
| CFG = Config() | |
| def auto_fix_json(json_string: str, schema: str) -> str: | |
| """Fix the given JSON string to make it parseable and fully compliant with | |
| the provided schema using GPT-3. | |
| Args: | |
| json_string (str): The JSON string to fix. | |
| schema (str): The schema to use to fix the JSON. | |
| Returns: | |
| str: The fixed JSON string. | |
| """ | |
| # Try to fix the JSON using GPT: | |
| function_string = "def fix_json(json_string: str, schema:str=None) -> str:" | |
| args = [f"'''{json_string}'''", f"'''{schema}'''"] | |
| description_string = ( | |
| "This function takes a JSON string and ensures that it" | |
| " is parseable and fully compliant with the provided schema. If an object" | |
| " or field specified in the schema isn't contained within the correct JSON," | |
| " it is omitted. The function also escapes any double quotes within JSON" | |
| " string values to ensure that they are valid. If the JSON string contains" | |
| " any None or NaN values, they are replaced with null before being parsed." | |
| ) | |
| # If it doesn't already start with a "`", add one: | |
| if not json_string.startswith("`"): | |
| json_string = "```json\n" + json_string + "\n```" | |
| result_string = call_ai_function( | |
| function_string, args, description_string, model=CFG.fast_llm_model | |
| ) | |
| logger.debug("------------ JSON FIX ATTEMPT ---------------") | |
| logger.debug(f"Original JSON: {json_string}") | |
| logger.debug("-----------") | |
| logger.debug(f"Fixed JSON: {result_string}") | |
| logger.debug("----------- END OF FIX ATTEMPT ----------------") | |
| try: | |
| json.loads(result_string) # just check the validity | |
| return result_string | |
| except json.JSONDecodeError: # noqa: E722 | |
| # Get the call stack: | |
| # import traceback | |
| # call_stack = traceback.format_exc() | |
| # print(f"Failed to fix JSON: '{json_string}' "+call_stack) | |
| return "failed" | |
| def fix_json_using_multiple_techniques(assistant_reply: str) -> Dict[Any, Any]: | |
| """Fix the given JSON string to make it parseable and fully compliant with two techniques. | |
| Args: | |
| json_string (str): The JSON string to fix. | |
| Returns: | |
| str: The fixed JSON string. | |
| """ | |
| # Parse and print Assistant response | |
| assistant_reply_json = fix_and_parse_json(assistant_reply) | |
| if assistant_reply_json == {}: | |
| assistant_reply_json = attempt_to_fix_json_by_finding_outermost_brackets( | |
| assistant_reply | |
| ) | |
| if assistant_reply_json != {}: | |
| return assistant_reply_json | |
| logger.error( | |
| "Error: The following AI output couldn't be converted to a JSON:\n", | |
| assistant_reply, | |
| ) | |
| if CFG.speak_mode: | |
| say_text("I have received an invalid JSON response from the OpenAI API.") | |
| return {} | |
| def fix_and_parse_json( | |
| json_to_load: str, try_to_fix_with_gpt: bool = True | |
| ) -> Dict[Any, Any]: | |
| """Fix and parse JSON string | |
| Args: | |
| json_to_load (str): The JSON string. | |
| try_to_fix_with_gpt (bool, optional): Try to fix the JSON with GPT. | |
| Defaults to True. | |
| Returns: | |
| str or dict[Any, Any]: The parsed JSON. | |
| """ | |
| with contextlib.suppress(json.JSONDecodeError): | |
| json_to_load = json_to_load.replace("\t", "") | |
| return json.loads(json_to_load) | |
| with contextlib.suppress(json.JSONDecodeError): | |
| json_to_load = correct_json(json_to_load) | |
| return json.loads(json_to_load) | |
| # Let's do something manually: | |
| # sometimes GPT responds with something BEFORE the braces: | |
| # "I'm sorry, I don't understand. Please try again." | |
| # {"text": "I'm sorry, I don't understand. Please try again.", | |
| # "confidence": 0.0} | |
| # So let's try to find the first brace and then parse the rest | |
| # of the string | |
| try: | |
| brace_index = json_to_load.index("{") | |
| maybe_fixed_json = json_to_load[brace_index:] | |
| last_brace_index = maybe_fixed_json.rindex("}") | |
| maybe_fixed_json = maybe_fixed_json[: last_brace_index + 1] | |
| return json.loads(maybe_fixed_json) | |
| except (json.JSONDecodeError, ValueError) as e: | |
| return try_ai_fix(try_to_fix_with_gpt, e, json_to_load) | |
| def try_ai_fix( | |
| try_to_fix_with_gpt: bool, exception: Exception, json_to_load: str | |
| ) -> Dict[Any, Any]: | |
| """Try to fix the JSON with the AI | |
| Args: | |
| try_to_fix_with_gpt (bool): Whether to try to fix the JSON with the AI. | |
| exception (Exception): The exception that was raised. | |
| json_to_load (str): The JSON string to load. | |
| Raises: | |
| exception: If try_to_fix_with_gpt is False. | |
| Returns: | |
| str or dict[Any, Any]: The JSON string or dictionary. | |
| """ | |
| if not try_to_fix_with_gpt: | |
| raise exception | |
| if CFG.debug_mode: | |
| logger.warn( | |
| "Warning: Failed to parse AI output, attempting to fix." | |
| "\n If you see this warning frequently, it's likely that" | |
| " your prompt is confusing the AI. Try changing it up" | |
| " slightly." | |
| ) | |
| # Now try to fix this up using the ai_functions | |
| ai_fixed_json = auto_fix_json(json_to_load, JSON_SCHEMA) | |
| if ai_fixed_json != "failed": | |
| return json.loads(ai_fixed_json) | |
| # This allows the AI to react to the error message, | |
| # which usually results in it correcting its ways. | |
| # logger.error("Failed to fix AI output, telling the AI.") | |
| return {} | |
| def attempt_to_fix_json_by_finding_outermost_brackets(json_string: str): | |
| if CFG.speak_mode and CFG.debug_mode: | |
| say_text( | |
| "I have received an invalid JSON response from the OpenAI API. " | |
| "Trying to fix it now." | |
| ) | |
| logger.error("Attempting to fix JSON by finding outermost brackets\n") | |
| try: | |
| json_pattern = regex.compile(r"\{(?:[^{}]|(?R))*\}") | |
| json_match = json_pattern.search(json_string) | |
| if json_match: | |
| # Extract the valid JSON object from the string | |
| json_string = json_match.group(0) | |
| logger.typewriter_log( | |
| title="Apparently json was fixed.", title_color=Fore.GREEN | |
| ) | |
| if CFG.speak_mode and CFG.debug_mode: | |
| say_text("Apparently json was fixed.") | |
| else: | |
| return {} | |
| except (json.JSONDecodeError, ValueError): | |
| if CFG.debug_mode: | |
| logger.error(f"Error: Invalid JSON: {json_string}\n") | |
| if CFG.speak_mode: | |
| say_text("Didn't work. I will have to ignore this response then.") | |
| logger.error("Error: Invalid JSON, setting it to empty JSON now.\n") | |
| json_string = {} | |
| return fix_and_parse_json(json_string) | |