letta-nightly 0.6.4.dev20241217104233__py3-none-any.whl → 0.6.5.dev20241218055539__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 +1 -1
- letta/agent.py +68 -65
- letta/client/client.py +1 -0
- letta/constants.py +6 -1
- letta/embeddings.py +3 -9
- letta/functions/function_sets/base.py +9 -57
- letta/functions/schema_generator.py +1 -1
- letta/llm_api/anthropic.py +38 -13
- letta/llm_api/llm_api_tools.py +12 -1
- letta/local_llm/function_parser.py +1 -1
- letta/orm/errors.py +8 -0
- letta/orm/sqlalchemy_base.py +24 -17
- letta/providers.py +2 -0
- letta/schemas/agent.py +35 -0
- letta/schemas/sandbox_config.py +2 -1
- letta/server/rest_api/app.py +32 -7
- letta/server/rest_api/routers/v1/tools.py +1 -1
- letta/server/server.py +81 -57
- letta/services/agent_manager.py +3 -0
- letta/services/tool_execution_sandbox.py +54 -45
- letta/settings.py +9 -4
- letta/utils.py +8 -0
- {letta_nightly-0.6.4.dev20241217104233.dist-info → letta_nightly-0.6.5.dev20241218055539.dist-info}/METADATA +1 -1
- {letta_nightly-0.6.4.dev20241217104233.dist-info → letta_nightly-0.6.5.dev20241218055539.dist-info}/RECORD +27 -27
- {letta_nightly-0.6.4.dev20241217104233.dist-info → letta_nightly-0.6.5.dev20241218055539.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.4.dev20241217104233.dist-info → letta_nightly-0.6.5.dev20241218055539.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.4.dev20241217104233.dist-info → letta_nightly-0.6.5.dev20241218055539.dist-info}/entry_points.txt +0 -0
letta/__init__.py
CHANGED
letta/agent.py
CHANGED
|
@@ -18,6 +18,7 @@ from letta.constants import (
|
|
|
18
18
|
MESSAGE_SUMMARY_WARNING_FRAC,
|
|
19
19
|
O1_BASE_TOOLS,
|
|
20
20
|
REQ_HEARTBEAT_MESSAGE,
|
|
21
|
+
STRUCTURED_OUTPUT_MODELS,
|
|
21
22
|
)
|
|
22
23
|
from letta.errors import LLMError
|
|
23
24
|
from letta.helpers import ToolRulesSolver
|
|
@@ -63,6 +64,7 @@ from letta.system import (
|
|
|
63
64
|
)
|
|
64
65
|
from letta.utils import (
|
|
65
66
|
count_tokens,
|
|
67
|
+
get_friendly_error_msg,
|
|
66
68
|
get_local_time,
|
|
67
69
|
get_tool_call_id,
|
|
68
70
|
get_utc_time,
|
|
@@ -258,9 +260,6 @@ class Agent(BaseAgent):
|
|
|
258
260
|
|
|
259
261
|
self.user = user
|
|
260
262
|
|
|
261
|
-
# link tools
|
|
262
|
-
self.link_tools(agent_state.tools)
|
|
263
|
-
|
|
264
263
|
# initialize a tool rules solver
|
|
265
264
|
if agent_state.tool_rules:
|
|
266
265
|
# if there are tool rules, print out a warning
|
|
@@ -276,6 +275,7 @@ class Agent(BaseAgent):
|
|
|
276
275
|
|
|
277
276
|
# gpt-4, gpt-3.5-turbo, ...
|
|
278
277
|
self.model = self.agent_state.llm_config.model
|
|
278
|
+
self.check_tool_rules()
|
|
279
279
|
|
|
280
280
|
# state managers
|
|
281
281
|
self.block_manager = BlockManager()
|
|
@@ -295,8 +295,6 @@ class Agent(BaseAgent):
|
|
|
295
295
|
self.agent_manager = AgentManager()
|
|
296
296
|
|
|
297
297
|
# State needed for heartbeat pausing
|
|
298
|
-
self.pause_heartbeats_start = None
|
|
299
|
-
self.pause_heartbeats_minutes = 0
|
|
300
298
|
|
|
301
299
|
self.first_message_verify_mono = first_message_verify_mono
|
|
302
300
|
|
|
@@ -381,6 +379,16 @@ class Agent(BaseAgent):
|
|
|
381
379
|
# Create the agent in the DB
|
|
382
380
|
self.update_state()
|
|
383
381
|
|
|
382
|
+
def check_tool_rules(self):
|
|
383
|
+
if self.model not in STRUCTURED_OUTPUT_MODELS:
|
|
384
|
+
if len(self.tool_rules_solver.init_tool_rules) > 1:
|
|
385
|
+
raise ValueError(
|
|
386
|
+
"Multiple initial tools are not supported for non-structured models. Please use only one initial tool rule."
|
|
387
|
+
)
|
|
388
|
+
self.supports_structured_output = False
|
|
389
|
+
else:
|
|
390
|
+
self.supports_structured_output = True
|
|
391
|
+
|
|
384
392
|
def update_memory_if_change(self, new_memory: Memory) -> bool:
|
|
385
393
|
"""
|
|
386
394
|
Update internal memory object and system prompt if there have been modifications.
|
|
@@ -415,11 +423,21 @@ class Agent(BaseAgent):
|
|
|
415
423
|
return True
|
|
416
424
|
return False
|
|
417
425
|
|
|
418
|
-
def execute_tool_and_persist_state(self, function_name,
|
|
426
|
+
def execute_tool_and_persist_state(self, function_name: str, function_args: dict, target_letta_tool: Tool):
|
|
419
427
|
"""
|
|
420
428
|
Execute tool modifications and persist the state of the agent.
|
|
421
429
|
Note: only some agent state modifications will be persisted, such as data in the AgentState ORM and block data
|
|
422
430
|
"""
|
|
431
|
+
# TODO: Get rid of this. This whole piece is pretty shady, that we exec the function to just get the type hints for args.
|
|
432
|
+
env = {}
|
|
433
|
+
env.update(globals())
|
|
434
|
+
exec(target_letta_tool.source_code, env)
|
|
435
|
+
callable_func = env[target_letta_tool.json_schema["name"]]
|
|
436
|
+
spec = inspect.getfullargspec(callable_func).annotations
|
|
437
|
+
for name, arg in function_args.items():
|
|
438
|
+
if isinstance(function_args[name], dict):
|
|
439
|
+
function_args[name] = spec[name](**function_args[name])
|
|
440
|
+
|
|
423
441
|
# TODO: add agent manager here
|
|
424
442
|
orig_memory_str = self.agent_state.memory.compile()
|
|
425
443
|
|
|
@@ -432,11 +450,11 @@ class Agent(BaseAgent):
|
|
|
432
450
|
if function_name in BASE_TOOLS or function_name in O1_BASE_TOOLS:
|
|
433
451
|
# base tools are allowed to access the `Agent` object and run on the database
|
|
434
452
|
function_args["self"] = self # need to attach self to arg since it's dynamically linked
|
|
435
|
-
function_response =
|
|
453
|
+
function_response = callable_func(**function_args)
|
|
436
454
|
else:
|
|
437
455
|
# execute tool in a sandbox
|
|
438
456
|
# TODO: allow agent_state to specify which sandbox to execute tools in
|
|
439
|
-
sandbox_run_result = ToolExecutionSandbox(function_name, function_args, self.
|
|
457
|
+
sandbox_run_result = ToolExecutionSandbox(function_name, function_args, self.user).run(
|
|
440
458
|
agent_state=self.agent_state.__deepcopy__()
|
|
441
459
|
)
|
|
442
460
|
function_response, updated_agent_state = sandbox_run_result.func_return, sandbox_run_result.agent_state
|
|
@@ -446,12 +464,9 @@ class Agent(BaseAgent):
|
|
|
446
464
|
except Exception as e:
|
|
447
465
|
# Need to catch error here, or else trunction wont happen
|
|
448
466
|
# TODO: modify to function execution error
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if len(error_msg) > MAX_ERROR_MESSAGE_CHAR_LIMIT:
|
|
453
|
-
error_msg = error_msg[:MAX_ERROR_MESSAGE_CHAR_LIMIT]
|
|
454
|
-
raise ValueError(error_msg)
|
|
467
|
+
function_response = get_friendly_error_msg(
|
|
468
|
+
function_name=function_name, exception_name=type(e).__name__, exception_message=str(e)
|
|
469
|
+
)
|
|
455
470
|
|
|
456
471
|
return function_response
|
|
457
472
|
|
|
@@ -464,27 +479,6 @@ class Agent(BaseAgent):
|
|
|
464
479
|
def messages(self, value):
|
|
465
480
|
raise Exception("Modifying message list directly not allowed")
|
|
466
481
|
|
|
467
|
-
def link_tools(self, tools: List[Tool]):
|
|
468
|
-
"""Bind a tool object (schema + python function) to the agent object"""
|
|
469
|
-
|
|
470
|
-
# Store the functions schemas (this is passed as an argument to ChatCompletion)
|
|
471
|
-
self.functions = []
|
|
472
|
-
self.functions_python = {}
|
|
473
|
-
env = {}
|
|
474
|
-
env.update(globals())
|
|
475
|
-
for tool in tools:
|
|
476
|
-
try:
|
|
477
|
-
# WARNING: name may not be consistent?
|
|
478
|
-
# if tool.module: # execute the whole module
|
|
479
|
-
# exec(tool.module, env)
|
|
480
|
-
# else:
|
|
481
|
-
exec(tool.source_code, env)
|
|
482
|
-
self.functions_python[tool.json_schema["name"]] = env[tool.json_schema["name"]]
|
|
483
|
-
self.functions.append(tool.json_schema)
|
|
484
|
-
except Exception:
|
|
485
|
-
warnings.warn(f"WARNING: tool {tool.name} failed to link")
|
|
486
|
-
assert all([callable(f) for k, f in self.functions_python.items()]), self.functions_python
|
|
487
|
-
|
|
488
482
|
def _load_messages_from_recall(self, message_ids: List[str]) -> List[Message]:
|
|
489
483
|
"""Load a list of messages from recall storage"""
|
|
490
484
|
|
|
@@ -588,14 +582,32 @@ class Agent(BaseAgent):
|
|
|
588
582
|
empty_response_retry_limit: int = 3,
|
|
589
583
|
backoff_factor: float = 0.5, # delay multiplier for exponential backoff
|
|
590
584
|
max_delay: float = 10.0, # max delay between retries
|
|
585
|
+
step_count: Optional[int] = None,
|
|
591
586
|
) -> ChatCompletionResponse:
|
|
592
587
|
"""Get response from LLM API with robust retry mechanism."""
|
|
593
588
|
|
|
594
589
|
allowed_tool_names = self.tool_rules_solver.get_allowed_tool_names()
|
|
590
|
+
agent_state_tool_jsons = [t.json_schema for t in self.agent_state.tools]
|
|
591
|
+
|
|
595
592
|
allowed_functions = (
|
|
596
|
-
|
|
593
|
+
agent_state_tool_jsons
|
|
594
|
+
if not allowed_tool_names
|
|
595
|
+
else [func for func in agent_state_tool_jsons if func["name"] in allowed_tool_names]
|
|
597
596
|
)
|
|
598
597
|
|
|
598
|
+
# For the first message, force the initial tool if one is specified
|
|
599
|
+
force_tool_call = None
|
|
600
|
+
if (
|
|
601
|
+
step_count is not None
|
|
602
|
+
and step_count == 0
|
|
603
|
+
and not self.supports_structured_output
|
|
604
|
+
and len(self.tool_rules_solver.init_tool_rules) > 0
|
|
605
|
+
):
|
|
606
|
+
force_tool_call = self.tool_rules_solver.init_tool_rules[0].tool_name
|
|
607
|
+
# Force a tool call if exactly one tool is specified
|
|
608
|
+
elif step_count is not None and step_count > 0 and len(allowed_tool_names) == 1:
|
|
609
|
+
force_tool_call = allowed_tool_names[0]
|
|
610
|
+
|
|
599
611
|
for attempt in range(1, empty_response_retry_limit + 1):
|
|
600
612
|
try:
|
|
601
613
|
response = create(
|
|
@@ -603,9 +615,10 @@ class Agent(BaseAgent):
|
|
|
603
615
|
messages=message_sequence,
|
|
604
616
|
user_id=self.agent_state.created_by_id,
|
|
605
617
|
functions=allowed_functions,
|
|
606
|
-
functions_python=self.functions_python,
|
|
618
|
+
# functions_python=self.functions_python, do we need this?
|
|
607
619
|
function_call=function_call,
|
|
608
620
|
first_message=first_message,
|
|
621
|
+
force_tool_call=force_tool_call,
|
|
609
622
|
stream=stream,
|
|
610
623
|
stream_interface=self.interface,
|
|
611
624
|
)
|
|
@@ -711,10 +724,13 @@ class Agent(BaseAgent):
|
|
|
711
724
|
function_name = function_call.name
|
|
712
725
|
printd(f"Request to call function {function_name} with tool_call_id: {tool_call_id}")
|
|
713
726
|
|
|
714
|
-
# Failure case 1: function name is wrong
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
727
|
+
# Failure case 1: function name is wrong (not in agent_state.tools)
|
|
728
|
+
target_letta_tool = None
|
|
729
|
+
for t in self.agent_state.tools:
|
|
730
|
+
if t.name == function_name:
|
|
731
|
+
target_letta_tool = t
|
|
732
|
+
|
|
733
|
+
if not target_letta_tool:
|
|
718
734
|
error_msg = f"No function named {function_name}"
|
|
719
735
|
function_response = package_function_response(False, error_msg)
|
|
720
736
|
messages.append(
|
|
@@ -782,14 +798,8 @@ class Agent(BaseAgent):
|
|
|
782
798
|
# this is because the function/tool role message is only created once the function/tool has executed/returned
|
|
783
799
|
self.interface.function_message(f"Running {function_name}({function_args})", msg_obj=messages[-1])
|
|
784
800
|
try:
|
|
785
|
-
spec = inspect.getfullargspec(function_to_call).annotations
|
|
786
|
-
|
|
787
|
-
for name, arg in function_args.items():
|
|
788
|
-
if isinstance(function_args[name], dict):
|
|
789
|
-
function_args[name] = spec[name](**function_args[name])
|
|
790
|
-
|
|
791
801
|
# handle tool execution (sandbox) and state updates
|
|
792
|
-
function_response = self.execute_tool_and_persist_state(function_name,
|
|
802
|
+
function_response = self.execute_tool_and_persist_state(function_name, function_args, target_letta_tool)
|
|
793
803
|
|
|
794
804
|
# handle trunction
|
|
795
805
|
if function_name in ["conversation_search", "conversation_search_date", "archival_memory_search"]:
|
|
@@ -801,8 +811,7 @@ class Agent(BaseAgent):
|
|
|
801
811
|
truncate = True
|
|
802
812
|
|
|
803
813
|
# get the function response limit
|
|
804
|
-
|
|
805
|
-
return_char_limit = tool_obj.return_char_limit
|
|
814
|
+
return_char_limit = target_letta_tool.return_char_limit
|
|
806
815
|
function_response_string = validate_function_response(
|
|
807
816
|
function_response, return_char_limit=return_char_limit, truncate=truncate
|
|
808
817
|
)
|
|
@@ -897,6 +906,7 @@ class Agent(BaseAgent):
|
|
|
897
906
|
step_count = 0
|
|
898
907
|
while True:
|
|
899
908
|
kwargs["first_message"] = False
|
|
909
|
+
kwargs["step_count"] = step_count
|
|
900
910
|
step_response = self.inner_step(
|
|
901
911
|
messages=next_input_message,
|
|
902
912
|
**kwargs,
|
|
@@ -972,6 +982,7 @@ class Agent(BaseAgent):
|
|
|
972
982
|
first_message_retry_limit: int = FIRST_MESSAGE_ATTEMPTS,
|
|
973
983
|
skip_verify: bool = False,
|
|
974
984
|
stream: bool = False, # TODO move to config?
|
|
985
|
+
step_count: Optional[int] = None,
|
|
975
986
|
) -> AgentStepResponse:
|
|
976
987
|
"""Runs a single step in the agent loop (generates at most one LLM call)"""
|
|
977
988
|
|
|
@@ -1014,7 +1025,9 @@ class Agent(BaseAgent):
|
|
|
1014
1025
|
else:
|
|
1015
1026
|
response = self._get_ai_reply(
|
|
1016
1027
|
message_sequence=input_message_sequence,
|
|
1028
|
+
first_message=first_message,
|
|
1017
1029
|
stream=stream,
|
|
1030
|
+
step_count=step_count,
|
|
1018
1031
|
)
|
|
1019
1032
|
|
|
1020
1033
|
# Step 3: check if LLM wanted to call a function
|
|
@@ -1235,17 +1248,6 @@ class Agent(BaseAgent):
|
|
|
1235
1248
|
|
|
1236
1249
|
printd(f"Ran summarizer, messages length {prior_len} -> {len(self.messages)}")
|
|
1237
1250
|
|
|
1238
|
-
def heartbeat_is_paused(self):
|
|
1239
|
-
"""Check if there's a requested pause on timed heartbeats"""
|
|
1240
|
-
|
|
1241
|
-
# Check if the pause has been initiated
|
|
1242
|
-
if self.pause_heartbeats_start is None:
|
|
1243
|
-
return False
|
|
1244
|
-
|
|
1245
|
-
# Check if it's been more than pause_heartbeats_minutes since pause_heartbeats_start
|
|
1246
|
-
elapsed_time = get_utc_time() - self.pause_heartbeats_start
|
|
1247
|
-
return elapsed_time.total_seconds() < self.pause_heartbeats_minutes * 60
|
|
1248
|
-
|
|
1249
1251
|
def _swap_system_message_in_buffer(self, new_system_message: str):
|
|
1250
1252
|
"""Update the system message (NOT prompt) of the Agent (requires updating the internal buffer)"""
|
|
1251
1253
|
assert isinstance(new_system_message, str)
|
|
@@ -1370,7 +1372,7 @@ class Agent(BaseAgent):
|
|
|
1370
1372
|
agent_manager: AgentManager,
|
|
1371
1373
|
):
|
|
1372
1374
|
"""Attach a source to the agent using the SourcesAgents ORM relationship.
|
|
1373
|
-
|
|
1375
|
+
|
|
1374
1376
|
Args:
|
|
1375
1377
|
user: User performing the action
|
|
1376
1378
|
source_id: ID of the source to attach
|
|
@@ -1553,9 +1555,10 @@ class Agent(BaseAgent):
|
|
|
1553
1555
|
num_tokens_external_memory_summary = count_tokens(external_memory_summary)
|
|
1554
1556
|
|
|
1555
1557
|
# tokens taken up by function definitions
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1558
|
+
agent_state_tool_jsons = [t.json_schema for t in self.agent_state.tools]
|
|
1559
|
+
if agent_state_tool_jsons:
|
|
1560
|
+
available_functions_definitions = [ChatCompletionRequestTool(type="function", function=f) for f in agent_state_tool_jsons]
|
|
1561
|
+
num_tokens_available_functions_definitions = num_tokens_from_functions(functions=agent_state_tool_jsons, model=self.model)
|
|
1559
1562
|
else:
|
|
1560
1563
|
available_functions_definitions = []
|
|
1561
1564
|
num_tokens_available_functions_definitions = 0
|
letta/client/client.py
CHANGED
|
@@ -2156,6 +2156,7 @@ class LocalClient(AbstractClient):
|
|
|
2156
2156
|
"block_ids": [b.id for b in memory.get_blocks()] + block_ids,
|
|
2157
2157
|
"tool_ids": tool_ids,
|
|
2158
2158
|
"tool_rules": tool_rules,
|
|
2159
|
+
"include_base_tools": include_base_tools,
|
|
2159
2160
|
"system": system,
|
|
2160
2161
|
"agent_type": agent_type,
|
|
2161
2162
|
"llm_config": llm_config if llm_config else self._default_llm_config,
|
letta/constants.py
CHANGED
|
@@ -23,6 +23,7 @@ MIN_CONTEXT_WINDOW = 4096
|
|
|
23
23
|
|
|
24
24
|
# embeddings
|
|
25
25
|
MAX_EMBEDDING_DIM = 4096 # maximum supported embeding size - do NOT change or else DBs will need to be reset
|
|
26
|
+
DEFAULT_EMBEDDING_CHUNK_SIZE = 300
|
|
26
27
|
|
|
27
28
|
# tokenizers
|
|
28
29
|
EMBEDDING_TO_TOKENIZER_MAP = {
|
|
@@ -37,7 +38,8 @@ DEFAULT_HUMAN = "basic"
|
|
|
37
38
|
DEFAULT_PRESET = "memgpt_chat"
|
|
38
39
|
|
|
39
40
|
# Base tools that cannot be edited, as they access agent state directly
|
|
40
|
-
|
|
41
|
+
# Note that we don't include "conversation_search_date" for now
|
|
42
|
+
BASE_TOOLS = ["send_message", "conversation_search", "archival_memory_insert", "archival_memory_search"]
|
|
41
43
|
O1_BASE_TOOLS = ["send_thinking_message", "send_final_message"]
|
|
42
44
|
# Base memory tools CAN be edited, and are added by default by the server
|
|
43
45
|
BASE_MEMORY_TOOLS = ["core_memory_append", "core_memory_replace"]
|
|
@@ -48,6 +50,9 @@ BASE_MEMORY_TOOLS = ["core_memory_append", "core_memory_replace"]
|
|
|
48
50
|
DEFAULT_MESSAGE_TOOL = "send_message"
|
|
49
51
|
DEFAULT_MESSAGE_TOOL_KWARG = "message"
|
|
50
52
|
|
|
53
|
+
# Structured output models
|
|
54
|
+
STRUCTURED_OUTPUT_MODELS = {"gpt-4o", "gpt-4o-mini"}
|
|
55
|
+
|
|
51
56
|
# LOGGER_LOG_LEVEL is use to convert Text to Logging level value for logging mostly for Cli input to setting level
|
|
52
57
|
LOGGER_LOG_LEVELS = {"CRITICAL": CRITICAL, "ERROR": ERROR, "WARN": WARN, "WARNING": WARNING, "INFO": INFO, "DEBUG": DEBUG, "NOTSET": NOTSET}
|
|
53
58
|
|
letta/embeddings.py
CHANGED
|
@@ -234,16 +234,10 @@ def embedding_model(config: EmbeddingConfig, user_id: Optional[uuid.UUID] = None
|
|
|
234
234
|
)
|
|
235
235
|
elif endpoint_type == "ollama":
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
ollama_additional_kwargs = {}
|
|
240
|
-
callback_manager = None
|
|
241
|
-
|
|
242
|
-
model = OllamaEmbedding(
|
|
243
|
-
model_name=config.embedding_model,
|
|
237
|
+
model = OllamaEmbeddings(
|
|
238
|
+
model=config.embedding_model,
|
|
244
239
|
base_url=config.embedding_endpoint,
|
|
245
|
-
ollama_additional_kwargs=
|
|
246
|
-
callback_manager=callback_manager or None,
|
|
240
|
+
ollama_additional_kwargs={},
|
|
247
241
|
)
|
|
248
242
|
return model
|
|
249
243
|
|
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
1
|
from typing import Optional
|
|
3
2
|
|
|
4
3
|
from letta.agent import Agent
|
|
5
|
-
from letta.constants import MAX_PAUSE_HEARTBEATS
|
|
6
|
-
from letta.services.agent_manager import AgentManager
|
|
7
|
-
|
|
8
|
-
# import math
|
|
9
|
-
# from letta.utils import json_dumps
|
|
10
|
-
|
|
11
|
-
### Functions / tools the agent can use
|
|
12
|
-
# All functions should return a response string (or None)
|
|
13
|
-
# If the function fails, throw an exception
|
|
14
4
|
|
|
15
5
|
|
|
16
6
|
def send_message(self: "Agent", message: str) -> Optional[str]:
|
|
@@ -28,36 +18,6 @@ def send_message(self: "Agent", message: str) -> Optional[str]:
|
|
|
28
18
|
return None
|
|
29
19
|
|
|
30
20
|
|
|
31
|
-
# Construct the docstring dynamically (since it should use the external constants)
|
|
32
|
-
pause_heartbeats_docstring = f"""
|
|
33
|
-
Temporarily ignore timed heartbeats. You may still receive messages from manual heartbeats and other events.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
minutes (int): Number of minutes to ignore heartbeats for. Max value of {MAX_PAUSE_HEARTBEATS} minutes ({MAX_PAUSE_HEARTBEATS // 60} hours).
|
|
37
|
-
|
|
38
|
-
Returns:
|
|
39
|
-
str: Function status response
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def pause_heartbeats(self: "Agent", minutes: int) -> Optional[str]:
|
|
44
|
-
import datetime
|
|
45
|
-
|
|
46
|
-
from letta.constants import MAX_PAUSE_HEARTBEATS
|
|
47
|
-
|
|
48
|
-
minutes = min(MAX_PAUSE_HEARTBEATS, minutes)
|
|
49
|
-
|
|
50
|
-
# Record the current time
|
|
51
|
-
self.pause_heartbeats_start = datetime.datetime.now(datetime.timezone.utc)
|
|
52
|
-
# And record how long the pause should go for
|
|
53
|
-
self.pause_heartbeats_minutes = int(minutes)
|
|
54
|
-
|
|
55
|
-
return f"Pausing timed heartbeats for {minutes} min"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
pause_heartbeats.__doc__ = pause_heartbeats_docstring
|
|
59
|
-
|
|
60
|
-
|
|
61
21
|
def conversation_search(self: "Agent", query: str, page: Optional[int] = 0) -> Optional[str]:
|
|
62
22
|
"""
|
|
63
23
|
Search prior conversation history using case-insensitive string matching.
|
|
@@ -84,19 +44,19 @@ def conversation_search(self: "Agent", query: str, page: Optional[int] = 0) -> O
|
|
|
84
44
|
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
|
85
45
|
# TODO: add paging by page number. currently cursor only works with strings.
|
|
86
46
|
# original: start=page * count
|
|
87
|
-
|
|
47
|
+
messages = self.message_manager.list_user_messages_for_agent(
|
|
88
48
|
agent_id=self.agent_state.id,
|
|
89
49
|
actor=self.user,
|
|
90
50
|
query_text=query,
|
|
91
51
|
limit=count,
|
|
92
52
|
)
|
|
93
|
-
total = len(
|
|
53
|
+
total = len(messages)
|
|
94
54
|
num_pages = math.ceil(total / count) - 1 # 0 index
|
|
95
|
-
if len(
|
|
55
|
+
if len(messages) == 0:
|
|
96
56
|
results_str = f"No results found."
|
|
97
57
|
else:
|
|
98
|
-
results_pref = f"Showing {len(
|
|
99
|
-
results_formatted = [
|
|
58
|
+
results_pref = f"Showing {len(messages)} of {total} results (page {page}/{num_pages}):"
|
|
59
|
+
results_formatted = [message.text for message in messages]
|
|
100
60
|
results_str = f"{results_pref} {json_dumps(results_formatted)}"
|
|
101
61
|
return results_str
|
|
102
62
|
|
|
@@ -114,6 +74,7 @@ def conversation_search_date(self: "Agent", start_date: str, end_date: str, page
|
|
|
114
74
|
str: Query result string
|
|
115
75
|
"""
|
|
116
76
|
import math
|
|
77
|
+
from datetime import datetime
|
|
117
78
|
|
|
118
79
|
from letta.constants import RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
|
119
80
|
from letta.utils import json_dumps
|
|
@@ -142,7 +103,6 @@ def conversation_search_date(self: "Agent", start_date: str, end_date: str, page
|
|
|
142
103
|
start_date=start_datetime,
|
|
143
104
|
end_date=end_datetime,
|
|
144
105
|
limit=count,
|
|
145
|
-
# start_date=start_date, end_date=end_date, limit=count, start=page * count
|
|
146
106
|
)
|
|
147
107
|
total = len(results)
|
|
148
108
|
num_pages = math.ceil(total / count) - 1 # 0 index
|
|
@@ -186,10 +146,8 @@ def archival_memory_search(self: "Agent", query: str, page: Optional[int] = 0, s
|
|
|
186
146
|
Returns:
|
|
187
147
|
str: Query result string
|
|
188
148
|
"""
|
|
189
|
-
import math
|
|
190
149
|
|
|
191
150
|
from letta.constants import RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
|
192
|
-
from letta.utils import json_dumps
|
|
193
151
|
|
|
194
152
|
if page is None or (isinstance(page, str) and page.lower().strip() == "none"):
|
|
195
153
|
page = 0
|
|
@@ -198,7 +156,7 @@ def archival_memory_search(self: "Agent", query: str, page: Optional[int] = 0, s
|
|
|
198
156
|
except:
|
|
199
157
|
raise ValueError(f"'page' argument must be an integer")
|
|
200
158
|
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
|
201
|
-
|
|
159
|
+
|
|
202
160
|
try:
|
|
203
161
|
# Get results using passage manager
|
|
204
162
|
all_results = self.agent_manager.list_passages(
|
|
@@ -207,7 +165,7 @@ def archival_memory_search(self: "Agent", query: str, page: Optional[int] = 0, s
|
|
|
207
165
|
query_text=query,
|
|
208
166
|
limit=count + start, # Request enough results to handle offset
|
|
209
167
|
embedding_config=self.agent_state.embedding_config,
|
|
210
|
-
embed_query=True
|
|
168
|
+
embed_query=True,
|
|
211
169
|
)
|
|
212
170
|
|
|
213
171
|
# Apply pagination
|
|
@@ -215,13 +173,7 @@ def archival_memory_search(self: "Agent", query: str, page: Optional[int] = 0, s
|
|
|
215
173
|
paged_results = all_results[start:end]
|
|
216
174
|
|
|
217
175
|
# Format results to match previous implementation
|
|
218
|
-
formatted_results = [
|
|
219
|
-
{
|
|
220
|
-
"timestamp": str(result.created_at),
|
|
221
|
-
"content": result.text
|
|
222
|
-
}
|
|
223
|
-
for result in paged_results
|
|
224
|
-
]
|
|
176
|
+
formatted_results = [{"timestamp": str(result.created_at), "content": result.text} for result in paged_results]
|
|
225
177
|
|
|
226
178
|
return formatted_results, len(formatted_results)
|
|
227
179
|
|
|
@@ -386,7 +386,7 @@ def generate_schema(function, name: Optional[str] = None, description: Optional[
|
|
|
386
386
|
# append the heartbeat
|
|
387
387
|
# TODO: don't hard-code
|
|
388
388
|
# TODO: if terminal, don't include this
|
|
389
|
-
if function.__name__ not in ["send_message"
|
|
389
|
+
if function.__name__ not in ["send_message"]:
|
|
390
390
|
schema["parameters"]["properties"]["request_heartbeat"] = {
|
|
391
391
|
"type": "boolean",
|
|
392
392
|
"description": "Request an immediate heartbeat after function execution. Set to `True` if you want to send a follow-up message or run a follow-up function.",
|
letta/llm_api/anthropic.py
CHANGED
|
@@ -99,16 +99,20 @@ def convert_tools_to_anthropic_format(tools: List[Tool]) -> List[dict]:
|
|
|
99
99
|
- 1 level less of nesting
|
|
100
100
|
- "parameters" -> "input_schema"
|
|
101
101
|
"""
|
|
102
|
-
|
|
102
|
+
formatted_tools = []
|
|
103
103
|
for tool in tools:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"
|
|
104
|
+
formatted_tool = {
|
|
105
|
+
"name" : tool.function.name,
|
|
106
|
+
"description" : tool.function.description,
|
|
107
|
+
"input_schema" : tool.function.parameters or {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"properties": {},
|
|
110
|
+
"required": []
|
|
109
111
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
}
|
|
113
|
+
formatted_tools.append(formatted_tool)
|
|
114
|
+
|
|
115
|
+
return formatted_tools
|
|
112
116
|
|
|
113
117
|
|
|
114
118
|
def merge_tool_results_into_user_messages(messages: List[dict]):
|
|
@@ -258,10 +262,24 @@ def convert_anthropic_response_to_chatcompletion(
|
|
|
258
262
|
),
|
|
259
263
|
)
|
|
260
264
|
]
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
elif len(response_json["content"]) == 1:
|
|
266
|
+
if response_json["content"][0]["type"] == "tool_use":
|
|
267
|
+
# function call only
|
|
268
|
+
content = None
|
|
269
|
+
tool_calls = [
|
|
270
|
+
ToolCall(
|
|
271
|
+
id=response_json["content"][0]["id"],
|
|
272
|
+
type="function",
|
|
273
|
+
function=FunctionCall(
|
|
274
|
+
name=response_json["content"][0]["name"],
|
|
275
|
+
arguments=json.dumps(response_json["content"][0]["input"], indent=2),
|
|
276
|
+
),
|
|
277
|
+
)
|
|
278
|
+
]
|
|
279
|
+
else:
|
|
280
|
+
# inner mono only
|
|
281
|
+
content = strip_xml_tags(string=response_json["content"][0]["text"], tag=inner_thoughts_xml_tag)
|
|
282
|
+
tool_calls = None
|
|
265
283
|
else:
|
|
266
284
|
raise RuntimeError("Unexpected type for content in response_json.")
|
|
267
285
|
|
|
@@ -323,6 +341,14 @@ def anthropic_chat_completions_request(
|
|
|
323
341
|
if anthropic_tools is not None:
|
|
324
342
|
data["tools"] = anthropic_tools
|
|
325
343
|
|
|
344
|
+
# TODO: Add support for other tool_choice options like "auto", "any"
|
|
345
|
+
if len(anthropic_tools) == 1:
|
|
346
|
+
data["tool_choice"] = {
|
|
347
|
+
"type": "tool", # Changed from "function" to "tool"
|
|
348
|
+
"name": anthropic_tools[0]["name"], # Directly specify name without nested "function" object
|
|
349
|
+
"disable_parallel_tool_use": True # Force single tool use
|
|
350
|
+
}
|
|
351
|
+
|
|
326
352
|
# Move 'system' to the top level
|
|
327
353
|
# 'messages: Unexpected role "system". The Messages API accepts a top-level `system` parameter, not "system" as an input message role.'
|
|
328
354
|
assert data["messages"][0]["role"] == "system", f"Expected 'system' role in messages[0]:\n{data['messages'][0]}"
|
|
@@ -358,7 +384,6 @@ def anthropic_chat_completions_request(
|
|
|
358
384
|
data.pop("top_p", None)
|
|
359
385
|
data.pop("presence_penalty", None)
|
|
360
386
|
data.pop("user", None)
|
|
361
|
-
data.pop("tool_choice", None)
|
|
362
387
|
|
|
363
388
|
response_json = make_post_request(url, headers, data)
|
|
364
389
|
return convert_anthropic_response_to_chatcompletion(response_json=response_json, inner_thoughts_xml_tag=inner_thoughts_xml_tag)
|
letta/llm_api/llm_api_tools.py
CHANGED
|
@@ -113,6 +113,7 @@ def create(
|
|
|
113
113
|
function_call: str = "auto",
|
|
114
114
|
# hint
|
|
115
115
|
first_message: bool = False,
|
|
116
|
+
force_tool_call: Optional[str] = None, # Force a specific tool to be called
|
|
116
117
|
# use tool naming?
|
|
117
118
|
# if false, will use deprecated 'functions' style
|
|
118
119
|
use_tool_naming: bool = True,
|
|
@@ -252,6 +253,16 @@ def create(
|
|
|
252
253
|
if not use_tool_naming:
|
|
253
254
|
raise NotImplementedError("Only tool calling supported on Anthropic API requests")
|
|
254
255
|
|
|
256
|
+
tool_call = None
|
|
257
|
+
if force_tool_call is not None:
|
|
258
|
+
tool_call = {
|
|
259
|
+
"type": "function",
|
|
260
|
+
"function": {
|
|
261
|
+
"name": force_tool_call
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
assert functions is not None
|
|
265
|
+
|
|
255
266
|
return anthropic_chat_completions_request(
|
|
256
267
|
url=llm_config.model_endpoint,
|
|
257
268
|
api_key=model_settings.anthropic_api_key,
|
|
@@ -259,7 +270,7 @@ def create(
|
|
|
259
270
|
model=llm_config.model,
|
|
260
271
|
messages=[cast_message_to_subtype(m.to_openai_dict()) for m in messages],
|
|
261
272
|
tools=[{"type": "function", "function": f} for f in functions] if functions else None,
|
|
262
|
-
|
|
273
|
+
tool_choice=tool_call,
|
|
263
274
|
# user=str(user_id),
|
|
264
275
|
# NOTE: max_tokens is required for Anthropic API
|
|
265
276
|
max_tokens=1024, # TODO make dynamic
|
letta/orm/errors.py
CHANGED
|
@@ -12,3 +12,11 @@ class UniqueConstraintViolationError(ValueError):
|
|
|
12
12
|
|
|
13
13
|
class ForeignKeyConstraintViolationError(ValueError):
|
|
14
14
|
"""Custom exception for foreign key constraint violations."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DatabaseTimeoutError(Exception):
|
|
18
|
+
"""Custom exception for database timeout issues."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, message="Database operation timed out", original_exception=None):
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.original_exception = original_exception
|