letta-nightly 0.1.7.dev20240924104148__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +24 -0
- letta/__main__.py +3 -0
- letta/agent.py +1427 -0
- letta/agent_store/chroma.py +295 -0
- letta/agent_store/db.py +546 -0
- letta/agent_store/lancedb.py +177 -0
- letta/agent_store/milvus.py +198 -0
- letta/agent_store/qdrant.py +201 -0
- letta/agent_store/storage.py +188 -0
- letta/benchmark/benchmark.py +96 -0
- letta/benchmark/constants.py +14 -0
- letta/cli/cli.py +689 -0
- letta/cli/cli_config.py +1282 -0
- letta/cli/cli_load.py +166 -0
- letta/client/__init__.py +0 -0
- letta/client/admin.py +171 -0
- letta/client/client.py +2360 -0
- letta/client/streaming.py +90 -0
- letta/client/utils.py +61 -0
- letta/config.py +484 -0
- letta/configs/anthropic.json +13 -0
- letta/configs/letta_hosted.json +11 -0
- letta/configs/openai.json +12 -0
- letta/constants.py +134 -0
- letta/credentials.py +140 -0
- letta/data_sources/connectors.py +247 -0
- letta/embeddings.py +218 -0
- letta/errors.py +26 -0
- letta/functions/__init__.py +0 -0
- letta/functions/function_sets/base.py +174 -0
- letta/functions/function_sets/extras.py +132 -0
- letta/functions/functions.py +105 -0
- letta/functions/schema_generator.py +205 -0
- letta/humans/__init__.py +0 -0
- letta/humans/examples/basic.txt +1 -0
- letta/humans/examples/cs_phd.txt +9 -0
- letta/interface.py +314 -0
- letta/llm_api/__init__.py +0 -0
- letta/llm_api/anthropic.py +383 -0
- letta/llm_api/azure_openai.py +155 -0
- letta/llm_api/cohere.py +396 -0
- letta/llm_api/google_ai.py +468 -0
- letta/llm_api/llm_api_tools.py +485 -0
- letta/llm_api/openai.py +470 -0
- letta/local_llm/README.md +3 -0
- letta/local_llm/__init__.py +0 -0
- letta/local_llm/chat_completion_proxy.py +279 -0
- letta/local_llm/constants.py +31 -0
- letta/local_llm/function_parser.py +68 -0
- letta/local_llm/grammars/__init__.py +0 -0
- letta/local_llm/grammars/gbnf_grammar_generator.py +1324 -0
- letta/local_llm/grammars/json.gbnf +26 -0
- letta/local_llm/grammars/json_func_calls_with_inner_thoughts.gbnf +32 -0
- letta/local_llm/groq/api.py +97 -0
- letta/local_llm/json_parser.py +202 -0
- letta/local_llm/koboldcpp/api.py +62 -0
- letta/local_llm/koboldcpp/settings.py +23 -0
- letta/local_llm/llamacpp/api.py +58 -0
- letta/local_llm/llamacpp/settings.py +22 -0
- letta/local_llm/llm_chat_completion_wrappers/__init__.py +0 -0
- letta/local_llm/llm_chat_completion_wrappers/airoboros.py +452 -0
- letta/local_llm/llm_chat_completion_wrappers/chatml.py +470 -0
- letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py +387 -0
- letta/local_llm/llm_chat_completion_wrappers/dolphin.py +246 -0
- letta/local_llm/llm_chat_completion_wrappers/llama3.py +345 -0
- letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py +156 -0
- letta/local_llm/llm_chat_completion_wrappers/wrapper_base.py +11 -0
- letta/local_llm/llm_chat_completion_wrappers/zephyr.py +345 -0
- letta/local_llm/lmstudio/api.py +100 -0
- letta/local_llm/lmstudio/settings.py +29 -0
- letta/local_llm/ollama/api.py +88 -0
- letta/local_llm/ollama/settings.py +32 -0
- letta/local_llm/settings/__init__.py +0 -0
- letta/local_llm/settings/deterministic_mirostat.py +45 -0
- letta/local_llm/settings/settings.py +72 -0
- letta/local_llm/settings/simple.py +28 -0
- letta/local_llm/utils.py +265 -0
- letta/local_llm/vllm/api.py +63 -0
- letta/local_llm/webui/api.py +60 -0
- letta/local_llm/webui/legacy_api.py +58 -0
- letta/local_llm/webui/legacy_settings.py +23 -0
- letta/local_llm/webui/settings.py +24 -0
- letta/log.py +76 -0
- letta/main.py +437 -0
- letta/memory.py +440 -0
- letta/metadata.py +884 -0
- letta/openai_backcompat/__init__.py +0 -0
- letta/openai_backcompat/openai_object.py +437 -0
- letta/persistence_manager.py +148 -0
- letta/personas/__init__.py +0 -0
- letta/personas/examples/anna_pa.txt +13 -0
- letta/personas/examples/google_search_persona.txt +15 -0
- letta/personas/examples/memgpt_doc.txt +6 -0
- letta/personas/examples/memgpt_starter.txt +4 -0
- letta/personas/examples/sam.txt +14 -0
- letta/personas/examples/sam_pov.txt +14 -0
- letta/personas/examples/sam_simple_pov_gpt35.txt +13 -0
- letta/personas/examples/sqldb/test.db +0 -0
- letta/prompts/__init__.py +0 -0
- letta/prompts/gpt_summarize.py +14 -0
- letta/prompts/gpt_system.py +26 -0
- letta/prompts/system/memgpt_base.txt +49 -0
- letta/prompts/system/memgpt_chat.txt +58 -0
- letta/prompts/system/memgpt_chat_compressed.txt +13 -0
- letta/prompts/system/memgpt_chat_fstring.txt +51 -0
- letta/prompts/system/memgpt_doc.txt +50 -0
- letta/prompts/system/memgpt_gpt35_extralong.txt +53 -0
- letta/prompts/system/memgpt_intuitive_knowledge.txt +31 -0
- letta/prompts/system/memgpt_modified_chat.txt +23 -0
- letta/pytest.ini +0 -0
- letta/schemas/agent.py +117 -0
- letta/schemas/api_key.py +21 -0
- letta/schemas/block.py +135 -0
- letta/schemas/document.py +21 -0
- letta/schemas/embedding_config.py +54 -0
- letta/schemas/enums.py +35 -0
- letta/schemas/job.py +38 -0
- letta/schemas/letta_base.py +80 -0
- letta/schemas/letta_message.py +175 -0
- letta/schemas/letta_request.py +23 -0
- letta/schemas/letta_response.py +28 -0
- letta/schemas/llm_config.py +54 -0
- letta/schemas/memory.py +224 -0
- letta/schemas/message.py +727 -0
- letta/schemas/openai/chat_completion_request.py +123 -0
- letta/schemas/openai/chat_completion_response.py +136 -0
- letta/schemas/openai/chat_completions.py +123 -0
- letta/schemas/openai/embedding_response.py +11 -0
- letta/schemas/openai/openai.py +157 -0
- letta/schemas/organization.py +20 -0
- letta/schemas/passage.py +80 -0
- letta/schemas/source.py +62 -0
- letta/schemas/tool.py +143 -0
- letta/schemas/usage.py +18 -0
- letta/schemas/user.py +33 -0
- letta/server/__init__.py +0 -0
- letta/server/constants.py +6 -0
- letta/server/rest_api/__init__.py +0 -0
- letta/server/rest_api/admin/__init__.py +0 -0
- letta/server/rest_api/admin/agents.py +21 -0
- letta/server/rest_api/admin/tools.py +83 -0
- letta/server/rest_api/admin/users.py +98 -0
- letta/server/rest_api/app.py +193 -0
- letta/server/rest_api/auth/__init__.py +0 -0
- letta/server/rest_api/auth/index.py +43 -0
- letta/server/rest_api/auth_token.py +22 -0
- letta/server/rest_api/interface.py +726 -0
- letta/server/rest_api/routers/__init__.py +0 -0
- letta/server/rest_api/routers/openai/__init__.py +0 -0
- letta/server/rest_api/routers/openai/assistants/__init__.py +0 -0
- letta/server/rest_api/routers/openai/assistants/assistants.py +115 -0
- letta/server/rest_api/routers/openai/assistants/schemas.py +121 -0
- letta/server/rest_api/routers/openai/assistants/threads.py +336 -0
- letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +131 -0
- letta/server/rest_api/routers/v1/__init__.py +15 -0
- letta/server/rest_api/routers/v1/agents.py +543 -0
- letta/server/rest_api/routers/v1/blocks.py +73 -0
- letta/server/rest_api/routers/v1/jobs.py +46 -0
- letta/server/rest_api/routers/v1/llms.py +28 -0
- letta/server/rest_api/routers/v1/organizations.py +61 -0
- letta/server/rest_api/routers/v1/sources.py +199 -0
- letta/server/rest_api/routers/v1/tools.py +103 -0
- letta/server/rest_api/routers/v1/users.py +109 -0
- letta/server/rest_api/static_files.py +74 -0
- letta/server/rest_api/utils.py +69 -0
- letta/server/server.py +1995 -0
- letta/server/startup.sh +8 -0
- letta/server/static_files/assets/index-0cbf7ad5.js +274 -0
- letta/server/static_files/assets/index-156816da.css +1 -0
- letta/server/static_files/assets/index-486e3228.js +274 -0
- letta/server/static_files/favicon.ico +0 -0
- letta/server/static_files/index.html +39 -0
- letta/server/static_files/memgpt_logo_transparent.png +0 -0
- letta/server/utils.py +46 -0
- letta/server/ws_api/__init__.py +0 -0
- letta/server/ws_api/example_client.py +104 -0
- letta/server/ws_api/interface.py +108 -0
- letta/server/ws_api/protocol.py +100 -0
- letta/server/ws_api/server.py +145 -0
- letta/settings.py +165 -0
- letta/streaming_interface.py +396 -0
- letta/system.py +207 -0
- letta/utils.py +1065 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/LICENSE +190 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/METADATA +98 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/RECORD +189 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/WHEEL +4 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/entry_points.txt +3 -0
letta/utils.py
ADDED
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import difflib
|
|
3
|
+
import hashlib
|
|
4
|
+
import inspect
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import pickle
|
|
9
|
+
import platform
|
|
10
|
+
import random
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import uuid
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from datetime import datetime, timedelta, timezone
|
|
17
|
+
from functools import wraps
|
|
18
|
+
from typing import List, Union, _GenericAlias, get_type_hints
|
|
19
|
+
from urllib.parse import urljoin, urlparse
|
|
20
|
+
|
|
21
|
+
import demjson3 as demjson
|
|
22
|
+
import pytz
|
|
23
|
+
import tiktoken
|
|
24
|
+
|
|
25
|
+
import letta
|
|
26
|
+
from letta.constants import (
|
|
27
|
+
CLI_WARNING_PREFIX,
|
|
28
|
+
CORE_MEMORY_HUMAN_CHAR_LIMIT,
|
|
29
|
+
CORE_MEMORY_PERSONA_CHAR_LIMIT,
|
|
30
|
+
FUNCTION_RETURN_CHAR_LIMIT,
|
|
31
|
+
LETTA_DIR,
|
|
32
|
+
TOOL_CALL_ID_MAX_LEN,
|
|
33
|
+
)
|
|
34
|
+
from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
|
|
35
|
+
|
|
36
|
+
DEBUG = False
|
|
37
|
+
if "LOG_LEVEL" in os.environ:
|
|
38
|
+
if os.environ["LOG_LEVEL"] == "DEBUG":
|
|
39
|
+
DEBUG = True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
ADJECTIVE_BANK = [
|
|
43
|
+
"beautiful",
|
|
44
|
+
"gentle",
|
|
45
|
+
"angry",
|
|
46
|
+
"vivacious",
|
|
47
|
+
"grumpy",
|
|
48
|
+
"luxurious",
|
|
49
|
+
"fierce",
|
|
50
|
+
"delicate",
|
|
51
|
+
"fluffy",
|
|
52
|
+
"radiant",
|
|
53
|
+
"elated",
|
|
54
|
+
"magnificent",
|
|
55
|
+
"sassy",
|
|
56
|
+
"ecstatic",
|
|
57
|
+
"lustrous",
|
|
58
|
+
"gleaming",
|
|
59
|
+
"sorrowful",
|
|
60
|
+
"majestic",
|
|
61
|
+
"proud",
|
|
62
|
+
"dynamic",
|
|
63
|
+
"energetic",
|
|
64
|
+
"mysterious",
|
|
65
|
+
"loyal",
|
|
66
|
+
"brave",
|
|
67
|
+
"decisive",
|
|
68
|
+
"frosty",
|
|
69
|
+
"cheerful",
|
|
70
|
+
"adorable",
|
|
71
|
+
"melancholy",
|
|
72
|
+
"vibrant",
|
|
73
|
+
"elegant",
|
|
74
|
+
"gracious",
|
|
75
|
+
"inquisitive",
|
|
76
|
+
"opulent",
|
|
77
|
+
"peaceful",
|
|
78
|
+
"rebellious",
|
|
79
|
+
"scintillating",
|
|
80
|
+
"dazzling",
|
|
81
|
+
"whimsical",
|
|
82
|
+
"impeccable",
|
|
83
|
+
"meticulous",
|
|
84
|
+
"resilient",
|
|
85
|
+
"charming",
|
|
86
|
+
"vivacious",
|
|
87
|
+
"creative",
|
|
88
|
+
"intuitive",
|
|
89
|
+
"compassionate",
|
|
90
|
+
"innovative",
|
|
91
|
+
"enthusiastic",
|
|
92
|
+
"tremendous",
|
|
93
|
+
"effervescent",
|
|
94
|
+
"tenacious",
|
|
95
|
+
"fearless",
|
|
96
|
+
"sophisticated",
|
|
97
|
+
"witty",
|
|
98
|
+
"optimistic",
|
|
99
|
+
"exquisite",
|
|
100
|
+
"sincere",
|
|
101
|
+
"generous",
|
|
102
|
+
"kindhearted",
|
|
103
|
+
"serene",
|
|
104
|
+
"amiable",
|
|
105
|
+
"adventurous",
|
|
106
|
+
"bountiful",
|
|
107
|
+
"courageous",
|
|
108
|
+
"diligent",
|
|
109
|
+
"exotic",
|
|
110
|
+
"grateful",
|
|
111
|
+
"harmonious",
|
|
112
|
+
"imaginative",
|
|
113
|
+
"jubilant",
|
|
114
|
+
"keen",
|
|
115
|
+
"luminous",
|
|
116
|
+
"nurturing",
|
|
117
|
+
"outgoing",
|
|
118
|
+
"passionate",
|
|
119
|
+
"quaint",
|
|
120
|
+
"resourceful",
|
|
121
|
+
"sturdy",
|
|
122
|
+
"tactful",
|
|
123
|
+
"unassuming",
|
|
124
|
+
"versatile",
|
|
125
|
+
"wondrous",
|
|
126
|
+
"youthful",
|
|
127
|
+
"zealous",
|
|
128
|
+
"ardent",
|
|
129
|
+
"benevolent",
|
|
130
|
+
"capricious",
|
|
131
|
+
"dedicated",
|
|
132
|
+
"empathetic",
|
|
133
|
+
"fabulous",
|
|
134
|
+
"gregarious",
|
|
135
|
+
"humble",
|
|
136
|
+
"intriguing",
|
|
137
|
+
"jovial",
|
|
138
|
+
"kind",
|
|
139
|
+
"lovable",
|
|
140
|
+
"mindful",
|
|
141
|
+
"noble",
|
|
142
|
+
"original",
|
|
143
|
+
"pleasant",
|
|
144
|
+
"quixotic",
|
|
145
|
+
"reliable",
|
|
146
|
+
"spirited",
|
|
147
|
+
"tranquil",
|
|
148
|
+
"unique",
|
|
149
|
+
"venerable",
|
|
150
|
+
"warmhearted",
|
|
151
|
+
"xenodochial",
|
|
152
|
+
"yearning",
|
|
153
|
+
"zesty",
|
|
154
|
+
"amusing",
|
|
155
|
+
"blissful",
|
|
156
|
+
"calm",
|
|
157
|
+
"daring",
|
|
158
|
+
"enthusiastic",
|
|
159
|
+
"faithful",
|
|
160
|
+
"graceful",
|
|
161
|
+
"honest",
|
|
162
|
+
"incredible",
|
|
163
|
+
"joyful",
|
|
164
|
+
"kind",
|
|
165
|
+
"lovely",
|
|
166
|
+
"merry",
|
|
167
|
+
"noble",
|
|
168
|
+
"optimistic",
|
|
169
|
+
"peaceful",
|
|
170
|
+
"quirky",
|
|
171
|
+
"respectful",
|
|
172
|
+
"sweet",
|
|
173
|
+
"trustworthy",
|
|
174
|
+
"understanding",
|
|
175
|
+
"vibrant",
|
|
176
|
+
"witty",
|
|
177
|
+
"xenial",
|
|
178
|
+
"youthful",
|
|
179
|
+
"zealous",
|
|
180
|
+
"ambitious",
|
|
181
|
+
"brilliant",
|
|
182
|
+
"careful",
|
|
183
|
+
"devoted",
|
|
184
|
+
"energetic",
|
|
185
|
+
"friendly",
|
|
186
|
+
"glorious",
|
|
187
|
+
"humorous",
|
|
188
|
+
"intelligent",
|
|
189
|
+
"jovial",
|
|
190
|
+
"knowledgeable",
|
|
191
|
+
"loyal",
|
|
192
|
+
"modest",
|
|
193
|
+
"nice",
|
|
194
|
+
"obedient",
|
|
195
|
+
"patient",
|
|
196
|
+
"quiet",
|
|
197
|
+
"resilient",
|
|
198
|
+
"selfless",
|
|
199
|
+
"tolerant",
|
|
200
|
+
"unique",
|
|
201
|
+
"versatile",
|
|
202
|
+
"warm",
|
|
203
|
+
"xerothermic",
|
|
204
|
+
"yielding",
|
|
205
|
+
"zestful",
|
|
206
|
+
"amazing",
|
|
207
|
+
"bold",
|
|
208
|
+
"charming",
|
|
209
|
+
"determined",
|
|
210
|
+
"exciting",
|
|
211
|
+
"funny",
|
|
212
|
+
"happy",
|
|
213
|
+
"imaginative",
|
|
214
|
+
"jolly",
|
|
215
|
+
"keen",
|
|
216
|
+
"loving",
|
|
217
|
+
"magnificent",
|
|
218
|
+
"nifty",
|
|
219
|
+
"outstanding",
|
|
220
|
+
"polite",
|
|
221
|
+
"quick",
|
|
222
|
+
"reliable",
|
|
223
|
+
"sincere",
|
|
224
|
+
"thoughtful",
|
|
225
|
+
"unusual",
|
|
226
|
+
"valuable",
|
|
227
|
+
"wonderful",
|
|
228
|
+
"xenodochial",
|
|
229
|
+
"zealful",
|
|
230
|
+
"admirable",
|
|
231
|
+
"bright",
|
|
232
|
+
"clever",
|
|
233
|
+
"dedicated",
|
|
234
|
+
"extraordinary",
|
|
235
|
+
"generous",
|
|
236
|
+
"hardworking",
|
|
237
|
+
"inspiring",
|
|
238
|
+
"jubilant",
|
|
239
|
+
"kindhearted",
|
|
240
|
+
"lively",
|
|
241
|
+
"miraculous",
|
|
242
|
+
"neat",
|
|
243
|
+
"openminded",
|
|
244
|
+
"passionate",
|
|
245
|
+
"remarkable",
|
|
246
|
+
"stunning",
|
|
247
|
+
"truthful",
|
|
248
|
+
"upbeat",
|
|
249
|
+
"vivacious",
|
|
250
|
+
"welcoming",
|
|
251
|
+
"yare",
|
|
252
|
+
"zealous",
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
NOUN_BANK = [
|
|
256
|
+
"lizard",
|
|
257
|
+
"firefighter",
|
|
258
|
+
"banana",
|
|
259
|
+
"castle",
|
|
260
|
+
"dolphin",
|
|
261
|
+
"elephant",
|
|
262
|
+
"forest",
|
|
263
|
+
"giraffe",
|
|
264
|
+
"harbor",
|
|
265
|
+
"iceberg",
|
|
266
|
+
"jewelry",
|
|
267
|
+
"kangaroo",
|
|
268
|
+
"library",
|
|
269
|
+
"mountain",
|
|
270
|
+
"notebook",
|
|
271
|
+
"orchard",
|
|
272
|
+
"penguin",
|
|
273
|
+
"quilt",
|
|
274
|
+
"rainbow",
|
|
275
|
+
"squirrel",
|
|
276
|
+
"teapot",
|
|
277
|
+
"umbrella",
|
|
278
|
+
"volcano",
|
|
279
|
+
"waterfall",
|
|
280
|
+
"xylophone",
|
|
281
|
+
"yacht",
|
|
282
|
+
"zebra",
|
|
283
|
+
"apple",
|
|
284
|
+
"butterfly",
|
|
285
|
+
"caterpillar",
|
|
286
|
+
"dragonfly",
|
|
287
|
+
"elephant",
|
|
288
|
+
"flamingo",
|
|
289
|
+
"gorilla",
|
|
290
|
+
"hippopotamus",
|
|
291
|
+
"iguana",
|
|
292
|
+
"jellyfish",
|
|
293
|
+
"koala",
|
|
294
|
+
"lemur",
|
|
295
|
+
"mongoose",
|
|
296
|
+
"nighthawk",
|
|
297
|
+
"octopus",
|
|
298
|
+
"panda",
|
|
299
|
+
"quokka",
|
|
300
|
+
"rhinoceros",
|
|
301
|
+
"salamander",
|
|
302
|
+
"tortoise",
|
|
303
|
+
"unicorn",
|
|
304
|
+
"vulture",
|
|
305
|
+
"walrus",
|
|
306
|
+
"xenopus",
|
|
307
|
+
"yak",
|
|
308
|
+
"zebu",
|
|
309
|
+
"asteroid",
|
|
310
|
+
"balloon",
|
|
311
|
+
"compass",
|
|
312
|
+
"dinosaur",
|
|
313
|
+
"eagle",
|
|
314
|
+
"firefly",
|
|
315
|
+
"galaxy",
|
|
316
|
+
"hedgehog",
|
|
317
|
+
"island",
|
|
318
|
+
"jaguar",
|
|
319
|
+
"kettle",
|
|
320
|
+
"lion",
|
|
321
|
+
"mammoth",
|
|
322
|
+
"nucleus",
|
|
323
|
+
"owl",
|
|
324
|
+
"pumpkin",
|
|
325
|
+
"quasar",
|
|
326
|
+
"reindeer",
|
|
327
|
+
"snail",
|
|
328
|
+
"tiger",
|
|
329
|
+
"universe",
|
|
330
|
+
"vampire",
|
|
331
|
+
"wombat",
|
|
332
|
+
"xerus",
|
|
333
|
+
"yellowhammer",
|
|
334
|
+
"zeppelin",
|
|
335
|
+
"alligator",
|
|
336
|
+
"buffalo",
|
|
337
|
+
"cactus",
|
|
338
|
+
"donkey",
|
|
339
|
+
"emerald",
|
|
340
|
+
"falcon",
|
|
341
|
+
"gazelle",
|
|
342
|
+
"hamster",
|
|
343
|
+
"icicle",
|
|
344
|
+
"jackal",
|
|
345
|
+
"kitten",
|
|
346
|
+
"leopard",
|
|
347
|
+
"mushroom",
|
|
348
|
+
"narwhal",
|
|
349
|
+
"opossum",
|
|
350
|
+
"peacock",
|
|
351
|
+
"quail",
|
|
352
|
+
"rabbit",
|
|
353
|
+
"scorpion",
|
|
354
|
+
"toucan",
|
|
355
|
+
"urchin",
|
|
356
|
+
"viper",
|
|
357
|
+
"wolf",
|
|
358
|
+
"xray",
|
|
359
|
+
"yucca",
|
|
360
|
+
"zebu",
|
|
361
|
+
"acorn",
|
|
362
|
+
"biscuit",
|
|
363
|
+
"cupcake",
|
|
364
|
+
"daisy",
|
|
365
|
+
"eyeglasses",
|
|
366
|
+
"frisbee",
|
|
367
|
+
"goblin",
|
|
368
|
+
"hamburger",
|
|
369
|
+
"icicle",
|
|
370
|
+
"jackfruit",
|
|
371
|
+
"kaleidoscope",
|
|
372
|
+
"lighthouse",
|
|
373
|
+
"marshmallow",
|
|
374
|
+
"nectarine",
|
|
375
|
+
"obelisk",
|
|
376
|
+
"pancake",
|
|
377
|
+
"quicksand",
|
|
378
|
+
"raspberry",
|
|
379
|
+
"spinach",
|
|
380
|
+
"truffle",
|
|
381
|
+
"umbrella",
|
|
382
|
+
"volleyball",
|
|
383
|
+
"walnut",
|
|
384
|
+
"xylophonist",
|
|
385
|
+
"yogurt",
|
|
386
|
+
"zucchini",
|
|
387
|
+
"asterisk",
|
|
388
|
+
"blackberry",
|
|
389
|
+
"chimpanzee",
|
|
390
|
+
"dumpling",
|
|
391
|
+
"espresso",
|
|
392
|
+
"fireplace",
|
|
393
|
+
"gnome",
|
|
394
|
+
"hedgehog",
|
|
395
|
+
"illustration",
|
|
396
|
+
"jackhammer",
|
|
397
|
+
"kumquat",
|
|
398
|
+
"lemongrass",
|
|
399
|
+
"mandolin",
|
|
400
|
+
"nugget",
|
|
401
|
+
"ostrich",
|
|
402
|
+
"parakeet",
|
|
403
|
+
"quiche",
|
|
404
|
+
"racquet",
|
|
405
|
+
"seashell",
|
|
406
|
+
"tadpole",
|
|
407
|
+
"unicorn",
|
|
408
|
+
"vaccination",
|
|
409
|
+
"wolverine",
|
|
410
|
+
"xenophobia",
|
|
411
|
+
"yam",
|
|
412
|
+
"zeppelin",
|
|
413
|
+
"accordion",
|
|
414
|
+
"broccoli",
|
|
415
|
+
"carousel",
|
|
416
|
+
"daffodil",
|
|
417
|
+
"eggplant",
|
|
418
|
+
"flamingo",
|
|
419
|
+
"grapefruit",
|
|
420
|
+
"harpsichord",
|
|
421
|
+
"impression",
|
|
422
|
+
"jackrabbit",
|
|
423
|
+
"kitten",
|
|
424
|
+
"llama",
|
|
425
|
+
"mandarin",
|
|
426
|
+
"nachos",
|
|
427
|
+
"obelisk",
|
|
428
|
+
"papaya",
|
|
429
|
+
"quokka",
|
|
430
|
+
"rooster",
|
|
431
|
+
"sunflower",
|
|
432
|
+
"turnip",
|
|
433
|
+
"ukulele",
|
|
434
|
+
"viper",
|
|
435
|
+
"waffle",
|
|
436
|
+
"xylograph",
|
|
437
|
+
"yeti",
|
|
438
|
+
"zephyr",
|
|
439
|
+
"abacus",
|
|
440
|
+
"blueberry",
|
|
441
|
+
"crocodile",
|
|
442
|
+
"dandelion",
|
|
443
|
+
"echidna",
|
|
444
|
+
"fig",
|
|
445
|
+
"giraffe",
|
|
446
|
+
"hamster",
|
|
447
|
+
"iguana",
|
|
448
|
+
"jackal",
|
|
449
|
+
"kiwi",
|
|
450
|
+
"lobster",
|
|
451
|
+
"marmot",
|
|
452
|
+
"noodle",
|
|
453
|
+
"octopus",
|
|
454
|
+
"platypus",
|
|
455
|
+
"quail",
|
|
456
|
+
"raccoon",
|
|
457
|
+
"starfish",
|
|
458
|
+
"tulip",
|
|
459
|
+
"urchin",
|
|
460
|
+
"vampire",
|
|
461
|
+
"walrus",
|
|
462
|
+
"xylophone",
|
|
463
|
+
"yak",
|
|
464
|
+
"zebra",
|
|
465
|
+
]
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def deduplicate(target_list: list) -> list:
|
|
469
|
+
seen = set()
|
|
470
|
+
dedup_list = []
|
|
471
|
+
for i in target_list:
|
|
472
|
+
if i not in seen:
|
|
473
|
+
seen.add(i)
|
|
474
|
+
dedup_list.append(i)
|
|
475
|
+
|
|
476
|
+
return dedup_list
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def smart_urljoin(base_url: str, relative_url: str) -> str:
|
|
480
|
+
"""urljoin is stupid and wants a trailing / at the end of the endpoint address, or it will chop the suffix off"""
|
|
481
|
+
if not base_url.endswith("/"):
|
|
482
|
+
base_url += "/"
|
|
483
|
+
return urljoin(base_url, relative_url)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def is_utc_datetime(dt: datetime) -> bool:
|
|
487
|
+
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) == timedelta(0)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def get_tool_call_id() -> str:
|
|
491
|
+
return str(uuid.uuid4())[:TOOL_CALL_ID_MAX_LEN]
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def assistant_function_to_tool(assistant_message: dict) -> dict:
|
|
495
|
+
assert "function_call" in assistant_message
|
|
496
|
+
new_msg = copy.deepcopy(assistant_message)
|
|
497
|
+
function_call = new_msg.pop("function_call")
|
|
498
|
+
new_msg["tool_calls"] = [
|
|
499
|
+
{
|
|
500
|
+
"id": get_tool_call_id(),
|
|
501
|
+
"type": "function",
|
|
502
|
+
"function": function_call,
|
|
503
|
+
}
|
|
504
|
+
]
|
|
505
|
+
return new_msg
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def is_optional_type(hint):
|
|
509
|
+
"""Check if the type hint is an Optional type."""
|
|
510
|
+
if isinstance(hint, _GenericAlias):
|
|
511
|
+
return hint.__origin__ is Union and type(None) in hint.__args__
|
|
512
|
+
return False
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def enforce_types(func):
|
|
516
|
+
@wraps(func)
|
|
517
|
+
def wrapper(*args, **kwargs):
|
|
518
|
+
# Get type hints, excluding the return type hint
|
|
519
|
+
hints = {k: v for k, v in get_type_hints(func).items() if k != "return"}
|
|
520
|
+
|
|
521
|
+
# Get the function's argument names
|
|
522
|
+
arg_names = inspect.getfullargspec(func).args
|
|
523
|
+
|
|
524
|
+
# Pair each argument with its corresponding type hint
|
|
525
|
+
args_with_hints = dict(zip(arg_names[1:], args[1:])) # Skipping 'self'
|
|
526
|
+
|
|
527
|
+
# Check types of arguments
|
|
528
|
+
for arg_name, arg_value in args_with_hints.items():
|
|
529
|
+
hint = hints.get(arg_name)
|
|
530
|
+
if hint and not isinstance(arg_value, hint) and not (is_optional_type(hint) and arg_value is None):
|
|
531
|
+
raise ValueError(f"Argument {arg_name} does not match type {hint}")
|
|
532
|
+
|
|
533
|
+
# Check types of keyword arguments
|
|
534
|
+
for arg_name, arg_value in kwargs.items():
|
|
535
|
+
hint = hints.get(arg_name)
|
|
536
|
+
if hint and not isinstance(arg_value, hint) and not (is_optional_type(hint) and arg_value is None):
|
|
537
|
+
raise ValueError(f"Argument {arg_name} does not match type {hint}")
|
|
538
|
+
|
|
539
|
+
return func(*args, **kwargs)
|
|
540
|
+
|
|
541
|
+
return wrapper
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def annotate_message_json_list_with_tool_calls(messages: List[dict], allow_tool_roles: bool = False):
|
|
545
|
+
"""Add in missing tool_call_id fields to a list of messages using function call style
|
|
546
|
+
|
|
547
|
+
Walk through the list forwards:
|
|
548
|
+
- If we encounter an assistant message that calls a function ("function_call") but doesn't have a "tool_call_id" field
|
|
549
|
+
- Generate the tool_call_id
|
|
550
|
+
- Then check if the subsequent message is a role == "function" message
|
|
551
|
+
- If so, then att
|
|
552
|
+
"""
|
|
553
|
+
tool_call_index = None
|
|
554
|
+
tool_call_id = None
|
|
555
|
+
updated_messages = []
|
|
556
|
+
|
|
557
|
+
for i, message in enumerate(messages):
|
|
558
|
+
if "role" not in message:
|
|
559
|
+
raise ValueError(f"message missing 'role' field:\n{message}")
|
|
560
|
+
|
|
561
|
+
# If we find a function call w/o a tool call ID annotation, annotate it
|
|
562
|
+
if message["role"] == "assistant" and "function_call" in message:
|
|
563
|
+
if "tool_call_id" in message and message["tool_call_id"] is not None:
|
|
564
|
+
printd(f"Message already has tool_call_id")
|
|
565
|
+
tool_call_id = message["tool_call_id"]
|
|
566
|
+
else:
|
|
567
|
+
tool_call_id = str(uuid.uuid4())
|
|
568
|
+
message["tool_call_id"] = tool_call_id
|
|
569
|
+
tool_call_index = i
|
|
570
|
+
|
|
571
|
+
# After annotating the call, we expect to find a follow-up response (also unannotated)
|
|
572
|
+
elif message["role"] == "function":
|
|
573
|
+
# We should have a new tool call id in the buffer
|
|
574
|
+
if tool_call_id is None:
|
|
575
|
+
# raise ValueError(
|
|
576
|
+
print(
|
|
577
|
+
f"Got a function call role, but did not have a saved tool_call_id ready to use (i={i}, total={len(messages)}):\n{messages[:i]}\n{message}"
|
|
578
|
+
)
|
|
579
|
+
# allow a soft fail in this case
|
|
580
|
+
message["tool_call_id"] = str(uuid.uuid4())
|
|
581
|
+
elif "tool_call_id" in message:
|
|
582
|
+
raise ValueError(
|
|
583
|
+
f"Got a function call role, but it already had a saved tool_call_id (i={i}, total={len(messages)}):\n{messages[:i]}\n{message}"
|
|
584
|
+
)
|
|
585
|
+
elif i != tool_call_index + 1:
|
|
586
|
+
raise ValueError(
|
|
587
|
+
f"Got a function call role, saved tool_call_id came earlier than i-1 (i={i}, total={len(messages)}):\n{messages[:i]}\n{message}"
|
|
588
|
+
)
|
|
589
|
+
else:
|
|
590
|
+
message["tool_call_id"] = tool_call_id
|
|
591
|
+
tool_call_id = None # wipe the buffer
|
|
592
|
+
|
|
593
|
+
elif message["role"] == "assistant" and "tool_calls" in message and message["tool_calls"] is not None:
|
|
594
|
+
if not allow_tool_roles:
|
|
595
|
+
raise NotImplementedError(
|
|
596
|
+
f"tool_call_id annotation is meant for deprecated functions style, but got role 'assistant' with 'tool_calls' in message (i={i}, total={len(messages)}):\n{messages[:i]}\n{message}"
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
if len(message["tool_calls"]) != 1:
|
|
600
|
+
raise NotImplementedError(
|
|
601
|
+
f"Got unexpected format for tool_calls inside assistant message (i={i}, total={len(messages)}):\n{messages[:i]}\n{message}"
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
assistant_tool_call = message["tool_calls"][0]
|
|
605
|
+
if "id" in assistant_tool_call and assistant_tool_call["id"] is not None:
|
|
606
|
+
printd(f"Message already has id (tool_call_id)")
|
|
607
|
+
tool_call_id = assistant_tool_call["id"]
|
|
608
|
+
else:
|
|
609
|
+
tool_call_id = str(uuid.uuid4())
|
|
610
|
+
message["tool_calls"][0]["id"] = tool_call_id
|
|
611
|
+
# also just put it at the top level for ease-of-access
|
|
612
|
+
# message["tool_call_id"] = tool_call_id
|
|
613
|
+
tool_call_index = i
|
|
614
|
+
|
|
615
|
+
elif message["role"] == "tool":
|
|
616
|
+
if not allow_tool_roles:
|
|
617
|
+
raise NotImplementedError(
|
|
618
|
+
f"tool_call_id annotation is meant for deprecated functions style, but got role 'tool' in message (i={i}, total={len(messages)}):\n{messages[:i]}\n{message}"
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# if "tool_call_id" not in message or message["tool_call_id"] is None:
|
|
622
|
+
# raise ValueError(f"Got a tool call role, but there's no tool_call_id:\n{messages[:i]}\n{message}")
|
|
623
|
+
|
|
624
|
+
# We should have a new tool call id in the buffer
|
|
625
|
+
if tool_call_id is None:
|
|
626
|
+
# raise ValueError(
|
|
627
|
+
print(
|
|
628
|
+
f"Got a tool call role, but did not have a saved tool_call_id ready to use (i={i}, total={len(messages)}):\n{messages[:i]}\n{message}"
|
|
629
|
+
)
|
|
630
|
+
# allow a soft fail in this case
|
|
631
|
+
message["tool_call_id"] = str(uuid.uuid4())
|
|
632
|
+
elif "tool_call_id" in message and message["tool_call_id"] is not None:
|
|
633
|
+
if tool_call_id is not None and tool_call_id != message["tool_call_id"]:
|
|
634
|
+
# just wipe it
|
|
635
|
+
# raise ValueError(
|
|
636
|
+
# f"Got a tool call role, but it already had a saved tool_call_id (i={i}, total={len(messages)}):\n{messages[:i]}\n{message}"
|
|
637
|
+
# )
|
|
638
|
+
message["tool_call_id"] = tool_call_id
|
|
639
|
+
tool_call_id = None # wipe the buffer
|
|
640
|
+
else:
|
|
641
|
+
tool_call_id = None
|
|
642
|
+
elif i != tool_call_index + 1:
|
|
643
|
+
raise ValueError(
|
|
644
|
+
f"Got a tool call role, saved tool_call_id came earlier than i-1 (i={i}, total={len(messages)}):\n{messages[:i]}\n{message}"
|
|
645
|
+
)
|
|
646
|
+
else:
|
|
647
|
+
message["tool_call_id"] = tool_call_id
|
|
648
|
+
tool_call_id = None # wipe the buffer
|
|
649
|
+
|
|
650
|
+
else:
|
|
651
|
+
# eg role == 'user', nothing to do here
|
|
652
|
+
pass
|
|
653
|
+
|
|
654
|
+
updated_messages.append(copy.deepcopy(message))
|
|
655
|
+
|
|
656
|
+
return updated_messages
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def version_less_than(version_a: str, version_b: str) -> bool:
|
|
660
|
+
"""Compare versions to check if version_a is less than version_b."""
|
|
661
|
+
# Regular expression to match version strings of the format int.int.int
|
|
662
|
+
version_pattern = re.compile(r"^\d+\.\d+\.\d+$")
|
|
663
|
+
|
|
664
|
+
# Assert that version strings match the required format
|
|
665
|
+
if not version_pattern.match(version_a) or not version_pattern.match(version_b):
|
|
666
|
+
raise ValueError("Version strings must be in the format 'int.int.int'")
|
|
667
|
+
|
|
668
|
+
# Split the version strings into parts
|
|
669
|
+
parts_a = [int(part) for part in version_a.split(".")]
|
|
670
|
+
parts_b = [int(part) for part in version_b.split(".")]
|
|
671
|
+
|
|
672
|
+
# Compare version parts
|
|
673
|
+
return parts_a < parts_b
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def create_random_username() -> str:
|
|
677
|
+
"""Generate a random username by combining an adjective and a noun."""
|
|
678
|
+
adjective = random.choice(ADJECTIVE_BANK).capitalize()
|
|
679
|
+
noun = random.choice(NOUN_BANK).capitalize()
|
|
680
|
+
return adjective + noun
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def verify_first_message_correctness(
|
|
684
|
+
response: ChatCompletionResponse, require_send_message: bool = True, require_monologue: bool = False
|
|
685
|
+
) -> bool:
|
|
686
|
+
"""Can be used to enforce that the first message always uses send_message"""
|
|
687
|
+
response_message = response.choices[0].message
|
|
688
|
+
|
|
689
|
+
# First message should be a call to send_message with a non-empty content
|
|
690
|
+
if (hasattr(response_message, "function_call") and response_message.function_call is not None) and (
|
|
691
|
+
hasattr(response_message, "tool_calls") and response_message.tool_calls is not None
|
|
692
|
+
):
|
|
693
|
+
printd(f"First message includes both function call AND tool call: {response_message}")
|
|
694
|
+
return False
|
|
695
|
+
elif hasattr(response_message, "function_call") and response_message.function_call is not None:
|
|
696
|
+
function_call = response_message.function_call
|
|
697
|
+
elif hasattr(response_message, "tool_calls") and response_message.tool_calls is not None:
|
|
698
|
+
function_call = response_message.tool_calls[0].function
|
|
699
|
+
else:
|
|
700
|
+
printd(f"First message didn't include function call: {response_message}")
|
|
701
|
+
return False
|
|
702
|
+
|
|
703
|
+
function_name = function_call.name if function_call is not None else ""
|
|
704
|
+
if require_send_message and function_name != "send_message" and function_name != "archival_memory_search":
|
|
705
|
+
printd(f"First message function call wasn't send_message or archival_memory_search: {response_message}")
|
|
706
|
+
return False
|
|
707
|
+
|
|
708
|
+
if require_monologue and (not response_message.content or response_message.content is None or response_message.content == ""):
|
|
709
|
+
printd(f"First message missing internal monologue: {response_message}")
|
|
710
|
+
return False
|
|
711
|
+
|
|
712
|
+
if response_message.content:
|
|
713
|
+
### Extras
|
|
714
|
+
monologue = response_message.content
|
|
715
|
+
|
|
716
|
+
def contains_special_characters(s):
|
|
717
|
+
special_characters = '(){}[]"'
|
|
718
|
+
return any(char in s for char in special_characters)
|
|
719
|
+
|
|
720
|
+
if contains_special_characters(monologue):
|
|
721
|
+
printd(f"First message internal monologue contained special characters: {response_message}")
|
|
722
|
+
return False
|
|
723
|
+
# if 'functions' in monologue or 'send_message' in monologue or 'inner thought' in monologue.lower():
|
|
724
|
+
if "functions" in monologue or "send_message" in monologue:
|
|
725
|
+
# Sometimes the syntax won't be correct and internal syntax will leak into message.context
|
|
726
|
+
printd(f"First message internal monologue contained reserved words: {response_message}")
|
|
727
|
+
return False
|
|
728
|
+
|
|
729
|
+
return True
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def is_valid_url(url):
|
|
733
|
+
try:
|
|
734
|
+
result = urlparse(url)
|
|
735
|
+
return all([result.scheme, result.netloc])
|
|
736
|
+
except ValueError:
|
|
737
|
+
return False
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@contextmanager
|
|
741
|
+
def suppress_stdout():
|
|
742
|
+
"""Used to temporarily stop stdout (eg for the 'MockLLM' message)"""
|
|
743
|
+
new_stdout = io.StringIO()
|
|
744
|
+
old_stdout = sys.stdout
|
|
745
|
+
sys.stdout = new_stdout
|
|
746
|
+
try:
|
|
747
|
+
yield
|
|
748
|
+
finally:
|
|
749
|
+
sys.stdout = old_stdout
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def open_folder_in_explorer(folder_path):
|
|
753
|
+
"""
|
|
754
|
+
Opens the specified folder in the system's native file explorer.
|
|
755
|
+
|
|
756
|
+
:param folder_path: Absolute path to the folder to be opened.
|
|
757
|
+
"""
|
|
758
|
+
if not os.path.exists(folder_path):
|
|
759
|
+
raise ValueError(f"The specified folder {folder_path} does not exist.")
|
|
760
|
+
|
|
761
|
+
# Determine the operating system
|
|
762
|
+
os_name = platform.system()
|
|
763
|
+
|
|
764
|
+
# Open the folder based on the operating system
|
|
765
|
+
if os_name == "Windows":
|
|
766
|
+
# Windows: use 'explorer' command
|
|
767
|
+
subprocess.run(["explorer", folder_path], check=True)
|
|
768
|
+
elif os_name == "Darwin":
|
|
769
|
+
# macOS: use 'open' command
|
|
770
|
+
subprocess.run(["open", folder_path], check=True)
|
|
771
|
+
elif os_name == "Linux":
|
|
772
|
+
# Linux: use 'xdg-open' command (works for most Linux distributions)
|
|
773
|
+
subprocess.run(["xdg-open", folder_path], check=True)
|
|
774
|
+
else:
|
|
775
|
+
raise OSError(f"Unsupported operating system {os_name}.")
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
# Custom unpickler
|
|
779
|
+
class OpenAIBackcompatUnpickler(pickle.Unpickler):
|
|
780
|
+
def find_class(self, module, name):
|
|
781
|
+
if module == "openai.openai_object":
|
|
782
|
+
from letta.openai_backcompat.openai_object import OpenAIObject
|
|
783
|
+
|
|
784
|
+
return OpenAIObject
|
|
785
|
+
return super().find_class(module, name)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def count_tokens(s: str, model: str = "gpt-4") -> int:
|
|
789
|
+
encoding = tiktoken.encoding_for_model(model)
|
|
790
|
+
return len(encoding.encode(s))
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def printd(*args, **kwargs):
|
|
794
|
+
if DEBUG:
|
|
795
|
+
print(*args, **kwargs)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def united_diff(str1, str2):
|
|
799
|
+
lines1 = str1.splitlines(True)
|
|
800
|
+
lines2 = str2.splitlines(True)
|
|
801
|
+
diff = difflib.unified_diff(lines1, lines2)
|
|
802
|
+
return "".join(diff)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def parse_formatted_time(formatted_time):
|
|
806
|
+
# parse times returned by letta.utils.get_formatted_time()
|
|
807
|
+
return datetime.strptime(formatted_time, "%Y-%m-%d %I:%M:%S %p %Z%z")
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def datetime_to_timestamp(dt):
|
|
811
|
+
# convert datetime object to integer timestamp
|
|
812
|
+
return int(dt.timestamp())
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def timestamp_to_datetime(ts):
|
|
816
|
+
# convert integer timestamp to datetime object
|
|
817
|
+
return datetime.fromtimestamp(ts)
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def get_local_time_military():
|
|
821
|
+
# Get the current time in UTC
|
|
822
|
+
current_time_utc = datetime.now(pytz.utc)
|
|
823
|
+
|
|
824
|
+
# Convert to San Francisco's time zone (PST/PDT)
|
|
825
|
+
sf_time_zone = pytz.timezone("America/Los_Angeles")
|
|
826
|
+
local_time = current_time_utc.astimezone(sf_time_zone)
|
|
827
|
+
|
|
828
|
+
# You may format it as you desire
|
|
829
|
+
formatted_time = local_time.strftime("%Y-%m-%d %H:%M:%S %Z%z")
|
|
830
|
+
|
|
831
|
+
return formatted_time
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def get_local_time_timezone(timezone="America/Los_Angeles"):
|
|
835
|
+
# Get the current time in UTC
|
|
836
|
+
current_time_utc = datetime.now(pytz.utc)
|
|
837
|
+
|
|
838
|
+
# Convert to San Francisco's time zone (PST/PDT)
|
|
839
|
+
sf_time_zone = pytz.timezone(timezone)
|
|
840
|
+
local_time = current_time_utc.astimezone(sf_time_zone)
|
|
841
|
+
|
|
842
|
+
# You may format it as you desire, including AM/PM
|
|
843
|
+
formatted_time = local_time.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
|
|
844
|
+
|
|
845
|
+
return formatted_time
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def get_local_time(timezone=None):
|
|
849
|
+
if timezone is not None:
|
|
850
|
+
time_str = get_local_time_timezone(timezone)
|
|
851
|
+
else:
|
|
852
|
+
# Get the current time, which will be in the local timezone of the computer
|
|
853
|
+
local_time = datetime.now().astimezone()
|
|
854
|
+
|
|
855
|
+
# You may format it as you desire, including AM/PM
|
|
856
|
+
time_str = local_time.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
|
|
857
|
+
|
|
858
|
+
return time_str.strip()
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def get_utc_time() -> datetime:
|
|
862
|
+
"""Get the current UTC time"""
|
|
863
|
+
# return datetime.now(pytz.utc)
|
|
864
|
+
return datetime.now(timezone.utc)
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def format_datetime(dt):
|
|
868
|
+
return dt.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def parse_json(string) -> dict:
|
|
872
|
+
"""Parse JSON string into JSON with both json and demjson"""
|
|
873
|
+
result = None
|
|
874
|
+
try:
|
|
875
|
+
result = json_loads(string)
|
|
876
|
+
return result
|
|
877
|
+
except Exception as e:
|
|
878
|
+
print(f"Error parsing json with json package: {e}")
|
|
879
|
+
|
|
880
|
+
try:
|
|
881
|
+
result = demjson.decode(string)
|
|
882
|
+
return result
|
|
883
|
+
except demjson.JSONDecodeError as e:
|
|
884
|
+
print(f"Error parsing json with demjson package: {e}")
|
|
885
|
+
raise e
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def validate_function_response(function_response_string: any, strict: bool = False, truncate: bool = True) -> str:
|
|
889
|
+
"""Check to make sure that a function used by Letta returned a valid response
|
|
890
|
+
|
|
891
|
+
Responses need to be strings (or None) that fall under a certain text count limit.
|
|
892
|
+
"""
|
|
893
|
+
if not isinstance(function_response_string, str):
|
|
894
|
+
# Soft correction for a few basic types
|
|
895
|
+
|
|
896
|
+
if function_response_string is None:
|
|
897
|
+
# function_response_string = "Empty (no function output)"
|
|
898
|
+
function_response_string = "None" # backcompat
|
|
899
|
+
|
|
900
|
+
elif isinstance(function_response_string, dict):
|
|
901
|
+
if strict:
|
|
902
|
+
# TODO add better error message
|
|
903
|
+
raise ValueError(function_response_string)
|
|
904
|
+
|
|
905
|
+
# Allow dict through since it will be cast to json.dumps()
|
|
906
|
+
try:
|
|
907
|
+
# TODO find a better way to do this that won't result in double escapes
|
|
908
|
+
function_response_string = json_dumps(function_response_string)
|
|
909
|
+
except:
|
|
910
|
+
raise ValueError(function_response_string)
|
|
911
|
+
|
|
912
|
+
else:
|
|
913
|
+
if strict:
|
|
914
|
+
# TODO add better error message
|
|
915
|
+
raise ValueError(function_response_string)
|
|
916
|
+
|
|
917
|
+
# Try to convert to a string, but throw a warning to alert the user
|
|
918
|
+
try:
|
|
919
|
+
function_response_string = str(function_response_string)
|
|
920
|
+
except:
|
|
921
|
+
raise ValueError(function_response_string)
|
|
922
|
+
|
|
923
|
+
# Now check the length and make sure it doesn't go over the limit
|
|
924
|
+
# TODO we should change this to a max token limit that's variable based on tokens remaining (or context-window)
|
|
925
|
+
if truncate and len(function_response_string) > FUNCTION_RETURN_CHAR_LIMIT:
|
|
926
|
+
print(
|
|
927
|
+
f"{CLI_WARNING_PREFIX}function return was over limit ({len(function_response_string)} > {FUNCTION_RETURN_CHAR_LIMIT}) and was truncated"
|
|
928
|
+
)
|
|
929
|
+
function_response_string = f"{function_response_string[:FUNCTION_RETURN_CHAR_LIMIT]}... [NOTE: function output was truncated since it exceeded the character limit ({len(function_response_string)} > {FUNCTION_RETURN_CHAR_LIMIT})]"
|
|
930
|
+
|
|
931
|
+
return function_response_string
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def list_agent_config_files(sort="last_modified"):
|
|
935
|
+
"""List all agent config files, ignoring dotfiles."""
|
|
936
|
+
agent_dir = os.path.join(LETTA_DIR, "agents")
|
|
937
|
+
files = os.listdir(agent_dir)
|
|
938
|
+
|
|
939
|
+
# Remove dotfiles like .DS_Store
|
|
940
|
+
files = [file for file in files if not file.startswith(".")]
|
|
941
|
+
|
|
942
|
+
# Remove anything that's not a directory
|
|
943
|
+
files = [file for file in files if os.path.isdir(os.path.join(agent_dir, file))]
|
|
944
|
+
|
|
945
|
+
if sort is not None:
|
|
946
|
+
if sort == "last_modified":
|
|
947
|
+
# Sort the directories by last modified (most recent first)
|
|
948
|
+
files.sort(key=lambda x: os.path.getmtime(os.path.join(agent_dir, x)), reverse=True)
|
|
949
|
+
else:
|
|
950
|
+
raise ValueError(f"Unrecognized sorting option {sort}")
|
|
951
|
+
|
|
952
|
+
return files
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def list_human_files():
|
|
956
|
+
"""List all humans files"""
|
|
957
|
+
defaults_dir = os.path.join(letta.__path__[0], "humans", "examples")
|
|
958
|
+
user_dir = os.path.join(LETTA_DIR, "humans")
|
|
959
|
+
|
|
960
|
+
letta_defaults = os.listdir(defaults_dir)
|
|
961
|
+
letta_defaults = [os.path.join(defaults_dir, f) for f in letta_defaults if f.endswith(".txt")]
|
|
962
|
+
|
|
963
|
+
if os.path.exists(user_dir):
|
|
964
|
+
user_added = os.listdir(user_dir)
|
|
965
|
+
user_added = [os.path.join(user_dir, f) for f in user_added]
|
|
966
|
+
else:
|
|
967
|
+
user_added = []
|
|
968
|
+
return letta_defaults + user_added
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def list_persona_files():
|
|
972
|
+
"""List all personas files"""
|
|
973
|
+
defaults_dir = os.path.join(letta.__path__[0], "personas", "examples")
|
|
974
|
+
user_dir = os.path.join(LETTA_DIR, "personas")
|
|
975
|
+
|
|
976
|
+
letta_defaults = os.listdir(defaults_dir)
|
|
977
|
+
letta_defaults = [os.path.join(defaults_dir, f) for f in letta_defaults if f.endswith(".txt")]
|
|
978
|
+
|
|
979
|
+
if os.path.exists(user_dir):
|
|
980
|
+
user_added = os.listdir(user_dir)
|
|
981
|
+
user_added = [os.path.join(user_dir, f) for f in user_added]
|
|
982
|
+
else:
|
|
983
|
+
user_added = []
|
|
984
|
+
return letta_defaults + user_added
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def get_human_text(name: str, enforce_limit=True):
|
|
988
|
+
for file_path in list_human_files():
|
|
989
|
+
file = os.path.basename(file_path)
|
|
990
|
+
if f"{name}.txt" == file or name == file:
|
|
991
|
+
human_text = open(file_path, "r", encoding="utf-8").read().strip()
|
|
992
|
+
if enforce_limit and len(human_text) > CORE_MEMORY_HUMAN_CHAR_LIMIT:
|
|
993
|
+
raise ValueError(f"Contents of {name}.txt is over the character limit ({len(human_text)} > {CORE_MEMORY_HUMAN_CHAR_LIMIT})")
|
|
994
|
+
return human_text
|
|
995
|
+
|
|
996
|
+
raise ValueError(f"Human {name}.txt not found")
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def get_persona_text(name: str, enforce_limit=True):
|
|
1000
|
+
for file_path in list_persona_files():
|
|
1001
|
+
file = os.path.basename(file_path)
|
|
1002
|
+
if f"{name}.txt" == file or name == file:
|
|
1003
|
+
persona_text = open(file_path, "r", encoding="utf-8").read().strip()
|
|
1004
|
+
if enforce_limit and len(persona_text) > CORE_MEMORY_PERSONA_CHAR_LIMIT:
|
|
1005
|
+
raise ValueError(
|
|
1006
|
+
f"Contents of {name}.txt is over the character limit ({len(persona_text)} > {CORE_MEMORY_PERSONA_CHAR_LIMIT})"
|
|
1007
|
+
)
|
|
1008
|
+
return persona_text
|
|
1009
|
+
|
|
1010
|
+
raise ValueError(f"Persona {name}.txt not found")
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def get_human_text(name: str):
|
|
1014
|
+
for file_path in list_human_files():
|
|
1015
|
+
file = os.path.basename(file_path)
|
|
1016
|
+
if f"{name}.txt" == file or name == file:
|
|
1017
|
+
return open(file_path, "r", encoding="utf-8").read().strip()
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def get_schema_diff(schema_a, schema_b):
|
|
1021
|
+
# Assuming f_schema and linked_function['json_schema'] are your JSON schemas
|
|
1022
|
+
f_schema_json = json_dumps(schema_a)
|
|
1023
|
+
linked_function_json = json_dumps(schema_b)
|
|
1024
|
+
|
|
1025
|
+
# Compute the difference using difflib
|
|
1026
|
+
difference = list(difflib.ndiff(f_schema_json.splitlines(keepends=True), linked_function_json.splitlines(keepends=True)))
|
|
1027
|
+
|
|
1028
|
+
# Filter out lines that don't represent changes
|
|
1029
|
+
difference = [line for line in difference if line.startswith("+ ") or line.startswith("- ")]
|
|
1030
|
+
|
|
1031
|
+
return "".join(difference)
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
# datetime related
|
|
1035
|
+
def validate_date_format(date_str):
|
|
1036
|
+
"""Validate the given date string in the format 'YYYY-MM-DD'."""
|
|
1037
|
+
try:
|
|
1038
|
+
datetime.strptime(date_str, "%Y-%m-%d")
|
|
1039
|
+
return True
|
|
1040
|
+
except (ValueError, TypeError):
|
|
1041
|
+
return False
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def extract_date_from_timestamp(timestamp):
|
|
1045
|
+
"""Extracts and returns the date from the given timestamp."""
|
|
1046
|
+
# Extracts the date (ignoring the time and timezone)
|
|
1047
|
+
match = re.match(r"(\d{4}-\d{2}-\d{2})", timestamp)
|
|
1048
|
+
return match.group(1) if match else None
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def create_uuid_from_string(val: str):
|
|
1052
|
+
"""
|
|
1053
|
+
Generate consistent UUID from a string
|
|
1054
|
+
from: https://samos-it.com/posts/python-create-uuid-from-random-string-of-words.html
|
|
1055
|
+
"""
|
|
1056
|
+
hex_string = hashlib.md5(val.encode("UTF-8")).hexdigest()
|
|
1057
|
+
return uuid.UUID(hex=hex_string)
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def json_dumps(data, indent=2):
|
|
1061
|
+
return json.dumps(data, indent=indent, ensure_ascii=False)
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def json_loads(data):
|
|
1065
|
+
return json.loads(data, strict=False)
|