letta-nightly 0.1.7.dev20240924104148__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.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +24 -0
- letta/__main__.py +3 -0
- letta/agent.py +1427 -0
- letta/agent_store/chroma.py +295 -0
- letta/agent_store/db.py +546 -0
- letta/agent_store/lancedb.py +177 -0
- letta/agent_store/milvus.py +198 -0
- letta/agent_store/qdrant.py +201 -0
- letta/agent_store/storage.py +188 -0
- letta/benchmark/benchmark.py +96 -0
- letta/benchmark/constants.py +14 -0
- letta/cli/cli.py +689 -0
- letta/cli/cli_config.py +1282 -0
- letta/cli/cli_load.py +166 -0
- letta/client/__init__.py +0 -0
- letta/client/admin.py +171 -0
- letta/client/client.py +2360 -0
- letta/client/streaming.py +90 -0
- letta/client/utils.py +61 -0
- letta/config.py +484 -0
- letta/configs/anthropic.json +13 -0
- letta/configs/letta_hosted.json +11 -0
- letta/configs/openai.json +12 -0
- letta/constants.py +134 -0
- letta/credentials.py +140 -0
- letta/data_sources/connectors.py +247 -0
- letta/embeddings.py +218 -0
- letta/errors.py +26 -0
- letta/functions/__init__.py +0 -0
- letta/functions/function_sets/base.py +174 -0
- letta/functions/function_sets/extras.py +132 -0
- letta/functions/functions.py +105 -0
- letta/functions/schema_generator.py +205 -0
- letta/humans/__init__.py +0 -0
- letta/humans/examples/basic.txt +1 -0
- letta/humans/examples/cs_phd.txt +9 -0
- letta/interface.py +314 -0
- letta/llm_api/__init__.py +0 -0
- letta/llm_api/anthropic.py +383 -0
- letta/llm_api/azure_openai.py +155 -0
- letta/llm_api/cohere.py +396 -0
- letta/llm_api/google_ai.py +468 -0
- letta/llm_api/llm_api_tools.py +485 -0
- letta/llm_api/openai.py +470 -0
- letta/local_llm/README.md +3 -0
- letta/local_llm/__init__.py +0 -0
- letta/local_llm/chat_completion_proxy.py +279 -0
- letta/local_llm/constants.py +31 -0
- letta/local_llm/function_parser.py +68 -0
- letta/local_llm/grammars/__init__.py +0 -0
- letta/local_llm/grammars/gbnf_grammar_generator.py +1324 -0
- letta/local_llm/grammars/json.gbnf +26 -0
- letta/local_llm/grammars/json_func_calls_with_inner_thoughts.gbnf +32 -0
- letta/local_llm/groq/api.py +97 -0
- letta/local_llm/json_parser.py +202 -0
- letta/local_llm/koboldcpp/api.py +62 -0
- letta/local_llm/koboldcpp/settings.py +23 -0
- letta/local_llm/llamacpp/api.py +58 -0
- letta/local_llm/llamacpp/settings.py +22 -0
- letta/local_llm/llm_chat_completion_wrappers/__init__.py +0 -0
- letta/local_llm/llm_chat_completion_wrappers/airoboros.py +452 -0
- letta/local_llm/llm_chat_completion_wrappers/chatml.py +470 -0
- letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py +387 -0
- letta/local_llm/llm_chat_completion_wrappers/dolphin.py +246 -0
- letta/local_llm/llm_chat_completion_wrappers/llama3.py +345 -0
- letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py +156 -0
- letta/local_llm/llm_chat_completion_wrappers/wrapper_base.py +11 -0
- letta/local_llm/llm_chat_completion_wrappers/zephyr.py +345 -0
- letta/local_llm/lmstudio/api.py +100 -0
- letta/local_llm/lmstudio/settings.py +29 -0
- letta/local_llm/ollama/api.py +88 -0
- letta/local_llm/ollama/settings.py +32 -0
- letta/local_llm/settings/__init__.py +0 -0
- letta/local_llm/settings/deterministic_mirostat.py +45 -0
- letta/local_llm/settings/settings.py +72 -0
- letta/local_llm/settings/simple.py +28 -0
- letta/local_llm/utils.py +265 -0
- letta/local_llm/vllm/api.py +63 -0
- letta/local_llm/webui/api.py +60 -0
- letta/local_llm/webui/legacy_api.py +58 -0
- letta/local_llm/webui/legacy_settings.py +23 -0
- letta/local_llm/webui/settings.py +24 -0
- letta/log.py +76 -0
- letta/main.py +437 -0
- letta/memory.py +440 -0
- letta/metadata.py +884 -0
- letta/openai_backcompat/__init__.py +0 -0
- letta/openai_backcompat/openai_object.py +437 -0
- letta/persistence_manager.py +148 -0
- letta/personas/__init__.py +0 -0
- letta/personas/examples/anna_pa.txt +13 -0
- letta/personas/examples/google_search_persona.txt +15 -0
- letta/personas/examples/memgpt_doc.txt +6 -0
- letta/personas/examples/memgpt_starter.txt +4 -0
- letta/personas/examples/sam.txt +14 -0
- letta/personas/examples/sam_pov.txt +14 -0
- letta/personas/examples/sam_simple_pov_gpt35.txt +13 -0
- letta/personas/examples/sqldb/test.db +0 -0
- letta/prompts/__init__.py +0 -0
- letta/prompts/gpt_summarize.py +14 -0
- letta/prompts/gpt_system.py +26 -0
- letta/prompts/system/memgpt_base.txt +49 -0
- letta/prompts/system/memgpt_chat.txt +58 -0
- letta/prompts/system/memgpt_chat_compressed.txt +13 -0
- letta/prompts/system/memgpt_chat_fstring.txt +51 -0
- letta/prompts/system/memgpt_doc.txt +50 -0
- letta/prompts/system/memgpt_gpt35_extralong.txt +53 -0
- letta/prompts/system/memgpt_intuitive_knowledge.txt +31 -0
- letta/prompts/system/memgpt_modified_chat.txt +23 -0
- letta/pytest.ini +0 -0
- letta/schemas/agent.py +117 -0
- letta/schemas/api_key.py +21 -0
- letta/schemas/block.py +135 -0
- letta/schemas/document.py +21 -0
- letta/schemas/embedding_config.py +54 -0
- letta/schemas/enums.py +35 -0
- letta/schemas/job.py +38 -0
- letta/schemas/letta_base.py +80 -0
- letta/schemas/letta_message.py +175 -0
- letta/schemas/letta_request.py +23 -0
- letta/schemas/letta_response.py +28 -0
- letta/schemas/llm_config.py +54 -0
- letta/schemas/memory.py +224 -0
- letta/schemas/message.py +727 -0
- letta/schemas/openai/chat_completion_request.py +123 -0
- letta/schemas/openai/chat_completion_response.py +136 -0
- letta/schemas/openai/chat_completions.py +123 -0
- letta/schemas/openai/embedding_response.py +11 -0
- letta/schemas/openai/openai.py +157 -0
- letta/schemas/organization.py +20 -0
- letta/schemas/passage.py +80 -0
- letta/schemas/source.py +62 -0
- letta/schemas/tool.py +143 -0
- letta/schemas/usage.py +18 -0
- letta/schemas/user.py +33 -0
- letta/server/__init__.py +0 -0
- letta/server/constants.py +6 -0
- letta/server/rest_api/__init__.py +0 -0
- letta/server/rest_api/admin/__init__.py +0 -0
- letta/server/rest_api/admin/agents.py +21 -0
- letta/server/rest_api/admin/tools.py +83 -0
- letta/server/rest_api/admin/users.py +98 -0
- letta/server/rest_api/app.py +193 -0
- letta/server/rest_api/auth/__init__.py +0 -0
- letta/server/rest_api/auth/index.py +43 -0
- letta/server/rest_api/auth_token.py +22 -0
- letta/server/rest_api/interface.py +726 -0
- letta/server/rest_api/routers/__init__.py +0 -0
- letta/server/rest_api/routers/openai/__init__.py +0 -0
- letta/server/rest_api/routers/openai/assistants/__init__.py +0 -0
- letta/server/rest_api/routers/openai/assistants/assistants.py +115 -0
- letta/server/rest_api/routers/openai/assistants/schemas.py +121 -0
- letta/server/rest_api/routers/openai/assistants/threads.py +336 -0
- letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +131 -0
- letta/server/rest_api/routers/v1/__init__.py +15 -0
- letta/server/rest_api/routers/v1/agents.py +543 -0
- letta/server/rest_api/routers/v1/blocks.py +73 -0
- letta/server/rest_api/routers/v1/jobs.py +46 -0
- letta/server/rest_api/routers/v1/llms.py +28 -0
- letta/server/rest_api/routers/v1/organizations.py +61 -0
- letta/server/rest_api/routers/v1/sources.py +199 -0
- letta/server/rest_api/routers/v1/tools.py +103 -0
- letta/server/rest_api/routers/v1/users.py +109 -0
- letta/server/rest_api/static_files.py +74 -0
- letta/server/rest_api/utils.py +69 -0
- letta/server/server.py +1995 -0
- letta/server/startup.sh +8 -0
- letta/server/static_files/assets/index-0cbf7ad5.js +274 -0
- letta/server/static_files/assets/index-156816da.css +1 -0
- letta/server/static_files/assets/index-486e3228.js +274 -0
- letta/server/static_files/favicon.ico +0 -0
- letta/server/static_files/index.html +39 -0
- letta/server/static_files/memgpt_logo_transparent.png +0 -0
- letta/server/utils.py +46 -0
- letta/server/ws_api/__init__.py +0 -0
- letta/server/ws_api/example_client.py +104 -0
- letta/server/ws_api/interface.py +108 -0
- letta/server/ws_api/protocol.py +100 -0
- letta/server/ws_api/server.py +145 -0
- letta/settings.py +165 -0
- letta/streaming_interface.py +396 -0
- letta/system.py +207 -0
- letta/utils.py +1065 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/LICENSE +190 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/METADATA +98 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/RECORD +189 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/WHEEL +4 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from typing import List, Optional, Union
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from letta.schemas.message import Message
|
|
8
|
+
from letta.schemas.openai.chat_completion_request import ChatCompletionRequest, Tool
|
|
9
|
+
from letta.schemas.openai.chat_completion_response import (
|
|
10
|
+
ChatCompletionResponse,
|
|
11
|
+
Choice,
|
|
12
|
+
FunctionCall,
|
|
13
|
+
)
|
|
14
|
+
from letta.schemas.openai.chat_completion_response import (
|
|
15
|
+
Message as ChoiceMessage, # NOTE: avoid conflict with our own Letta Message datatype
|
|
16
|
+
)
|
|
17
|
+
from letta.schemas.openai.chat_completion_response import ToolCall, UsageStatistics
|
|
18
|
+
from letta.utils import get_utc_time, smart_urljoin
|
|
19
|
+
|
|
20
|
+
BASE_URL = "https://api.anthropic.com/v1"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# https://docs.anthropic.com/claude/docs/models-overview
|
|
24
|
+
# Sadly hardcoded
|
|
25
|
+
MODEL_LIST = [
|
|
26
|
+
{
|
|
27
|
+
"name": "claude-3-opus-20240229",
|
|
28
|
+
"context_window": 200000,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "claude-3-sonnet-20240229",
|
|
32
|
+
"context_window": 200000,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "claude-3-haiku-20240307",
|
|
36
|
+
"context_window": 200000,
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
DUMMY_FIRST_USER_MESSAGE = "User initializing bootup sequence."
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def antropic_get_model_context_window(url: str, api_key: Union[str, None], model: str) -> int:
|
|
44
|
+
for model_dict in anthropic_get_model_list(url=url, api_key=api_key):
|
|
45
|
+
if model_dict["name"] == model:
|
|
46
|
+
return model_dict["context_window"]
|
|
47
|
+
raise ValueError(f"Can't find model '{model}' in Anthropic model list")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def anthropic_get_model_list(url: str, api_key: Union[str, None]) -> dict:
|
|
51
|
+
"""https://docs.anthropic.com/claude/docs/models-overview"""
|
|
52
|
+
|
|
53
|
+
# NOTE: currently there is no GET /models, so we need to hardcode
|
|
54
|
+
return MODEL_LIST
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def convert_tools_to_anthropic_format(tools: List[Tool], inner_thoughts_in_kwargs: Optional[bool] = True) -> List[dict]:
|
|
58
|
+
"""See: https://docs.anthropic.com/claude/docs/tool-use
|
|
59
|
+
|
|
60
|
+
OpenAI style:
|
|
61
|
+
"tools": [{
|
|
62
|
+
"type": "function",
|
|
63
|
+
"function": {
|
|
64
|
+
"name": "find_movies",
|
|
65
|
+
"description": "find ....",
|
|
66
|
+
"parameters": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
PARAM: {
|
|
70
|
+
"type": PARAM_TYPE, # eg "string"
|
|
71
|
+
"description": PARAM_DESCRIPTION,
|
|
72
|
+
},
|
|
73
|
+
...
|
|
74
|
+
},
|
|
75
|
+
"required": List[str],
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
Anthropic style:
|
|
82
|
+
"tools": [{
|
|
83
|
+
"name": "find_movies",
|
|
84
|
+
"description": "find ....",
|
|
85
|
+
"input_schema": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {
|
|
88
|
+
PARAM: {
|
|
89
|
+
"type": PARAM_TYPE, # eg "string"
|
|
90
|
+
"description": PARAM_DESCRIPTION,
|
|
91
|
+
},
|
|
92
|
+
...
|
|
93
|
+
},
|
|
94
|
+
"required": List[str],
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
Two small differences:
|
|
100
|
+
- 1 level less of nesting
|
|
101
|
+
- "parameters" -> "input_schema"
|
|
102
|
+
"""
|
|
103
|
+
tools_dict_list = []
|
|
104
|
+
for tool in tools:
|
|
105
|
+
tools_dict_list.append(
|
|
106
|
+
{
|
|
107
|
+
"name": tool.function.name,
|
|
108
|
+
"description": tool.function.description,
|
|
109
|
+
"input_schema": tool.function.parameters,
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
return tools_dict_list
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def merge_tool_results_into_user_messages(messages: List[dict]):
|
|
116
|
+
"""Anthropic API doesn't allow role 'tool'->'user' sequences
|
|
117
|
+
|
|
118
|
+
Example HTTP error:
|
|
119
|
+
messages: roles must alternate between "user" and "assistant", but found multiple "user" roles in a row
|
|
120
|
+
|
|
121
|
+
From: https://docs.anthropic.com/claude/docs/tool-use
|
|
122
|
+
You may be familiar with other APIs that return tool use as separate from the model's primary output,
|
|
123
|
+
or which use a special-purpose tool or function message role.
|
|
124
|
+
In contrast, Anthropic's models and API are built around alternating user and assistant messages,
|
|
125
|
+
where each message is an array of rich content blocks: text, image, tool_use, and tool_result.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
# TODO walk through the messages list
|
|
129
|
+
# When a dict (dict_A) with 'role' == 'user' is followed by a dict with 'role' == 'user' (dict B), do the following
|
|
130
|
+
# dict_A["content"] = dict_A["content"] + dict_B["content"]
|
|
131
|
+
|
|
132
|
+
# The result should be a new merged_messages list that doesn't have any back-to-back dicts with 'role' == 'user'
|
|
133
|
+
merged_messages = []
|
|
134
|
+
if not messages:
|
|
135
|
+
return merged_messages
|
|
136
|
+
|
|
137
|
+
# Start with the first message in the list
|
|
138
|
+
current_message = messages[0]
|
|
139
|
+
|
|
140
|
+
for next_message in messages[1:]:
|
|
141
|
+
if current_message["role"] == "user" and next_message["role"] == "user":
|
|
142
|
+
# Merge contents of the next user message into current one
|
|
143
|
+
current_content = (
|
|
144
|
+
current_message["content"]
|
|
145
|
+
if isinstance(current_message["content"], list)
|
|
146
|
+
else [{"type": "text", "text": current_message["content"]}]
|
|
147
|
+
)
|
|
148
|
+
next_content = (
|
|
149
|
+
next_message["content"]
|
|
150
|
+
if isinstance(next_message["content"], list)
|
|
151
|
+
else [{"type": "text", "text": next_message["content"]}]
|
|
152
|
+
)
|
|
153
|
+
merged_content = current_content + next_content
|
|
154
|
+
current_message["content"] = merged_content
|
|
155
|
+
else:
|
|
156
|
+
# Append the current message to result as it's complete
|
|
157
|
+
merged_messages.append(current_message)
|
|
158
|
+
# Move on to the next message
|
|
159
|
+
current_message = next_message
|
|
160
|
+
|
|
161
|
+
# Append the last processed message to the result
|
|
162
|
+
merged_messages.append(current_message)
|
|
163
|
+
|
|
164
|
+
return merged_messages
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def remap_finish_reason(stop_reason: str) -> str:
|
|
168
|
+
"""Remap Anthropic's 'stop_reason' to OpenAI 'finish_reason'
|
|
169
|
+
|
|
170
|
+
OpenAI: 'stop', 'length', 'function_call', 'content_filter', null
|
|
171
|
+
see: https://platform.openai.com/docs/guides/text-generation/chat-completions-api
|
|
172
|
+
|
|
173
|
+
From: https://docs.anthropic.com/claude/reference/migrating-from-text-completions-to-messages#stop-reason
|
|
174
|
+
|
|
175
|
+
Messages have a stop_reason of one of the following values:
|
|
176
|
+
"end_turn": The conversational turn ended naturally.
|
|
177
|
+
"stop_sequence": One of your specified custom stop sequences was generated.
|
|
178
|
+
"max_tokens": (unchanged)
|
|
179
|
+
|
|
180
|
+
"""
|
|
181
|
+
if stop_reason == "end_turn":
|
|
182
|
+
return "stop"
|
|
183
|
+
elif stop_reason == "stop_sequence":
|
|
184
|
+
return "stop"
|
|
185
|
+
elif stop_reason == "max_tokens":
|
|
186
|
+
return "length"
|
|
187
|
+
elif stop_reason == "tool_use":
|
|
188
|
+
return "function_call"
|
|
189
|
+
else:
|
|
190
|
+
raise ValueError(f"Unexpected stop_reason: {stop_reason}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def strip_xml_tags(string: str, tag: Optional[str]) -> str:
|
|
194
|
+
if tag is None:
|
|
195
|
+
return string
|
|
196
|
+
# Construct the regular expression pattern to find the start and end tags
|
|
197
|
+
tag_pattern = f"<{tag}.*?>|</{tag}>"
|
|
198
|
+
# Use the regular expression to replace the tags with an empty string
|
|
199
|
+
return re.sub(tag_pattern, "", string)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def convert_anthropic_response_to_chatcompletion(
|
|
203
|
+
response_json: dict, # REST response from Google AI API
|
|
204
|
+
inner_thoughts_xml_tag: Optional[str] = None,
|
|
205
|
+
) -> ChatCompletionResponse:
|
|
206
|
+
"""
|
|
207
|
+
Example response from Claude 3:
|
|
208
|
+
response.json = {
|
|
209
|
+
'id': 'msg_01W1xg9hdRzbeN2CfZM7zD2w',
|
|
210
|
+
'type': 'message',
|
|
211
|
+
'role': 'assistant',
|
|
212
|
+
'content': [
|
|
213
|
+
{
|
|
214
|
+
'type': 'text',
|
|
215
|
+
'text': "<thinking>Analyzing user login event. This is Chad's first
|
|
216
|
+
interaction with me. I will adjust my personality and rapport accordingly.</thinking>"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
'type':
|
|
220
|
+
'tool_use',
|
|
221
|
+
'id': 'toolu_01Ka4AuCmfvxiidnBZuNfP1u',
|
|
222
|
+
'name': 'core_memory_append',
|
|
223
|
+
'input': {
|
|
224
|
+
'name': 'human',
|
|
225
|
+
'content': 'Chad is logging in for the first time. I will aim to build a warm
|
|
226
|
+
and welcoming rapport.',
|
|
227
|
+
'request_heartbeat': True
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
'model': 'claude-3-haiku-20240307',
|
|
232
|
+
'stop_reason': 'tool_use',
|
|
233
|
+
'stop_sequence': None,
|
|
234
|
+
'usage': {
|
|
235
|
+
'input_tokens': 3305,
|
|
236
|
+
'output_tokens': 141
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
"""
|
|
240
|
+
prompt_tokens = response_json["usage"]["input_tokens"]
|
|
241
|
+
completion_tokens = response_json["usage"]["output_tokens"]
|
|
242
|
+
|
|
243
|
+
finish_reason = remap_finish_reason(response_json["stop_reason"])
|
|
244
|
+
|
|
245
|
+
if isinstance(response_json["content"], list):
|
|
246
|
+
# inner mono + function call
|
|
247
|
+
# TODO relax asserts
|
|
248
|
+
assert len(response_json["content"]) == 2, response_json
|
|
249
|
+
assert response_json["content"][0]["type"] == "text", response_json
|
|
250
|
+
assert response_json["content"][1]["type"] == "tool_use", response_json
|
|
251
|
+
content = strip_xml_tags(string=response_json["content"][0]["text"], tag=inner_thoughts_xml_tag)
|
|
252
|
+
tool_calls = [
|
|
253
|
+
ToolCall(
|
|
254
|
+
id=response_json["content"][1]["id"],
|
|
255
|
+
type="function",
|
|
256
|
+
function=FunctionCall(
|
|
257
|
+
name=response_json["content"][1]["name"],
|
|
258
|
+
arguments=json.dumps(response_json["content"][1]["input"], indent=2),
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
]
|
|
262
|
+
else:
|
|
263
|
+
# just inner mono
|
|
264
|
+
content = strip_xml_tags(string=response_json["content"], tag=inner_thoughts_xml_tag)
|
|
265
|
+
tool_calls = None
|
|
266
|
+
|
|
267
|
+
assert response_json["role"] == "assistant", response_json
|
|
268
|
+
choice = Choice(
|
|
269
|
+
index=0,
|
|
270
|
+
finish_reason=finish_reason,
|
|
271
|
+
message=ChoiceMessage(
|
|
272
|
+
role=response_json["role"],
|
|
273
|
+
content=content,
|
|
274
|
+
tool_calls=tool_calls,
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return ChatCompletionResponse(
|
|
279
|
+
id=response_json["id"],
|
|
280
|
+
choices=[choice],
|
|
281
|
+
created=get_utc_time(),
|
|
282
|
+
model=response_json["model"],
|
|
283
|
+
usage=UsageStatistics(
|
|
284
|
+
prompt_tokens=prompt_tokens,
|
|
285
|
+
completion_tokens=completion_tokens,
|
|
286
|
+
total_tokens=prompt_tokens + completion_tokens,
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def anthropic_chat_completions_request(
|
|
292
|
+
url: str,
|
|
293
|
+
api_key: str,
|
|
294
|
+
data: ChatCompletionRequest,
|
|
295
|
+
inner_thoughts_xml_tag: Optional[str] = "thinking",
|
|
296
|
+
) -> ChatCompletionResponse:
|
|
297
|
+
"""https://docs.anthropic.com/claude/docs/tool-use"""
|
|
298
|
+
from letta.utils import printd
|
|
299
|
+
|
|
300
|
+
url = smart_urljoin(url, "messages")
|
|
301
|
+
headers = {
|
|
302
|
+
"Content-Type": "application/json",
|
|
303
|
+
"x-api-key": api_key,
|
|
304
|
+
# NOTE: beta headers for tool calling
|
|
305
|
+
"anthropic-version": "2023-06-01",
|
|
306
|
+
"anthropic-beta": "tools-2024-04-04",
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# convert the tools
|
|
310
|
+
anthropic_tools = None if data.tools is None else convert_tools_to_anthropic_format(data.tools)
|
|
311
|
+
|
|
312
|
+
# pydantic -> dict
|
|
313
|
+
data = data.model_dump(exclude_none=True)
|
|
314
|
+
|
|
315
|
+
if "functions" in data:
|
|
316
|
+
raise ValueError(f"'functions' unexpected in Anthropic API payload")
|
|
317
|
+
|
|
318
|
+
# If tools == None, strip from the payload
|
|
319
|
+
if "tools" in data and data["tools"] is None:
|
|
320
|
+
data.pop("tools")
|
|
321
|
+
data.pop("tool_choice", None) # extra safe, should exist always (default="auto")
|
|
322
|
+
# Remap to our converted tools
|
|
323
|
+
if anthropic_tools is not None:
|
|
324
|
+
data["tools"] = anthropic_tools
|
|
325
|
+
|
|
326
|
+
# Move 'system' to the top level
|
|
327
|
+
# 'messages: Unexpected role "system". The Messages API accepts a top-level `system` parameter, not "system" as an input message role.'
|
|
328
|
+
assert data["messages"][0]["role"] == "system", f"Expected 'system' role in messages[0]:\n{data['messages'][0]}"
|
|
329
|
+
data["system"] = data["messages"][0]["content"]
|
|
330
|
+
data["messages"] = data["messages"][1:]
|
|
331
|
+
|
|
332
|
+
# set `content` to None if missing
|
|
333
|
+
for message in data["messages"]:
|
|
334
|
+
if "content" not in message:
|
|
335
|
+
message["content"] = None
|
|
336
|
+
|
|
337
|
+
# Convert to Anthropic format
|
|
338
|
+
|
|
339
|
+
msg_objs = [Message.dict_to_message(user_id=None, agent_id=None, openai_message_dict=m) for m in data["messages"]]
|
|
340
|
+
data["messages"] = [m.to_anthropic_dict(inner_thoughts_xml_tag=inner_thoughts_xml_tag) for m in msg_objs]
|
|
341
|
+
|
|
342
|
+
# Handling Anthropic special requirement for 'user' message in front
|
|
343
|
+
# messages: first message must use the "user" role'
|
|
344
|
+
if data["messages"][0]["role"] != "user":
|
|
345
|
+
data["messages"] = [{"role": "user", "content": DUMMY_FIRST_USER_MESSAGE}] + data["messages"]
|
|
346
|
+
|
|
347
|
+
# Handle Anthropic's restriction on alternating user/assistant messages
|
|
348
|
+
data["messages"] = merge_tool_results_into_user_messages(data["messages"])
|
|
349
|
+
|
|
350
|
+
# Anthropic also wants max_tokens in the input
|
|
351
|
+
# It's also part of ChatCompletions
|
|
352
|
+
assert "max_tokens" in data, data
|
|
353
|
+
|
|
354
|
+
# Remove extra fields used by OpenAI but not Anthropic
|
|
355
|
+
data.pop("frequency_penalty", None)
|
|
356
|
+
data.pop("logprobs", None)
|
|
357
|
+
data.pop("n", None)
|
|
358
|
+
data.pop("top_p", None)
|
|
359
|
+
data.pop("presence_penalty", None)
|
|
360
|
+
data.pop("user", None)
|
|
361
|
+
data.pop("tool_choice", None)
|
|
362
|
+
|
|
363
|
+
printd(f"Sending request to {url}")
|
|
364
|
+
try:
|
|
365
|
+
response = requests.post(url, headers=headers, json=data)
|
|
366
|
+
printd(f"response = {response}")
|
|
367
|
+
response.raise_for_status() # Raises HTTPError for 4XX/5XX status
|
|
368
|
+
response = response.json() # convert to dict from string
|
|
369
|
+
printd(f"response.json = {response}")
|
|
370
|
+
response = convert_anthropic_response_to_chatcompletion(response_json=response, inner_thoughts_xml_tag=inner_thoughts_xml_tag)
|
|
371
|
+
return response
|
|
372
|
+
except requests.exceptions.HTTPError as http_err:
|
|
373
|
+
# Handle HTTP errors (e.g., response 4XX, 5XX)
|
|
374
|
+
printd(f"Got HTTPError, exception={http_err}, payload={data}")
|
|
375
|
+
raise http_err
|
|
376
|
+
except requests.exceptions.RequestException as req_err:
|
|
377
|
+
# Handle other requests-related errors (e.g., connection error)
|
|
378
|
+
printd(f"Got RequestException, exception={req_err}")
|
|
379
|
+
raise req_err
|
|
380
|
+
except Exception as e:
|
|
381
|
+
# Handle other potential errors
|
|
382
|
+
printd(f"Got unknown Exception, exception={e}")
|
|
383
|
+
raise e
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
|
|
6
|
+
from letta.schemas.openai.embedding_response import EmbeddingResponse
|
|
7
|
+
from letta.utils import smart_urljoin
|
|
8
|
+
|
|
9
|
+
MODEL_TO_AZURE_ENGINE = {
|
|
10
|
+
"gpt-4-1106-preview": "gpt-4",
|
|
11
|
+
"gpt-4": "gpt-4",
|
|
12
|
+
"gpt-4-32k": "gpt-4-32k",
|
|
13
|
+
"gpt-3.5": "gpt-35-turbo",
|
|
14
|
+
"gpt-3.5-turbo": "gpt-35-turbo",
|
|
15
|
+
"gpt-3.5-turbo-16k": "gpt-35-turbo-16k",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def clean_azure_endpoint(raw_endpoint_name: str) -> str:
|
|
20
|
+
"""Make sure the endpoint is of format 'https://YOUR_RESOURCE_NAME.openai.azure.com'"""
|
|
21
|
+
if raw_endpoint_name is None:
|
|
22
|
+
raise ValueError(raw_endpoint_name)
|
|
23
|
+
endpoint_address = raw_endpoint_name.strip("/").replace(".openai.azure.com", "")
|
|
24
|
+
endpoint_address = endpoint_address.replace("http://", "")
|
|
25
|
+
endpoint_address = endpoint_address.replace("https://", "")
|
|
26
|
+
return endpoint_address
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def azure_openai_get_model_list(url: str, api_key: Union[str, None], api_version: str) -> dict:
|
|
30
|
+
"""https://learn.microsoft.com/en-us/rest/api/azureopenai/models/list?view=rest-azureopenai-2023-05-15&tabs=HTTP"""
|
|
31
|
+
from letta.utils import printd
|
|
32
|
+
|
|
33
|
+
# https://xxx.openai.azure.com/openai/models?api-version=xxx
|
|
34
|
+
url = smart_urljoin(url, "openai")
|
|
35
|
+
url = smart_urljoin(url, f"models?api-version={api_version}")
|
|
36
|
+
|
|
37
|
+
headers = {"Content-Type": "application/json"}
|
|
38
|
+
if api_key is not None:
|
|
39
|
+
headers["api-key"] = f"{api_key}"
|
|
40
|
+
|
|
41
|
+
printd(f"Sending request to {url}")
|
|
42
|
+
try:
|
|
43
|
+
response = requests.get(url, headers=headers)
|
|
44
|
+
response.raise_for_status() # Raises HTTPError for 4XX/5XX status
|
|
45
|
+
response = response.json() # convert to dict from string
|
|
46
|
+
printd(f"response = {response}")
|
|
47
|
+
return response
|
|
48
|
+
except requests.exceptions.HTTPError as http_err:
|
|
49
|
+
# Handle HTTP errors (e.g., response 4XX, 5XX)
|
|
50
|
+
try:
|
|
51
|
+
response = response.json()
|
|
52
|
+
except:
|
|
53
|
+
pass
|
|
54
|
+
printd(f"Got HTTPError, exception={http_err}, response={response}")
|
|
55
|
+
raise http_err
|
|
56
|
+
except requests.exceptions.RequestException as req_err:
|
|
57
|
+
# Handle other requests-related errors (e.g., connection error)
|
|
58
|
+
try:
|
|
59
|
+
response = response.json()
|
|
60
|
+
except:
|
|
61
|
+
pass
|
|
62
|
+
printd(f"Got RequestException, exception={req_err}, response={response}")
|
|
63
|
+
raise req_err
|
|
64
|
+
except Exception as e:
|
|
65
|
+
# Handle other potential errors
|
|
66
|
+
try:
|
|
67
|
+
response = response.json()
|
|
68
|
+
except:
|
|
69
|
+
pass
|
|
70
|
+
printd(f"Got unknown Exception, exception={e}, response={response}")
|
|
71
|
+
raise e
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def azure_openai_chat_completions_request(
|
|
75
|
+
resource_name: str, deployment_id: str, api_version: str, api_key: str, data: dict
|
|
76
|
+
) -> ChatCompletionResponse:
|
|
77
|
+
"""https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions"""
|
|
78
|
+
from letta.utils import printd
|
|
79
|
+
|
|
80
|
+
assert resource_name is not None, "Missing required field when calling Azure OpenAI"
|
|
81
|
+
assert deployment_id is not None, "Missing required field when calling Azure OpenAI"
|
|
82
|
+
assert api_version is not None, "Missing required field when calling Azure OpenAI"
|
|
83
|
+
assert api_key is not None, "Missing required field when calling Azure OpenAI"
|
|
84
|
+
|
|
85
|
+
resource_name = clean_azure_endpoint(resource_name)
|
|
86
|
+
url = f"https://{resource_name}.openai.azure.com/openai/deployments/{deployment_id}/chat/completions?api-version={api_version}"
|
|
87
|
+
headers = {"Content-Type": "application/json", "api-key": f"{api_key}"}
|
|
88
|
+
|
|
89
|
+
# If functions == None, strip from the payload
|
|
90
|
+
if "functions" in data and data["functions"] is None:
|
|
91
|
+
data.pop("functions")
|
|
92
|
+
data.pop("function_call", None) # extra safe, should exist always (default="auto")
|
|
93
|
+
|
|
94
|
+
if "tools" in data and data["tools"] is None:
|
|
95
|
+
data.pop("tools")
|
|
96
|
+
data.pop("tool_choice", None) # extra safe, should exist always (default="auto")
|
|
97
|
+
|
|
98
|
+
printd(f"Sending request to {url}")
|
|
99
|
+
try:
|
|
100
|
+
data["messages"] = [i.to_openai_dict() for i in data["messages"]]
|
|
101
|
+
response = requests.post(url, headers=headers, json=data)
|
|
102
|
+
printd(f"response = {response}")
|
|
103
|
+
response.raise_for_status() # Raises HTTPError for 4XX/5XX status
|
|
104
|
+
response = response.json() # convert to dict from string
|
|
105
|
+
printd(f"response.json = {response}")
|
|
106
|
+
# NOTE: azure openai does not include "content" in the response when it is None, so we need to add it
|
|
107
|
+
if "content" not in response["choices"][0].get("message"):
|
|
108
|
+
response["choices"][0]["message"]["content"] = None
|
|
109
|
+
response = ChatCompletionResponse(**response) # convert to 'dot-dict' style which is the openai python client default
|
|
110
|
+
return response
|
|
111
|
+
except requests.exceptions.HTTPError as http_err:
|
|
112
|
+
# Handle HTTP errors (e.g., response 4XX, 5XX)
|
|
113
|
+
printd(f"Got HTTPError, exception={http_err}, payload={data}")
|
|
114
|
+
raise http_err
|
|
115
|
+
except requests.exceptions.RequestException as req_err:
|
|
116
|
+
# Handle other requests-related errors (e.g., connection error)
|
|
117
|
+
printd(f"Got RequestException, exception={req_err}")
|
|
118
|
+
raise req_err
|
|
119
|
+
except Exception as e:
|
|
120
|
+
# Handle other potential errors
|
|
121
|
+
printd(f"Got unknown Exception, exception={e}")
|
|
122
|
+
raise e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def azure_openai_embeddings_request(
|
|
126
|
+
resource_name: str, deployment_id: str, api_version: str, api_key: str, data: dict
|
|
127
|
+
) -> EmbeddingResponse:
|
|
128
|
+
"""https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#embeddings"""
|
|
129
|
+
from letta.utils import printd
|
|
130
|
+
|
|
131
|
+
resource_name = clean_azure_endpoint(resource_name)
|
|
132
|
+
url = f"https://{resource_name}.openai.azure.com/openai/deployments/{deployment_id}/embeddings?api-version={api_version}"
|
|
133
|
+
headers = {"Content-Type": "application/json", "api-key": f"{api_key}"}
|
|
134
|
+
|
|
135
|
+
printd(f"Sending request to {url}")
|
|
136
|
+
try:
|
|
137
|
+
response = requests.post(url, headers=headers, json=data)
|
|
138
|
+
printd(f"response = {response}")
|
|
139
|
+
response.raise_for_status() # Raises HTTPError for 4XX/5XX status
|
|
140
|
+
response = response.json() # convert to dict from string
|
|
141
|
+
printd(f"response.json = {response}")
|
|
142
|
+
response = EmbeddingResponse(**response) # convert to 'dot-dict' style which is the openai python client default
|
|
143
|
+
return response
|
|
144
|
+
except requests.exceptions.HTTPError as http_err:
|
|
145
|
+
# Handle HTTP errors (e.g., response 4XX, 5XX)
|
|
146
|
+
printd(f"Got HTTPError, exception={http_err}, payload={data}")
|
|
147
|
+
raise http_err
|
|
148
|
+
except requests.exceptions.RequestException as req_err:
|
|
149
|
+
# Handle other requests-related errors (e.g., connection error)
|
|
150
|
+
printd(f"Got RequestException, exception={req_err}")
|
|
151
|
+
raise req_err
|
|
152
|
+
except Exception as e:
|
|
153
|
+
# Handle other potential errors
|
|
154
|
+
printd(f"Got unknown Exception, exception={e}")
|
|
155
|
+
raise e
|