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.
@@ -0,0 +1,6 @@
1
+ from .llm_config import llm_from_config
2
+ from .llm_interface import LLMInterface
3
+ from .llm_tool import tool
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["LLMInterface", "llm_from_config", "tool"]
@@ -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}")