mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-13 21:33:32 +08:00
fix: resolve git conflicts in LLM abstraction layer
- Fix gui() function import in __init__.py (use cli.selector) - Fix prompt builder system message merging logic - Add default max_tokens for Anthropic API in claude.py - Fix openai tool_call arguments parsing with json.loads - Fix test_builder.py PromptConfig import and assertions
This commit is contained in:
162
pyproject.toml
162
pyproject.toml
@@ -1,84 +1,78 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "llm-abstraction"
|
name = "llm-abstraction"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Provider-agnostic LLM abstraction layer"
|
description = "Provider-agnostic LLM abstraction layer"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Affaan Mustafa", email = "affaan@example.com"}
|
{name = "Affaan Mustafa", email = "affaan@example.com"}
|
||||||
]
|
]
|
||||||
keywords = ["llm", "openai", "anthropic", "ollama", "ai"]
|
keywords = ["llm", "openai", "anthropic", "ollama", "ai"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anthropic>=0.25.0",
|
"anthropic>=0.25.0",
|
||||||
"openai>=1.30.0",
|
"openai>=1.30.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=8.0",
|
"pytest>=8.0",
|
||||||
"pytest-asyncio>=0.23",
|
"pytest-asyncio>=0.23",
|
||||||
"pytest-cov>=4.1",
|
"pytest-cov>=4.1",
|
||||||
"ruff>=0.4",
|
"pytest-mock>=3.12",
|
||||||
"mypy>=1.10",
|
"ruff>=0.4",
|
||||||
"ruff>=0.4",
|
"mypy>=1.10",
|
||||||
]
|
]
|
||||||
test = [
|
|
||||||
"pytest>=8.0",
|
[project.urls]
|
||||||
"pytest-asyncio>=0.23",
|
Homepage = "https://github.com/affaan-m/everything-claude-code"
|
||||||
"pytest-cov>=4.1",
|
Repository = "https://github.com/affaan-m/everything-claude-code"
|
||||||
"pytest-mock>=3.12",
|
|
||||||
]
|
[project.scripts]
|
||||||
|
llm-select = "llm.cli.selector:main"
|
||||||
[project.urls]
|
|
||||||
Homepage = "https://github.com/affaan-m/everything-claude-code"
|
[build-system]
|
||||||
Repository = "https://github.com/affaan-m/everything-claude-code"
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
[project.scripts]
|
|
||||||
llm-select = "llm.cli.selector:main"
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/llm"]
|
||||||
[build-system]
|
|
||||||
requires = ["hatchling"]
|
[tool.pytest.ini_options]
|
||||||
build-backend = "hatchling.build"
|
testpaths = ["tests"]
|
||||||
|
asyncio_mode = "auto"
|
||||||
[tool.hatch.build.targets.wheel]
|
filterwarnings = ["ignore::DeprecationWarning"]
|
||||||
packages = ["src/llm"]
|
|
||||||
|
[tool.coverage.run]
|
||||||
[tool.pytest.ini_options]
|
source = ["src/llm"]
|
||||||
testpaths = ["tests"]
|
branch = true
|
||||||
asyncio_mode = "auto"
|
|
||||||
filterwarnings = ["ignore::DeprecationWarning"]
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
[tool.coverage.run]
|
"pragma: no cover",
|
||||||
source = ["src/llm"]
|
"if TYPE_CHECKING:",
|
||||||
branch = true
|
"raise NotImplementedError",
|
||||||
|
]
|
||||||
[tool.coverage.report]
|
|
||||||
exclude_lines = [
|
[tool.ruff]
|
||||||
"pragma: no cover",
|
src-path = ["src"]
|
||||||
"if TYPE_CHECKING:",
|
target-version = "py311"
|
||||||
"raise NotImplementedError",
|
|
||||||
]
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "N", "W", "UP"]
|
||||||
[tool.ruff]
|
ignore = ["E501"]
|
||||||
src-path = ["src"]
|
|
||||||
target-version = "py311"
|
[tool.mypy]
|
||||||
|
python_version = "3.11"
|
||||||
[tool.ruff.lint]
|
src_paths = ["src"]
|
||||||
select = ["E", "F", "I", "N", "W", "UP"]
|
warn_return_any = true
|
||||||
ignore = ["E501"]
|
warn_unused_ignores = true
|
||||||
|
|
||||||
[tool.mypy]
|
|
||||||
python_version = "3.11"
|
|
||||||
src_paths = ["src"]
|
|
||||||
warn_return_any = true
|
|
||||||
warn_unused_ignores = true
|
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
"""
|
"""
|
||||||
LLM Abstraction Layer
|
LLM Abstraction Layer
|
||||||
|
|
||||||
Provider-agnostic interface for multiple LLM backends.
|
Provider-agnostic interface for multiple LLM backends.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from llm.core.interface import LLMProvider
|
from llm.core.interface import LLMProvider
|
||||||
from llm.core.types import LLMInput, LLMOutput, Message, ToolCall, ToolDefinition, ToolResult
|
from llm.core.types import LLMInput, LLMOutput, Message, ToolCall, ToolDefinition, ToolResult
|
||||||
from llm.providers import get_provider
|
from llm.providers import get_provider
|
||||||
from llm.tools import ToolExecutor, ToolRegistry
|
from llm.tools import ToolExecutor, ToolRegistry
|
||||||
from llm.cli.selector import interactive_select
|
from llm.cli.selector import interactive_select
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"LLMProvider",
|
"LLMProvider",
|
||||||
"LLMInput",
|
"LLMInput",
|
||||||
"LLMOutput",
|
"LLMOutput",
|
||||||
"Message",
|
"Message",
|
||||||
"get_provider",
|
"get_provider",
|
||||||
"ToolCall",
|
"ToolCall",
|
||||||
"ToolDefinition",
|
"ToolDefinition",
|
||||||
"ToolResult",
|
"ToolResult",
|
||||||
"ToolExecutor",
|
"ToolExecutor",
|
||||||
"ToolRegistry",
|
"ToolRegistry",
|
||||||
"interactive_select",
|
"interactive_select",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def gui() -> None:
|
def gui() -> None:
|
||||||
from llm.gui.selector import main
|
from llm.cli.selector import main
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|||||||
@@ -1,101 +1,102 @@
|
|||||||
"""Prompt builder for normalizing prompts across providers."""
|
"""Prompt builder for normalizing prompts across providers."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from llm.core.types import LLMInput, Message, Role, ToolDefinition
|
from llm.core.types import LLMInput, Message, Role, ToolDefinition
|
||||||
from llm.providers.claude import ClaudeProvider
|
from llm.providers.claude import ClaudeProvider
|
||||||
from llm.providers.openai import OpenAIProvider
|
from llm.providers.openai import OpenAIProvider
|
||||||
from llm.providers.ollama import OllamaProvider
|
from llm.providers.ollama import OllamaProvider
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PromptConfig:
|
class PromptConfig:
|
||||||
system_template: str | None = None
|
system_template: str | None = None
|
||||||
user_template: str | None = None
|
user_template: str | None = None
|
||||||
include_tools_in_system: bool = True
|
include_tools_in_system: bool = True
|
||||||
tool_format: str = "native"
|
tool_format: str = "native"
|
||||||
|
|
||||||
|
|
||||||
class PromptBuilder:
|
class PromptBuilder:
|
||||||
def __init__(self, config: PromptConfig | None = None) -> None:
|
def __init__(self, config: PromptConfig | None = None) -> None:
|
||||||
self.config = config or PromptConfig()
|
self.config = config or PromptConfig()
|
||||||
|
|
||||||
def build(self, messages: list[Message], tools: list[ToolDefinition] | None = None) -> list[Message]:
|
def build(self, messages: list[Message], tools: list[ToolDefinition] | None = None) -> list[Message]:
|
||||||
if not messages:
|
if not messages:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
result: list[Message] = []
|
result: list[Message] = []
|
||||||
system_parts: list[str] = []
|
system_parts: list[str] = []
|
||||||
|
|
||||||
if self.config.system_template:
|
if self.config.system_template:
|
||||||
system_parts.append(self.config.system_template)
|
system_parts.append(self.config.system_template)
|
||||||
|
|
||||||
if tools and self.config.include_tools_in_system:
|
if tools and self.config.include_tools_in_system:
|
||||||
tools_desc = self._format_tools(tools)
|
tools_desc = self._format_tools(tools)
|
||||||
system_parts.append(f"\n\n## Available Tools\n{tools_desc}")
|
system_parts.append(f"\n\n## Available Tools\n{tools_desc}")
|
||||||
|
|
||||||
if messages[0].role == Role.SYSTEM:
|
if messages[0].role == Role.SYSTEM:
|
||||||
system_parts.insert(0, messages[0].content)
|
system_parts.insert(0, messages[0].content)
|
||||||
result.extend(messages[1:])
|
result.insert(0, Message(role=Role.SYSTEM, content="\n\n".join(system_parts)))
|
||||||
else:
|
result.extend(messages[1:])
|
||||||
if system_parts:
|
else:
|
||||||
result.insert(0, Message(role=Role.SYSTEM, content="\n\n".join(system_parts)))
|
if system_parts:
|
||||||
result.extend(messages)
|
result.insert(0, Message(role=Role.SYSTEM, content="\n\n".join(system_parts)))
|
||||||
|
result.extend(messages)
|
||||||
return result
|
|
||||||
|
return result
|
||||||
def _format_tools(self, tools: list[ToolDefinition]) -> str:
|
|
||||||
lines = []
|
def _format_tools(self, tools: list[ToolDefinition]) -> str:
|
||||||
for tool in tools:
|
lines = []
|
||||||
lines.append(f"### {tool.name}")
|
for tool in tools:
|
||||||
lines.append(tool.description)
|
lines.append(f"### {tool.name}")
|
||||||
if tool.parameters:
|
lines.append(tool.description)
|
||||||
lines.append("Parameters:")
|
if tool.parameters:
|
||||||
lines.append(self._format_parameters(tool.parameters))
|
lines.append("Parameters:")
|
||||||
return "\n".join(lines)
|
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:
|
def _format_parameters(self, params: dict[str, Any]) -> str:
|
||||||
return str(params)
|
if "properties" not in params:
|
||||||
lines = []
|
return str(params)
|
||||||
required = params.get("required", [])
|
lines = []
|
||||||
for name, spec in params["properties"].items():
|
required = params.get("required", [])
|
||||||
prop_type = spec.get("type", "any")
|
for name, spec in params["properties"].items():
|
||||||
desc = spec.get("description", "")
|
prop_type = spec.get("type", "any")
|
||||||
required_mark = "(required)" if name in required else "(optional)"
|
desc = spec.get("description", "")
|
||||||
lines.append(f" - {name}: {prop_type} {required_mark} - {desc}")
|
required_mark = "(required)" if name in required else "(optional)"
|
||||||
return "\n".join(lines) if lines else str(params)
|
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": {
|
_PROVIDER_TEMPLATE_MAP: dict[str, dict[str, Any]] = {
|
||||||
"include_tools_in_system": False,
|
"claude": {
|
||||||
"tool_format": "anthropic",
|
"include_tools_in_system": False,
|
||||||
},
|
"tool_format": "anthropic",
|
||||||
"openai": {
|
},
|
||||||
"include_tools_in_system": False,
|
"openai": {
|
||||||
"tool_format": "openai",
|
"include_tools_in_system": False,
|
||||||
},
|
"tool_format": "openai",
|
||||||
"ollama": {
|
},
|
||||||
"include_tools_in_system": True,
|
"ollama": {
|
||||||
"tool_format": "text",
|
"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(), {})
|
def get_provider_builder(provider_name: str) -> PromptBuilder:
|
||||||
config = PromptConfig(**config_dict)
|
config_dict = _PROVIDER_TEMPLATE_MAP.get(provider_name.lower(), {})
|
||||||
return PromptBuilder(config)
|
config = PromptConfig(**config_dict)
|
||||||
|
return PromptBuilder(config)
|
||||||
|
|
||||||
def adapt_messages_for_provider(
|
|
||||||
messages: list[Message],
|
def adapt_messages_for_provider(
|
||||||
provider: str,
|
messages: list[Message],
|
||||||
tools: list[ToolDefinition] | None = None,
|
provider: str,
|
||||||
) -> list[Message]:
|
tools: list[ToolDefinition] | None = None,
|
||||||
builder = get_provider_builder(provider)
|
) -> list[Message]:
|
||||||
return builder.build(messages, tools)
|
builder = get_provider_builder(provider)
|
||||||
|
return builder.build(messages, tools)
|
||||||
|
|||||||
@@ -1,103 +1,105 @@
|
|||||||
"""Claude provider adapter."""
|
"""Claude provider adapter."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from anthropic import Anthropic
|
from anthropic import Anthropic
|
||||||
|
|
||||||
from llm.core.interface import (
|
from llm.core.interface import (
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
ContextLengthError,
|
ContextLengthError,
|
||||||
LLMProvider,
|
LLMProvider,
|
||||||
RateLimitError,
|
RateLimitError,
|
||||||
)
|
)
|
||||||
from llm.core.types import LLMInput, LLMOutput, Message, ModelInfo, ProviderType, ToolCall
|
from llm.core.types import LLMInput, LLMOutput, Message, ModelInfo, ProviderType, ToolCall
|
||||||
|
|
||||||
|
|
||||||
class ClaudeProvider(LLMProvider):
|
class ClaudeProvider(LLMProvider):
|
||||||
provider_type = ProviderType.CLAUDE
|
provider_type = ProviderType.CLAUDE
|
||||||
|
|
||||||
def __init__(self, api_key: str | None = None, base_url: str | None = None) -> None:
|
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.client = Anthropic(api_key=api_key or os.environ.get("ANTHROPIC_API_KEY"), base_url=base_url)
|
||||||
self._models = [
|
self._models = [
|
||||||
ModelInfo(
|
ModelInfo(
|
||||||
name="claude-opus-4-5",
|
name="claude-opus-4-5",
|
||||||
provider=ProviderType.CLAUDE,
|
provider=ProviderType.CLAUDE,
|
||||||
supports_tools=True,
|
supports_tools=True,
|
||||||
supports_vision=True,
|
supports_vision=True,
|
||||||
max_tokens=8192,
|
max_tokens=8192,
|
||||||
context_window=200000,
|
context_window=200000,
|
||||||
),
|
),
|
||||||
ModelInfo(
|
ModelInfo(
|
||||||
name="claude-sonnet-4-7",
|
name="claude-sonnet-4-7",
|
||||||
provider=ProviderType.CLAUDE,
|
provider=ProviderType.CLAUDE,
|
||||||
supports_tools=True,
|
supports_tools=True,
|
||||||
supports_vision=True,
|
supports_vision=True,
|
||||||
max_tokens=8192,
|
max_tokens=8192,
|
||||||
context_window=200000,
|
context_window=200000,
|
||||||
),
|
),
|
||||||
ModelInfo(
|
ModelInfo(
|
||||||
name="claude-haiku-4-7",
|
name="claude-haiku-4-7",
|
||||||
provider=ProviderType.CLAUDE,
|
provider=ProviderType.CLAUDE,
|
||||||
supports_tools=True,
|
supports_tools=True,
|
||||||
supports_vision=False,
|
supports_vision=False,
|
||||||
max_tokens=4096,
|
max_tokens=4096,
|
||||||
context_window=200000,
|
context_window=200000,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def generate(self, input: LLMInput) -> LLMOutput:
|
def generate(self, input: LLMInput) -> LLMOutput:
|
||||||
try:
|
try:
|
||||||
params: dict[str, Any] = {
|
params: dict[str, Any] = {
|
||||||
"model": input.model or "claude-sonnet-4-7",
|
"model": input.model or "claude-sonnet-4-7",
|
||||||
"messages": [msg.to_dict() for msg in input.messages],
|
"messages": [msg.to_dict() for msg in input.messages],
|
||||||
"temperature": input.temperature,
|
"temperature": input.temperature,
|
||||||
}
|
}
|
||||||
if input.max_tokens:
|
if input.max_tokens:
|
||||||
params["max_tokens"] = input.max_tokens
|
params["max_tokens"] = input.max_tokens
|
||||||
if input.tools:
|
else:
|
||||||
params["tools"] = [tool.to_dict() for tool in input.tools]
|
params["max_tokens"] = 8192 # required by Anthropic API
|
||||||
|
if input.tools:
|
||||||
response = self.client.messages.create(**params)
|
params["tools"] = [tool.to_dict() for tool in input.tools]
|
||||||
|
|
||||||
tool_calls = None
|
response = self.client.messages.create(**params)
|
||||||
if response.content and hasattr(response.content[0], "type"):
|
|
||||||
if response.content[0].type == "tool_use":
|
tool_calls = None
|
||||||
tool_calls = [
|
if response.content and hasattr(response.content[0], "type"):
|
||||||
ToolCall(
|
if response.content[0].type == "tool_use":
|
||||||
id=getattr(response.content[0], "id", ""),
|
tool_calls = [
|
||||||
name=getattr(response.content[0], "name", ""),
|
ToolCall(
|
||||||
arguments=getattr(response.content[0].input, "__dict__", {}),
|
id=getattr(response.content[0], "id", ""),
|
||||||
)
|
name=getattr(response.content[0], "name", ""),
|
||||||
]
|
arguments=getattr(response.content[0].input, "__dict__", {}),
|
||||||
|
)
|
||||||
return LLMOutput(
|
]
|
||||||
content=response.content[0].text if response.content else "",
|
|
||||||
tool_calls=tool_calls,
|
return LLMOutput(
|
||||||
model=response.model,
|
content=response.content[0].text if response.content else "",
|
||||||
usage={
|
tool_calls=tool_calls,
|
||||||
"input_tokens": response.usage.input_tokens,
|
model=response.model,
|
||||||
"output_tokens": response.usage.output_tokens,
|
usage={
|
||||||
},
|
"input_tokens": response.usage.input_tokens,
|
||||||
stop_reason=response.stop_reason,
|
"output_tokens": response.usage.output_tokens,
|
||||||
)
|
},
|
||||||
except Exception as e:
|
stop_reason=response.stop_reason,
|
||||||
msg = str(e)
|
)
|
||||||
if "401" in msg or "authentication" in msg.lower():
|
except Exception as e:
|
||||||
raise AuthenticationError(msg, provider=ProviderType.CLAUDE) from e
|
msg = str(e)
|
||||||
if "429" in msg or "rate_limit" in msg.lower():
|
if "401" in msg or "authentication" in msg.lower():
|
||||||
raise RateLimitError(msg, provider=ProviderType.CLAUDE) from e
|
raise AuthenticationError(msg, provider=ProviderType.CLAUDE) from e
|
||||||
if "context" in msg.lower() and "length" in msg.lower():
|
if "429" in msg or "rate_limit" in msg.lower():
|
||||||
raise ContextLengthError(msg, provider=ProviderType.CLAUDE) from e
|
raise RateLimitError(msg, provider=ProviderType.CLAUDE) from e
|
||||||
raise
|
if "context" in msg.lower() and "length" in msg.lower():
|
||||||
|
raise ContextLengthError(msg, provider=ProviderType.CLAUDE) from e
|
||||||
def list_models(self) -> list[ModelInfo]:
|
raise
|
||||||
return self._models.copy()
|
|
||||||
|
def list_models(self) -> list[ModelInfo]:
|
||||||
def validate_config(self) -> bool:
|
return self._models.copy()
|
||||||
return bool(self.client.api_key)
|
|
||||||
|
def validate_config(self) -> bool:
|
||||||
def get_default_model(self) -> str:
|
return bool(self.client.api_key)
|
||||||
return "claude-sonnet-4-7"
|
|
||||||
|
def get_default_model(self) -> str:
|
||||||
|
return "claude-sonnet-4-7"
|
||||||
|
|||||||
@@ -1,113 +1,114 @@
|
|||||||
"""OpenAI provider adapter."""
|
"""OpenAI provider adapter."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import json
|
||||||
from typing import Any
|
import os
|
||||||
|
from typing import Any
|
||||||
from openai import OpenAI
|
|
||||||
|
from openai import OpenAI
|
||||||
from llm.core.interface import (
|
|
||||||
AuthenticationError,
|
from llm.core.interface import (
|
||||||
ContextLengthError,
|
AuthenticationError,
|
||||||
LLMProvider,
|
ContextLengthError,
|
||||||
RateLimitError,
|
LLMProvider,
|
||||||
)
|
RateLimitError,
|
||||||
from llm.core.types import LLMInput, LLMOutput, Message, ModelInfo, ProviderType, ToolCall
|
)
|
||||||
|
from llm.core.types import LLMInput, LLMOutput, Message, ModelInfo, ProviderType, ToolCall
|
||||||
|
|
||||||
class OpenAIProvider(LLMProvider):
|
|
||||||
provider_type = ProviderType.OPENAI
|
class OpenAIProvider(LLMProvider):
|
||||||
|
provider_type = ProviderType.OPENAI
|
||||||
def __init__(self, api_key: str | None = None, base_url: str | None = None) -> None:
|
|
||||||
self.client = OpenAI(api_key=api_key or os.environ.get("OPENAI_API_KEY"), base_url=base_url)
|
def __init__(self, api_key: str | None = None, base_url: str | None = None) -> None:
|
||||||
self._models = [
|
self.client = OpenAI(api_key=api_key or os.environ.get("OPENAI_API_KEY"), base_url=base_url)
|
||||||
ModelInfo(
|
self._models = [
|
||||||
name="gpt-4o",
|
ModelInfo(
|
||||||
provider=ProviderType.OPENAI,
|
name="gpt-4o",
|
||||||
supports_tools=True,
|
provider=ProviderType.OPENAI,
|
||||||
supports_vision=True,
|
supports_tools=True,
|
||||||
max_tokens=4096,
|
supports_vision=True,
|
||||||
context_window=128000,
|
max_tokens=4096,
|
||||||
),
|
context_window=128000,
|
||||||
ModelInfo(
|
),
|
||||||
name="gpt-4o-mini",
|
ModelInfo(
|
||||||
provider=ProviderType.OPENAI,
|
name="gpt-4o-mini",
|
||||||
supports_tools=True,
|
provider=ProviderType.OPENAI,
|
||||||
supports_vision=True,
|
supports_tools=True,
|
||||||
max_tokens=4096,
|
supports_vision=True,
|
||||||
context_window=128000,
|
max_tokens=4096,
|
||||||
),
|
context_window=128000,
|
||||||
ModelInfo(
|
),
|
||||||
name="gpt-4-turbo",
|
ModelInfo(
|
||||||
provider=ProviderType.OPENAI,
|
name="gpt-4-turbo",
|
||||||
supports_tools=True,
|
provider=ProviderType.OPENAI,
|
||||||
supports_vision=True,
|
supports_tools=True,
|
||||||
max_tokens=4096,
|
supports_vision=True,
|
||||||
context_window=128000,
|
max_tokens=4096,
|
||||||
),
|
context_window=128000,
|
||||||
ModelInfo(
|
),
|
||||||
name="gpt-3.5-turbo",
|
ModelInfo(
|
||||||
provider=ProviderType.OPENAI,
|
name="gpt-3.5-turbo",
|
||||||
supports_tools=True,
|
provider=ProviderType.OPENAI,
|
||||||
supports_vision=False,
|
supports_tools=True,
|
||||||
max_tokens=4096,
|
supports_vision=False,
|
||||||
context_window=16385,
|
max_tokens=4096,
|
||||||
),
|
context_window=16385,
|
||||||
]
|
),
|
||||||
|
]
|
||||||
def generate(self, input: LLMInput) -> LLMOutput:
|
|
||||||
try:
|
def generate(self, input: LLMInput) -> LLMOutput:
|
||||||
params: dict[str, Any] = {
|
try:
|
||||||
"model": input.model or "gpt-4o-mini",
|
params: dict[str, Any] = {
|
||||||
"messages": [msg.to_dict() for msg in input.messages],
|
"model": input.model or "gpt-4o-mini",
|
||||||
"temperature": input.temperature,
|
"messages": [msg.to_dict() for msg in input.messages],
|
||||||
}
|
"temperature": input.temperature,
|
||||||
if input.max_tokens:
|
}
|
||||||
params["max_tokens"] = input.max_tokens
|
if input.max_tokens:
|
||||||
if input.tools:
|
params["max_tokens"] = input.max_tokens
|
||||||
params["tools"] = [tool.to_dict() for tool in input.tools]
|
if input.tools:
|
||||||
|
params["tools"] = [tool.to_dict() for tool in input.tools]
|
||||||
response = self.client.chat.completions.create(**params)
|
|
||||||
choice = response.choices[0]
|
response = self.client.chat.completions.create(**params)
|
||||||
|
choice = response.choices[0]
|
||||||
tool_calls = None
|
|
||||||
if choice.message.tool_calls:
|
tool_calls = None
|
||||||
tool_calls = [
|
if choice.message.tool_calls:
|
||||||
ToolCall(
|
tool_calls = [
|
||||||
id=tc.id or "",
|
ToolCall(
|
||||||
name=tc.function.name,
|
id=tc.id or "",
|
||||||
arguments={} if tc.function.arguments == "" else tc.function.arguments,
|
name=tc.function.name,
|
||||||
)
|
arguments={} if not tc.function.arguments else json.loads(tc.function.arguments),
|
||||||
for tc in choice.message.tool_calls
|
)
|
||||||
]
|
for tc in choice.message.tool_calls
|
||||||
|
]
|
||||||
return LLMOutput(
|
|
||||||
content=choice.message.content or "",
|
return LLMOutput(
|
||||||
tool_calls=tool_calls,
|
content=choice.message.content or "",
|
||||||
model=response.model,
|
tool_calls=tool_calls,
|
||||||
usage={
|
model=response.model,
|
||||||
"prompt_tokens": response.usage.prompt_tokens,
|
usage={
|
||||||
"completion_tokens": response.usage.completion_tokens,
|
"prompt_tokens": response.usage.prompt_tokens,
|
||||||
"total_tokens": response.usage.total_tokens,
|
"completion_tokens": response.usage.completion_tokens,
|
||||||
},
|
"total_tokens": response.usage.total_tokens,
|
||||||
stop_reason=choice.finish_reason,
|
},
|
||||||
)
|
stop_reason=choice.finish_reason,
|
||||||
except Exception as e:
|
)
|
||||||
msg = str(e)
|
except Exception as e:
|
||||||
if "401" in msg or "authentication" in msg.lower():
|
msg = str(e)
|
||||||
raise AuthenticationError(msg, provider=ProviderType.OPENAI) from e
|
if "401" in msg or "authentication" in msg.lower():
|
||||||
if "429" in msg or "rate_limit" in msg.lower():
|
raise AuthenticationError(msg, provider=ProviderType.OPENAI) from e
|
||||||
raise RateLimitError(msg, provider=ProviderType.OPENAI) from e
|
if "429" in msg or "rate_limit" in msg.lower():
|
||||||
if "context" in msg.lower() and "length" in msg.lower():
|
raise RateLimitError(msg, provider=ProviderType.OPENAI) from e
|
||||||
raise ContextLengthError(msg, provider=ProviderType.OPENAI) from e
|
if "context" in msg.lower() and "length" in msg.lower():
|
||||||
raise
|
raise ContextLengthError(msg, provider=ProviderType.OPENAI) from e
|
||||||
|
raise
|
||||||
def list_models(self) -> list[ModelInfo]:
|
|
||||||
return self._models.copy()
|
def list_models(self) -> list[ModelInfo]:
|
||||||
|
return self._models.copy()
|
||||||
def validate_config(self) -> bool:
|
|
||||||
return bool(self.client.api_key)
|
def validate_config(self) -> bool:
|
||||||
|
return bool(self.client.api_key)
|
||||||
def get_default_model(self) -> str:
|
|
||||||
return "gpt-4o-mini"
|
def get_default_model(self) -> str:
|
||||||
|
return "gpt-4o-mini"
|
||||||
|
|||||||
@@ -1,61 +1,69 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from llm.core.types import LLMInput, Message, Role, ToolDefinition
|
from llm.core.types import LLMInput, Message, Role, ToolDefinition
|
||||||
from llm.prompt import PromptBuilder, adapt_messages_for_provider
|
from llm.prompt import PromptBuilder, adapt_messages_for_provider
|
||||||
|
from llm.prompt.builder import PromptConfig
|
||||||
|
|
||||||
class TestPromptBuilder:
|
|
||||||
def test_build_without_system(self):
|
class TestPromptBuilder:
|
||||||
messages = [Message(role=Role.USER, content="Hello")]
|
def test_build_without_system(self):
|
||||||
builder = PromptBuilder()
|
messages = [Message(role=Role.USER, content="Hello")]
|
||||||
result = builder.build(messages)
|
builder = PromptBuilder()
|
||||||
|
result = builder.build(messages)
|
||||||
assert len(result) == 1
|
|
||||||
assert result[0].role == Role.USER
|
assert len(result) == 1
|
||||||
|
assert result[0].role == Role.USER
|
||||||
def test_build_with_system(self):
|
|
||||||
messages = [
|
def test_build_with_system(self):
|
||||||
Message(role=Role.SYSTEM, content="You are helpful."),
|
messages = [
|
||||||
Message(role=Role.USER, content="Hello"),
|
Message(role=Role.SYSTEM, content="You are helpful."),
|
||||||
]
|
Message(role=Role.USER, content="Hello"),
|
||||||
builder = PromptBuilder()
|
]
|
||||||
result = builder.build(messages)
|
builder = PromptBuilder()
|
||||||
|
result = builder.build(messages)
|
||||||
assert len(result) == 2
|
|
||||||
assert result[0].role == Role.SYSTEM
|
assert len(result) == 2
|
||||||
|
assert result[0].role == Role.SYSTEM
|
||||||
def test_build_adds_system_from_config(self):
|
|
||||||
messages = [Message(role=Role.USER, content="Hello")]
|
def test_build_adds_system_from_config(self):
|
||||||
builder = PromptBuilder(system_template="You are a pirate.")
|
messages = [Message(role=Role.USER, content="Hello")]
|
||||||
result = builder.build(messages)
|
builder = PromptBuilder(system_template="You are a pirate.")
|
||||||
|
result = builder.build(messages)
|
||||||
assert len(result) == 2
|
|
||||||
assert "pirate" in result[0].content
|
assert len(result) == 2
|
||||||
|
assert "pirate" in result[0].content
|
||||||
def test_build_with_tools(self):
|
|
||||||
messages = [Message(role=Role.USER, content="Search for something")]
|
def test_build_adds_system_from_config(self):
|
||||||
tools = [
|
messages = [Message(role=Role.USER, content="Hello")]
|
||||||
ToolDefinition(name="search", description="Search the web", parameters={}),
|
builder = PromptBuilder(config=PromptConfig(system_template="You are a pirate."))
|
||||||
]
|
result = builder.build(messages)
|
||||||
builder = PromptBuilder(include_tools_in_system=True)
|
|
||||||
result = builder.build(messages, tools)
|
assert len(result) == 2
|
||||||
|
assert "pirate" in result[0].content
|
||||||
assert len(result) == 2
|
def test_build_with_tools(self):
|
||||||
assert "search" in result[0].content
|
messages = [Message(role=Role.USER, content="Search for something")]
|
||||||
assert "Available Tools" in result[0].content
|
tools = [
|
||||||
|
ToolDefinition(name="search", description="Search the web", parameters={}),
|
||||||
|
]
|
||||||
class TestAdaptMessagesForProvider:
|
builder = PromptBuilder(include_tools_in_system=True)
|
||||||
def test_adapt_for_claude(self):
|
result = builder.build(messages, tools)
|
||||||
messages = [Message(role=Role.USER, content="Hello")]
|
|
||||||
result = adapt_messages_for_provider(messages, "claude")
|
assert len(result) == 2
|
||||||
assert len(result) == 1
|
assert "search" in result[0].content
|
||||||
|
assert "Available Tools" in result[0].content
|
||||||
def test_adapt_for_openai(self):
|
|
||||||
messages = [Message(role=Role.USER, content="Hello")]
|
|
||||||
result = adapt_messages_for_provider(messages, "openai")
|
class TestAdaptMessagesForProvider:
|
||||||
assert len(result) == 1
|
def test_adapt_for_claude(self):
|
||||||
|
messages = [Message(role=Role.USER, content="Hello")]
|
||||||
def test_adapt_for_ollama(self):
|
result = adapt_messages_for_provider(messages, "claude")
|
||||||
messages = [Message(role=Role.USER, content="Hello")]
|
assert len(result) == 1
|
||||||
result = adapt_messages_for_provider(messages, "ollama")
|
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user