hbllmutils.response.parsable
Parsable LLM task functionality with automatic retry mechanism for output parsing.
This module provides parsable LLM task functionality with an automatic retry mechanism for output parsing. It extends the base LLM task to support parsing of model outputs with configurable retry logic when parsing fails. The module includes exception handling for parse failures and tracking of all retry attempts.
The module is designed to handle scenarios where LLM outputs need to be parsed into specific formats (such as JSON, XML, or structured data), with automatic retry when parsing fails due to malformed or unexpected output. This is particularly useful when working with LLMs that may occasionally produce outputs that do not conform to the expected format.
The module contains the following main components:
OutputParseWithException- Data class for storing failed parse attemptsOutputParseFailed- Exception raised when all parsing attempts failParsableLLMTask- LLM task with automatic output parsing and retry logic
- Architecture:
The module uses a template method pattern where subclasses implement the
ParsableLLMTask._parse_and_validate()method to define their specific parsing logic. The base class handles the retry mechanism, exception tracking, and logging automatically.- Retry Mechanism:
When parsing fails, the task will:
Log the parsing error with attempt count
Store the failed output and exception for debugging
Request a new response from the model
Attempt to parse the new response
Repeat until success or max retries reached
If all retries are exhausted, an
OutputParseFailedexception is raised containing all failed attempts for comprehensive debugging.
Note
The retry mechanism sends new requests to the LLM for each failed parse attempt, which may incur additional API costs and latency. Consider setting appropriate max_retries values based on your use case.
Warning
Ensure that your parsing logic in ParsableLLMTask._parse_and_validate()
raises exceptions that match the types specified in
ParsableLLMTask.__exceptions__. Other exception types will propagate
immediately without retry.
Example:
>>> import json
>>> from hbllmutils.model import LLMModel
>>> from hbllmutils.response import ParsableLLMTask, extract_code, parse_json
>>>
>>> class JSONParsableTask(ParsableLLMTask):
... '''A task that parses JSON responses from the model.'''
... __exceptions__ = (json.JSONDecodeError, KeyError)
...
... def _parse_and_validate(self, content: str):
... # Extract code block and parse JSON
... data = parse_json(extract_code(content))
...
... # Validate required fields
... if 'answer' not in data:
... raise KeyError("Missing 'answer' field")
...
... return data
>>>
>>> # Initialize model and task
>>> model = LLMModel(...)
>>> task = JSONParsableTask(model, default_max_retries=3)
>>>
>>> # Ask question with automatic parsing and retry
>>> result = task.ask_then_parse(
... input_content="What is the capital of France? Answer in JSON with 'answer' key",
... max_retries=5
... )
>>> print(result['answer'])
Paris
>>>
>>> # Handle parsing failures
>>> try:
... result = task.ask_then_parse("Invalid request")
... except OutputParseFailed as e:
... print(f"Failed after {len(e.tries)} attempts")
... for i, attempt in enumerate(e.tries):
... print(f"Attempt {i+1}: {attempt.exception}")
OutputParseWithException
- class hbllmutils.response.parsable.OutputParseWithException(output: str, exception: Exception)[source]
Data class to store a failed parse attempt with its output and exception.
This class encapsulates information about a single failed parsing attempt, including both the raw output that failed to parse and the exception that was raised during the parsing process. It is used by
OutputParseFailedto provide comprehensive debugging information about all failed attempts.The class is immutable (frozen dataclass) to ensure that stored failure information cannot be accidentally modified.
- Variables:
output (str) – The raw output string from the model that failed to parse.
exception (Exception) – The exception that occurred during parsing attempt.
Example:
>>> import json >>> attempt = OutputParseWithException( ... output='{"invalid": json}', ... exception=json.JSONDecodeError("Expecting ',' delimiter", "", 0) ... ) >>> print(attempt.output) {"invalid": json} >>> print(type(attempt.exception)) <class 'json.decoder.JSONDecodeError'> >>> >>> # Used in debugging failed parsing attempts >>> for attempt in failed_attempts: ... print(f"Output: {attempt.output[:50]}...") ... print(f"Error: {attempt.exception}")
OutputParseFailed
- class hbllmutils.response.parsable.OutputParseFailed(message: str, tries: List[OutputParseWithException])[source]
Exception raised when output parsing fails after all retry attempts.
This exception is raised by
ParsableLLMTask.ask_then_parse()when the task exhausts all retry attempts without successfully parsing the model’s output. It contains comprehensive information about all failed attempts, including the raw outputs and exceptions, which is invaluable for debugging parsing issues.The exception message includes the total number of failed attempts, and the
triesattribute provides detailed information about each individual failure for in-depth analysis.- Variables:
tries (List[OutputParseWithException]) – List of all failed parse attempts with their outputs and exceptions. Each element is an
OutputParseWithExceptioninstance containing the raw output and the exception that occurred.
Example:
>>> import json >>> tries = [ ... OutputParseWithException( ... output='{"incomplete": ', ... exception=json.JSONDecodeError("Expecting value", "", 15) ... ), ... OutputParseWithException( ... output='not json at all', ... exception=json.JSONDecodeError("Expecting value", "", 0) ... ) ... ] >>> exc = OutputParseFailed("Parsing failed after 2 tries", tries) >>> print(str(exc)) Parsing failed after 2 tries >>> print(len(exc.tries)) 2 >>> >>> # Accessing detailed failure information >>> for i, attempt in enumerate(exc.tries, 1): ... print(f"Attempt {i}:") ... print(f" Output: {attempt.output}") ... print(f" Error: {attempt.exception}") Attempt 1: Output: {"incomplete": Error: Expecting value: line 1 column 15 (char 15) Attempt 2: Output: not json at all Error: Expecting value: line 1 column 0 (char 0) >>> >>> # Handling in try-except block >>> try: ... result = task.ask_then_parse("some input") ... except OutputParseFailed as e: ... print(f"All {len(e.tries)} parsing attempts failed") ... # Log or analyze failures ... for attempt in e.tries: ... logger.error(f"Failed output: {attempt.output}")
- __init__(message: str, tries: List[OutputParseWithException])[source]
Initialize the OutputParseFailed exception.
- Parameters:
message (str) – The error message describing the failure. Typically includes the total number of failed attempts.
tries (List[OutputParseWithException]) – List of all failed parse attempts. Each element should be an
OutputParseWithExceptioninstance containing the output and exception from that attempt.
Example:
>>> tries = [ ... OutputParseWithException("bad output 1", ValueError("error 1")), ... OutputParseWithException("bad output 2", ValueError("error 2")) ... ] >>> exc = OutputParseFailed("Parsing failed after 2 tries", tries) >>> raise exc Traceback (most recent call last): ... OutputParseFailed: Parsing failed after 2 tries
ParsableLLMTask
- class hbllmutils.response.parsable.ParsableLLMTask(model: str | LLMModel, history: LLMHistory | None = None, default_max_retries: int = 5)[source]
An LLM task that supports automatic parsing of model outputs with retry mechanism.
This class extends
LLMTaskto provide automatic parsing of model outputs with configurable retry logic. When parsing fails, it will retry up to a maximum number of times before raising anOutputParseFailedexception. This is useful when the model’s output needs to be parsed into a specific format (e.g., JSON, XML, structured data) and the model may occasionally produce malformed output.The class uses a template method pattern where subclasses implement the
_parse_and_validate()method to define their specific parsing logic. The base class handles the retry mechanism, exception tracking, and logging automatically.- Workflow:
Send request to model (optionally with new input content)
Receive raw text response from model
Attempt to parse response using
_parse_and_validate()If parsing succeeds, return parsed result
If parsing fails with an exception in
__exceptions__:Log the failure with attempt count
Store the failed output and exception
Send a new request to the model
Repeat from step 2
If max retries reached, raise
OutputParseFailedwith all attempts
- Variables:
__exceptions__ (Union[Type[Exception], Tuple[Type[Exception], ...]]) – Exception types to catch during parsing attempts. Can be a single exception type or a tuple of exception types. Only exceptions matching these types will trigger a retry; other exceptions will propagate immediately. Defaults to
Exception(catches all).default_max_retries (int) – Default maximum number of retry attempts for parsing. Used when max_retries is not specified in
ask_then_parse().
Note
Each retry sends a new request to the LLM, which may incur additional API costs and increase response time. Set appropriate max_retries values based on your use case and budget constraints.
Warning
Ensure that
_parse_and_validate()raises exceptions that match the types specified in__exceptions__. Other exception types will not trigger retries and will propagate immediately.Example:
>>> import json >>> from hbllmutils.model import LLMModel >>> from hbllmutils.response import ParsableLLMTask, extract_code, parse_json >>> >>> class JSONParsableTask(ParsableLLMTask): ... '''Task that parses JSON responses with validation.''' ... __exceptions__ = (json.JSONDecodeError, KeyError, ValueError) ... ... def _parse_and_validate(self, content: str): ... # Extract code block if present ... data = parse_json(extract_code(content)) ... ... # Validate structure ... if 'result' not in data: ... raise KeyError("Missing 'result' field") ... if not isinstance(data['result'], (int, float)): ... raise ValueError("Result must be numeric") ... ... return data['result'] >>> >>> # Initialize with custom default retries >>> model = LLMModel(...) >>> task = JSONParsableTask(model, default_max_retries=3) >>> >>> # Simple usage with default retries >>> result = task.ask_then_parse(input_content="Calculate 2+2") >>> print(result) 4 >>> >>> # Usage with custom max_retries for specific request >>> result = task.ask_then_parse( ... input_content="What is 10*5?", ... max_retries=10, ... temperature=0.7 ... ) >>> print(result) 50 >>> >>> # Handling parse failures >>> try: ... result = task.ask_then_parse("Invalid request") ... except OutputParseFailed as e: ... print(f"Failed after {len(e.tries)} attempts") ... for i, attempt in enumerate(e.tries, 1): ... print(f"Attempt {i}: {attempt.exception}")
- __exceptions__
alias of
Exception
- __init__(model: str | LLMModel, history: LLMHistory | None = None, default_max_retries: int = 5)[source]
Initialize the ParsableLLMTask.
- Parameters:
model (LLMModelTyping) – The LLM model to use for generating responses. Can be a model name string, an LLMModel instance, or None to load the default model from configuration.
history (Optional[LLMHistory]) – Optional conversation history. If None, a new empty history will be created. The history tracks the conversation context across multiple interactions.
default_max_retries (int) – Default maximum number of retry attempts for parsing. Must be a positive integer. This value is used when max_retries is not specified in
ask_then_parse(). Defaults to 5.
- Raises:
ValueError – If default_max_retries is not a positive integer.
Example:
>>> from hbllmutils.model import LLMModel >>> from hbllmutils.history import LLMHistory >>> >>> # Simple initialization with defaults >>> model = LLMModel(...) >>> task = ParsableLLMTask(model) >>> print(task.default_max_retries) 5 >>> >>> # Initialize with custom default retries >>> task = ParsableLLMTask(model, default_max_retries=10) >>> print(task.default_max_retries) 10 >>> >>> # Initialize with existing history >>> history = LLMHistory().with_system_prompt("You are a helpful assistant.") >>> task = ParsableLLMTask(model, history=history, default_max_retries=3) >>> len(task.history) 1
- ask_then_parse(input_content: str | None = None, max_retries: int | None = None, **params: Any) Any[source]
Ask the model a question and parse the response with automatic retry on parse failure.
This method will repeatedly ask the model and attempt to parse the output until either parsing succeeds or the maximum number of retries is reached. Each failed attempt is logged and tracked. If all retries fail, an
OutputParseFailedexception is raised containing all failed attempts for debugging.The method uses
_parse_and_validate()to parse outputs and will catch exceptions specified in__exceptions__. Other exceptions will propagate immediately without retry.- Workflow:
Preprocess input content using
_preprocess_input_content()Send request to model using
ask()Attempt to parse response using
_parse_and_validate()On success: return parsed result
On failure (matching
__exceptions__):Log warning with attempt count
Store failed output and exception
Increment retry counter
Repeat from step 2 if retries remain
If max retries exhausted: raise
OutputParseFailed
- Parameters:
input_content (Optional[str]) – Optional user input content to add to the history before asking. If None, uses the existing history without modification. The content will be preprocessed by
_preprocess_input_content().max_retries (Optional[int]) – Maximum number of retry attempts. If None, uses
default_max_retries. Must be a positive integer if provided. Each retry sends a new request to the model.params (dict) – Additional parameters to pass to the
ask()method. Common parameters include temperature, max_tokens, top_p, etc., depending on the model.
- Returns:
The successfully parsed output from the model. The return type depends on the implementation of
_parse_and_validate().- Return type:
Any
- Raises:
ValueError – If max_retries is provided and is not a positive integer.
OutputParseFailed – If parsing fails after all retry attempts. The exception contains all failed attempts in its
triesattribute, each with the raw output and exception for debugging.Exception – Any exception not matching
__exceptions__will propagate immediately without retry.
Note
Each retry sends a new request to the LLM, which may incur additional API costs and increase total response time. Monitor your usage and set appropriate retry limits.
Warning
The method does not modify the conversation history with failed attempts. Only successful interactions are recorded in the history.
Example:
>>> import json >>> class NumberTask(ParsableLLMTask): ... '''Task that parses numeric responses.''' ... __exceptions__ = (ValueError, TypeError) ... ... def _parse_and_validate(self, content: str): ... value = float(content.strip()) ... if value < 0: ... raise ValueError("Value must be non-negative") ... return value >>> >>> task = NumberTask(model) >>> >>> # Simple usage with default retries >>> result = task.ask_then_parse(input_content="What is 2+2?") >>> print(result) 4.0 >>> >>> # Usage with custom max_retries and model parameters >>> result = task.ask_then_parse( ... input_content="Calculate 10*5", ... max_retries=3, ... temperature=0.7, ... max_tokens=100 ... ) >>> print(result) 50.0 >>> >>> # Handling parse failures >>> try: ... result = task.ask_then_parse( ... input_content="Invalid request", ... max_retries=2 ... ) ... except OutputParseFailed as e: ... print(f"Failed after {len(e.tries)} attempts") ... for i, attempt in enumerate(e.tries, 1): ... print(f"Attempt {i}:") ... print(f" Output: {attempt.output[:50]}...") ... print(f" Error: {attempt.exception}") Failed after 2 attempts Attempt 1: Output: I'm not sure what you're asking... Error: could not convert string to float: "I'm not sure what you're asking" Attempt 2: Output: Please clarify your question... Error: could not convert string to float: "Please clarify your question" >>> >>> # Example with JSON parsing >>> class JSONTask(ParsableLLMTask): ... __exceptions__ = (json.JSONDecodeError, KeyError) ... ... def _parse_and_validate(self, content: str): ... data = json.loads(content) ... if 'answer' not in data: ... raise KeyError("Missing 'answer' field") ... return data['answer'] >>> >>> task = JSONTask(model) >>> result = task.ask_then_parse( ... input_content="What is the capital of France? Answer in JSON", ... max_retries=5 ... ) >>> print(result) Paris