fix: Address lint issues in parser implementation

- Fix loop variable usage in detect_and_parse method
- Replace try-except-pass with proper exception handling
- Clean up docstring formatting to remove whitespace on blank lines

Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
Devin AI
2025-09-12 13:55:27 +00:00
parent c84bdac4a6
commit 41ad22a573

View File

@@ -5,27 +5,25 @@ different formats (ReAct, OpenAI Harmony, etc.), converting them into structured
AgentAction or AgentFinish objects with automatic format detection. AgentAction or AgentFinish objects with automatic format detection.
""" """
import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, Union
import re
import json
from json_repair import repair_json from json_repair import repair_json
from crewai.agents.constants import ( from crewai.agents.constants import (
ACTION_INPUT_ONLY_REGEX,
ACTION_INPUT_REGEX, ACTION_INPUT_REGEX,
ACTION_REGEX, ACTION_REGEX,
ACTION_INPUT_ONLY_REGEX,
FINAL_ANSWER_ACTION, FINAL_ANSWER_ACTION,
MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE,
MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE,
UNABLE_TO_REPAIR_JSON_RESULTS,
HARMONY_START_PATTERN,
HARMONY_ANALYSIS_CHANNEL, HARMONY_ANALYSIS_CHANNEL,
HARMONY_COMMENTARY_CHANNEL, HARMONY_COMMENTARY_CHANNEL,
HARMONY_FINAL_ANSWER_ERROR_MESSAGE, HARMONY_FINAL_ANSWER_ERROR_MESSAGE,
HARMONY_MISSING_CONTENT_ERROR_MESSAGE, HARMONY_MISSING_CONTENT_ERROR_MESSAGE,
HARMONY_START_PATTERN,
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
@@ -71,49 +69,47 @@ class OutputParserException(Exception):
class BaseOutputParser(ABC): class BaseOutputParser(ABC):
"""Abstract base class for output parsers.""" """Abstract base class for output parsers."""
@abstractmethod @abstractmethod
def can_parse(self, text: str) -> bool: def can_parse(self, text: str) -> bool:
"""Check if this parser can handle the given text format.""" """Check if this parser can handle the given text format."""
pass
@abstractmethod @abstractmethod
def parse_text(self, text: str) -> Union[AgentAction, AgentFinish]: def parse_text(self, text: str) -> AgentAction | AgentFinish:
"""Parse the text into AgentAction or AgentFinish.""" """Parse the text into AgentAction or AgentFinish."""
pass
class OutputFormatRegistry: class OutputFormatRegistry:
"""Registry for managing different output format parsers.""" """Registry for managing different output format parsers."""
def __init__(self): def __init__(self):
self._parsers: Dict[str, BaseOutputParser] = {} self._parsers: dict[str, BaseOutputParser] = {}
def register(self, name: str, parser: BaseOutputParser) -> None: def register(self, name: str, parser: BaseOutputParser) -> None:
"""Register a parser for a specific format.""" """Register a parser for a specific format."""
self._parsers[name] = parser self._parsers[name] = parser
def detect_and_parse(self, text: str) -> Union[AgentAction, AgentFinish]: def detect_and_parse(self, text: str) -> AgentAction | AgentFinish:
"""Automatically detect format and parse with appropriate parser.""" """Automatically detect format and parse with appropriate parser."""
for name, parser in self._parsers.items(): for parser in self._parsers.values():
if parser.can_parse(text): if parser.can_parse(text):
return parser.parse_text(text) return parser.parse_text(text)
return self._parsers.get('react', ReActParser()).parse_text(text) return self._parsers.get('react', ReActParser()).parse_text(text)
class ReActParser(BaseOutputParser): class ReActParser(BaseOutputParser):
"""Parser for ReAct format outputs.""" """Parser for ReAct format outputs."""
def can_parse(self, text: str) -> bool: def can_parse(self, text: str) -> bool:
"""Check if text follows ReAct format.""" """Check if text follows ReAct format."""
return ( return (
FINAL_ANSWER_ACTION in text or FINAL_ANSWER_ACTION in text or
ACTION_INPUT_REGEX.search(text) is not None or ACTION_INPUT_REGEX.search(text) is not None or
ACTION_REGEX.search(text) is not None ACTION_REGEX.search(text) is not None
) )
def parse_text(self, text: str) -> Union[AgentAction, AgentFinish]: def parse_text(self, text: str) -> AgentAction | AgentFinish:
"""Parse ReAct format text.""" """Parse ReAct format text."""
thought = _extract_thought(text) thought = _extract_thought(text)
includes_answer = FINAL_ANSWER_ACTION in text includes_answer = FINAL_ANSWER_ACTION in text
@@ -127,7 +123,7 @@ class ReActParser(BaseOutputParser):
final_answer = final_answer[:-3].rstrip() final_answer = final_answer[:-3].rstrip()
return AgentFinish(thought=thought, output=final_answer, text=text) return AgentFinish(thought=thought, output=final_answer, text=text)
elif action_match: if action_match:
action = action_match.group(1) action = action_match.group(1)
clean_action = _clean_action(action) clean_action = _clean_action(action)
@@ -144,44 +140,43 @@ class ReActParser(BaseOutputParser):
raise OutputParserException( raise OutputParserException(
f"{MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE}\n{_I18N.slice('final_answer_format')}", f"{MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE}\n{_I18N.slice('final_answer_format')}",
) )
elif not ACTION_INPUT_ONLY_REGEX.search(text): if not ACTION_INPUT_ONLY_REGEX.search(text):
raise OutputParserException( raise OutputParserException(
MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE, MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE,
) )
else: err_format = _I18N.slice("format_without_tools")
err_format = _I18N.slice("format_without_tools") error = f"{err_format}"
error = f"{err_format}" raise OutputParserException(error)
raise OutputParserException(error)
class HarmonyParser(BaseOutputParser): class HarmonyParser(BaseOutputParser):
"""Parser for OpenAI Harmony format outputs.""" """Parser for OpenAI Harmony format outputs."""
def can_parse(self, text: str) -> bool: def can_parse(self, text: str) -> bool:
"""Check if text follows OpenAI Harmony format.""" """Check if text follows OpenAI Harmony format."""
return HARMONY_START_PATTERN.search(text) is not None return HARMONY_START_PATTERN.search(text) is not None
def parse_text(self, text: str) -> Union[AgentAction, AgentFinish]: def parse_text(self, text: str) -> AgentAction | AgentFinish:
"""Parse OpenAI Harmony format text.""" """Parse OpenAI Harmony format text."""
matches = HARMONY_START_PATTERN.findall(text) matches = HARMONY_START_PATTERN.findall(text)
if not matches: if not matches:
raise OutputParserException(HARMONY_MISSING_CONTENT_ERROR_MESSAGE) raise OutputParserException(HARMONY_MISSING_CONTENT_ERROR_MESSAGE)
channel, tool_name, content = matches[-1] channel, tool_name, content = matches[-1]
content = content.strip() content = content.strip()
if channel == HARMONY_ANALYSIS_CHANNEL: if channel == HARMONY_ANALYSIS_CHANNEL:
return AgentFinish( return AgentFinish(
thought=f"Analysis: {content}", thought=f"Analysis: {content}",
output=content, output=content,
text=text text=text
) )
elif channel == HARMONY_COMMENTARY_CHANNEL and tool_name: if channel == HARMONY_COMMENTARY_CHANNEL and tool_name:
thought_content = content thought_content = content
tool_input = content tool_input = content
try: try:
json_match = re.search(r'\{.*\}', content, re.DOTALL) json_match = re.search(r'\{.*\}', content, re.DOTALL)
if json_match: if json_match:
@@ -189,20 +184,19 @@ class HarmonyParser(BaseOutputParser):
thought_content = content[:json_match.start()].strip() thought_content = content[:json_match.start()].strip()
if not thought_content: if not thought_content:
thought_content = f"Using tool {tool_name}" thought_content = f"Using tool {tool_name}"
except: except Exception:
pass tool_input = content
safe_tool_input = _safe_repair_json(tool_input) safe_tool_input = _safe_repair_json(tool_input)
return AgentAction( return AgentAction(
thought=thought_content, thought=thought_content,
tool=tool_name, tool=tool_name,
tool_input=safe_tool_input, tool_input=safe_tool_input,
text=text text=text
) )
else: raise OutputParserException(HARMONY_FINAL_ANSWER_ERROR_MESSAGE)
raise OutputParserException(HARMONY_FINAL_ANSWER_ERROR_MESSAGE)
_format_registry = OutputFormatRegistry() _format_registry = OutputFormatRegistry()
@@ -212,33 +206,33 @@ _format_registry.register('harmony', HarmonyParser())
def parse(text: str) -> AgentAction | AgentFinish: def parse(text: str) -> AgentAction | AgentFinish:
"""Parse agent output text into AgentAction or AgentFinish. """Parse agent output text into AgentAction or AgentFinish.
Automatically detects the format (ReAct, OpenAI Harmony, etc.) and uses Automatically detects the format (ReAct, OpenAI Harmony, etc.) and uses
the appropriate parser. Maintains backward compatibility with existing ReAct format. the appropriate parser. Maintains backward compatibility with existing ReAct format.
Supports multiple formats: Supports multiple formats:
ReAct format: ReAct format:
Thought: agent thought here Thought: agent thought here
Action: search Action: search
Action Input: what is the temperature in SF? Action Input: what is the temperature in SF?
Or for final answers: Or for final answers:
Thought: agent thought here Thought: agent thought here
Final Answer: The temperature is 100 degrees Final Answer: The temperature is 100 degrees
OpenAI Harmony format: OpenAI Harmony format:
<|start|>assistant<|channel|>analysis<|message|>The temperature is 100 degrees<|end|> <|start|>assistant<|channel|>analysis<|message|>The temperature is 100 degrees<|end|>
Or for tool actions: Or for tool actions:
<|start|>assistant<|channel|>commentary to=search<|message|>{"query": "temperature in SF"}<|call|> <|start|>assistant<|channel|>commentary to=search<|message|>{"query": "temperature in SF"}<|call|>
Args: Args:
text: The agent output text to parse. text: The agent output text to parse.
Returns: Returns:
AgentAction or AgentFinish based on the content. AgentAction or AgentFinish based on the content.
Raises: Raises:
OutputParserException: If the text format is invalid or unsupported. OutputParserException: If the text format is invalid or unsupported.
""" """
@@ -261,8 +255,7 @@ def _extract_thought(text: str) -> str:
return "" return ""
thought = text[:thought_index].strip() thought = text[:thought_index].strip()
# Remove any triple backticks from the thought string # Remove any triple backticks from the thought string
thought = thought.replace("```", "").strip() return thought.replace("```", "").strip()
return thought
def _clean_action(text: str) -> str: def _clean_action(text: str) -> str: