Skip to content
10 changes: 10 additions & 0 deletions src/counterpoint/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from .chat import Chat, Message
from .context import RunContext
from .exceptions import (
CounterpointConfigError,
CounterpointError,
ToolError,
ToolDefinitionError,
)
Comment on lines +3 to +8

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better readability and consistency, it's good practice to sort imports. In this case, sorting them logically by hierarchy would be best, with base classes appearing before their subclasses. This also makes the import order consistent with the order in __all__.

Suggested change
from .exceptions import (
CounterpointConfigError,
CounterpointError,
ToolError,
ToolDefinitionError,
)
from .exceptions import (
CounterpointError,
CounterpointConfigError,
ToolError,
ToolDefinitionError,
)

from .generators import Generator
from .pipeline import Pipeline
from .rate_limiter import RateLimiter, RateLimiterStrategy
Expand All @@ -19,4 +25,8 @@
"RateLimiterStrategy",
"RateLimiter",
"RunContext",
"CounterpointError",
"CounterpointConfigError",
"ToolError",
"ToolDefinitionError",
]
12 changes: 11 additions & 1 deletion src/counterpoint/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pydantic import BaseModel, Field

from counterpoint.context import RunContext
from counterpoint.exceptions import CounterpointConfigError
from counterpoint.tools import ToolCall

Role = Literal["assistant", "user", "system", "tool"]
Expand Down Expand Up @@ -81,6 +82,15 @@ def transcript(self) -> str:

@property
def output(self) -> OutputType:
"""Parsed output as the configured model.

Raises
------
CounterpointConfigError
If no `output_model` is configured on the chat/pipeline.
"""
if self.output_model is None:
raise ValueError("Output model not set")
raise CounterpointConfigError(
"Output model not set. Call Pipeline.with_output(...) before accessing Chat.output."
)
return self.last.parse(self.output_model)
23 changes: 23 additions & 0 deletions src/counterpoint/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Counterpoint-specific exception classes.

These exceptions provide clearer error semantics for configuration,
template, tool, parsing, and pipeline related issues compared to using
generic built-ins.
"""


class CounterpointError(Exception):
"""Base class for all Counterpoint errors."""


class CounterpointConfigError(CounterpointError):
"""Raised for invalid or missing configuration/state in Counterpoint."""


class ToolError(CounterpointError):
"""Base class for tool-related errors."""


class ToolDefinitionError(ToolError):
"""Raised when a tool is defined incorrectly (e.g., bad annotations)."""

13 changes: 11 additions & 2 deletions src/counterpoint/rate_limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from pydantic import BaseModel, Field, PrivateAttr

from .exceptions import CounterpointConfigError


class RateLimiterStrategy(BaseModel):
min_interval: float
Expand Down Expand Up @@ -91,7 +93,7 @@ def _register_rate_limiter(rate_limiter: RateLimiter) -> None:
def get_rate_limiter(
rate_limiter_id: str,
) -> RateLimiter:
"""Get or create a rate limiter.
"""Get a rate limiter by id.

Parameters
----------
Expand All @@ -102,8 +104,15 @@ def get_rate_limiter(
-------
RateLimiter
The rate limiter.

Raises
------
CounterpointConfigError
If a rate limiter with the given id is not registered.
"""
try:
return _rate_limiters[rate_limiter_id]
except KeyError as err:
raise ValueError(f"Rate limiter with id {rate_limiter_id} not found") from err
raise CounterpointConfigError(
f"Rate limiter with id {rate_limiter_id} not found"
) from err
3 changes: 2 additions & 1 deletion src/counterpoint/templates/prompts_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pydantic import BaseModel, Field

from counterpoint.chat import Message
from counterpoint.exceptions import CounterpointConfigError

from .environment import create_message_environment

Expand Down Expand Up @@ -35,7 +36,7 @@ async def render_messages_template(
# 2. There are no message blocks. In this case, we will create a single user message with the rendered output.
if messages:
if rendered_output.strip():
raise ValueError(
raise CounterpointConfigError(
"Template contains message blocks but rendered output is not empty."
)
return messages
Expand Down
5 changes: 3 additions & 2 deletions src/counterpoint/tools/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pydantic import BaseModel, Field, create_model

from counterpoint.context import RunContext
from counterpoint.exceptions import ToolDefinitionError

from ._docstring_parser import parse_docstring

Expand Down Expand Up @@ -54,7 +55,7 @@ def from_callable(cls, fn: Callable) -> "Tool":

Raises
------
ValueError
ToolDefinitionError
If the function lacks proper annotations or docstring.
"""
sig = inspect.signature(fn)
Expand All @@ -65,7 +66,7 @@ def from_callable(cls, fn: Callable) -> "Tool":

for name, param in sig.parameters.items():
if param.annotation is inspect.Parameter.empty:
raise ValueError(
raise ToolDefinitionError(
f"Tool `{fn.__name__}` parameter `{name}` must have a type annotation"
)

Expand Down
3 changes: 2 additions & 1 deletion tests/test_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pydantic import BaseModel

from counterpoint.chat import Chat, Message
from counterpoint.exceptions import CounterpointConfigError


def test_chat_output():
Expand Down Expand Up @@ -33,5 +34,5 @@ def test_chat_output_without_output_model():
]

chat = Chat(messages=messages)
with pytest.raises(ValueError):
with pytest.raises(CounterpointConfigError):
chat.output
4 changes: 3 additions & 1 deletion tests/test_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ async def test_multi_message_template_parsing(prompts_manager):


async def test_invalid_template(prompts_manager):
with pytest.raises(ValueError):
from counterpoint.exceptions import CounterpointConfigError

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to PEP 8, imports should usually be at the top of the file. This local import should be moved to the top-level imports to improve code style and maintain consistency with other files in the project (e.g., tests/test_chat.py). After moving the import, this line and the following blank line can be removed.


with pytest.raises(CounterpointConfigError):
await prompts_manager.render_template("invalid.j2")


Expand Down
Loading