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,468 @@
1
+ import uuid
2
+ from typing import List, Optional
3
+
4
+ import requests
5
+
6
+ from letta.constants import NON_USER_MSG_PREFIX
7
+ from letta.local_llm.json_parser import clean_json_string_extra_backslash
8
+ from letta.local_llm.utils import count_tokens
9
+ from letta.schemas.openai.chat_completion_request import Tool
10
+ from letta.schemas.openai.chat_completion_response import (
11
+ ChatCompletionResponse,
12
+ Choice,
13
+ FunctionCall,
14
+ Message,
15
+ ToolCall,
16
+ UsageStatistics,
17
+ )
18
+ from letta.utils import get_tool_call_id, get_utc_time
19
+
20
+ # from letta.data_types import ToolCall
21
+
22
+
23
+ SUPPORTED_MODELS = [
24
+ "gemini-pro",
25
+ ]
26
+
27
+
28
+ def google_ai_get_model_details(service_endpoint: str, api_key: str, model: str, key_in_header: bool = True) -> List[dict]:
29
+ from letta.utils import printd
30
+
31
+ # Two ways to pass the key: https://ai.google.dev/tutorials/setup
32
+ if key_in_header:
33
+ url = f"https://{service_endpoint}.googleapis.com/v1beta/models/{model}"
34
+ headers = {"Content-Type": "application/json", "x-goog-api-key": api_key}
35
+ else:
36
+ url = f"https://{service_endpoint}.googleapis.com/v1beta/models/{model}?key={api_key}"
37
+ headers = {"Content-Type": "application/json"}
38
+
39
+ try:
40
+ response = requests.get(url, headers=headers)
41
+ printd(f"response = {response}")
42
+ response.raise_for_status() # Raises HTTPError for 4XX/5XX status
43
+ response = response.json() # convert to dict from string
44
+ printd(f"response.json = {response}")
45
+
46
+ # Grab the models out
47
+ return response
48
+
49
+ except requests.exceptions.HTTPError as http_err:
50
+ # Handle HTTP errors (e.g., response 4XX, 5XX)
51
+ printd(f"Got HTTPError, exception={http_err}")
52
+ # Print the HTTP status code
53
+ print(f"HTTP Error: {http_err.response.status_code}")
54
+ # Print the response content (error message from server)
55
+ print(f"Message: {http_err.response.text}")
56
+ raise http_err
57
+
58
+ except requests.exceptions.RequestException as req_err:
59
+ # Handle other requests-related errors (e.g., connection error)
60
+ printd(f"Got RequestException, exception={req_err}")
61
+ raise req_err
62
+
63
+ except Exception as e:
64
+ # Handle other potential errors
65
+ printd(f"Got unknown Exception, exception={e}")
66
+ raise e
67
+
68
+
69
+ def google_ai_get_model_context_window(service_endpoint: str, api_key: str, model: str, key_in_header: bool = True) -> int:
70
+ model_details = google_ai_get_model_details(
71
+ service_endpoint=service_endpoint, api_key=api_key, model=model, key_in_header=key_in_header
72
+ )
73
+ # TODO should this be:
74
+ # return model_details["inputTokenLimit"] + model_details["outputTokenLimit"]
75
+ return int(model_details["inputTokenLimit"])
76
+
77
+
78
+ def google_ai_get_model_list(service_endpoint: str, api_key: str, key_in_header: bool = True) -> List[dict]:
79
+ from letta.utils import printd
80
+
81
+ # Two ways to pass the key: https://ai.google.dev/tutorials/setup
82
+ if key_in_header:
83
+ url = f"https://{service_endpoint}.googleapis.com/v1beta/models"
84
+ headers = {"Content-Type": "application/json", "x-goog-api-key": api_key}
85
+ else:
86
+ url = f"https://{service_endpoint}.googleapis.com/v1beta/models?key={api_key}"
87
+ headers = {"Content-Type": "application/json"}
88
+
89
+ try:
90
+ response = requests.get(url, headers=headers)
91
+ printd(f"response = {response}")
92
+ response.raise_for_status() # Raises HTTPError for 4XX/5XX status
93
+ response = response.json() # convert to dict from string
94
+ printd(f"response.json = {response}")
95
+
96
+ # Grab the models out
97
+ model_list = response["models"]
98
+ return model_list
99
+
100
+ except requests.exceptions.HTTPError as http_err:
101
+ # Handle HTTP errors (e.g., response 4XX, 5XX)
102
+ printd(f"Got HTTPError, exception={http_err}")
103
+ # Print the HTTP status code
104
+ print(f"HTTP Error: {http_err.response.status_code}")
105
+ # Print the response content (error message from server)
106
+ print(f"Message: {http_err.response.text}")
107
+ raise http_err
108
+
109
+ except requests.exceptions.RequestException as req_err:
110
+ # Handle other requests-related errors (e.g., connection error)
111
+ printd(f"Got RequestException, exception={req_err}")
112
+ raise req_err
113
+
114
+ except Exception as e:
115
+ # Handle other potential errors
116
+ printd(f"Got unknown Exception, exception={e}")
117
+ raise e
118
+
119
+
120
+ def add_dummy_model_messages(messages: List[dict]) -> List[dict]:
121
+ """Google AI API requires all function call returns are immediately followed by a 'model' role message.
122
+
123
+ In Letta, the 'model' will often call a function (e.g. send_message) that itself yields to the user,
124
+ so there is no natural follow-up 'model' role message.
125
+
126
+ To satisfy the Google AI API restrictions, we can add a dummy 'yield' message
127
+ with role == 'model' that is placed in-betweeen and function output
128
+ (role == 'tool') and user message (role == 'user').
129
+ """
130
+ dummy_yield_message = {"role": "model", "parts": [{"text": f"{NON_USER_MSG_PREFIX}Function call returned, waiting for user response."}]}
131
+ messages_with_padding = []
132
+ for i, message in enumerate(messages):
133
+ messages_with_padding.append(message)
134
+ # Check if the current message role is 'tool' and the next message role is 'user'
135
+ if message["role"] in ["tool", "function"] and (i + 1 < len(messages) and messages[i + 1]["role"] == "user"):
136
+ messages_with_padding.append(dummy_yield_message)
137
+
138
+ return messages_with_padding
139
+
140
+
141
+ # TODO use pydantic model as input
142
+ def to_google_ai(openai_message_dict: dict) -> dict:
143
+
144
+ # TODO supports "parts" as part of multimodal support
145
+ assert not isinstance(openai_message_dict["content"], list), "Multi-part content is message not yet supported"
146
+ if openai_message_dict["role"] == "user":
147
+ google_ai_message_dict = {
148
+ "role": "user",
149
+ "parts": [{"text": openai_message_dict["content"]}],
150
+ }
151
+ elif openai_message_dict["role"] == "assistant":
152
+ google_ai_message_dict = {
153
+ "role": "model", # NOTE: diff
154
+ "parts": [{"text": openai_message_dict["content"]}],
155
+ }
156
+ elif openai_message_dict["role"] == "tool":
157
+ google_ai_message_dict = {
158
+ "role": "function", # NOTE: diff
159
+ "parts": [{"text": openai_message_dict["content"]}],
160
+ }
161
+ else:
162
+ raise ValueError(f"Unsupported conversion (OpenAI -> Google AI) from role {openai_message_dict['role']}")
163
+
164
+
165
+ # TODO convert return type to pydantic
166
+ def convert_tools_to_google_ai_format(tools: List[Tool], inner_thoughts_in_kwargs: Optional[bool] = True) -> List[dict]:
167
+ """
168
+ OpenAI style:
169
+ "tools": [{
170
+ "type": "function",
171
+ "function": {
172
+ "name": "find_movies",
173
+ "description": "find ....",
174
+ "parameters": {
175
+ "type": "object",
176
+ "properties": {
177
+ PARAM: {
178
+ "type": PARAM_TYPE, # eg "string"
179
+ "description": PARAM_DESCRIPTION,
180
+ },
181
+ ...
182
+ },
183
+ "required": List[str],
184
+ }
185
+ }
186
+ }
187
+ ]
188
+
189
+ Google AI style:
190
+ "tools": [{
191
+ "functionDeclarations": [{
192
+ "name": "find_movies",
193
+ "description": "find movie titles currently playing in theaters based on any description, genre, title words, etc.",
194
+ "parameters": {
195
+ "type": "OBJECT",
196
+ "properties": {
197
+ "location": {
198
+ "type": "STRING",
199
+ "description": "The city and state, e.g. San Francisco, CA or a zip code e.g. 95616"
200
+ },
201
+ "description": {
202
+ "type": "STRING",
203
+ "description": "Any kind of description including category or genre, title words, attributes, etc."
204
+ }
205
+ },
206
+ "required": ["description"]
207
+ }
208
+ }, {
209
+ "name": "find_theaters",
210
+ ...
211
+ """
212
+ function_list = [
213
+ dict(
214
+ name=t.function.name,
215
+ description=t.function.description,
216
+ parameters=t.function.parameters, # TODO need to unpack
217
+ )
218
+ for t in tools
219
+ ]
220
+
221
+ # Correct casing + add inner thoughts if needed
222
+ for func in function_list:
223
+ func["parameters"]["type"] = "OBJECT"
224
+ for param_name, param_fields in func["parameters"]["properties"].items():
225
+ param_fields["type"] = param_fields["type"].upper()
226
+ # Add inner thoughts
227
+ if inner_thoughts_in_kwargs:
228
+ from letta.local_llm.constants import (
229
+ INNER_THOUGHTS_KWARG,
230
+ INNER_THOUGHTS_KWARG_DESCRIPTION,
231
+ )
232
+
233
+ func["parameters"]["properties"][INNER_THOUGHTS_KWARG] = {
234
+ "type": "STRING",
235
+ "description": INNER_THOUGHTS_KWARG_DESCRIPTION,
236
+ }
237
+ func["parameters"]["required"].append(INNER_THOUGHTS_KWARG)
238
+
239
+ return [{"functionDeclarations": function_list}]
240
+
241
+
242
+ def convert_google_ai_response_to_chatcompletion(
243
+ response_json: dict, # REST response from Google AI API
244
+ model: str, # Required since not returned
245
+ input_messages: Optional[List[dict]] = None, # Required if the API doesn't return UsageMetadata
246
+ pull_inner_thoughts_from_args: Optional[bool] = True,
247
+ ) -> ChatCompletionResponse:
248
+ """Google AI API response format is not the same as ChatCompletion, requires unpacking
249
+
250
+ Example:
251
+ {
252
+ "candidates": [
253
+ {
254
+ "content": {
255
+ "parts": [
256
+ {
257
+ "text": " OK. Barbie is showing in two theaters in Mountain View, CA: AMC Mountain View 16 and Regal Edwards 14."
258
+ }
259
+ ]
260
+ }
261
+ }
262
+ ],
263
+ "usageMetadata": {
264
+ "promptTokenCount": 9,
265
+ "candidatesTokenCount": 27,
266
+ "totalTokenCount": 36
267
+ }
268
+ }
269
+ """
270
+ try:
271
+ choices = []
272
+ for candidate in response_json["candidates"]:
273
+ content = candidate["content"]
274
+
275
+ role = content["role"]
276
+ assert role == "model", f"Unknown role in response: {role}"
277
+
278
+ parts = content["parts"]
279
+ # TODO support parts / multimodal
280
+ assert len(parts) == 1, f"Multi-part not yet supported:\n{parts}"
281
+ response_message = parts[0]
282
+
283
+ # Convert the actual message style to OpenAI style
284
+ if "functionCall" in response_message and response_message["functionCall"] is not None:
285
+ function_call = response_message["functionCall"]
286
+ assert isinstance(function_call, dict), function_call
287
+ function_name = function_call["name"]
288
+ assert isinstance(function_name, str), function_name
289
+ function_args = function_call["args"]
290
+ assert isinstance(function_args, dict), function_args
291
+
292
+ # NOTE: this also involves stripping the inner monologue out of the function
293
+ if pull_inner_thoughts_from_args:
294
+ from letta.local_llm.constants import INNER_THOUGHTS_KWARG
295
+
296
+ assert INNER_THOUGHTS_KWARG in function_args, f"Couldn't find inner thoughts in function args:\n{function_call}"
297
+ inner_thoughts = function_args.pop(INNER_THOUGHTS_KWARG)
298
+ assert inner_thoughts is not None, f"Expected non-null inner thoughts function arg:\n{function_call}"
299
+ else:
300
+ inner_thoughts = None
301
+
302
+ # Google AI API doesn't generate tool call IDs
303
+ openai_response_message = Message(
304
+ role="assistant", # NOTE: "model" -> "assistant"
305
+ content=inner_thoughts,
306
+ tool_calls=[
307
+ ToolCall(
308
+ id=get_tool_call_id(),
309
+ type="function",
310
+ function=FunctionCall(
311
+ name=function_name,
312
+ arguments=clean_json_string_extra_backslash(json_dumps(function_args)),
313
+ ),
314
+ )
315
+ ],
316
+ )
317
+
318
+ else:
319
+
320
+ # Inner thoughts are the content by default
321
+ inner_thoughts = response_message["text"]
322
+
323
+ # Google AI API doesn't generate tool call IDs
324
+ openai_response_message = Message(
325
+ role="assistant", # NOTE: "model" -> "assistant"
326
+ content=inner_thoughts,
327
+ )
328
+
329
+ # Google AI API uses different finish reason strings than OpenAI
330
+ # OpenAI: 'stop', 'length', 'function_call', 'content_filter', null
331
+ # see: https://platform.openai.com/docs/guides/text-generation/chat-completions-api
332
+ # Google AI API: FINISH_REASON_UNSPECIFIED, STOP, MAX_TOKENS, SAFETY, RECITATION, OTHER
333
+ # see: https://ai.google.dev/api/python/google/ai/generativelanguage/Candidate/FinishReason
334
+ finish_reason = candidate["finishReason"]
335
+ if finish_reason == "STOP":
336
+ openai_finish_reason = (
337
+ "function_call"
338
+ if openai_response_message.tool_calls is not None and len(openai_response_message.tool_calls) > 0
339
+ else "stop"
340
+ )
341
+ elif finish_reason == "MAX_TOKENS":
342
+ openai_finish_reason = "length"
343
+ elif finish_reason == "SAFETY":
344
+ openai_finish_reason = "content_filter"
345
+ elif finish_reason == "RECITATION":
346
+ openai_finish_reason = "content_filter"
347
+ else:
348
+ raise ValueError(f"Unrecognized finish reason in Google AI response: {finish_reason}")
349
+
350
+ choices.append(
351
+ Choice(
352
+ finish_reason=openai_finish_reason,
353
+ index=candidate["index"],
354
+ message=openai_response_message,
355
+ )
356
+ )
357
+
358
+ if len(choices) > 1:
359
+ raise UserWarning(f"Unexpected number of candidates in response (expected 1, got {len(choices)})")
360
+
361
+ # NOTE: some of the Google AI APIs show UsageMetadata in the response, but it seems to not exist?
362
+ # "usageMetadata": {
363
+ # "promptTokenCount": 9,
364
+ # "candidatesTokenCount": 27,
365
+ # "totalTokenCount": 36
366
+ # }
367
+ if "usageMetadata" in response_json:
368
+ usage = UsageStatistics(
369
+ prompt_tokens=response_json["usageMetadata"]["promptTokenCount"],
370
+ completion_tokens=response_json["usageMetadata"]["candidatesTokenCount"],
371
+ total_tokens=response_json["usageMetadata"]["totalTokenCount"],
372
+ )
373
+ else:
374
+ # Count it ourselves
375
+ assert input_messages is not None, f"Didn't get UsageMetadata from the API response, so input_messages is required"
376
+ prompt_tokens = count_tokens(json_dumps(input_messages)) # NOTE: this is a very rough approximation
377
+ completion_tokens = count_tokens(json_dumps(openai_response_message.model_dump())) # NOTE: this is also approximate
378
+ total_tokens = prompt_tokens + completion_tokens
379
+ usage = UsageStatistics(
380
+ prompt_tokens=prompt_tokens,
381
+ completion_tokens=completion_tokens,
382
+ total_tokens=total_tokens,
383
+ )
384
+
385
+ response_id = str(uuid.uuid4())
386
+ return ChatCompletionResponse(
387
+ id=response_id,
388
+ choices=choices,
389
+ model=model, # NOTE: Google API doesn't pass back model in the response
390
+ created=get_utc_time(),
391
+ usage=usage,
392
+ )
393
+ except KeyError as e:
394
+ raise e
395
+
396
+
397
+ # TODO convert 'data' type to pydantic
398
+ def google_ai_chat_completions_request(
399
+ service_endpoint: str,
400
+ model: str,
401
+ api_key: str,
402
+ data: dict,
403
+ key_in_header: bool = True,
404
+ add_postfunc_model_messages: bool = True,
405
+ # NOTE: Google AI API doesn't support mixing parts 'text' and 'function',
406
+ # so there's no clean way to put inner thoughts in the same message as a function call
407
+ inner_thoughts_in_kwargs: bool = True,
408
+ ) -> ChatCompletionResponse:
409
+ """https://ai.google.dev/docs/function_calling
410
+
411
+ From https://ai.google.dev/api/rest#service-endpoint:
412
+ "A service endpoint is a base URL that specifies the network address of an API service.
413
+ One service might have multiple service endpoints.
414
+ This service has the following service endpoint and all URIs below are relative to this service endpoint:
415
+ https://xxx.googleapis.com
416
+ """
417
+ from letta.utils import printd
418
+
419
+ assert service_endpoint is not None, "Missing service_endpoint when calling Google AI"
420
+ assert api_key is not None, "Missing api_key when calling Google AI"
421
+ assert model in SUPPORTED_MODELS, f"Model '{model}' not in supported models: {', '.join(SUPPORTED_MODELS)}"
422
+
423
+ # Two ways to pass the key: https://ai.google.dev/tutorials/setup
424
+ if key_in_header:
425
+ url = f"https://{service_endpoint}.googleapis.com/v1beta/models/{model}:generateContent"
426
+ headers = {"Content-Type": "application/json", "x-goog-api-key": api_key}
427
+ else:
428
+ url = f"https://{service_endpoint}.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
429
+ headers = {"Content-Type": "application/json"}
430
+
431
+ # data["contents"][-1]["role"] = "model"
432
+ if add_postfunc_model_messages:
433
+ data["contents"] = add_dummy_model_messages(data["contents"])
434
+
435
+ printd(f"Sending request to {url}")
436
+ try:
437
+ response = requests.post(url, headers=headers, json=data)
438
+ printd(f"response = {response}")
439
+ response.raise_for_status() # Raises HTTPError for 4XX/5XX status
440
+ response = response.json() # convert to dict from string
441
+ printd(f"response.json = {response}")
442
+
443
+ # Convert Google AI response to ChatCompletion style
444
+ return convert_google_ai_response_to_chatcompletion(
445
+ response_json=response,
446
+ model=model,
447
+ input_messages=data["contents"],
448
+ pull_inner_thoughts_from_args=inner_thoughts_in_kwargs,
449
+ )
450
+
451
+ except requests.exceptions.HTTPError as http_err:
452
+ # Handle HTTP errors (e.g., response 4XX, 5XX)
453
+ printd(f"Got HTTPError, exception={http_err}, payload={data}")
454
+ # Print the HTTP status code
455
+ print(f"HTTP Error: {http_err.response.status_code}")
456
+ # Print the response content (error message from server)
457
+ print(f"Message: {http_err.response.text}")
458
+ raise http_err
459
+
460
+ except requests.exceptions.RequestException as req_err:
461
+ # Handle other requests-related errors (e.g., connection error)
462
+ printd(f"Got RequestException, exception={req_err}")
463
+ raise req_err
464
+
465
+ except Exception as e:
466
+ # Handle other potential errors
467
+ printd(f"Got unknown Exception, exception={e}")
468
+ raise e