From 1a747097f223034bf4959926054511795923ad0c Mon Sep 17 00:00:00 2001 From: Boube <109886533+Cb2i@users.noreply.github.com> Date: Fri, 3 Jul 2026 23:14:55 -0400 Subject: [PATCH] fix: docs/COMMAND-REGISTRY.json check fails on fresh Windows clone (missing .gitattributes) (#2437) * fix: add .gitattributes to force LF line endings for text files npm run command-registry:check (part of npm test) fails on a fresh clone on Windows with the common core.autocrlf=true setting: git checks out docs/COMMAND-REGISTRY.json with CRLF, but generate-command-registry.js always writes LF, so the strict string comparison in checkRegistry() never matches. Forcing LF via .gitattributes makes checkouts consistent across platforms regardless of a contributor's local autocrlf setting. * fix: normalize CRLF line endings to LF per .gitattributes pyproject.toml, src/llm/__init__.py, src/llm/prompt/builder.py, src/llm/providers/claude.py, and tests/test_builder.py had CRLF line endings committed to the repo, inconsistent with the rest of the codebase. Renormalized via 'git add --renormalize .' now that .gitattributes enforces eol=lf. --------- Co-authored-by: Affaan Mustafa --- .gitattributes | 7 + pyproject.toml | 158 ++++++++++----------- src/llm/__init__.py | 66 ++++----- src/llm/prompt/builder.py | 198 +++++++++++++------------- src/llm/providers/claude.py | 274 ++++++++++++++++++------------------ tests/test_builder.py | 106 +++++++------- 6 files changed, 408 insertions(+), 401 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..9e2661e94 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +* text=auto eol=lf + +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary diff --git a/pyproject.toml b/pyproject.toml index 2b7448267..d6c1eb67c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,79 +1,79 @@ -[project] -name = "llm-abstraction" -version = "0.1.0" -description = "Provider-agnostic LLM abstraction layer" -readme = "README.md" -requires-python = ">=3.11" -license = {text = "MIT"} -authors = [ - {name = "Affaan Mustafa", email = "affaan@example.com"} -] -keywords = ["llm", "openai", "anthropic", "ollama", "ai"] -classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", -] - -dependencies = [ - "anthropic>=0.111.0", - "openai>=1.30.0", -] - -[project.optional-dependencies] -dev = [ - "pytest>=9.1.1", - "pytest-asyncio>=0.23", - "pytest-cov>=7.1.0", - "pytest-mock>=3.12", - "ruff>=0.4", - "mypy>=2.1.0", - "pyyaml>=6.0", -] - -[project.urls] -Homepage = "https://github.com/affaan-m/everything-claude-code" -Repository = "https://github.com/affaan-m/everything-claude-code" - -[project.scripts] -llm-select = "llm.cli.selector:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["src/llm"] - -[tool.pytest.ini_options] -testpaths = ["tests"] -asyncio_mode = "auto" -filterwarnings = ["ignore::DeprecationWarning"] - -[tool.coverage.run] -source = ["src/llm"] -branch = true - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:", - "raise NotImplementedError", -] - -[tool.ruff] -src-path = ["src"] -target-version = "py311" - -[tool.ruff.lint] -select = ["E", "F", "I", "N", "W", "UP"] -ignore = ["E501"] - -[tool.mypy] -python_version = "3.11" -src_paths = ["src"] -warn_return_any = true -warn_unused_ignores = true +[project] +name = "llm-abstraction" +version = "0.1.0" +description = "Provider-agnostic LLM abstraction layer" +readme = "README.md" +requires-python = ">=3.11" +license = {text = "MIT"} +authors = [ + {name = "Affaan Mustafa", email = "affaan@example.com"} +] +keywords = ["llm", "openai", "anthropic", "ollama", "ai"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +dependencies = [ + "anthropic>=0.111.0", + "openai>=1.30.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=9.1.1", + "pytest-asyncio>=0.23", + "pytest-cov>=7.1.0", + "pytest-mock>=3.12", + "ruff>=0.4", + "mypy>=2.1.0", + "pyyaml>=6.0", +] + +[project.urls] +Homepage = "https://github.com/affaan-m/everything-claude-code" +Repository = "https://github.com/affaan-m/everything-claude-code" + +[project.scripts] +llm-select = "llm.cli.selector:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/llm"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +asyncio_mode = "auto" +filterwarnings = ["ignore::DeprecationWarning"] + +[tool.coverage.run] +source = ["src/llm"] +branch = true + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "raise NotImplementedError", +] + +[tool.ruff] +src-path = ["src"] +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "W", "UP"] +ignore = ["E501"] + +[tool.mypy] +python_version = "3.11" +src_paths = ["src"] +warn_return_any = true +warn_unused_ignores = true diff --git a/src/llm/__init__.py b/src/llm/__init__.py index 21e48e000..16eb21067 100644 --- a/src/llm/__init__.py +++ b/src/llm/__init__.py @@ -1,33 +1,33 @@ -""" -LLM Abstraction Layer - -Provider-agnostic interface for multiple LLM backends. -""" - -from llm.core.interface import LLMProvider -from llm.core.types import LLMInput, LLMOutput, Message, ToolCall, ToolDefinition, ToolResult -from llm.providers import get_provider -from llm.tools import ToolExecutor, ToolRegistry -from llm.cli.selector import interactive_select - -__version__ = "0.1.0" - -__all__ = ( - "LLMInput", - "LLMOutput", - "LLMProvider", - "Message", - "ToolCall", - "ToolDefinition", - "ToolResult", - "ToolExecutor", - "ToolRegistry", - "get_provider", - "interactive_select", -) - - -def gui() -> None: - from llm.cli.selector import main - main() - +""" +LLM Abstraction Layer + +Provider-agnostic interface for multiple LLM backends. +""" + +from llm.core.interface import LLMProvider +from llm.core.types import LLMInput, LLMOutput, Message, ToolCall, ToolDefinition, ToolResult +from llm.providers import get_provider +from llm.tools import ToolExecutor, ToolRegistry +from llm.cli.selector import interactive_select + +__version__ = "0.1.0" + +__all__ = ( + "LLMInput", + "LLMOutput", + "LLMProvider", + "Message", + "ToolCall", + "ToolDefinition", + "ToolResult", + "ToolExecutor", + "ToolRegistry", + "get_provider", + "interactive_select", +) + + +def gui() -> None: + from llm.cli.selector import main + main() + diff --git a/src/llm/prompt/builder.py b/src/llm/prompt/builder.py index 0c873b7be..4f475ce6b 100644 --- a/src/llm/prompt/builder.py +++ b/src/llm/prompt/builder.py @@ -1,24 +1,24 @@ -"""Prompt builder for normalizing prompts across providers.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any - -from llm.core.types import LLMInput, Message, Role, ToolDefinition -from llm.providers.claude import ClaudeProvider -from llm.providers.openai import OpenAIProvider -from llm.providers.ollama import OllamaProvider - - -@dataclass -class PromptConfig: - system_template: str | None = None - user_template: str | None = None - include_tools_in_system: bool = True - tool_format: str = "native" - - +"""Prompt builder for normalizing prompts across providers.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from llm.core.types import LLMInput, Message, Role, ToolDefinition +from llm.providers.claude import ClaudeProvider +from llm.providers.openai import OpenAIProvider +from llm.providers.ollama import OllamaProvider + + +@dataclass +class PromptConfig: + system_template: str | None = None + user_template: str | None = None + include_tools_in_system: bool = True + tool_format: str = "native" + + class PromptBuilder: def __init__( self, @@ -45,81 +45,81 @@ class PromptBuilder: config = PromptConfig(**{key: value for key, value in overrides.items() if value is not None}) self.config = config - - def build(self, messages: list[Message], tools: list[ToolDefinition] | None = None) -> list[Message]: - if not messages: - return [] - - result: list[Message] = [] - system_parts: list[str] = [] - - if self.config.system_template: - system_parts.append(self.config.system_template) - - if tools and self.config.include_tools_in_system: - tools_desc = self._format_tools(tools) - system_parts.append(f"\n\n## Available Tools\n{tools_desc}") - - if messages[0].role == Role.SYSTEM: - system_parts.insert(0, messages[0].content) - result.insert(0, Message(role=Role.SYSTEM, content="\n\n".join(system_parts))) - result.extend(messages[1:]) - else: - if system_parts: - result.insert(0, Message(role=Role.SYSTEM, content="\n\n".join(system_parts))) - result.extend(messages) - - return result - - def _format_tools(self, tools: list[ToolDefinition]) -> str: - lines = [] - for tool in tools: - lines.append(f"### {tool.name}") - lines.append(tool.description) - if tool.parameters: - lines.append("Parameters:") - lines.append(self._format_parameters(tool.parameters)) - return "\n".join(lines) - - def _format_parameters(self, params: dict[str, Any]) -> str: - if "properties" not in params: - return str(params) - lines = [] - required = params.get("required", []) - for name, spec in params["properties"].items(): - prop_type = spec.get("type", "any") - desc = spec.get("description", "") - required_mark = "(required)" if name in required else "(optional)" - lines.append(f" - {name}: {prop_type} {required_mark} - {desc}") - return "\n".join(lines) if lines else str(params) - - -_PROVIDER_TEMPLATE_MAP: dict[str, dict[str, Any]] = { - "claude": { - "include_tools_in_system": False, - "tool_format": "anthropic", - }, - "openai": { - "include_tools_in_system": False, - "tool_format": "openai", - }, - "ollama": { - "include_tools_in_system": True, - "tool_format": "text", - }, -} - - -def get_provider_builder(provider_name: str) -> PromptBuilder: - config_dict = _PROVIDER_TEMPLATE_MAP.get(provider_name.lower(), {}) - config = PromptConfig(**config_dict) - return PromptBuilder(config) - - -def adapt_messages_for_provider( - messages: list[Message], - provider: str, - tools: list[ToolDefinition] | None = None, -) -> list[Message]: - builder = get_provider_builder(provider) - return builder.build(messages, tools) + + def build(self, messages: list[Message], tools: list[ToolDefinition] | None = None) -> list[Message]: + if not messages: + return [] + + result: list[Message] = [] + system_parts: list[str] = [] + + if self.config.system_template: + system_parts.append(self.config.system_template) + + if tools and self.config.include_tools_in_system: + tools_desc = self._format_tools(tools) + system_parts.append(f"\n\n## Available Tools\n{tools_desc}") + + if messages[0].role == Role.SYSTEM: + system_parts.insert(0, messages[0].content) + result.insert(0, Message(role=Role.SYSTEM, content="\n\n".join(system_parts))) + result.extend(messages[1:]) + else: + if system_parts: + result.insert(0, Message(role=Role.SYSTEM, content="\n\n".join(system_parts))) + result.extend(messages) + + return result + + def _format_tools(self, tools: list[ToolDefinition]) -> str: + lines = [] + for tool in tools: + lines.append(f"### {tool.name}") + lines.append(tool.description) + if tool.parameters: + lines.append("Parameters:") + lines.append(self._format_parameters(tool.parameters)) + return "\n".join(lines) + + def _format_parameters(self, params: dict[str, Any]) -> str: + if "properties" not in params: + return str(params) + lines = [] + required = params.get("required", []) + for name, spec in params["properties"].items(): + prop_type = spec.get("type", "any") + desc = spec.get("description", "") + required_mark = "(required)" if name in required else "(optional)" + lines.append(f" - {name}: {prop_type} {required_mark} - {desc}") + return "\n".join(lines) if lines else str(params) + + +_PROVIDER_TEMPLATE_MAP: dict[str, dict[str, Any]] = { + "claude": { + "include_tools_in_system": False, + "tool_format": "anthropic", + }, + "openai": { + "include_tools_in_system": False, + "tool_format": "openai", + }, + "ollama": { + "include_tools_in_system": True, + "tool_format": "text", + }, +} + + +def get_provider_builder(provider_name: str) -> PromptBuilder: + config_dict = _PROVIDER_TEMPLATE_MAP.get(provider_name.lower(), {}) + config = PromptConfig(**config_dict) + return PromptBuilder(config) + + +def adapt_messages_for_provider( + messages: list[Message], + provider: str, + tools: list[ToolDefinition] | None = None, +) -> list[Message]: + builder = get_provider_builder(provider) + return builder.build(messages, tools) diff --git a/src/llm/providers/claude.py b/src/llm/providers/claude.py index 55cce8951..1acc7e677 100644 --- a/src/llm/providers/claude.py +++ b/src/llm/providers/claude.py @@ -1,137 +1,137 @@ -"""Claude provider adapter.""" - -from __future__ import annotations - -import os -from typing import Any - -from anthropic import Anthropic - -from llm.core.interface import ( - AuthenticationError, - ContextLengthError, - LLMProvider, - RateLimitError, -) -from llm.core.types import LLMInput, LLMOutput, ModelInfo, ProviderType, Role, ToolCall - -_DEFAULT_MODEL = "claude-sonnet-4-6" -_OPUS_ADAPTIVE_ONLY_PREFIXES = ("claude-opus-4-7", "claude-opus-4-8") - - -def _uses_adaptive_thinking_only(model: str) -> bool: - return any(model.startswith(prefix) for prefix in _OPUS_ADAPTIVE_ONLY_PREFIXES) - - -class ClaudeProvider(LLMProvider): - provider_type = ProviderType.CLAUDE - - def __init__(self, api_key: str | None = None, base_url: str | None = None) -> None: - self.client = Anthropic(api_key=api_key or os.environ.get("ANTHROPIC_API_KEY"), base_url=base_url) - self._models = [ - ModelInfo( - name="claude-opus-4-8", - provider=ProviderType.CLAUDE, - supports_tools=True, - supports_vision=True, - max_tokens=64000, - context_window=1_000_000, - ), - ModelInfo( - name="claude-sonnet-4-6", - provider=ProviderType.CLAUDE, - supports_tools=True, - supports_vision=True, - max_tokens=64000, - context_window=1_000_000, - ), - ModelInfo( - name="claude-haiku-4-5", - provider=ProviderType.CLAUDE, - supports_tools=True, - supports_vision=True, - max_tokens=16000, - context_window=200_000, - ), - ] - - def generate(self, input: LLMInput) -> LLMOutput: - try: - model = input.model or _DEFAULT_MODEL - system_parts = [msg.content for msg in input.messages if msg.role == Role.SYSTEM] - api_messages = [ - msg.to_dict() for msg in input.messages if msg.role not in (Role.SYSTEM,) - ] - - params: dict[str, Any] = { - "model": model, - "messages": api_messages, - "max_tokens": input.max_tokens if input.max_tokens else 16000, - "cache_control": {"type": "ephemeral"}, - } - if system_parts: - params["system"] = "\n\n".join(system_parts) - if input.tools: - params["tools"] = [tool.to_anthropic_tool() for tool in input.tools] - if not _uses_adaptive_thinking_only(model): - params["temperature"] = input.temperature - if _uses_adaptive_thinking_only(model): - params["thinking"] = {"type": "adaptive"} - - response = self.client.messages.create(**params) - - text_parts: list[str] = [] - tool_calls: list[ToolCall] = [] - for block in response.content or []: - block_type = getattr(block, "type", None) - if block_type == "text": - text = getattr(block, "text", "") - if text: - text_parts.append(text) - elif block_type == "tool_use": - raw_arguments = getattr(block, "input", {}) - arguments = ( - raw_arguments.copy() - if isinstance(raw_arguments, dict) - else getattr(raw_arguments, "__dict__", {}).copy() - ) - tool_calls.append( - ToolCall( - id=getattr(block, "id", ""), - name=getattr(block, "name", ""), - arguments=arguments, - ) - ) - - return LLMOutput( - content="".join(text_parts), - tool_calls=tool_calls or None, - model=response.model, - usage={ - "input_tokens": response.usage.input_tokens, - "output_tokens": response.usage.output_tokens, - "cache_creation_input_tokens": getattr( - response.usage, "cache_creation_input_tokens", 0 - ), - "cache_read_input_tokens": getattr(response.usage, "cache_read_input_tokens", 0), - }, - stop_reason=response.stop_reason, - ) - except Exception as e: - msg = str(e) - if "401" in msg or "authentication" in msg.lower(): - raise AuthenticationError(msg, provider=ProviderType.CLAUDE) from e - if "429" in msg or "rate_limit" in msg.lower(): - raise RateLimitError(msg, provider=ProviderType.CLAUDE) from e - if "context" in msg.lower() and "length" in msg.lower(): - raise ContextLengthError(msg, provider=ProviderType.CLAUDE) from e - raise - - def list_models(self) -> list[ModelInfo]: - return self._models.copy() - - def validate_config(self) -> bool: - return bool(self.client.api_key) - - def get_default_model(self) -> str: - return _DEFAULT_MODEL +"""Claude provider adapter.""" + +from __future__ import annotations + +import os +from typing import Any + +from anthropic import Anthropic + +from llm.core.interface import ( + AuthenticationError, + ContextLengthError, + LLMProvider, + RateLimitError, +) +from llm.core.types import LLMInput, LLMOutput, ModelInfo, ProviderType, Role, ToolCall + +_DEFAULT_MODEL = "claude-sonnet-4-6" +_OPUS_ADAPTIVE_ONLY_PREFIXES = ("claude-opus-4-7", "claude-opus-4-8") + + +def _uses_adaptive_thinking_only(model: str) -> bool: + return any(model.startswith(prefix) for prefix in _OPUS_ADAPTIVE_ONLY_PREFIXES) + + +class ClaudeProvider(LLMProvider): + provider_type = ProviderType.CLAUDE + + def __init__(self, api_key: str | None = None, base_url: str | None = None) -> None: + self.client = Anthropic(api_key=api_key or os.environ.get("ANTHROPIC_API_KEY"), base_url=base_url) + self._models = [ + ModelInfo( + name="claude-opus-4-8", + provider=ProviderType.CLAUDE, + supports_tools=True, + supports_vision=True, + max_tokens=64000, + context_window=1_000_000, + ), + ModelInfo( + name="claude-sonnet-4-6", + provider=ProviderType.CLAUDE, + supports_tools=True, + supports_vision=True, + max_tokens=64000, + context_window=1_000_000, + ), + ModelInfo( + name="claude-haiku-4-5", + provider=ProviderType.CLAUDE, + supports_tools=True, + supports_vision=True, + max_tokens=16000, + context_window=200_000, + ), + ] + + def generate(self, input: LLMInput) -> LLMOutput: + try: + model = input.model or _DEFAULT_MODEL + system_parts = [msg.content for msg in input.messages if msg.role == Role.SYSTEM] + api_messages = [ + msg.to_dict() for msg in input.messages if msg.role not in (Role.SYSTEM,) + ] + + params: dict[str, Any] = { + "model": model, + "messages": api_messages, + "max_tokens": input.max_tokens if input.max_tokens else 16000, + "cache_control": {"type": "ephemeral"}, + } + if system_parts: + params["system"] = "\n\n".join(system_parts) + if input.tools: + params["tools"] = [tool.to_anthropic_tool() for tool in input.tools] + if not _uses_adaptive_thinking_only(model): + params["temperature"] = input.temperature + if _uses_adaptive_thinking_only(model): + params["thinking"] = {"type": "adaptive"} + + response = self.client.messages.create(**params) + + text_parts: list[str] = [] + tool_calls: list[ToolCall] = [] + for block in response.content or []: + block_type = getattr(block, "type", None) + if block_type == "text": + text = getattr(block, "text", "") + if text: + text_parts.append(text) + elif block_type == "tool_use": + raw_arguments = getattr(block, "input", {}) + arguments = ( + raw_arguments.copy() + if isinstance(raw_arguments, dict) + else getattr(raw_arguments, "__dict__", {}).copy() + ) + tool_calls.append( + ToolCall( + id=getattr(block, "id", ""), + name=getattr(block, "name", ""), + arguments=arguments, + ) + ) + + return LLMOutput( + content="".join(text_parts), + tool_calls=tool_calls or None, + model=response.model, + usage={ + "input_tokens": response.usage.input_tokens, + "output_tokens": response.usage.output_tokens, + "cache_creation_input_tokens": getattr( + response.usage, "cache_creation_input_tokens", 0 + ), + "cache_read_input_tokens": getattr(response.usage, "cache_read_input_tokens", 0), + }, + stop_reason=response.stop_reason, + ) + except Exception as e: + msg = str(e) + if "401" in msg or "authentication" in msg.lower(): + raise AuthenticationError(msg, provider=ProviderType.CLAUDE) from e + if "429" in msg or "rate_limit" in msg.lower(): + raise RateLimitError(msg, provider=ProviderType.CLAUDE) from e + if "context" in msg.lower() and "length" in msg.lower(): + raise ContextLengthError(msg, provider=ProviderType.CLAUDE) from e + raise + + def list_models(self) -> list[ModelInfo]: + return self._models.copy() + + def validate_config(self) -> bool: + return bool(self.client.api_key) + + def get_default_model(self) -> str: + return _DEFAULT_MODEL diff --git a/tests/test_builder.py b/tests/test_builder.py index 64f57d828..df2f5da55 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -1,29 +1,29 @@ -import pytest -from llm.core.types import LLMInput, Message, Role, ToolDefinition -from llm.prompt import PromptBuilder, adapt_messages_for_provider -from llm.prompt.builder import PromptConfig - - -class TestPromptBuilder: - def test_build_without_system(self): - messages = [Message(role=Role.USER, content="Hello")] - builder = PromptBuilder() - result = builder.build(messages) - - assert len(result) == 1 - assert result[0].role == Role.USER - - def test_build_with_system(self): - messages = [ - Message(role=Role.SYSTEM, content="You are helpful."), - Message(role=Role.USER, content="Hello"), - ] - builder = PromptBuilder() - result = builder.build(messages) - - assert len(result) == 2 - assert result[0].role == Role.SYSTEM - +import pytest +from llm.core.types import LLMInput, Message, Role, ToolDefinition +from llm.prompt import PromptBuilder, adapt_messages_for_provider +from llm.prompt.builder import PromptConfig + + +class TestPromptBuilder: + def test_build_without_system(self): + messages = [Message(role=Role.USER, content="Hello")] + builder = PromptBuilder() + result = builder.build(messages) + + assert len(result) == 1 + assert result[0].role == Role.USER + + def test_build_with_system(self): + messages = [ + Message(role=Role.SYSTEM, content="You are helpful."), + Message(role=Role.USER, content="Hello"), + ] + builder = PromptBuilder() + result = builder.build(messages) + + assert len(result) == 2 + assert result[0].role == Role.SYSTEM + def test_build_adds_system_from_keyword_options(self): messages = [Message(role=Role.USER, content="Hello")] builder = PromptBuilder(system_template="You are a pirate.") @@ -55,30 +55,30 @@ class TestPromptBuilder: assert result == messages def test_build_with_tools(self): - messages = [Message(role=Role.USER, content="Search for something")] - tools = [ - ToolDefinition(name="search", description="Search the web", parameters={}), - ] - builder = PromptBuilder(include_tools_in_system=True) - result = builder.build(messages, tools) - - assert len(result) == 2 - assert "search" in result[0].content - assert "Available Tools" in result[0].content - - -class TestAdaptMessagesForProvider: - def test_adapt_for_claude(self): - messages = [Message(role=Role.USER, content="Hello")] - result = adapt_messages_for_provider(messages, "claude") - assert len(result) == 1 - - def test_adapt_for_openai(self): - messages = [Message(role=Role.USER, content="Hello")] - result = adapt_messages_for_provider(messages, "openai") - assert len(result) == 1 - - def test_adapt_for_ollama(self): - messages = [Message(role=Role.USER, content="Hello")] - result = adapt_messages_for_provider(messages, "ollama") - assert len(result) == 1 + messages = [Message(role=Role.USER, content="Search for something")] + tools = [ + ToolDefinition(name="search", description="Search the web", parameters={}), + ] + builder = PromptBuilder(include_tools_in_system=True) + result = builder.build(messages, tools) + + assert len(result) == 2 + assert "search" in result[0].content + assert "Available Tools" in result[0].content + + +class TestAdaptMessagesForProvider: + def test_adapt_for_claude(self): + messages = [Message(role=Role.USER, content="Hello")] + result = adapt_messages_for_provider(messages, "claude") + assert len(result) == 1 + + def test_adapt_for_openai(self): + messages = [Message(role=Role.USER, content="Hello")] + result = adapt_messages_for_provider(messages, "openai") + assert len(result) == 1 + + def test_adapt_for_ollama(self): + messages = [Message(role=Role.USER, content="Hello")] + result = adapt_messages_for_provider(messages, "ollama") + assert len(result) == 1