llm-interface 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm_interface/__init__.py +6 -0
- llm_interface/anthropic.py +194 -0
- llm_interface/llm_config.py +117 -0
- llm_interface/llm_interface.py +427 -0
- llm_interface/llm_tool.py +211 -0
- llm_interface/openai.py +162 -0
- llm_interface/pydantic_output_parser.py +99 -0
- llm_interface/remote_ollama.py +187 -0
- llm_interface/ssh.py +164 -0
- llm_interface/testing/__init__.py +0 -0
- llm_interface/testing/helpers.py +32 -0
- llm_interface/testing/mock_llm.py +84 -0
- llm_interface/utils.py +48 -0
- llm_interface-0.1.0.dist-info/LICENSE +201 -0
- llm_interface-0.1.0.dist-info/METADATA +177 -0
- llm_interface-0.1.0.dist-info/RECORD +17 -0
- llm_interface-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Copyright 2024 Niels Provos
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from anthropic import Anthropic, APIError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def translate_tools_for_anthropic(tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
20
|
+
"""
|
|
21
|
+
Translate a list of tools from Ollama/API format to Anthropic format.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
tools (List[Tool]): List of tool objects from the Ollama/API.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List[Dict[str, Any]]: Translated tools ready for Anthropic API consumption.
|
|
28
|
+
"""
|
|
29
|
+
anthropic_tools = []
|
|
30
|
+
|
|
31
|
+
for tool in tools:
|
|
32
|
+
# Extract the function from the tool
|
|
33
|
+
function = tool["function"]
|
|
34
|
+
|
|
35
|
+
# Assuming Tool objects have keys 'name', 'description', and 'parameters' which is a dict
|
|
36
|
+
translated_tool = {
|
|
37
|
+
"name": function["name"],
|
|
38
|
+
"description": function["description"],
|
|
39
|
+
"input_schema": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": function["parameters"]["properties"],
|
|
42
|
+
"required": function["parameters"]["required"],
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
anthropic_tools.append(translated_tool)
|
|
46
|
+
|
|
47
|
+
return anthropic_tools
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def translate_messages_for_anthropic(
|
|
51
|
+
messages: List[Dict[str, Any]]
|
|
52
|
+
) -> List[Dict[str, Any]]:
|
|
53
|
+
"""
|
|
54
|
+
Translate messages from Ollama/API format to Anthropic format.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
messages (List[Dict[str, Any]]): List of message dictionaries in Ollama format
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List[Dict[str, Any]]: Translated messages in Anthropic format
|
|
61
|
+
"""
|
|
62
|
+
translated_messages = []
|
|
63
|
+
|
|
64
|
+
for msg in messages:
|
|
65
|
+
if msg["role"] == "user":
|
|
66
|
+
# Regular user messages pass through unchanged
|
|
67
|
+
translated_messages.append({"role": "user", "content": msg["content"]})
|
|
68
|
+
|
|
69
|
+
elif msg["role"] == "assistant" and "tool_calls" in msg:
|
|
70
|
+
# Convert assistant tool calls to Anthropic format
|
|
71
|
+
tool_call = msg["tool_calls"][0] # Assume single tool call for now
|
|
72
|
+
translated_messages.append(
|
|
73
|
+
{
|
|
74
|
+
"role": "assistant",
|
|
75
|
+
"content": [
|
|
76
|
+
{
|
|
77
|
+
"type": "text",
|
|
78
|
+
"text": f"<thinking>I need to use {tool_call['function']['name']} to help answer this question.</thinking>",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"type": "tool_use",
|
|
82
|
+
"id": tool_call["id"],
|
|
83
|
+
"name": tool_call["function"]["name"],
|
|
84
|
+
"input": tool_call["function"]["arguments"],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
elif msg["role"] == "tool":
|
|
91
|
+
# Convert tool response to Anthropic's tool_result format
|
|
92
|
+
translated_messages.append(
|
|
93
|
+
{
|
|
94
|
+
"role": "user",
|
|
95
|
+
"content": [
|
|
96
|
+
{
|
|
97
|
+
"type": "tool_result",
|
|
98
|
+
"tool_use_id": msg["tool_call_id"],
|
|
99
|
+
"content": msg["content"],
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError(f"Unknown message role: {msg['role']}")
|
|
106
|
+
|
|
107
|
+
return translated_messages
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class AnthropicWrapper:
|
|
111
|
+
def __init__(self, api_key: str, max_tokens: int = 4096):
|
|
112
|
+
self.client = Anthropic(api_key=api_key)
|
|
113
|
+
self.max_tokens = max_tokens
|
|
114
|
+
|
|
115
|
+
def chat(
|
|
116
|
+
self,
|
|
117
|
+
messages: List[Dict[str, str]],
|
|
118
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
119
|
+
**kwargs,
|
|
120
|
+
) -> Dict[str, Any]:
|
|
121
|
+
"""
|
|
122
|
+
Conduct a chat conversation using the Anthropic API.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
messages (list[Mapping[str, str]]): A list of message dictionaries, each containing 'role' and 'content'.
|
|
126
|
+
**kwargs: Additional arguments to pass to the generate function.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
A dictionary containing the Anthropic response formatted to match Ollama's expected output.
|
|
130
|
+
"""
|
|
131
|
+
# Extract the system message from the messages and prepare it as a separate argument
|
|
132
|
+
system_message = next(
|
|
133
|
+
(msg["content"] for msg in messages if msg["role"] == "system"), None
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Filter out the system message to prevent duplication if it's not needed in the messages parameter
|
|
137
|
+
filtered_messages = [msg for msg in messages if msg["role"] != "system"]
|
|
138
|
+
|
|
139
|
+
# Translate messages into Anthropic format
|
|
140
|
+
if any(msg["role"] == "tool" for msg in filtered_messages):
|
|
141
|
+
filtered_messages = translate_messages_for_anthropic(filtered_messages)
|
|
142
|
+
|
|
143
|
+
# Common parameters
|
|
144
|
+
params = {
|
|
145
|
+
"max_tokens": kwargs.get("max_tokens", self.max_tokens),
|
|
146
|
+
"messages": filtered_messages,
|
|
147
|
+
"model": kwargs.get("model", "claude-3-5-sonnet-20240620"),
|
|
148
|
+
"system": system_message, # Pass the system message here
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Conditionally add temperature if it exists in kwargs
|
|
152
|
+
if "options" in kwargs:
|
|
153
|
+
if "temperature" in kwargs["options"]:
|
|
154
|
+
params["temperature"] = kwargs["options"]["temperature"]
|
|
155
|
+
|
|
156
|
+
if tools:
|
|
157
|
+
# Translate tools into Anthropic format
|
|
158
|
+
anthropic_tools = translate_tools_for_anthropic(tools)
|
|
159
|
+
params["tools"] = anthropic_tools
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
# Call the function with the constructed parameters
|
|
163
|
+
response = self.client.messages.create(**params)
|
|
164
|
+
|
|
165
|
+
# Handle tool calls if present
|
|
166
|
+
if any(block.type == "tool_use" for block in response.content):
|
|
167
|
+
# Find all tool use blocks
|
|
168
|
+
tool_use_blocks = [
|
|
169
|
+
block for block in response.content if block.type == "tool_use"
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"message": {
|
|
174
|
+
"content": "",
|
|
175
|
+
"tool_calls": [
|
|
176
|
+
{
|
|
177
|
+
"id": tool_block.id,
|
|
178
|
+
"name": tool_block.name,
|
|
179
|
+
"arguments": tool_block.input, # Anthropic uses 'input' instead of 'arguments'
|
|
180
|
+
}
|
|
181
|
+
for tool_block in tool_use_blocks
|
|
182
|
+
],
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Extract content blocks as text and simulate Ollama-like response
|
|
187
|
+
content = "".join(
|
|
188
|
+
block.text for block in response.content if block.type == "text"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return {"message": {"content": content}}
|
|
192
|
+
|
|
193
|
+
except APIError as e:
|
|
194
|
+
raise e
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Literal, Optional
|
|
3
|
+
|
|
4
|
+
from .anthropic import AnthropicWrapper
|
|
5
|
+
from .llm_interface import LLMInterface
|
|
6
|
+
from .openai import OpenAIWrapper
|
|
7
|
+
from .remote_ollama import RemoteOllama
|
|
8
|
+
from .ssh import SSHConnection
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def llm_from_config(
|
|
12
|
+
provider: Literal["ollama", "remote_ollama", "openai"] = "ollama",
|
|
13
|
+
model_name: str = "llama3",
|
|
14
|
+
max_tokens: int = 4096,
|
|
15
|
+
host: Optional[str] = None,
|
|
16
|
+
hostname: Optional[str] = None,
|
|
17
|
+
username: Optional[str] = None,
|
|
18
|
+
log_dir: str = "logs",
|
|
19
|
+
use_cache: bool = True,
|
|
20
|
+
) -> LLMInterface:
|
|
21
|
+
"""
|
|
22
|
+
Creates and configures a language model interface based on specified provider and parameters.
|
|
23
|
+
|
|
24
|
+
This function initializes a LLMInterface instance with the appropriate wrapper/client
|
|
25
|
+
based on the selected provider (ollama, remote_ollama, openai, or anthropic).
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
provider (Literal["ollama", "remote_ollama", "openai"]): The LLM provider to use.
|
|
29
|
+
Defaults to "ollama".
|
|
30
|
+
model_name (str): Name of the model to use. Defaults to "llama3".
|
|
31
|
+
max_tokens (int): Maximum number of tokens for model responses. Defaults to 4096.
|
|
32
|
+
host (Optional[str]): Host address for local ollama instance. Only used with "ollama" provider.
|
|
33
|
+
hostname (Optional[str]): Remote hostname for SSH connection. Required for "remote_ollama".
|
|
34
|
+
username (Optional[str]): Username for SSH connection. Required for "remote_ollama".
|
|
35
|
+
log_dir (str): Directory for storing logs. Defaults to "logs".
|
|
36
|
+
use_cache (bool): Whether to cache model responses. Defaults to True.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
LLMInterface: Configured interface for interacting with the specified LLM.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If required API keys are not found in environment variables,
|
|
43
|
+
or if an invalid provider is specified.
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
>>> # Create an OpenAI interface
|
|
47
|
+
>>> llm = llm_from_config(provider="openai", model_name="gpt-4")
|
|
48
|
+
|
|
49
|
+
>>> # Create a local Ollama interface
|
|
50
|
+
>>> llm = llm_from_config(provider="ollama", model_name="llama2")
|
|
51
|
+
|
|
52
|
+
>>> # Create a remote Ollama interface
|
|
53
|
+
>>> llm = llm_from_config(
|
|
54
|
+
... provider="remote_ollama",
|
|
55
|
+
... hostname="example.com",
|
|
56
|
+
... username="user"
|
|
57
|
+
... )
|
|
58
|
+
"""
|
|
59
|
+
match provider:
|
|
60
|
+
case "openai":
|
|
61
|
+
api_key = os.getenv("OPENAI_API_KEY")
|
|
62
|
+
if api_key is None:
|
|
63
|
+
raise ValueError("OPENAI_API_KEY not found in environment variables")
|
|
64
|
+
wrapper = OpenAIWrapper(api_key=api_key, max_tokens=max_tokens)
|
|
65
|
+
# add gpt-4o once the switch is made
|
|
66
|
+
support_structured_outputs = model_name in [
|
|
67
|
+
"gpt-4o-mini",
|
|
68
|
+
"gpt-4o-mini-2024-07-18",
|
|
69
|
+
"gpt-4o-2024-08-06",
|
|
70
|
+
"gpt-4o-2024-11-20",
|
|
71
|
+
"gpt-4o",
|
|
72
|
+
]
|
|
73
|
+
support_json_mode = model_name not in ["o1-mini", "o1-preview"]
|
|
74
|
+
support_system_prompt = model_name not in ["o1-mini", "o1-preview"]
|
|
75
|
+
return LLMInterface(
|
|
76
|
+
model_name=model_name,
|
|
77
|
+
log_dir=log_dir,
|
|
78
|
+
client=wrapper,
|
|
79
|
+
support_json_mode=support_json_mode,
|
|
80
|
+
support_structured_outputs=support_structured_outputs,
|
|
81
|
+
support_system_prompt=support_system_prompt,
|
|
82
|
+
use_cache=use_cache,
|
|
83
|
+
)
|
|
84
|
+
case "anthropic":
|
|
85
|
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
86
|
+
if api_key is None:
|
|
87
|
+
raise ValueError("ANTHROPIC_API_KEY not found in environment variables")
|
|
88
|
+
wrapper = AnthropicWrapper(api_key=api_key, max_tokens=max_tokens)
|
|
89
|
+
return LLMInterface(
|
|
90
|
+
model_name=model_name,
|
|
91
|
+
log_dir=log_dir,
|
|
92
|
+
client=wrapper,
|
|
93
|
+
support_json_mode=False,
|
|
94
|
+
use_cache=use_cache,
|
|
95
|
+
)
|
|
96
|
+
case "ollama" | "remote_ollama":
|
|
97
|
+
# Enable structured outputs for Llama 3+ models
|
|
98
|
+
supports_structured = True
|
|
99
|
+
if provider == "remote_ollama":
|
|
100
|
+
ssh = SSHConnection(
|
|
101
|
+
hostname=hostname,
|
|
102
|
+
username=username,
|
|
103
|
+
)
|
|
104
|
+
client = RemoteOllama(ssh_connection=ssh, model_name=model_name)
|
|
105
|
+
else:
|
|
106
|
+
client = None
|
|
107
|
+
return LLMInterface(
|
|
108
|
+
model_name=model_name,
|
|
109
|
+
log_dir=log_dir,
|
|
110
|
+
client=client,
|
|
111
|
+
host=host,
|
|
112
|
+
support_json_mode=True,
|
|
113
|
+
support_structured_outputs=supports_structured,
|
|
114
|
+
use_cache=use_cache,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
raise ValueError(f"Invalid LLM provider in config: {provider}")
|