chore: refactor parser & constants, improve tools_handler, update tests

- Move parser constants to dedicated module with pre-compiled regex
- Refactor CrewAgentParser to module functions; remove unused params
- Improve tools_handler with instance attributes
- Update tests to use module-level parser functions
This commit is contained in:
Greyson LaLonde
2025-08-29 14:35:08 -04:00
committed by GitHub
parent ec1eff02a8
commit e4c4b81e63
6 changed files with 249 additions and 188 deletions

View File

@@ -1,5 +1,5 @@
from .cache.cache_handler import CacheHandler from crewai.agents.cache.cache_handler import CacheHandler
from .parser import CrewAgentParser from crewai.agents.parser import parse, AgentAction, AgentFinish, OutputParserException
from .tools_handler import ToolsHandler from crewai.agents.tools_handler import ToolsHandler
__all__ = ["CacheHandler", "CrewAgentParser", "ToolsHandler"] __all__ = ["CacheHandler", "parse", "AgentAction", "AgentFinish", "OutputParserException", "ToolsHandler"]

View File

@@ -0,0 +1,27 @@
"""Constants for agent-related modules."""
import re
from typing import Final
# crewai.agents.parser constants
FINAL_ANSWER_ACTION: Final[str] = "Final Answer:"
MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE: Final[str] = (
"I did it wrong. Invalid Format: I missed the 'Action:' after 'Thought:'. I will do right next, and don't use a tool I have already used.\n"
)
MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE: Final[str] = (
"I did it wrong. Invalid Format: I missed the 'Action Input:' after 'Action:'. I will do right next, and don't use a tool I have already used.\n"
)
FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE: Final[str] = (
"I did it wrong. Tried to both perform Action and give a Final Answer at the same time, I must do one or the other"
)
UNABLE_TO_REPAIR_JSON_RESULTS: Final[list[str]] = ['""', "{}"]
ACTION_INPUT_REGEX: Final[re.Pattern[str]] = re.compile(
r"Action\s*\d*\s*:\s*(.*?)\s*Action\s*\d*\s*Input\s*\d*\s*:\s*(.*)", re.DOTALL
)
ACTION_REGEX: Final[re.Pattern[str]] = re.compile(
r"Action\s*\d*\s*:\s*(.*?)", re.DOTALL
)
ACTION_INPUT_ONLY_REGEX: Final[re.Pattern[str]] = re.compile(
r"\s*Action\s*\d*\s*Input\s*\d*\s*:\s*(.*)", re.DOTALL
)

View File

@@ -1,50 +1,67 @@
import re """Agent output parsing module for ReAct-style LLM responses.
from typing import Any, Optional, Union
This module provides parsing functionality for agent outputs that follow
the ReAct (Reasoning and Acting) format, converting them into structured
AgentAction or AgentFinish objects.
"""
from dataclasses import dataclass
from json_repair import repair_json from json_repair import repair_json
from crewai.agents.constants import (
ACTION_INPUT_REGEX,
ACTION_REGEX,
ACTION_INPUT_ONLY_REGEX,
FINAL_ANSWER_ACTION,
MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE,
MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE,
UNABLE_TO_REPAIR_JSON_RESULTS,
)
from crewai.utilities import I18N from crewai.utilities import I18N
FINAL_ANSWER_ACTION = "Final Answer:" _I18N = I18N()
MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE = "I did it wrong. Invalid Format: I missed the 'Action:' after 'Thought:'. I will do right next, and don't use a tool I have already used.\n"
MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE = "I did it wrong. Invalid Format: I missed the 'Action Input:' after 'Action:'. I will do right next, and don't use a tool I have already used.\n"
FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE = "I did it wrong. Tried to both perform Action and give a Final Answer at the same time, I must do one or the other"
@dataclass
class AgentAction: class AgentAction:
"""Represents an action to be taken by an agent."""
thought: str thought: str
tool: str tool: str
tool_input: str tool_input: str
text: str text: str
result: str result: str | None = None
def __init__(self, thought: str, tool: str, tool_input: str, text: str):
self.thought = thought
self.tool = tool
self.tool_input = tool_input
self.text = text
@dataclass
class AgentFinish: class AgentFinish:
"""Represents the final answer from an agent."""
thought: str thought: str
output: str output: str
text: str text: str
def __init__(self, thought: str, output: str, text: str):
self.thought = thought
self.output = output
self.text = text
class OutputParserException(Exception): class OutputParserException(Exception):
error: str """Exception raised when output parsing fails.
def __init__(self, error: str): Attributes:
error: The error message.
"""
def __init__(self, error: str) -> None:
"""Initialize OutputParserException.
Args:
error: The error message.
"""
self.error = error self.error = error
super().__init__(error)
class CrewAgentParser: def parse(text: str) -> AgentAction | AgentFinish:
"""Parses ReAct-style LLM calls that have a single tool input. """Parse agent output text into AgentAction or AgentFinish.
Expects output to be in one of two formats. Expects output to be in one of two formats.
@@ -62,108 +79,117 @@ class CrewAgentParser:
Thought: agent thought here Thought: agent thought here
Final Answer: The temperature is 100 degrees Final Answer: The temperature is 100 degrees
Args:
text: The agent output text to parse.
Returns:
AgentAction or AgentFinish based on the content.
Raises:
OutputParserException: If the text format is invalid.
""" """
thought = _extract_thought(text)
includes_answer = FINAL_ANSWER_ACTION in text
action_match = ACTION_INPUT_REGEX.search(text)
_i18n: I18N = I18N() if includes_answer:
agent: Any = None final_answer = text.split(FINAL_ANSWER_ACTION)[-1].strip()
# Check whether the final answer ends with triple backticks.
if final_answer.endswith("```"):
# Count occurrences of triple backticks in the final answer.
count = final_answer.count("```")
# If count is odd then it's an unmatched trailing set; remove it.
if count % 2 != 0:
final_answer = final_answer[:-3].rstrip()
return AgentFinish(thought=thought, output=final_answer, text=text)
def __init__(self, agent: Optional[Any] = None): elif action_match:
self.agent = agent action = action_match.group(1)
clean_action = _clean_action(action)
@staticmethod action_input = action_match.group(2).strip()
def parse_text(text: str) -> Union[AgentAction, AgentFinish]:
"""
Static method to parse text into an AgentAction or AgentFinish without needing to instantiate the class.
Args: tool_input = action_input.strip(" ").strip('"')
text: The text to parse. safe_tool_input = _safe_repair_json(tool_input)
Returns: return AgentAction(
Either an AgentAction or AgentFinish based on the parsed content. thought=thought, tool=clean_action, tool_input=safe_tool_input, text=text
"""
parser = CrewAgentParser()
return parser.parse(text)
def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
thought = self._extract_thought(text)
includes_answer = FINAL_ANSWER_ACTION in text
regex = (
r"Action\s*\d*\s*:[\s]*(.*?)[\s]*Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
) )
action_match = re.search(regex, text, re.DOTALL)
if includes_answer:
final_answer = text.split(FINAL_ANSWER_ACTION)[-1].strip()
# Check whether the final answer ends with triple backticks.
if final_answer.endswith("```"):
# Count occurrences of triple backticks in the final answer.
count = final_answer.count("```")
# If count is odd then it's an unmatched trailing set; remove it.
if count % 2 != 0:
final_answer = final_answer[:-3].rstrip()
return AgentFinish(thought, final_answer, text)
elif action_match: if not ACTION_REGEX.search(text):
action = action_match.group(1) raise OutputParserException(
clean_action = self._clean_action(action) f"{MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE}\n{_I18N.slice('final_answer_format')}",
)
elif not ACTION_INPUT_ONLY_REGEX.search(text):
raise OutputParserException(
MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE,
)
else:
err_format = _I18N.slice("format_without_tools")
error = f"{err_format}"
raise OutputParserException(
error,
)
action_input = action_match.group(2).strip()
tool_input = action_input.strip(" ").strip('"') def _extract_thought(text: str) -> str:
safe_tool_input = self._safe_repair_json(tool_input) """Extract the thought portion from the text.
return AgentAction(thought, clean_action, safe_tool_input, text) Args:
text: The full agent output text.
if not re.search(r"Action\s*\d*\s*:[\s]*(.*?)", text, re.DOTALL): Returns:
raise OutputParserException( The extracted thought string.
f"{MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE}\n{self._i18n.slice('final_answer_format')}", """
) thought_index = text.find("\nAction")
elif not re.search( if thought_index == -1:
r"[\s]*Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)", text, re.DOTALL thought_index = text.find("\nFinal Answer")
): if thought_index == -1:
raise OutputParserException( return ""
MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE, thought = text[:thought_index].strip()
) # Remove any triple backticks from the thought string
else: thought = thought.replace("```", "").strip()
format = self._i18n.slice("format_without_tools") return thought
error = f"{format}"
raise OutputParserException(
error,
)
def _extract_thought(self, text: str) -> str:
thought_index = text.find("\nAction")
if thought_index == -1:
thought_index = text.find("\nFinal Answer")
if thought_index == -1:
return ""
thought = text[:thought_index].strip()
# Remove any triple backticks from the thought string
thought = thought.replace("```", "").strip()
return thought
def _clean_action(self, text: str) -> str: def _clean_action(text: str) -> str:
"""Clean action string by removing non-essential formatting characters.""" """Clean action string by removing non-essential formatting characters.
return text.strip().strip("*").strip()
def _safe_repair_json(self, tool_input: str) -> str: Args:
UNABLE_TO_REPAIR_JSON_RESULTS = ['""', "{}"] text: The action text to clean.
# Skip repair if the input starts and ends with square brackets Returns:
# Explanation: The JSON parser has issues handling inputs that are enclosed in square brackets ('[]'). The cleaned action string.
# These are typically valid JSON arrays or strings that do not require repair. Attempting to repair such inputs """
# might lead to unintended alterations, such as wrapping the entire input in additional layers or modifying return text.strip().strip("*").strip()
# the structure in a way that changes its meaning. By skipping the repair for inputs that start and end with
# square brackets, we preserve the integrity of these valid JSON structures and avoid unnecessary modifications.
if tool_input.startswith("[") and tool_input.endswith("]"):
return tool_input
# Before repair, handle common LLM issues:
# 1. Replace """ with " to avoid JSON parser errors
tool_input = tool_input.replace('"""', '"') def _safe_repair_json(tool_input: str) -> str:
"""Safely repair JSON input.
result = repair_json(tool_input) Args:
if result in UNABLE_TO_REPAIR_JSON_RESULTS: tool_input: The tool input string to repair.
return tool_input
return str(result) Returns:
The repaired JSON string or original if repair fails.
"""
# Skip repair if the input starts and ends with square brackets
# Explanation: The JSON parser has issues handling inputs that are enclosed in square brackets ('[]').
# These are typically valid JSON arrays or strings that do not require repair. Attempting to repair such inputs
# might lead to unintended alterations, such as wrapping the entire input in additional layers or modifying
# the structure in a way that changes its meaning. By skipping the repair for inputs that start and end with
# square brackets, we preserve the integrity of these valid JSON structures and avoid unnecessary modifications.
if tool_input.startswith("[") and tool_input.endswith("]"):
return tool_input
# Before repair, handle common LLM issues:
# 1. Replace """ with " to avoid JSON parser errors
tool_input = tool_input.replace('"""', '"')
result = repair_json(tool_input)
if result in UNABLE_TO_REPAIR_JSON_RESULTS:
return tool_input
return str(result)

View File

@@ -1,29 +1,41 @@
from typing import Any, Optional, Union """Tools handler for managing tool execution and caching."""
from ..tools.cache_tools.cache_tools import CacheTools from crewai.tools.cache_tools.cache_tools import CacheTools
from ..tools.tool_calling import InstructorToolCalling, ToolCalling from crewai.tools.tool_calling import InstructorToolCalling, ToolCalling
from .cache.cache_handler import CacheHandler from crewai.agents.cache.cache_handler import CacheHandler
class ToolsHandler: class ToolsHandler:
"""Callback handler for tool usage.""" """Callback handler for tool usage.
last_used_tool: Optional[ToolCalling] = None Attributes:
cache: Optional[CacheHandler] last_used_tool: The most recently used tool calling instance.
cache: Optional cache handler for storing tool outputs.
"""
def __init__(self, cache: Optional[CacheHandler] = None): def __init__(self, cache: CacheHandler | None = None) -> None:
"""Initialize the callback handler.""" """Initialize the callback handler.
self.cache = cache
self.last_used_tool = None Args:
cache: Optional cache handler for storing tool outputs.
"""
self.cache: CacheHandler | None = cache
self.last_used_tool: ToolCalling | InstructorToolCalling | None = None
def on_tool_use( def on_tool_use(
self, self,
calling: Union[ToolCalling, InstructorToolCalling], calling: ToolCalling | InstructorToolCalling,
output: str, output: str,
should_cache: bool = True, should_cache: bool = True,
) -> Any: ) -> None:
"""Run when tool ends running.""" """Run when tool ends running.
self.last_used_tool = calling # type: ignore # BUG?: Incompatible types in assignment (expression has type "Union[ToolCalling, InstructorToolCalling]", variable has type "ToolCalling")
Args:
calling: The tool calling instance.
output: The output from the tool execution.
should_cache: Whether to cache the tool output.
"""
self.last_used_tool = calling
if self.cache and should_cache and calling.tool_name != CacheTools().name: if self.cache and should_cache and calling.tool_name != CacheTools().name:
self.cache.add( self.cache.add(
tool=calling.tool_name, tool=calling.tool_name,

View File

@@ -2,12 +2,12 @@ import json
import re import re
from typing import Any, Callable, Dict, List, Optional, Sequence, Union from typing import Any, Callable, Dict, List, Optional, Sequence, Union
from crewai.agents.constants import FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE
from crewai.agents.parser import ( from crewai.agents.parser import (
FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE,
AgentAction, AgentAction,
AgentFinish, AgentFinish,
CrewAgentParser,
OutputParserException, OutputParserException,
parse,
) )
from crewai.llm import LLM from crewai.llm import LLM
from crewai.llms.base_llm import BaseLLM from crewai.llms.base_llm import BaseLLM
@@ -25,6 +25,7 @@ from crewai.cli.config import Settings
console = Console() console = Console()
def parse_tools(tools: List[BaseTool]) -> List[CrewStructuredTool]: def parse_tools(tools: List[BaseTool]) -> List[CrewStructuredTool]:
"""Parse tools to be used for the task.""" """Parse tools to be used for the task."""
tools_list = [] tools_list = []
@@ -122,7 +123,7 @@ def format_message_for_llm(prompt: str, role: str = "user") -> Dict[str, str]:
def format_answer(answer: str) -> Union[AgentAction, AgentFinish]: def format_answer(answer: str) -> Union[AgentAction, AgentFinish]:
"""Format a response from the LLM into an AgentAction or AgentFinish.""" """Format a response from the LLM into an AgentAction or AgentFinish."""
try: try:
return CrewAgentParser.parse_text(answer) return parse(answer)
except Exception: except Exception:
# If parsing fails, return a default AgentFinish # If parsing fails, return a default AgentFinish
return AgentFinish( return AgentFinish(
@@ -446,9 +447,16 @@ def show_agent_logs(
def _print_current_organization(): def _print_current_organization():
settings = Settings() settings = Settings()
if settings.org_uuid: if settings.org_uuid:
console.print(f"Fetching agent from organization: {settings.org_name} ({settings.org_uuid})", style="bold blue") console.print(
f"Fetching agent from organization: {settings.org_name} ({settings.org_uuid})",
style="bold blue",
)
else: else:
console.print("No organization currently set. We recommend setting one before using: `crewai org switch <org_id>` command.", style="yellow") console.print(
"No organization currently set. We recommend setting one before using: `crewai org switch <org_id>` command.",
style="yellow",
)
def load_agent_from_repository(from_repository: str) -> Dict[str, Any]: def load_agent_from_repository(from_repository: str) -> Dict[str, Any]:
attributes: Dict[str, Any] = {} attributes: Dict[str, Any] = {}

View File

@@ -5,17 +5,10 @@ from crewai.agents.crew_agent_executor import (
AgentFinish, AgentFinish,
OutputParserException, OutputParserException,
) )
from crewai.agents.parser import CrewAgentParser from crewai.agents import parser
@pytest.fixture def test_valid_action_parsing_special_characters():
def parser():
agent = MockAgent()
p = CrewAgentParser(agent)
return p
def test_valid_action_parsing_special_characters(parser):
text = "Thought: Let's find the temperature\nAction: search\nAction Input: what's the temperature in SF?" text = "Thought: Let's find the temperature\nAction: search\nAction Input: what's the temperature in SF?"
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -23,7 +16,7 @@ def test_valid_action_parsing_special_characters(parser):
assert result.tool_input == "what's the temperature in SF?" assert result.tool_input == "what's the temperature in SF?"
def test_valid_action_parsing_with_json_tool_input(parser): def test_valid_action_parsing_with_json_tool_input():
text = """ text = """
Thought: Let's find the information Thought: Let's find the information
Action: query Action: query
@@ -36,7 +29,7 @@ def test_valid_action_parsing_with_json_tool_input(parser):
assert result.tool_input == expected_tool_input assert result.tool_input == expected_tool_input
def test_valid_action_parsing_with_quotes(parser): def test_valid_action_parsing_with_quotes():
text = 'Thought: Let\'s find the temperature\nAction: search\nAction Input: "temperature in SF"' text = 'Thought: Let\'s find the temperature\nAction: search\nAction Input: "temperature in SF"'
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -44,7 +37,7 @@ def test_valid_action_parsing_with_quotes(parser):
assert result.tool_input == "temperature in SF" assert result.tool_input == "temperature in SF"
def test_valid_action_parsing_with_curly_braces(parser): def test_valid_action_parsing_with_curly_braces():
text = "Thought: Let's find the temperature\nAction: search\nAction Input: {temperature in SF}" text = "Thought: Let's find the temperature\nAction: search\nAction Input: {temperature in SF}"
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -52,7 +45,7 @@ def test_valid_action_parsing_with_curly_braces(parser):
assert result.tool_input == "{temperature in SF}" assert result.tool_input == "{temperature in SF}"
def test_valid_action_parsing_with_angle_brackets(parser): def test_valid_action_parsing_with_angle_brackets():
text = "Thought: Let's find the temperature\nAction: search\nAction Input: <temperature in SF>" text = "Thought: Let's find the temperature\nAction: search\nAction Input: <temperature in SF>"
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -60,7 +53,7 @@ def test_valid_action_parsing_with_angle_brackets(parser):
assert result.tool_input == "<temperature in SF>" assert result.tool_input == "<temperature in SF>"
def test_valid_action_parsing_with_parentheses(parser): def test_valid_action_parsing_with_parentheses():
text = "Thought: Let's find the temperature\nAction: search\nAction Input: (temperature in SF)" text = "Thought: Let's find the temperature\nAction: search\nAction Input: (temperature in SF)"
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -68,7 +61,7 @@ def test_valid_action_parsing_with_parentheses(parser):
assert result.tool_input == "(temperature in SF)" assert result.tool_input == "(temperature in SF)"
def test_valid_action_parsing_with_mixed_brackets(parser): def test_valid_action_parsing_with_mixed_brackets():
text = "Thought: Let's find the temperature\nAction: search\nAction Input: [temperature in {SF}]" text = "Thought: Let's find the temperature\nAction: search\nAction Input: [temperature in {SF}]"
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -76,7 +69,7 @@ def test_valid_action_parsing_with_mixed_brackets(parser):
assert result.tool_input == "[temperature in {SF}]" assert result.tool_input == "[temperature in {SF}]"
def test_valid_action_parsing_with_nested_quotes(parser): def test_valid_action_parsing_with_nested_quotes():
text = "Thought: Let's find the temperature\nAction: search\nAction Input: \"what's the temperature in 'SF'?\"" text = "Thought: Let's find the temperature\nAction: search\nAction Input: \"what's the temperature in 'SF'?\""
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -84,7 +77,7 @@ def test_valid_action_parsing_with_nested_quotes(parser):
assert result.tool_input == "what's the temperature in 'SF'?" assert result.tool_input == "what's the temperature in 'SF'?"
def test_valid_action_parsing_with_incomplete_json(parser): def test_valid_action_parsing_with_incomplete_json():
text = 'Thought: Let\'s find the temperature\nAction: search\nAction Input: {"query": "temperature in SF"' text = 'Thought: Let\'s find the temperature\nAction: search\nAction Input: {"query": "temperature in SF"'
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -92,7 +85,7 @@ def test_valid_action_parsing_with_incomplete_json(parser):
assert result.tool_input == '{"query": "temperature in SF"}' assert result.tool_input == '{"query": "temperature in SF"}'
def test_valid_action_parsing_with_special_characters(parser): def test_valid_action_parsing_with_special_characters():
text = "Thought: Let's find the temperature\nAction: search\nAction Input: what is the temperature in SF? @$%^&*" text = "Thought: Let's find the temperature\nAction: search\nAction Input: what is the temperature in SF? @$%^&*"
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -100,7 +93,7 @@ def test_valid_action_parsing_with_special_characters(parser):
assert result.tool_input == "what is the temperature in SF? @$%^&*" assert result.tool_input == "what is the temperature in SF? @$%^&*"
def test_valid_action_parsing_with_combination(parser): def test_valid_action_parsing_with_combination():
text = 'Thought: Let\'s find the temperature\nAction: search\nAction Input: "[what is the temperature in SF?]"' text = 'Thought: Let\'s find the temperature\nAction: search\nAction Input: "[what is the temperature in SF?]"'
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -108,7 +101,7 @@ def test_valid_action_parsing_with_combination(parser):
assert result.tool_input == "[what is the temperature in SF?]" assert result.tool_input == "[what is the temperature in SF?]"
def test_valid_action_parsing_with_mixed_quotes(parser): def test_valid_action_parsing_with_mixed_quotes():
text = "Thought: Let's find the temperature\nAction: search\nAction Input: \"what's the temperature in SF?\"" text = "Thought: Let's find the temperature\nAction: search\nAction Input: \"what's the temperature in SF?\""
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -116,7 +109,7 @@ def test_valid_action_parsing_with_mixed_quotes(parser):
assert result.tool_input == "what's the temperature in SF?" assert result.tool_input == "what's the temperature in SF?"
def test_valid_action_parsing_with_newlines(parser): def test_valid_action_parsing_with_newlines():
text = "Thought: Let's find the temperature\nAction: search\nAction Input: what is\nthe temperature in SF?" text = "Thought: Let's find the temperature\nAction: search\nAction Input: what is\nthe temperature in SF?"
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -124,7 +117,7 @@ def test_valid_action_parsing_with_newlines(parser):
assert result.tool_input == "what is\nthe temperature in SF?" assert result.tool_input == "what is\nthe temperature in SF?"
def test_valid_action_parsing_with_escaped_characters(parser): def test_valid_action_parsing_with_escaped_characters():
text = "Thought: Let's find the temperature\nAction: search\nAction Input: what is the temperature in SF? \\n" text = "Thought: Let's find the temperature\nAction: search\nAction Input: what is the temperature in SF? \\n"
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -132,7 +125,7 @@ def test_valid_action_parsing_with_escaped_characters(parser):
assert result.tool_input == "what is the temperature in SF? \\n" assert result.tool_input == "what is the temperature in SF? \\n"
def test_valid_action_parsing_with_json_string(parser): def test_valid_action_parsing_with_json_string():
text = 'Thought: Let\'s find the temperature\nAction: search\nAction Input: {"query": "temperature in SF"}' text = 'Thought: Let\'s find the temperature\nAction: search\nAction Input: {"query": "temperature in SF"}'
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -140,7 +133,7 @@ def test_valid_action_parsing_with_json_string(parser):
assert result.tool_input == '{"query": "temperature in SF"}' assert result.tool_input == '{"query": "temperature in SF"}'
def test_valid_action_parsing_with_unbalanced_quotes(parser): def test_valid_action_parsing_with_unbalanced_quotes():
text = "Thought: Let's find the temperature\nAction: search\nAction Input: \"what is the temperature in SF?" text = "Thought: Let's find the temperature\nAction: search\nAction Input: \"what is the temperature in SF?"
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -148,61 +141,61 @@ def test_valid_action_parsing_with_unbalanced_quotes(parser):
assert result.tool_input == "what is the temperature in SF?" assert result.tool_input == "what is the temperature in SF?"
def test_clean_action_no_formatting(parser): def test_clean_action_no_formatting():
action = "Ask question to senior researcher" action = "Ask question to senior researcher"
cleaned_action = parser._clean_action(action) cleaned_action = parser._clean_action(action)
assert cleaned_action == "Ask question to senior researcher" assert cleaned_action == "Ask question to senior researcher"
def test_clean_action_with_leading_asterisks(parser): def test_clean_action_with_leading_asterisks():
action = "** Ask question to senior researcher" action = "** Ask question to senior researcher"
cleaned_action = parser._clean_action(action) cleaned_action = parser._clean_action(action)
assert cleaned_action == "Ask question to senior researcher" assert cleaned_action == "Ask question to senior researcher"
def test_clean_action_with_trailing_asterisks(parser): def test_clean_action_with_trailing_asterisks():
action = "Ask question to senior researcher **" action = "Ask question to senior researcher **"
cleaned_action = parser._clean_action(action) cleaned_action = parser._clean_action(action)
assert cleaned_action == "Ask question to senior researcher" assert cleaned_action == "Ask question to senior researcher"
def test_clean_action_with_leading_and_trailing_asterisks(parser): def test_clean_action_with_leading_and_trailing_asterisks():
action = "** Ask question to senior researcher **" action = "** Ask question to senior researcher **"
cleaned_action = parser._clean_action(action) cleaned_action = parser._clean_action(action)
assert cleaned_action == "Ask question to senior researcher" assert cleaned_action == "Ask question to senior researcher"
def test_clean_action_with_multiple_leading_asterisks(parser): def test_clean_action_with_multiple_leading_asterisks():
action = "**** Ask question to senior researcher" action = "**** Ask question to senior researcher"
cleaned_action = parser._clean_action(action) cleaned_action = parser._clean_action(action)
assert cleaned_action == "Ask question to senior researcher" assert cleaned_action == "Ask question to senior researcher"
def test_clean_action_with_multiple_trailing_asterisks(parser): def test_clean_action_with_multiple_trailing_asterisks():
action = "Ask question to senior researcher ****" action = "Ask question to senior researcher ****"
cleaned_action = parser._clean_action(action) cleaned_action = parser._clean_action(action)
assert cleaned_action == "Ask question to senior researcher" assert cleaned_action == "Ask question to senior researcher"
def test_clean_action_with_spaces_and_asterisks(parser): def test_clean_action_with_spaces_and_asterisks():
action = " ** Ask question to senior researcher ** " action = " ** Ask question to senior researcher ** "
cleaned_action = parser._clean_action(action) cleaned_action = parser._clean_action(action)
assert cleaned_action == "Ask question to senior researcher" assert cleaned_action == "Ask question to senior researcher"
def test_clean_action_with_only_asterisks(parser): def test_clean_action_with_only_asterisks():
action = "****" action = "****"
cleaned_action = parser._clean_action(action) cleaned_action = parser._clean_action(action)
assert cleaned_action == "" assert cleaned_action == ""
def test_clean_action_with_empty_string(parser): def test_clean_action_with_empty_string():
action = "" action = ""
cleaned_action = parser._clean_action(action) cleaned_action = parser._clean_action(action)
assert cleaned_action == "" assert cleaned_action == ""
def test_valid_final_answer_parsing(parser): def test_valid_final_answer_parsing():
text = ( text = (
"Thought: I found the information\nFinal Answer: The temperature is 100 degrees" "Thought: I found the information\nFinal Answer: The temperature is 100 degrees"
) )
@@ -211,7 +204,7 @@ def test_valid_final_answer_parsing(parser):
assert result.output == "The temperature is 100 degrees" assert result.output == "The temperature is 100 degrees"
def test_missing_action_error(parser): def test_missing_action_error():
text = "Thought: Let's find the temperature\nAction Input: what is the temperature in SF?" text = "Thought: Let's find the temperature\nAction Input: what is the temperature in SF?"
with pytest.raises(OutputParserException) as exc_info: with pytest.raises(OutputParserException) as exc_info:
parser.parse(text) parser.parse(text)
@@ -220,27 +213,27 @@ def test_missing_action_error(parser):
) )
def test_missing_action_input_error(parser): def test_missing_action_input_error():
text = "Thought: Let's find the temperature\nAction: search" text = "Thought: Let's find the temperature\nAction: search"
with pytest.raises(OutputParserException) as exc_info: with pytest.raises(OutputParserException) as exc_info:
parser.parse(text) parser.parse(text)
assert "I missed the 'Action Input:' after 'Action:'." in str(exc_info.value) assert "I missed the 'Action Input:' after 'Action:'." in str(exc_info.value)
def test_safe_repair_json(parser): def test_safe_repair_json():
invalid_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": Senior Researcher' invalid_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": Senior Researcher'
expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}' expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}'
result = parser._safe_repair_json(invalid_json) result = parser._safe_repair_json(invalid_json)
assert result == expected_repaired_json assert result == expected_repaired_json
def test_safe_repair_json_unrepairable(parser): def test_safe_repair_json_unrepairable():
invalid_json = "{invalid_json" invalid_json = "{invalid_json"
result = parser._safe_repair_json(invalid_json) result = parser._safe_repair_json(invalid_json)
assert result == invalid_json # Should return the original if unrepairable assert result == invalid_json # Should return the original if unrepairable
def test_safe_repair_json_missing_quotes(parser): def test_safe_repair_json_missing_quotes():
invalid_json = ( invalid_json = (
'{task: "Research XAI", context: "Explainable AI", coworker: Senior Researcher}' '{task: "Research XAI", context: "Explainable AI", coworker: Senior Researcher}'
) )
@@ -249,77 +242,77 @@ def test_safe_repair_json_missing_quotes(parser):
assert result == expected_repaired_json assert result == expected_repaired_json
def test_safe_repair_json_unclosed_brackets(parser): def test_safe_repair_json_unclosed_brackets():
invalid_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"' invalid_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"'
expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}' expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}'
result = parser._safe_repair_json(invalid_json) result = parser._safe_repair_json(invalid_json)
assert result == expected_repaired_json assert result == expected_repaired_json
def test_safe_repair_json_extra_commas(parser): def test_safe_repair_json_extra_commas():
invalid_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher",}' invalid_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher",}'
expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}' expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}'
result = parser._safe_repair_json(invalid_json) result = parser._safe_repair_json(invalid_json)
assert result == expected_repaired_json assert result == expected_repaired_json
def test_safe_repair_json_trailing_commas(parser): def test_safe_repair_json_trailing_commas():
invalid_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher",}' invalid_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher",}'
expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}' expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}'
result = parser._safe_repair_json(invalid_json) result = parser._safe_repair_json(invalid_json)
assert result == expected_repaired_json assert result == expected_repaired_json
def test_safe_repair_json_single_quotes(parser): def test_safe_repair_json_single_quotes():
invalid_json = "{'task': 'Research XAI', 'context': 'Explainable AI', 'coworker': 'Senior Researcher'}" invalid_json = "{'task': 'Research XAI', 'context': 'Explainable AI', 'coworker': 'Senior Researcher'}"
expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}' expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}'
result = parser._safe_repair_json(invalid_json) result = parser._safe_repair_json(invalid_json)
assert result == expected_repaired_json assert result == expected_repaired_json
def test_safe_repair_json_mixed_quotes(parser): def test_safe_repair_json_mixed_quotes():
invalid_json = "{'task': \"Research XAI\", 'context': \"Explainable AI\", 'coworker': 'Senior Researcher'}" invalid_json = "{'task': \"Research XAI\", 'context': \"Explainable AI\", 'coworker': 'Senior Researcher'}"
expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}' expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}'
result = parser._safe_repair_json(invalid_json) result = parser._safe_repair_json(invalid_json)
assert result == expected_repaired_json assert result == expected_repaired_json
def test_safe_repair_json_unescaped_characters(parser): def test_safe_repair_json_unescaped_characters():
invalid_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher\n"}' invalid_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher\n"}'
expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}' expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}'
result = parser._safe_repair_json(invalid_json) result = parser._safe_repair_json(invalid_json)
assert result == expected_repaired_json assert result == expected_repaired_json
def test_safe_repair_json_missing_colon(parser): def test_safe_repair_json_missing_colon():
invalid_json = '{"task" "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}' invalid_json = '{"task" "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}'
expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}' expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}'
result = parser._safe_repair_json(invalid_json) result = parser._safe_repair_json(invalid_json)
assert result == expected_repaired_json assert result == expected_repaired_json
def test_safe_repair_json_missing_comma(parser): def test_safe_repair_json_missing_comma():
invalid_json = '{"task": "Research XAI" "context": "Explainable AI", "coworker": "Senior Researcher"}' invalid_json = '{"task": "Research XAI" "context": "Explainable AI", "coworker": "Senior Researcher"}'
expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}' expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}'
result = parser._safe_repair_json(invalid_json) result = parser._safe_repair_json(invalid_json)
assert result == expected_repaired_json assert result == expected_repaired_json
def test_safe_repair_json_unexpected_trailing_characters(parser): def test_safe_repair_json_unexpected_trailing_characters():
invalid_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"} random text' invalid_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"} random text'
expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}' expected_repaired_json = '{"task": "Research XAI", "context": "Explainable AI", "coworker": "Senior Researcher"}'
result = parser._safe_repair_json(invalid_json) result = parser._safe_repair_json(invalid_json)
assert result == expected_repaired_json assert result == expected_repaired_json
def test_safe_repair_json_special_characters_key(parser): def test_safe_repair_json_special_characters_key():
invalid_json = '{"task!@#": "Research XAI", "context$%^": "Explainable AI", "coworker&*()": "Senior Researcher"}' invalid_json = '{"task!@#": "Research XAI", "context$%^": "Explainable AI", "coworker&*()": "Senior Researcher"}'
expected_repaired_json = '{"task!@#": "Research XAI", "context$%^": "Explainable AI", "coworker&*()": "Senior Researcher"}' expected_repaired_json = '{"task!@#": "Research XAI", "context$%^": "Explainable AI", "coworker&*()": "Senior Researcher"}'
result = parser._safe_repair_json(invalid_json) result = parser._safe_repair_json(invalid_json)
assert result == expected_repaired_json assert result == expected_repaired_json
def test_parsing_with_whitespace(parser): def test_parsing_with_whitespace():
text = " Thought: Let's find the temperature \n Action: search \n Action Input: what is the temperature in SF? " text = " Thought: Let's find the temperature \n Action: search \n Action Input: what is the temperature in SF? "
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -327,7 +320,7 @@ def test_parsing_with_whitespace(parser):
assert result.tool_input == "what is the temperature in SF?" assert result.tool_input == "what is the temperature in SF?"
def test_parsing_with_special_characters(parser): def test_parsing_with_special_characters():
text = 'Thought: Let\'s find the temperature\nAction: search\nAction Input: "what is the temperature in SF?"' text = 'Thought: Let\'s find the temperature\nAction: search\nAction Input: "what is the temperature in SF?"'
result = parser.parse(text) result = parser.parse(text)
assert isinstance(result, AgentAction) assert isinstance(result, AgentAction)
@@ -335,7 +328,7 @@ def test_parsing_with_special_characters(parser):
assert result.tool_input == "what is the temperature in SF?" assert result.tool_input == "what is the temperature in SF?"
def test_integration_valid_and_invalid(parser): def test_integration_valid_and_invalid():
text = """ text = """
Thought: Let's find the temperature Thought: Let's find the temperature
Action: search Action: search
@@ -365,9 +358,4 @@ def test_integration_valid_and_invalid(parser):
assert isinstance(results[3], OutputParserException) assert isinstance(results[3], OutputParserException)
class MockAgent:
def increment_formatting_errors(self):
pass
# TODO: ADD TEST TO MAKE SURE ** REMOVAL DOESN'T MESS UP ANYTHING # TODO: ADD TEST TO MAKE SURE ** REMOVAL DOESN'T MESS UP ANYTHING