# SPDX-FileCopyrightText: Shcherbak AI AS
# SPDX-License-Identifier: Apache-2.0
"""
Module defining custom exceptions for the ContextGem framework.
"""
from __future__ import annotations
# Base exception
[docs]
class ContextGemError(Exception):
"""
Base class of the exceptions that the framework raises when a run fails, i.e.
of :class:`ModelAPIError`, :class:`ModelExtractionError` and
:class:`LLMToolLoopLimitError`. Catch it to handle any of them in one clause,
e.g. ``except ContextGemError``.
An error in the calling code, e.g. an invalid argument or an object that
cannot be used where it is passed, is raised as ``ValueError`` or ``TypeError``
instead, which includes the ``ValidationError`` of pydantic, a subclass
of ``ValueError``. A file that cannot be read or written, e.g. by ``from_disk()``
or ``to_disk()``, raises ``OSError``.
Can be imported from ``contextgem``.
:ivar message: Human-readable error description
:vartype message: str
:ivar retry_count: Number of retries attempted before failure
:vartype retry_count: int or None
:ivar original_error: The underlying exception that caused the failure
:vartype original_error: Exception or None
"""
def __init__(
self,
message: str,
retry_count: int | None = None,
original_error: Exception | None = None,
):
"""
Initialize the exception.
:param message: Human-readable error description
:type message: str
:param retry_count: Number of retries attempted before failure
:type retry_count: int or None
:param original_error: The underlying exception that caused the failure
:type original_error: Exception or None
"""
super().__init__(message)
self.message = message
self.retry_count = retry_count
self.original_error = original_error
def __str__(self) -> str:
"""
Return a string representation of the exception.
:return: Formatted error message with retry and original error information
:rtype: str
"""
base_msg = self.message
if self.retry_count is not None:
base_msg += f" (after {self.retry_count} retries)"
if self.original_error is not None:
base_msg += f" - Original error: {self.original_error}"
return base_msg
# Model exceptions
[docs]
class ModelAPIError(ContextGemError):
"""
Exception raised when a request to a model fails for good, and there is
no fallback model left to try.
This is the case when:
- the request fails with a non-transient error, e.g. an invalid request or
a failed authentication;
- the request fails with a transient error (request timeout, rate limit, server
or connection error), also after ``num_retries_failed_request`` retries;
- the response of an LLM is cut off at the max output tokens during extraction,
or is empty in a chat.
During extraction, if ``raise_exception_on_extraction_error=False`` is passed
to the extraction method, a warning is logged instead.
Can be imported from ``contextgem``.
:ivar message: Human-readable error description
:vartype message: str
:ivar retry_count: Number of retries attempted for the failed request
:vartype retry_count: int or None
:ivar original_error: The underlying exception that caused the failure
:vartype original_error: Exception or None
"""
# Tool calling / tooling exceptions
# Warnings
[docs]
class ContextGemWarning(UserWarning):
"""
Category of the warnings that the framework issues for something in the calling
code that will not have the expected effect, e.g. tools set on an LLM that is
used for extraction, or an output language set together with a custom system
message. Such a warning is attributed to the line of the calling code that
caused it, where possible.
The warnings are Python warnings, not log messages, i.e. the logging level does
not affect them. To filter them, use this category, e.g.
``warnings.filterwarnings("ignore", category=ContextGemWarning)``, or make them
errors, e.g. in the tests of the calling code: a filter by the module of
the framework does not match a warning attributed to the calling code.
What happens while the framework runs, e.g. an extraction that fails when
``raise_exception_on_extraction_error=False`` is passed to the extraction
method, is logged instead.
Can be imported from ``contextgem``.
"""
class _ToolReturnValueError(Exception):
"""
Internal exception that carries the error for an invalid value returned by
a tool function (a value other than a non-empty string) out of the tool-calling
loop, so that it is not handled as an error of the LLM API: it is an error
in the calling code. It is never raised to the calling code: the error
it carries is raised instead.
:ivar error: The error for the invalid value returned by the tool function
:vartype error: Exception
"""
def __init__(self, error: Exception):
"""
Initialize the exception.
:param error: The error for the invalid value returned by the tool function
:type error: Exception
"""
super().__init__(str(error))
self.error = error
# Decision model exceptions
class _InvalidDecisionResponseError(ValueError):
"""
Internal exception raised when the answer of a decision model to a question
cannot be used, which makes the whole response of the model invalid: the answer
is missing, refused, of another kind than the question, without the needed
probabilities, with a probability outside [0, 1], with options other than
the options offered, or with a zero probability of each option. A refused
answer to the question of an aspect can be used, as "not included in
the aspect", unless the model refused to answer the question on every paragraph.
:ivar question_name: Name of the question whose answer cannot be used
:vartype question_name: str
:ivar reason: What is wrong with the answer
:vartype reason: str
:ivar text_position: Position of the text that the question was asked on, among
the texts that the questions of the same call were asked on, if it is known
:vartype text_position: int | None
:ivar location: Where the text that the question was asked on is, as a part of
a message, e.g. " on paragraph 3 of 5 (index 2)". Empty if it is not known,
or if the text is not a part of a larger one.
:vartype location: str
"""
def __init__(self, question_name: str, reason: str):
"""
Initialize the exception.
:param question_name: Name of the question whose answer cannot be used
:type question_name: str
:param reason: What is wrong with the answer
:type reason: str
"""
super().__init__(
f"Invalid answer to the decision question `{question_name}`: {reason}"
)
self.question_name = question_name
self.reason = reason
self.text_position: int | None = None
self.location: str = ""