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,1282 @@
1
+ import ast
2
+ import builtins
3
+ import os
4
+ from enum import Enum
5
+ from typing import Annotated, List, Optional
6
+
7
+ import questionary
8
+ import typer
9
+ from prettytable.colortable import ColorTable, Themes
10
+ from tqdm import tqdm
11
+
12
+ from letta import utils
13
+ from letta.config import LettaConfig
14
+ from letta.constants import LETTA_DIR, LLM_MAX_TOKENS
15
+ from letta.credentials import SUPPORTED_AUTH_TYPES, LettaCredentials
16
+ from letta.llm_api.anthropic import (
17
+ anthropic_get_model_list,
18
+ antropic_get_model_context_window,
19
+ )
20
+ from letta.llm_api.azure_openai import azure_openai_get_model_list
21
+ from letta.llm_api.cohere import (
22
+ COHERE_VALID_MODEL_LIST,
23
+ cohere_get_model_context_window,
24
+ cohere_get_model_list,
25
+ )
26
+ from letta.llm_api.google_ai import (
27
+ google_ai_get_model_context_window,
28
+ google_ai_get_model_list,
29
+ )
30
+ from letta.llm_api.llm_api_tools import LLM_API_PROVIDER_OPTIONS
31
+ from letta.llm_api.openai import openai_get_model_list
32
+ from letta.local_llm.constants import (
33
+ DEFAULT_ENDPOINTS,
34
+ DEFAULT_OLLAMA_MODEL,
35
+ DEFAULT_WRAPPER_NAME,
36
+ )
37
+ from letta.local_llm.utils import get_available_wrappers
38
+ from letta.schemas.embedding_config import EmbeddingConfig
39
+ from letta.schemas.llm_config import LLMConfig
40
+ from letta.server.utils import shorten_key_middle
41
+
42
+ app = typer.Typer()
43
+
44
+
45
+ def get_azure_credentials():
46
+ creds = dict(
47
+ azure_key=os.getenv("AZURE_OPENAI_KEY"),
48
+ azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
49
+ azure_version=os.getenv("AZURE_OPENAI_VERSION"),
50
+ azure_deployment=os.getenv("AZURE_OPENAI_DEPLOYMENT"),
51
+ azure_embedding_deployment=os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT"),
52
+ )
53
+ # embedding endpoint and version default to non-embedding
54
+ creds["azure_embedding_endpoint"] = os.getenv("AZURE_OPENAI_EMBEDDING_ENDPOINT", creds["azure_endpoint"])
55
+ creds["azure_embedding_version"] = os.getenv("AZURE_OPENAI_EMBEDDING_VERSION", creds["azure_version"])
56
+ return creds
57
+
58
+
59
+ def get_openai_credentials() -> Optional[str]:
60
+ openai_key = os.getenv("OPENAI_API_KEY", None)
61
+ return openai_key
62
+
63
+
64
+ def get_google_ai_credentials() -> Optional[str]:
65
+ google_ai_key = os.getenv("GOOGLE_AI_API_KEY", None)
66
+ return google_ai_key
67
+
68
+
69
+ def configure_llm_endpoint(config: LettaConfig, credentials: LettaCredentials):
70
+ # configure model endpoint
71
+ model_endpoint_type, model_endpoint = None, None
72
+
73
+ # get default
74
+ default_model_endpoint_type = config.default_llm_config.model_endpoint_type if config.default_embedding_config else None
75
+ if (
76
+ config.default_llm_config
77
+ and config.default_llm_config.model_endpoint_type is not None
78
+ and config.default_llm_config.model_endpoint_type not in [provider for provider in LLM_API_PROVIDER_OPTIONS if provider != "local"]
79
+ ): # local model
80
+ default_model_endpoint_type = "local"
81
+
82
+ provider = questionary.select(
83
+ "Select LLM inference provider:",
84
+ choices=LLM_API_PROVIDER_OPTIONS,
85
+ default=default_model_endpoint_type,
86
+ ).ask()
87
+ if provider is None:
88
+ raise KeyboardInterrupt
89
+
90
+ # set: model_endpoint_type, model_endpoint
91
+ if provider == "openai":
92
+ # check for key
93
+ if credentials.openai_key is None:
94
+ # allow key to get pulled from env vars
95
+ openai_api_key = os.getenv("OPENAI_API_KEY", None)
96
+ # if we still can't find it, ask for it as input
97
+ if openai_api_key is None:
98
+ while openai_api_key is None or len(openai_api_key) == 0:
99
+ # Ask for API key as input
100
+ openai_api_key = questionary.password(
101
+ "Enter your OpenAI API key (starts with 'sk-', see https://platform.openai.com/api-keys):"
102
+ ).ask()
103
+ if openai_api_key is None:
104
+ raise KeyboardInterrupt
105
+ credentials.openai_key = openai_api_key
106
+ credentials.save()
107
+ else:
108
+ # Give the user an opportunity to overwrite the key
109
+ openai_api_key = None
110
+ default_input = (
111
+ shorten_key_middle(credentials.openai_key) if credentials.openai_key.startswith("sk-") else credentials.openai_key
112
+ )
113
+ openai_api_key = questionary.password(
114
+ "Enter your OpenAI API key (starts with 'sk-', see https://platform.openai.com/api-keys):",
115
+ default=default_input,
116
+ ).ask()
117
+ if openai_api_key is None:
118
+ raise KeyboardInterrupt
119
+ # If the user modified it, use the new one
120
+ if openai_api_key != default_input:
121
+ credentials.openai_key = openai_api_key
122
+ credentials.save()
123
+
124
+ model_endpoint_type = "openai"
125
+ model_endpoint = "https://api.openai.com/v1"
126
+ model_endpoint = questionary.text("Override default endpoint:", default=model_endpoint).ask()
127
+ if model_endpoint is None:
128
+ raise KeyboardInterrupt
129
+ provider = "openai"
130
+
131
+ elif provider == "azure":
132
+ # check for necessary vars
133
+ azure_creds = get_azure_credentials()
134
+ if not all([azure_creds["azure_key"], azure_creds["azure_endpoint"], azure_creds["azure_version"]]):
135
+ raise ValueError(
136
+ "Missing environment variables for Azure (see https://letta.readme.io/docs/endpoints#azure-openai). Please set then run `letta configure` again."
137
+ )
138
+ else:
139
+ credentials.azure_key = azure_creds["azure_key"]
140
+ credentials.azure_version = azure_creds["azure_version"]
141
+ credentials.azure_endpoint = azure_creds["azure_endpoint"]
142
+ if "azure_deployment" in azure_creds:
143
+ credentials.azure_deployment = azure_creds["azure_deployment"]
144
+ credentials.azure_embedding_version = azure_creds["azure_embedding_version"]
145
+ credentials.azure_embedding_endpoint = azure_creds["azure_embedding_endpoint"]
146
+ if "azure_embedding_deployment" in azure_creds:
147
+ credentials.azure_embedding_deployment = azure_creds["azure_embedding_deployment"]
148
+ credentials.save()
149
+
150
+ model_endpoint_type = "azure"
151
+ model_endpoint = azure_creds["azure_endpoint"]
152
+
153
+ elif provider == "google_ai":
154
+
155
+ # check for key
156
+ if credentials.google_ai_key is None:
157
+ # allow key to get pulled from env vars
158
+ google_ai_key = get_google_ai_credentials()
159
+ # if we still can't find it, ask for it as input
160
+ if google_ai_key is None:
161
+ while google_ai_key is None or len(google_ai_key) == 0:
162
+ # Ask for API key as input
163
+ google_ai_key = questionary.password(
164
+ "Enter your Google AI (Gemini) API key (see https://aistudio.google.com/app/apikey):"
165
+ ).ask()
166
+ if google_ai_key is None:
167
+ raise KeyboardInterrupt
168
+ credentials.google_ai_key = google_ai_key
169
+ else:
170
+ # Give the user an opportunity to overwrite the key
171
+ google_ai_key = None
172
+ default_input = shorten_key_middle(credentials.google_ai_key)
173
+
174
+ google_ai_key = questionary.password(
175
+ "Enter your Google AI (Gemini) API key (see https://aistudio.google.com/app/apikey):",
176
+ default=default_input,
177
+ ).ask()
178
+ if google_ai_key is None:
179
+ raise KeyboardInterrupt
180
+ # If the user modified it, use the new one
181
+ if google_ai_key != default_input:
182
+ credentials.google_ai_key = google_ai_key
183
+
184
+ default_input = os.getenv("GOOGLE_AI_SERVICE_ENDPOINT", None)
185
+ if default_input is None:
186
+ default_input = "generativelanguage"
187
+ google_ai_service_endpoint = questionary.text(
188
+ "Enter your Google AI (Gemini) service endpoint (see https://ai.google.dev/api/rest):",
189
+ default=default_input,
190
+ ).ask()
191
+ credentials.google_ai_service_endpoint = google_ai_service_endpoint
192
+
193
+ # write out the credentials
194
+ credentials.save()
195
+
196
+ model_endpoint_type = "google_ai"
197
+
198
+ elif provider == "anthropic":
199
+ # check for key
200
+ if credentials.anthropic_key is None:
201
+ # allow key to get pulled from env vars
202
+ anthropic_api_key = os.getenv("ANTHROPIC_API_KEY", None)
203
+ # if we still can't find it, ask for it as input
204
+ if anthropic_api_key is None:
205
+ while anthropic_api_key is None or len(anthropic_api_key) == 0:
206
+ # Ask for API key as input
207
+ anthropic_api_key = questionary.password(
208
+ "Enter your Anthropic API key (starts with 'sk-', see https://console.anthropic.com/settings/keys):"
209
+ ).ask()
210
+ if anthropic_api_key is None:
211
+ raise KeyboardInterrupt
212
+ credentials.anthropic_key = anthropic_api_key
213
+ credentials.save()
214
+ else:
215
+ # Give the user an opportunity to overwrite the key
216
+ anthropic_api_key = None
217
+ default_input = (
218
+ shorten_key_middle(credentials.anthropic_key) if credentials.anthropic_key.startswith("sk-") else credentials.anthropic_key
219
+ )
220
+ anthropic_api_key = questionary.password(
221
+ "Enter your Anthropic API key (starts with 'sk-', see https://console.anthropic.com/settings/keys):",
222
+ default=default_input,
223
+ ).ask()
224
+ if anthropic_api_key is None:
225
+ raise KeyboardInterrupt
226
+ # If the user modified it, use the new one
227
+ if anthropic_api_key != default_input:
228
+ credentials.anthropic_key = anthropic_api_key
229
+ credentials.save()
230
+
231
+ model_endpoint_type = "anthropic"
232
+ model_endpoint = "https://api.anthropic.com/v1"
233
+ model_endpoint = questionary.text("Override default endpoint:", default=model_endpoint).ask()
234
+ if model_endpoint is None:
235
+ raise KeyboardInterrupt
236
+ provider = "anthropic"
237
+
238
+ elif provider == "cohere":
239
+ # check for key
240
+ if credentials.cohere_key is None:
241
+ # allow key to get pulled from env vars
242
+ cohere_api_key = os.getenv("COHERE_API_KEY", None)
243
+ # if we still can't find it, ask for it as input
244
+ if cohere_api_key is None:
245
+ while cohere_api_key is None or len(cohere_api_key) == 0:
246
+ # Ask for API key as input
247
+ cohere_api_key = questionary.password("Enter your Cohere API key (see https://dashboard.cohere.com/api-keys):").ask()
248
+ if cohere_api_key is None:
249
+ raise KeyboardInterrupt
250
+ credentials.cohere_key = cohere_api_key
251
+ credentials.save()
252
+ else:
253
+ # Give the user an opportunity to overwrite the key
254
+ cohere_api_key = None
255
+ default_input = (
256
+ shorten_key_middle(credentials.cohere_key) if credentials.cohere_key.startswith("sk-") else credentials.cohere_key
257
+ )
258
+ cohere_api_key = questionary.password(
259
+ "Enter your Cohere API key (see https://dashboard.cohere.com/api-keys):",
260
+ default=default_input,
261
+ ).ask()
262
+ if cohere_api_key is None:
263
+ raise KeyboardInterrupt
264
+ # If the user modified it, use the new one
265
+ if cohere_api_key != default_input:
266
+ credentials.cohere_key = cohere_api_key
267
+ credentials.save()
268
+
269
+ model_endpoint_type = "cohere"
270
+ model_endpoint = "https://api.cohere.ai/v1"
271
+ model_endpoint = questionary.text("Override default endpoint:", default=model_endpoint).ask()
272
+ if model_endpoint is None:
273
+ raise KeyboardInterrupt
274
+ provider = "cohere"
275
+
276
+ else: # local models
277
+ # backend_options_old = ["webui", "webui-legacy", "llamacpp", "koboldcpp", "ollama", "lmstudio", "lmstudio-legacy", "vllm", "openai"]
278
+ backend_options = builtins.list(DEFAULT_ENDPOINTS.keys())
279
+ # assert backend_options_old == backend_options, (backend_options_old, backend_options)
280
+ default_model_endpoint_type = None
281
+ if config.default_llm_config and config.default_llm_config.model_endpoint_type in backend_options:
282
+ # set from previous config
283
+ default_model_endpoint_type = config.default_llm_config.model_endpoint_type
284
+ model_endpoint_type = questionary.select(
285
+ "Select LLM backend (select 'openai' if you have an OpenAI compatible proxy):",
286
+ backend_options,
287
+ default=default_model_endpoint_type,
288
+ ).ask()
289
+ if model_endpoint_type is None:
290
+ raise KeyboardInterrupt
291
+
292
+ # set default endpoint
293
+ # if OPENAI_API_BASE is set, assume that this is the IP+port the user wanted to use
294
+ default_model_endpoint = os.getenv("OPENAI_API_BASE")
295
+ # if OPENAI_API_BASE is not set, try to pull a default IP+port format from a hardcoded set
296
+ if default_model_endpoint is None:
297
+ if model_endpoint_type in DEFAULT_ENDPOINTS:
298
+ default_model_endpoint = DEFAULT_ENDPOINTS[model_endpoint_type]
299
+ model_endpoint = questionary.text("Enter default endpoint:", default=default_model_endpoint).ask()
300
+ if model_endpoint is None:
301
+ raise KeyboardInterrupt
302
+ while not utils.is_valid_url(model_endpoint):
303
+ typer.secho(f"Endpoint must be a valid address", fg=typer.colors.YELLOW)
304
+ model_endpoint = questionary.text("Enter default endpoint:", default=default_model_endpoint).ask()
305
+ if model_endpoint is None:
306
+ raise KeyboardInterrupt
307
+ elif config.default_llm_config and config.default_llm_config.model_endpoint:
308
+ model_endpoint = questionary.text("Enter default endpoint:", default=config.default_llm_config.model_endpoint).ask()
309
+ if model_endpoint is None:
310
+ raise KeyboardInterrupt
311
+ while not utils.is_valid_url(model_endpoint):
312
+ typer.secho(f"Endpoint must be a valid address", fg=typer.colors.YELLOW)
313
+ model_endpoint = questionary.text("Enter default endpoint:", default=config.default_llm_config.model_endpoint).ask()
314
+ if model_endpoint is None:
315
+ raise KeyboardInterrupt
316
+ else:
317
+ # default_model_endpoint = None
318
+ model_endpoint = None
319
+ model_endpoint = questionary.text("Enter default endpoint:").ask()
320
+ if model_endpoint is None:
321
+ raise KeyboardInterrupt
322
+ while not utils.is_valid_url(model_endpoint):
323
+ typer.secho(f"Endpoint must be a valid address", fg=typer.colors.YELLOW)
324
+ model_endpoint = questionary.text("Enter default endpoint:").ask()
325
+ if model_endpoint is None:
326
+ raise KeyboardInterrupt
327
+ else:
328
+ model_endpoint = default_model_endpoint
329
+ assert model_endpoint, f"Environment variable OPENAI_API_BASE must be set."
330
+
331
+ return model_endpoint_type, model_endpoint
332
+
333
+
334
+ def get_model_options(
335
+ credentials: LettaCredentials,
336
+ model_endpoint_type: str,
337
+ model_endpoint: str,
338
+ filter_list: bool = True,
339
+ filter_prefix: str = "gpt-",
340
+ ) -> list:
341
+ try:
342
+ if model_endpoint_type == "openai":
343
+ if credentials.openai_key is None:
344
+ raise ValueError("Missing OpenAI API key")
345
+ fetched_model_options_response = openai_get_model_list(url=model_endpoint, api_key=credentials.openai_key)
346
+
347
+ # Filter the list for "gpt" models only
348
+ if filter_list:
349
+ model_options = [obj["id"] for obj in fetched_model_options_response["data"] if obj["id"].startswith(filter_prefix)]
350
+ else:
351
+ model_options = [obj["id"] for obj in fetched_model_options_response["data"]]
352
+
353
+ elif model_endpoint_type == "azure":
354
+ if credentials.azure_key is None:
355
+ raise ValueError("Missing Azure key")
356
+ if credentials.azure_version is None:
357
+ raise ValueError("Missing Azure version")
358
+ fetched_model_options_response = azure_openai_get_model_list(
359
+ url=model_endpoint, api_key=credentials.azure_key, api_version=credentials.azure_version
360
+ )
361
+
362
+ # Filter the list for "gpt" models only
363
+ if filter_list:
364
+ model_options = [obj["id"] for obj in fetched_model_options_response["data"] if obj["id"].startswith(filter_prefix)]
365
+ else:
366
+ model_options = [obj["id"] for obj in fetched_model_options_response["data"]]
367
+
368
+ elif model_endpoint_type == "google_ai":
369
+ if credentials.google_ai_key is None:
370
+ raise ValueError("Missing Google AI API key")
371
+ if credentials.google_ai_service_endpoint is None:
372
+ raise ValueError("Missing Google AI service endpoint")
373
+ model_options = google_ai_get_model_list(
374
+ service_endpoint=credentials.google_ai_service_endpoint, api_key=credentials.google_ai_key
375
+ )
376
+ model_options = [str(m["name"]) for m in model_options]
377
+ model_options = [mo[len("models/") :] if mo.startswith("models/") else mo for mo in model_options]
378
+
379
+ # TODO remove manual filtering for gemini-pro
380
+ model_options = [mo for mo in model_options if str(mo).startswith("gemini") and "-pro" in str(mo)]
381
+ # model_options = ["gemini-pro"]
382
+
383
+ elif model_endpoint_type == "anthropic":
384
+ if credentials.anthropic_key is None:
385
+ raise ValueError("Missing Anthropic API key")
386
+ fetched_model_options = anthropic_get_model_list(url=model_endpoint, api_key=credentials.anthropic_key)
387
+ model_options = [obj["name"] for obj in fetched_model_options]
388
+
389
+ elif model_endpoint_type == "cohere":
390
+ if credentials.cohere_key is None:
391
+ raise ValueError("Missing Cohere API key")
392
+ fetched_model_options = cohere_get_model_list(url=model_endpoint, api_key=credentials.cohere_key)
393
+ model_options = [obj for obj in fetched_model_options]
394
+
395
+ else:
396
+ # Attempt to do OpenAI endpoint style model fetching
397
+ # TODO support local auth with api-key header
398
+ if credentials.openllm_auth_type == "bearer_token":
399
+ api_key = credentials.openllm_key
400
+ else:
401
+ api_key = None
402
+ fetched_model_options_response = openai_get_model_list(url=model_endpoint, api_key=api_key, fix_url=True)
403
+ model_options = [obj["id"] for obj in fetched_model_options_response["data"]]
404
+ # NOTE no filtering of local model options
405
+
406
+ # list
407
+ return model_options
408
+
409
+ except:
410
+ raise Exception(f"Failed to get model list from {model_endpoint}")
411
+
412
+
413
+ def configure_model(config: LettaConfig, credentials: LettaCredentials, model_endpoint_type: str, model_endpoint: str):
414
+ # set: model, model_wrapper
415
+ model, model_wrapper = None, None
416
+ if model_endpoint_type == "openai" or model_endpoint_type == "azure":
417
+ # Get the model list from the openai / azure endpoint
418
+ hardcoded_model_options = ["gpt-4", "gpt-4-32k", "gpt-4-1106-preview", "gpt-3.5-turbo", "gpt-3.5-turbo-16k"]
419
+ fetched_model_options = []
420
+ try:
421
+ fetched_model_options = get_model_options(
422
+ credentials=credentials, model_endpoint_type=model_endpoint_type, model_endpoint=model_endpoint
423
+ )
424
+ except Exception as e:
425
+ # NOTE: if this fails, it means the user's key is probably bad
426
+ typer.secho(
427
+ f"Failed to get model list from {model_endpoint} - make sure your API key and endpoints are correct!", fg=typer.colors.RED
428
+ )
429
+ raise e
430
+
431
+ # First ask if the user wants to see the full model list (some may be incompatible)
432
+ see_all_option_str = "[see all options]"
433
+ other_option_str = "[enter model name manually]"
434
+
435
+ # Check if the model we have set already is even in the list (informs our default)
436
+ valid_model = config.default_llm_config and config.default_llm_config.model in hardcoded_model_options
437
+ model = questionary.select(
438
+ "Select default model (recommended: gpt-4):",
439
+ choices=hardcoded_model_options + [see_all_option_str, other_option_str],
440
+ default=config.default_llm_config.model if valid_model else hardcoded_model_options[0],
441
+ ).ask()
442
+ if model is None:
443
+ raise KeyboardInterrupt
444
+
445
+ # If the user asked for the full list, show it
446
+ if model == see_all_option_str:
447
+ typer.secho(f"Warning: not all models shown are guaranteed to work with Letta", fg=typer.colors.RED)
448
+ model = questionary.select(
449
+ "Select default model (recommended: gpt-4):",
450
+ choices=fetched_model_options + [other_option_str],
451
+ default=config.default_llm_config.model if (valid_model and config.default_llm_config) else fetched_model_options[0],
452
+ ).ask()
453
+ if model is None:
454
+ raise KeyboardInterrupt
455
+
456
+ # Finally if the user asked to manually input, allow it
457
+ if model == other_option_str:
458
+ model = ""
459
+ while len(model) == 0:
460
+ model = questionary.text(
461
+ "Enter custom model name:",
462
+ ).ask()
463
+ if model is None:
464
+ raise KeyboardInterrupt
465
+
466
+ elif model_endpoint_type == "google_ai":
467
+ try:
468
+ fetched_model_options = get_model_options(
469
+ credentials=credentials, model_endpoint_type=model_endpoint_type, model_endpoint=model_endpoint
470
+ )
471
+ except Exception as e:
472
+ # NOTE: if this fails, it means the user's key is probably bad
473
+ typer.secho(
474
+ f"Failed to get model list from {model_endpoint} - make sure your API key and endpoints are correct!", fg=typer.colors.RED
475
+ )
476
+ raise e
477
+
478
+ model = questionary.select(
479
+ "Select default model:",
480
+ choices=fetched_model_options,
481
+ default=fetched_model_options[0],
482
+ ).ask()
483
+ if model is None:
484
+ raise KeyboardInterrupt
485
+
486
+ elif model_endpoint_type == "anthropic":
487
+ try:
488
+ fetched_model_options = get_model_options(
489
+ credentials=credentials, model_endpoint_type=model_endpoint_type, model_endpoint=model_endpoint
490
+ )
491
+ except Exception as e:
492
+ # NOTE: if this fails, it means the user's key is probably bad
493
+ typer.secho(
494
+ f"Failed to get model list from {model_endpoint} - make sure your API key and endpoints are correct!", fg=typer.colors.RED
495
+ )
496
+ raise e
497
+
498
+ model = questionary.select(
499
+ "Select default model:",
500
+ choices=fetched_model_options,
501
+ default=fetched_model_options[0],
502
+ ).ask()
503
+ if model is None:
504
+ raise KeyboardInterrupt
505
+
506
+ elif model_endpoint_type == "cohere":
507
+
508
+ fetched_model_options = []
509
+ try:
510
+ fetched_model_options = get_model_options(
511
+ credentials=credentials, model_endpoint_type=model_endpoint_type, model_endpoint=model_endpoint
512
+ )
513
+ except Exception as e:
514
+ # NOTE: if this fails, it means the user's key is probably bad
515
+ typer.secho(
516
+ f"Failed to get model list from {model_endpoint} - make sure your API key and endpoints are correct!", fg=typer.colors.RED
517
+ )
518
+ raise e
519
+
520
+ fetched_model_options = [m["name"] for m in fetched_model_options]
521
+ hardcoded_model_options = [m for m in fetched_model_options if m in COHERE_VALID_MODEL_LIST]
522
+
523
+ # First ask if the user wants to see the full model list (some may be incompatible)
524
+ see_all_option_str = "[see all options]"
525
+ other_option_str = "[enter model name manually]"
526
+
527
+ # Check if the model we have set already is even in the list (informs our default)
528
+ valid_model = config.default_llm_config.model in hardcoded_model_options
529
+ model = questionary.select(
530
+ "Select default model (recommended: command-r-plus):",
531
+ choices=hardcoded_model_options + [see_all_option_str, other_option_str],
532
+ default=config.default_llm_config.model if valid_model else hardcoded_model_options[0],
533
+ ).ask()
534
+ if model is None:
535
+ raise KeyboardInterrupt
536
+
537
+ # If the user asked for the full list, show it
538
+ if model == see_all_option_str:
539
+ typer.secho(f"Warning: not all models shown are guaranteed to work with Letta", fg=typer.colors.RED)
540
+ model = questionary.select(
541
+ "Select default model (recommended: command-r-plus):",
542
+ choices=fetched_model_options + [other_option_str],
543
+ default=config.default_llm_config.model if valid_model else fetched_model_options[0],
544
+ ).ask()
545
+ if model is None:
546
+ raise KeyboardInterrupt
547
+
548
+ # Finally if the user asked to manually input, allow it
549
+ if model == other_option_str:
550
+ model = ""
551
+ while len(model) == 0:
552
+ model = questionary.text(
553
+ "Enter custom model name:",
554
+ ).ask()
555
+ if model is None:
556
+ raise KeyboardInterrupt
557
+
558
+ else: # local models
559
+
560
+ # ask about local auth
561
+ if model_endpoint_type in ["groq"]: # TODO all llm engines under 'local' that will require api keys
562
+ use_local_auth = True
563
+ local_auth_type = "bearer_token"
564
+ local_auth_key = questionary.password(
565
+ "Enter your Groq API key:",
566
+ ).ask()
567
+ if local_auth_key is None:
568
+ raise KeyboardInterrupt
569
+ credentials.openllm_auth_type = local_auth_type
570
+ credentials.openllm_key = local_auth_key
571
+ credentials.save()
572
+ else:
573
+ use_local_auth = questionary.confirm(
574
+ "Is your LLM endpoint authenticated? (default no)",
575
+ default=False,
576
+ ).ask()
577
+ if use_local_auth is None:
578
+ raise KeyboardInterrupt
579
+ if use_local_auth:
580
+ local_auth_type = questionary.select(
581
+ "What HTTP authentication method does your endpoint require?",
582
+ choices=SUPPORTED_AUTH_TYPES,
583
+ default=SUPPORTED_AUTH_TYPES[0],
584
+ ).ask()
585
+ if local_auth_type is None:
586
+ raise KeyboardInterrupt
587
+ local_auth_key = questionary.password(
588
+ "Enter your authentication key:",
589
+ ).ask()
590
+ if local_auth_key is None:
591
+ raise KeyboardInterrupt
592
+ # credentials = LettaCredentials.load()
593
+ credentials.openllm_auth_type = local_auth_type
594
+ credentials.openllm_key = local_auth_key
595
+ credentials.save()
596
+
597
+ # ollama also needs model type
598
+ if model_endpoint_type == "ollama":
599
+ default_model = (
600
+ config.default_llm_config.model
601
+ if config.default_llm_config and config.default_llm_config.model_endpoint_type == "ollama"
602
+ else DEFAULT_OLLAMA_MODEL
603
+ )
604
+ model = questionary.text(
605
+ "Enter default model name (required for Ollama, see: https://letta.readme.io/docs/ollama):",
606
+ default=default_model,
607
+ ).ask()
608
+ if model is None:
609
+ raise KeyboardInterrupt
610
+ model = None if len(model) == 0 else model
611
+
612
+ default_model = (
613
+ config.default_llm_config.model if config.default_llm_config and config.default_llm_config.model_endpoint_type == "vllm" else ""
614
+ )
615
+
616
+ # vllm needs huggingface model tag
617
+ if model_endpoint_type in ["vllm", "groq"]:
618
+ try:
619
+ # Don't filter model list for vLLM since model list is likely much smaller than OpenAI/Azure endpoint
620
+ # + probably has custom model names
621
+ # TODO support local auth
622
+ model_options = get_model_options(
623
+ credentials=credentials, model_endpoint_type=model_endpoint_type, model_endpoint=model_endpoint
624
+ )
625
+ except:
626
+ print(f"Failed to get model list from {model_endpoint}, using defaults")
627
+ model_options = None
628
+
629
+ # If we got model options from vLLM endpoint, allow selection + custom input
630
+ if model_options is not None:
631
+ other_option_str = "other (enter name)"
632
+ valid_model = config.default_llm_config.model in model_options
633
+ model_options.append(other_option_str)
634
+ model = questionary.select(
635
+ "Select default model:",
636
+ choices=model_options,
637
+ default=config.default_llm_config.model if valid_model else model_options[0],
638
+ ).ask()
639
+ if model is None:
640
+ raise KeyboardInterrupt
641
+
642
+ # If we got custom input, ask for raw input
643
+ if model == other_option_str:
644
+ model = questionary.text(
645
+ "Enter HuggingFace model tag (e.g. ehartford/dolphin-2.2.1-mistral-7b):",
646
+ default=default_model,
647
+ ).ask()
648
+ if model is None:
649
+ raise KeyboardInterrupt
650
+ # TODO allow empty string for input?
651
+ model = None if len(model) == 0 else model
652
+
653
+ else:
654
+ model = questionary.text(
655
+ "Enter HuggingFace model tag (e.g. ehartford/dolphin-2.2.1-mistral-7b):",
656
+ default=default_model,
657
+ ).ask()
658
+ if model is None:
659
+ raise KeyboardInterrupt
660
+ model = None if len(model) == 0 else model
661
+
662
+ # model wrapper
663
+ available_model_wrappers = builtins.list(get_available_wrappers().keys())
664
+ model_wrapper = questionary.select(
665
+ f"Select default model wrapper (recommended: {DEFAULT_WRAPPER_NAME}):",
666
+ choices=available_model_wrappers,
667
+ default=DEFAULT_WRAPPER_NAME,
668
+ ).ask()
669
+ if model_wrapper is None:
670
+ raise KeyboardInterrupt
671
+
672
+ # set: context_window
673
+ if str(model) not in LLM_MAX_TOKENS:
674
+
675
+ context_length_options = [
676
+ str(2**12), # 4096
677
+ str(2**13), # 8192
678
+ str(2**14), # 16384
679
+ str(2**15), # 32768
680
+ str(2**18), # 262144
681
+ "custom", # enter yourself
682
+ ]
683
+
684
+ if model_endpoint_type == "google_ai":
685
+ try:
686
+ fetched_context_window = str(
687
+ google_ai_get_model_context_window(
688
+ service_endpoint=credentials.google_ai_service_endpoint, api_key=credentials.google_ai_key, model=model
689
+ )
690
+ )
691
+ print(f"Got context window {fetched_context_window} for model {model} (from Google API)")
692
+ context_length_options = [
693
+ fetched_context_window,
694
+ "custom",
695
+ ]
696
+ except Exception as e:
697
+ print(f"Failed to get model details for model '{model}' on Google AI API ({str(e)})")
698
+
699
+ context_window_input = questionary.select(
700
+ "Select your model's context window (see https://cloud.google.com/vertex-ai/generative-ai/docs/learn/model-versioning#gemini-model-versions):",
701
+ choices=context_length_options,
702
+ default=context_length_options[0],
703
+ ).ask()
704
+ if context_window_input is None:
705
+ raise KeyboardInterrupt
706
+
707
+ elif model_endpoint_type == "anthropic":
708
+ try:
709
+ fetched_context_window = str(
710
+ antropic_get_model_context_window(url=model_endpoint, api_key=credentials.anthropic_key, model=model)
711
+ )
712
+ print(f"Got context window {fetched_context_window} for model {model}")
713
+ context_length_options = [
714
+ fetched_context_window,
715
+ "custom",
716
+ ]
717
+ except Exception as e:
718
+ print(f"Failed to get model details for model '{model}' ({str(e)})")
719
+
720
+ context_window_input = questionary.select(
721
+ "Select your model's context window (see https://docs.anthropic.com/claude/docs/models-overview):",
722
+ choices=context_length_options,
723
+ default=context_length_options[0],
724
+ ).ask()
725
+ if context_window_input is None:
726
+ raise KeyboardInterrupt
727
+
728
+ elif model_endpoint_type == "cohere":
729
+ try:
730
+ fetched_context_window = str(
731
+ cohere_get_model_context_window(url=model_endpoint, api_key=credentials.cohere_key, model=model)
732
+ )
733
+ print(f"Got context window {fetched_context_window} for model {model}")
734
+ context_length_options = [
735
+ fetched_context_window,
736
+ "custom",
737
+ ]
738
+ except Exception as e:
739
+ print(f"Failed to get model details for model '{model}' ({str(e)})")
740
+
741
+ context_window_input = questionary.select(
742
+ "Select your model's context window (see https://docs.cohere.com/docs/command-r):",
743
+ choices=context_length_options,
744
+ default=context_length_options[0],
745
+ ).ask()
746
+ if context_window_input is None:
747
+ raise KeyboardInterrupt
748
+
749
+ else:
750
+
751
+ # Ask the user to specify the context length
752
+ context_window_input = questionary.select(
753
+ "Select your model's context window (for Mistral 7B models, this is probably 8k / 8192):",
754
+ choices=context_length_options,
755
+ default=str(LLM_MAX_TOKENS["DEFAULT"]),
756
+ ).ask()
757
+ if context_window_input is None:
758
+ raise KeyboardInterrupt
759
+
760
+ # If custom, ask for input
761
+ if context_window_input == "custom":
762
+ while True:
763
+ context_window_input = questionary.text("Enter context window (e.g. 8192)").ask()
764
+ if context_window_input is None:
765
+ raise KeyboardInterrupt
766
+ try:
767
+ context_window = int(context_window_input)
768
+ break
769
+ except ValueError:
770
+ print(f"Context window must be a valid integer")
771
+ else:
772
+ context_window = int(context_window_input)
773
+ else:
774
+ # Pull the context length from the models
775
+ context_window = int(LLM_MAX_TOKENS[str(model)])
776
+ return model, model_wrapper, context_window
777
+
778
+
779
+ def configure_embedding_endpoint(config: LettaConfig, credentials: LettaCredentials):
780
+ # configure embedding endpoint
781
+
782
+ default_embedding_endpoint_type = config.default_embedding_config.embedding_endpoint_type if config.default_embedding_config else None
783
+
784
+ embedding_endpoint_type, embedding_endpoint, embedding_dim, embedding_model = None, None, None, None
785
+ embedding_provider = questionary.select(
786
+ "Select embedding provider:", choices=["openai", "azure", "hugging-face", "local"], default=default_embedding_endpoint_type
787
+ ).ask()
788
+ if embedding_provider is None:
789
+ raise KeyboardInterrupt
790
+
791
+ if embedding_provider == "openai":
792
+ # check for key
793
+ if credentials.openai_key is None:
794
+ # allow key to get pulled from env vars
795
+ openai_api_key = os.getenv("OPENAI_API_KEY", None)
796
+ if openai_api_key is None:
797
+ # if we still can't find it, ask for it as input
798
+ while openai_api_key is None or len(openai_api_key) == 0:
799
+ # Ask for API key as input
800
+ openai_api_key = questionary.password(
801
+ "Enter your OpenAI API key (starts with 'sk-', see https://platform.openai.com/api-keys):"
802
+ ).ask()
803
+ if openai_api_key is None:
804
+ raise KeyboardInterrupt
805
+ credentials.openai_key = openai_api_key
806
+ credentials.save()
807
+
808
+ embedding_endpoint_type = "openai"
809
+ embedding_endpoint = "https://api.openai.com/v1"
810
+ embedding_dim = 1536
811
+ embedding_model = "text-embedding-ada-002"
812
+
813
+ elif embedding_provider == "azure":
814
+ # check for necessary vars
815
+ azure_creds = get_azure_credentials()
816
+ if not all([azure_creds["azure_key"], azure_creds["azure_embedding_endpoint"], azure_creds["azure_embedding_version"]]):
817
+ raise ValueError(
818
+ "Missing environment variables for Azure (see https://letta.readme.io/docs/endpoints#azure-openai). Please set then run `letta configure` again."
819
+ )
820
+ credentials.azure_key = azure_creds["azure_key"]
821
+ credentials.azure_version = azure_creds["azure_version"]
822
+ credentials.azure_embedding_endpoint = azure_creds["azure_embedding_endpoint"]
823
+ credentials.save()
824
+
825
+ embedding_endpoint_type = "azure"
826
+ embedding_endpoint = azure_creds["azure_embedding_endpoint"]
827
+ embedding_dim = 1536
828
+ embedding_model = "text-embedding-ada-002"
829
+
830
+ elif embedding_provider == "hugging-face":
831
+ # configure hugging face embedding endpoint (https://github.com/huggingface/text-embeddings-inference)
832
+ # supports custom model/endpoints
833
+ embedding_endpoint_type = "hugging-face"
834
+ embedding_endpoint = None
835
+
836
+ # get endpoint
837
+ embedding_endpoint = questionary.text("Enter default endpoint:").ask()
838
+ if embedding_endpoint is None:
839
+ raise KeyboardInterrupt
840
+ while not utils.is_valid_url(embedding_endpoint):
841
+ typer.secho(f"Endpoint must be a valid address", fg=typer.colors.YELLOW)
842
+ embedding_endpoint = questionary.text("Enter default endpoint:").ask()
843
+ if embedding_endpoint is None:
844
+ raise KeyboardInterrupt
845
+
846
+ # get model type
847
+ default_embedding_model = (
848
+ config.default_embedding_config.embedding_model if config.default_embedding_config else "BAAI/bge-large-en-v1.5"
849
+ )
850
+ embedding_model = questionary.text(
851
+ "Enter HuggingFace model tag (e.g. BAAI/bge-large-en-v1.5):",
852
+ default=default_embedding_model,
853
+ ).ask()
854
+ if embedding_model is None:
855
+ raise KeyboardInterrupt
856
+
857
+ # get model dimentions
858
+ default_embedding_dim = config.default_embedding_config.embedding_dim if config.default_embedding_config else "1024"
859
+ embedding_dim = questionary.text("Enter embedding model dimentions (e.g. 1024):", default=str(default_embedding_dim)).ask()
860
+ if embedding_dim is None:
861
+ raise KeyboardInterrupt
862
+ try:
863
+ embedding_dim = int(embedding_dim)
864
+ except Exception:
865
+ raise ValueError(f"Failed to cast {embedding_dim} to integer.")
866
+ elif embedding_provider == "ollama":
867
+ # configure ollama embedding endpoint
868
+ embedding_endpoint_type = "ollama"
869
+ embedding_endpoint = "http://localhost:11434/api/embeddings"
870
+ # Source: https://github.com/ollama/ollama/blob/main/docs/api.md#generate-embeddings:~:text=http%3A//localhost%3A11434/api/embeddings
871
+
872
+ # get endpoint (is this necessary?)
873
+ embedding_endpoint = questionary.text("Enter Ollama API endpoint:").ask()
874
+ if embedding_endpoint is None:
875
+ raise KeyboardInterrupt
876
+ while not utils.is_valid_url(embedding_endpoint):
877
+ typer.secho(f"Endpoint must be a valid address", fg=typer.colors.YELLOW)
878
+ embedding_endpoint = questionary.text("Enter Ollama API endpoint:").ask()
879
+ if embedding_endpoint is None:
880
+ raise KeyboardInterrupt
881
+
882
+ # get model type
883
+ default_embedding_model = (
884
+ config.default_embedding_config.embedding_model if config.default_embedding_config else "mxbai-embed-large"
885
+ )
886
+ embedding_model = questionary.text(
887
+ "Enter Ollama model tag (e.g. mxbai-embed-large):",
888
+ default=default_embedding_model,
889
+ ).ask()
890
+ if embedding_model is None:
891
+ raise KeyboardInterrupt
892
+
893
+ # get model dimensions
894
+ default_embedding_dim = config.default_embedding_config.embedding_dim if config.default_embedding_config else "512"
895
+ embedding_dim = questionary.text("Enter embedding model dimensions (e.g. 512):", default=str(default_embedding_dim)).ask()
896
+ if embedding_dim is None:
897
+ raise KeyboardInterrupt
898
+ try:
899
+ embedding_dim = int(embedding_dim)
900
+ except Exception:
901
+ raise ValueError(f"Failed to cast {embedding_dim} to integer.")
902
+ else: # local models
903
+ embedding_endpoint_type = "local"
904
+ embedding_endpoint = None
905
+ embedding_model = "BAAI/bge-small-en-v1.5"
906
+ embedding_dim = 384
907
+
908
+ return embedding_endpoint_type, embedding_endpoint, embedding_dim, embedding_model
909
+
910
+
911
+ def configure_archival_storage(config: LettaConfig, credentials: LettaCredentials):
912
+ # Configure archival storage backend
913
+ archival_storage_options = ["postgres", "chroma", "milvus", "qdrant"]
914
+ archival_storage_type = questionary.select(
915
+ "Select storage backend for archival data:", archival_storage_options, default=config.archival_storage_type
916
+ ).ask()
917
+ if archival_storage_type is None:
918
+ raise KeyboardInterrupt
919
+ archival_storage_uri, archival_storage_path = config.archival_storage_uri, config.archival_storage_path
920
+
921
+ # configure postgres
922
+ if archival_storage_type == "postgres":
923
+ archival_storage_uri = questionary.text(
924
+ "Enter postgres connection string (e.g. postgresql+pg8000://{user}:{password}@{ip}:5432/{database}):",
925
+ default=config.archival_storage_uri if config.archival_storage_uri else "",
926
+ ).ask()
927
+ if archival_storage_uri is None:
928
+ raise KeyboardInterrupt
929
+
930
+ # TODO: add back
931
+ ## configure lancedb
932
+ # if archival_storage_type == "lancedb":
933
+ # archival_storage_uri = questionary.text(
934
+ # "Enter lanncedb connection string (e.g. ./.lancedb",
935
+ # default=config.archival_storage_uri if config.archival_storage_uri else "./.lancedb",
936
+ # ).ask()
937
+
938
+ # configure chroma
939
+ if archival_storage_type == "chroma":
940
+ chroma_type = questionary.select("Select chroma backend:", ["http", "persistent"], default="persistent").ask()
941
+ if chroma_type is None:
942
+ raise KeyboardInterrupt
943
+ if chroma_type == "http":
944
+ archival_storage_uri = questionary.text("Enter chroma ip (e.g. localhost:8000):", default="localhost:8000").ask()
945
+ if archival_storage_uri is None:
946
+ raise KeyboardInterrupt
947
+ if chroma_type == "persistent":
948
+ archival_storage_path = os.path.join(LETTA_DIR, "chroma")
949
+
950
+ if archival_storage_type == "qdrant":
951
+ qdrant_type = questionary.select("Select Qdrant backend:", ["local", "server"], default="local").ask()
952
+ if qdrant_type is None:
953
+ raise KeyboardInterrupt
954
+ if qdrant_type == "server":
955
+ archival_storage_uri = questionary.text(
956
+ "Enter the Qdrant instance URI (Default: localhost:6333):", default="localhost:6333"
957
+ ).ask()
958
+ if archival_storage_uri is None:
959
+ raise KeyboardInterrupt
960
+ if qdrant_type == "local":
961
+ archival_storage_path = os.path.join(LETTA_DIR, "qdrant")
962
+
963
+ if archival_storage_type == "milvus":
964
+ default_milvus_uri = archival_storage_path = os.path.join(LETTA_DIR, "milvus.db")
965
+ archival_storage_uri = questionary.text(
966
+ f"Enter the Milvus connection URI (Default: {default_milvus_uri}):", default=default_milvus_uri
967
+ ).ask()
968
+ if archival_storage_uri is None:
969
+ raise KeyboardInterrupt
970
+ return archival_storage_type, archival_storage_uri, archival_storage_path
971
+
972
+ # TODO: allow configuring embedding model
973
+
974
+
975
+ def configure_recall_storage(config: LettaConfig, credentials: LettaCredentials):
976
+ # Configure recall storage backend
977
+ recall_storage_options = ["sqlite", "postgres"]
978
+ recall_storage_type = questionary.select(
979
+ "Select storage backend for recall data:", recall_storage_options, default=config.recall_storage_type
980
+ ).ask()
981
+ if recall_storage_type is None:
982
+ raise KeyboardInterrupt
983
+ recall_storage_uri, recall_storage_path = config.recall_storage_uri, config.recall_storage_path
984
+ # configure postgres
985
+ if recall_storage_type == "postgres":
986
+ recall_storage_uri = questionary.text(
987
+ "Enter postgres connection string (e.g. postgresql+pg8000://{user}:{password}@{ip}:5432/{database}):",
988
+ default=config.recall_storage_uri if config.recall_storage_uri else "",
989
+ ).ask()
990
+ if recall_storage_uri is None:
991
+ raise KeyboardInterrupt
992
+
993
+ return recall_storage_type, recall_storage_uri, recall_storage_path
994
+
995
+
996
+ @app.command()
997
+ def configure():
998
+ """Updates default Letta configurations
999
+
1000
+ This function and quickstart should be the ONLY place where LettaConfig.save() is called
1001
+ """
1002
+
1003
+ # check credentials
1004
+ credentials = LettaCredentials.load()
1005
+ openai_key = get_openai_credentials()
1006
+
1007
+ LettaConfig.create_config_dir()
1008
+
1009
+ # Will pre-populate with defaults, or what the user previously set
1010
+ config = LettaConfig.load()
1011
+ try:
1012
+ model_endpoint_type, model_endpoint = configure_llm_endpoint(
1013
+ config=config,
1014
+ credentials=credentials,
1015
+ )
1016
+ model, model_wrapper, context_window = configure_model(
1017
+ config=config,
1018
+ credentials=credentials,
1019
+ model_endpoint_type=str(model_endpoint_type),
1020
+ model_endpoint=str(model_endpoint),
1021
+ )
1022
+ embedding_endpoint_type, embedding_endpoint, embedding_dim, embedding_model = configure_embedding_endpoint(
1023
+ config=config,
1024
+ credentials=credentials,
1025
+ )
1026
+ archival_storage_type, archival_storage_uri, archival_storage_path = configure_archival_storage(
1027
+ config=config,
1028
+ credentials=credentials,
1029
+ )
1030
+ recall_storage_type, recall_storage_uri, recall_storage_path = configure_recall_storage(
1031
+ config=config,
1032
+ credentials=credentials,
1033
+ )
1034
+ except ValueError as e:
1035
+ typer.secho(str(e), fg=typer.colors.RED)
1036
+ return
1037
+
1038
+ # openai key might have gotten added along the way
1039
+ openai_key = credentials.openai_key if credentials.openai_key is not None else openai_key
1040
+
1041
+ # TODO: remove most of this (deplicated with User table)
1042
+ config = LettaConfig(
1043
+ default_llm_config=LLMConfig(
1044
+ model=model,
1045
+ model_endpoint=model_endpoint,
1046
+ model_endpoint_type=model_endpoint_type,
1047
+ model_wrapper=model_wrapper,
1048
+ context_window=context_window,
1049
+ ),
1050
+ default_embedding_config=EmbeddingConfig(
1051
+ embedding_endpoint_type=embedding_endpoint_type,
1052
+ embedding_endpoint=embedding_endpoint,
1053
+ embedding_dim=embedding_dim,
1054
+ embedding_model=embedding_model,
1055
+ ),
1056
+ # storage
1057
+ archival_storage_type=archival_storage_type,
1058
+ archival_storage_uri=archival_storage_uri,
1059
+ archival_storage_path=archival_storage_path,
1060
+ # recall storage
1061
+ recall_storage_type=recall_storage_type,
1062
+ recall_storage_uri=recall_storage_uri,
1063
+ recall_storage_path=recall_storage_path,
1064
+ # metadata storage (currently forced to match recall storage)
1065
+ metadata_storage_type=recall_storage_type,
1066
+ metadata_storage_uri=recall_storage_uri,
1067
+ metadata_storage_path=recall_storage_path,
1068
+ )
1069
+
1070
+ typer.secho(f"📖 Saving config to {config.config_path}", fg=typer.colors.GREEN)
1071
+ config.save()
1072
+
1073
+ from letta import create_client
1074
+
1075
+ client = create_client()
1076
+ print("User ID:", client.user_id)
1077
+
1078
+
1079
+ class ListChoice(str, Enum):
1080
+ agents = "agents"
1081
+ humans = "humans"
1082
+ personas = "personas"
1083
+ sources = "sources"
1084
+
1085
+
1086
+ @app.command()
1087
+ def list(arg: Annotated[ListChoice, typer.Argument]):
1088
+ from letta.client.client import create_client
1089
+
1090
+ client = create_client()
1091
+ table = ColorTable(theme=Themes.OCEAN)
1092
+ if arg == ListChoice.agents:
1093
+ """List all agents"""
1094
+ table.field_names = ["Name", "LLM Model", "Embedding Model", "Embedding Dim", "Persona", "Human", "Data Source", "Create Time"]
1095
+ for agent in tqdm(client.list_agents()):
1096
+ # TODO: add this function
1097
+ sources = client.list_attached_sources(agent_id=agent.id)
1098
+ source_names = [source.name for source in sources if source is not None]
1099
+ table.add_row(
1100
+ [
1101
+ agent.name,
1102
+ agent.llm_config.model,
1103
+ agent.embedding_config.embedding_model,
1104
+ agent.embedding_config.embedding_dim,
1105
+ agent.memory.get_block("persona").value[:100] + "...",
1106
+ agent.memory.get_block("human").value[:100] + "...",
1107
+ ",".join(source_names),
1108
+ utils.format_datetime(agent.created_at),
1109
+ ]
1110
+ )
1111
+ print(table)
1112
+ elif arg == ListChoice.humans:
1113
+ """List all humans"""
1114
+ table.field_names = ["Name", "Text"]
1115
+ for human in client.list_humans():
1116
+ table.add_row([human.name, human.value.replace("\n", "")[:100]])
1117
+ print(table)
1118
+ elif arg == ListChoice.personas:
1119
+ """List all personas"""
1120
+ table.field_names = ["Name", "Text"]
1121
+ for persona in client.list_personas():
1122
+ table.add_row([persona.name, persona.value.replace("\n", "")[:100]])
1123
+ print(table)
1124
+ elif arg == ListChoice.sources:
1125
+ """List all data sources"""
1126
+
1127
+ # create table
1128
+ table.field_names = ["Name", "Description", "Embedding Model", "Embedding Dim", "Created At"]
1129
+ # TODO: eventually look accross all storage connections
1130
+ # TODO: add data source stats
1131
+ # TODO: connect to agents
1132
+
1133
+ # get all sources
1134
+ for source in client.list_sources():
1135
+ # get attached agents
1136
+ table.add_row(
1137
+ [
1138
+ source.name,
1139
+ source.description,
1140
+ source.embedding_config.embedding_model,
1141
+ source.embedding_config.embedding_dim,
1142
+ utils.format_datetime(source.created_at),
1143
+ ]
1144
+ )
1145
+
1146
+ print(table)
1147
+ else:
1148
+ raise ValueError(f"Unknown argument {arg}")
1149
+ return table
1150
+
1151
+
1152
+ @app.command()
1153
+ def add_tool(
1154
+ filename: str = typer.Option(..., help="Path to the Python file containing the function"),
1155
+ name: Optional[str] = typer.Option(None, help="Name of the tool"),
1156
+ update: bool = typer.Option(True, help="Update the tool if it already exists"),
1157
+ tags: Optional[List[str]] = typer.Option(None, help="Tags for the tool"),
1158
+ ):
1159
+ """Add or update a tool from a Python file."""
1160
+ from letta.client.client import create_client
1161
+
1162
+ client = create_client(base_url=os.getenv("MEMGPT_BASE_URL"), token=os.getenv("MEMGPT_SERVER_PASS"))
1163
+
1164
+ # 1. Parse the Python file
1165
+ with open(filename, "r", encoding="utf-8") as file:
1166
+ source_code = file.read()
1167
+
1168
+ # 2. Parse the source code to extract the function
1169
+ # Note: here we assume it is one function only in the file.
1170
+ module = ast.parse(source_code)
1171
+ func_def = None
1172
+ for node in module.body:
1173
+ if isinstance(node, ast.FunctionDef):
1174
+ func_def = node
1175
+ break
1176
+
1177
+ if not func_def:
1178
+ raise ValueError("No function found in the provided file")
1179
+
1180
+ # 3. Compile the function to make it callable
1181
+ # Explanation courtesy of GPT-4:
1182
+ # Compile the AST (Abstract Syntax Tree) node representing the function definition into a code object
1183
+ # ast.Module creates a module node containing the function definition (func_def)
1184
+ # compile converts the AST into a code object that can be executed by the Python interpreter
1185
+ # The exec function executes the compiled code object in the current context,
1186
+ # effectively defining the function within the current namespace
1187
+ exec(compile(ast.Module([func_def], []), filename, "exec"))
1188
+ # Retrieve the function object by evaluating its name in the current namespace
1189
+ # eval looks up the function name in the current scope and returns the function object
1190
+ func = eval(func_def.name)
1191
+
1192
+ # 4. Add or update the tool
1193
+ tool = client.create_tool(func=func, name=name, tags=tags, update=update)
1194
+ print(f"Tool {tool.name} added successfully")
1195
+
1196
+
1197
+ @app.command()
1198
+ def list_tools():
1199
+ """List all available tools."""
1200
+ from letta.client.client import create_client
1201
+
1202
+ client = create_client(base_url=os.getenv("MEMGPT_BASE_URL"), token=os.getenv("MEMGPT_SERVER_PASS"))
1203
+
1204
+ tools = client.list_tools()
1205
+ for tool in tools:
1206
+ print(f"Tool: {tool.name}")
1207
+
1208
+
1209
+ @app.command()
1210
+ def add(
1211
+ option: str, # [human, persona]
1212
+ name: Annotated[str, typer.Option(help="Name of human/persona")],
1213
+ text: Annotated[Optional[str], typer.Option(help="Text of human/persona")] = None,
1214
+ filename: Annotated[Optional[str], typer.Option("-f", help="Specify filename")] = None,
1215
+ ):
1216
+ """Add a person/human"""
1217
+ from letta.client.client import create_client
1218
+
1219
+ client = create_client(base_url=os.getenv("MEMGPT_BASE_URL"), token=os.getenv("MEMGPT_SERVER_PASS"))
1220
+ if filename: # read from file
1221
+ assert text is None, "Cannot specify both text and filename"
1222
+ with open(filename, "r", encoding="utf-8") as f:
1223
+ text = f.read()
1224
+ else:
1225
+ assert text is not None, "Must specify either text or filename"
1226
+ if option == "persona":
1227
+ persona_id = client.get_persona_id(name)
1228
+ if persona_id:
1229
+ client.get_persona(persona_id)
1230
+ # config if user wants to overwrite
1231
+ if not questionary.confirm(f"Persona {name} already exists. Overwrite?").ask():
1232
+ return
1233
+ client.update_persona(persona_id, text=text)
1234
+ else:
1235
+ client.create_persona(name=name, text=text)
1236
+
1237
+ elif option == "human":
1238
+ human_id = client.get_human_id(name)
1239
+ if human_id:
1240
+ human = client.get_human(human_id)
1241
+ # config if user wants to overwrite
1242
+ if not questionary.confirm(f"Human {name} already exists. Overwrite?").ask():
1243
+ return
1244
+ client.update_human(human_id, text=text)
1245
+ else:
1246
+ human = client.create_human(name=name, text=text)
1247
+ else:
1248
+ raise ValueError(f"Unknown kind {option}")
1249
+
1250
+
1251
+ @app.command()
1252
+ def delete(option: str, name: str):
1253
+ """Delete a source from the archival memory."""
1254
+ from letta.client.client import create_client
1255
+
1256
+ client = create_client(base_url=os.getenv("MEMGPT_BASE_URL"), token=os.getenv("MEMGPT_API_KEY"))
1257
+ try:
1258
+ # delete from metadata
1259
+ if option == "source":
1260
+ # delete metadata
1261
+ source_id = client.get_source_id(name)
1262
+ assert source_id is not None, f"Source {name} does not exist"
1263
+ client.delete_source(source_id)
1264
+ elif option == "agent":
1265
+ agent_id = client.get_agent_id(name)
1266
+ assert agent_id is not None, f"Agent {name} does not exist"
1267
+ client.delete_agent(agent_id=agent_id)
1268
+ elif option == "human":
1269
+ human_id = client.get_human_id(name)
1270
+ assert human_id is not None, f"Human {name} does not exist"
1271
+ client.delete_human(human_id)
1272
+ elif option == "persona":
1273
+ persona_id = client.get_persona_id(name)
1274
+ assert persona_id is not None, f"Persona {name} does not exist"
1275
+ client.delete_persona(persona_id)
1276
+ else:
1277
+ raise ValueError(f"Option {option} not implemented")
1278
+
1279
+ typer.secho(f"Deleted {option} '{name}'", fg=typer.colors.GREEN)
1280
+
1281
+ except Exception as e:
1282
+ typer.secho(f"Failed to delete {option}'{name}'\n{e}", fg=typer.colors.RED)