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.

Files changed (189) hide show
  1. letta/__init__.py +24 -0
  2. letta/__main__.py +3 -0
  3. letta/agent.py +1427 -0
  4. letta/agent_store/chroma.py +295 -0
  5. letta/agent_store/db.py +546 -0
  6. letta/agent_store/lancedb.py +177 -0
  7. letta/agent_store/milvus.py +198 -0
  8. letta/agent_store/qdrant.py +201 -0
  9. letta/agent_store/storage.py +188 -0
  10. letta/benchmark/benchmark.py +96 -0
  11. letta/benchmark/constants.py +14 -0
  12. letta/cli/cli.py +689 -0
  13. letta/cli/cli_config.py +1282 -0
  14. letta/cli/cli_load.py +166 -0
  15. letta/client/__init__.py +0 -0
  16. letta/client/admin.py +171 -0
  17. letta/client/client.py +2360 -0
  18. letta/client/streaming.py +90 -0
  19. letta/client/utils.py +61 -0
  20. letta/config.py +484 -0
  21. letta/configs/anthropic.json +13 -0
  22. letta/configs/letta_hosted.json +11 -0
  23. letta/configs/openai.json +12 -0
  24. letta/constants.py +134 -0
  25. letta/credentials.py +140 -0
  26. letta/data_sources/connectors.py +247 -0
  27. letta/embeddings.py +218 -0
  28. letta/errors.py +26 -0
  29. letta/functions/__init__.py +0 -0
  30. letta/functions/function_sets/base.py +174 -0
  31. letta/functions/function_sets/extras.py +132 -0
  32. letta/functions/functions.py +105 -0
  33. letta/functions/schema_generator.py +205 -0
  34. letta/humans/__init__.py +0 -0
  35. letta/humans/examples/basic.txt +1 -0
  36. letta/humans/examples/cs_phd.txt +9 -0
  37. letta/interface.py +314 -0
  38. letta/llm_api/__init__.py +0 -0
  39. letta/llm_api/anthropic.py +383 -0
  40. letta/llm_api/azure_openai.py +155 -0
  41. letta/llm_api/cohere.py +396 -0
  42. letta/llm_api/google_ai.py +468 -0
  43. letta/llm_api/llm_api_tools.py +485 -0
  44. letta/llm_api/openai.py +470 -0
  45. letta/local_llm/README.md +3 -0
  46. letta/local_llm/__init__.py +0 -0
  47. letta/local_llm/chat_completion_proxy.py +279 -0
  48. letta/local_llm/constants.py +31 -0
  49. letta/local_llm/function_parser.py +68 -0
  50. letta/local_llm/grammars/__init__.py +0 -0
  51. letta/local_llm/grammars/gbnf_grammar_generator.py +1324 -0
  52. letta/local_llm/grammars/json.gbnf +26 -0
  53. letta/local_llm/grammars/json_func_calls_with_inner_thoughts.gbnf +32 -0
  54. letta/local_llm/groq/api.py +97 -0
  55. letta/local_llm/json_parser.py +202 -0
  56. letta/local_llm/koboldcpp/api.py +62 -0
  57. letta/local_llm/koboldcpp/settings.py +23 -0
  58. letta/local_llm/llamacpp/api.py +58 -0
  59. letta/local_llm/llamacpp/settings.py +22 -0
  60. letta/local_llm/llm_chat_completion_wrappers/__init__.py +0 -0
  61. letta/local_llm/llm_chat_completion_wrappers/airoboros.py +452 -0
  62. letta/local_llm/llm_chat_completion_wrappers/chatml.py +470 -0
  63. letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py +387 -0
  64. letta/local_llm/llm_chat_completion_wrappers/dolphin.py +246 -0
  65. letta/local_llm/llm_chat_completion_wrappers/llama3.py +345 -0
  66. letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py +156 -0
  67. letta/local_llm/llm_chat_completion_wrappers/wrapper_base.py +11 -0
  68. letta/local_llm/llm_chat_completion_wrappers/zephyr.py +345 -0
  69. letta/local_llm/lmstudio/api.py +100 -0
  70. letta/local_llm/lmstudio/settings.py +29 -0
  71. letta/local_llm/ollama/api.py +88 -0
  72. letta/local_llm/ollama/settings.py +32 -0
  73. letta/local_llm/settings/__init__.py +0 -0
  74. letta/local_llm/settings/deterministic_mirostat.py +45 -0
  75. letta/local_llm/settings/settings.py +72 -0
  76. letta/local_llm/settings/simple.py +28 -0
  77. letta/local_llm/utils.py +265 -0
  78. letta/local_llm/vllm/api.py +63 -0
  79. letta/local_llm/webui/api.py +60 -0
  80. letta/local_llm/webui/legacy_api.py +58 -0
  81. letta/local_llm/webui/legacy_settings.py +23 -0
  82. letta/local_llm/webui/settings.py +24 -0
  83. letta/log.py +76 -0
  84. letta/main.py +437 -0
  85. letta/memory.py +440 -0
  86. letta/metadata.py +884 -0
  87. letta/openai_backcompat/__init__.py +0 -0
  88. letta/openai_backcompat/openai_object.py +437 -0
  89. letta/persistence_manager.py +148 -0
  90. letta/personas/__init__.py +0 -0
  91. letta/personas/examples/anna_pa.txt +13 -0
  92. letta/personas/examples/google_search_persona.txt +15 -0
  93. letta/personas/examples/memgpt_doc.txt +6 -0
  94. letta/personas/examples/memgpt_starter.txt +4 -0
  95. letta/personas/examples/sam.txt +14 -0
  96. letta/personas/examples/sam_pov.txt +14 -0
  97. letta/personas/examples/sam_simple_pov_gpt35.txt +13 -0
  98. letta/personas/examples/sqldb/test.db +0 -0
  99. letta/prompts/__init__.py +0 -0
  100. letta/prompts/gpt_summarize.py +14 -0
  101. letta/prompts/gpt_system.py +26 -0
  102. letta/prompts/system/memgpt_base.txt +49 -0
  103. letta/prompts/system/memgpt_chat.txt +58 -0
  104. letta/prompts/system/memgpt_chat_compressed.txt +13 -0
  105. letta/prompts/system/memgpt_chat_fstring.txt +51 -0
  106. letta/prompts/system/memgpt_doc.txt +50 -0
  107. letta/prompts/system/memgpt_gpt35_extralong.txt +53 -0
  108. letta/prompts/system/memgpt_intuitive_knowledge.txt +31 -0
  109. letta/prompts/system/memgpt_modified_chat.txt +23 -0
  110. letta/pytest.ini +0 -0
  111. letta/schemas/agent.py +117 -0
  112. letta/schemas/api_key.py +21 -0
  113. letta/schemas/block.py +135 -0
  114. letta/schemas/document.py +21 -0
  115. letta/schemas/embedding_config.py +54 -0
  116. letta/schemas/enums.py +35 -0
  117. letta/schemas/job.py +38 -0
  118. letta/schemas/letta_base.py +80 -0
  119. letta/schemas/letta_message.py +175 -0
  120. letta/schemas/letta_request.py +23 -0
  121. letta/schemas/letta_response.py +28 -0
  122. letta/schemas/llm_config.py +54 -0
  123. letta/schemas/memory.py +224 -0
  124. letta/schemas/message.py +727 -0
  125. letta/schemas/openai/chat_completion_request.py +123 -0
  126. letta/schemas/openai/chat_completion_response.py +136 -0
  127. letta/schemas/openai/chat_completions.py +123 -0
  128. letta/schemas/openai/embedding_response.py +11 -0
  129. letta/schemas/openai/openai.py +157 -0
  130. letta/schemas/organization.py +20 -0
  131. letta/schemas/passage.py +80 -0
  132. letta/schemas/source.py +62 -0
  133. letta/schemas/tool.py +143 -0
  134. letta/schemas/usage.py +18 -0
  135. letta/schemas/user.py +33 -0
  136. letta/server/__init__.py +0 -0
  137. letta/server/constants.py +6 -0
  138. letta/server/rest_api/__init__.py +0 -0
  139. letta/server/rest_api/admin/__init__.py +0 -0
  140. letta/server/rest_api/admin/agents.py +21 -0
  141. letta/server/rest_api/admin/tools.py +83 -0
  142. letta/server/rest_api/admin/users.py +98 -0
  143. letta/server/rest_api/app.py +193 -0
  144. letta/server/rest_api/auth/__init__.py +0 -0
  145. letta/server/rest_api/auth/index.py +43 -0
  146. letta/server/rest_api/auth_token.py +22 -0
  147. letta/server/rest_api/interface.py +726 -0
  148. letta/server/rest_api/routers/__init__.py +0 -0
  149. letta/server/rest_api/routers/openai/__init__.py +0 -0
  150. letta/server/rest_api/routers/openai/assistants/__init__.py +0 -0
  151. letta/server/rest_api/routers/openai/assistants/assistants.py +115 -0
  152. letta/server/rest_api/routers/openai/assistants/schemas.py +121 -0
  153. letta/server/rest_api/routers/openai/assistants/threads.py +336 -0
  154. letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
  155. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +131 -0
  156. letta/server/rest_api/routers/v1/__init__.py +15 -0
  157. letta/server/rest_api/routers/v1/agents.py +543 -0
  158. letta/server/rest_api/routers/v1/blocks.py +73 -0
  159. letta/server/rest_api/routers/v1/jobs.py +46 -0
  160. letta/server/rest_api/routers/v1/llms.py +28 -0
  161. letta/server/rest_api/routers/v1/organizations.py +61 -0
  162. letta/server/rest_api/routers/v1/sources.py +199 -0
  163. letta/server/rest_api/routers/v1/tools.py +103 -0
  164. letta/server/rest_api/routers/v1/users.py +109 -0
  165. letta/server/rest_api/static_files.py +74 -0
  166. letta/server/rest_api/utils.py +69 -0
  167. letta/server/server.py +1995 -0
  168. letta/server/startup.sh +8 -0
  169. letta/server/static_files/assets/index-0cbf7ad5.js +274 -0
  170. letta/server/static_files/assets/index-156816da.css +1 -0
  171. letta/server/static_files/assets/index-486e3228.js +274 -0
  172. letta/server/static_files/favicon.ico +0 -0
  173. letta/server/static_files/index.html +39 -0
  174. letta/server/static_files/memgpt_logo_transparent.png +0 -0
  175. letta/server/utils.py +46 -0
  176. letta/server/ws_api/__init__.py +0 -0
  177. letta/server/ws_api/example_client.py +104 -0
  178. letta/server/ws_api/interface.py +108 -0
  179. letta/server/ws_api/protocol.py +100 -0
  180. letta/server/ws_api/server.py +145 -0
  181. letta/settings.py +165 -0
  182. letta/streaming_interface.py +396 -0
  183. letta/system.py +207 -0
  184. letta/utils.py +1065 -0
  185. letta_nightly-0.1.7.dev20240924104148.dist-info/LICENSE +190 -0
  186. letta_nightly-0.1.7.dev20240924104148.dist-info/METADATA +98 -0
  187. letta_nightly-0.1.7.dev20240924104148.dist-info/RECORD +189 -0
  188. letta_nightly-0.1.7.dev20240924104148.dist-info/WHEEL +4 -0
  189. 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