letta-nightly 0.5.5.dev20241122170833__py3-none-any.whl → 0.6.0.dev20241204051808__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 (70) hide show
  1. letta/__init__.py +2 -2
  2. letta/agent.py +155 -166
  3. letta/agent_store/chroma.py +2 -0
  4. letta/agent_store/db.py +1 -1
  5. letta/cli/cli.py +12 -8
  6. letta/cli/cli_config.py +1 -1
  7. letta/client/client.py +765 -137
  8. letta/config.py +2 -2
  9. letta/constants.py +10 -14
  10. letta/errors.py +12 -0
  11. letta/functions/function_sets/base.py +38 -1
  12. letta/functions/functions.py +40 -57
  13. letta/functions/helpers.py +0 -4
  14. letta/functions/schema_generator.py +279 -18
  15. letta/helpers/tool_rule_solver.py +6 -5
  16. letta/llm_api/helpers.py +99 -5
  17. letta/llm_api/openai.py +8 -2
  18. letta/local_llm/utils.py +13 -6
  19. letta/log.py +7 -9
  20. letta/main.py +1 -1
  21. letta/metadata.py +53 -38
  22. letta/o1_agent.py +1 -4
  23. letta/orm/__init__.py +2 -0
  24. letta/orm/block.py +7 -3
  25. letta/orm/blocks_agents.py +32 -0
  26. letta/orm/errors.py +8 -0
  27. letta/orm/mixins.py +8 -0
  28. letta/orm/organization.py +8 -1
  29. letta/orm/sandbox_config.py +56 -0
  30. letta/orm/sqlalchemy_base.py +68 -10
  31. letta/persistence_manager.py +1 -0
  32. letta/schemas/agent.py +57 -52
  33. letta/schemas/block.py +85 -26
  34. letta/schemas/blocks_agents.py +32 -0
  35. letta/schemas/enums.py +14 -0
  36. letta/schemas/letta_base.py +10 -1
  37. letta/schemas/letta_request.py +11 -23
  38. letta/schemas/letta_response.py +1 -2
  39. letta/schemas/memory.py +41 -76
  40. letta/schemas/message.py +3 -3
  41. letta/schemas/sandbox_config.py +114 -0
  42. letta/schemas/tool.py +37 -1
  43. letta/schemas/tool_rule.py +13 -5
  44. letta/server/rest_api/app.py +5 -4
  45. letta/server/rest_api/interface.py +12 -19
  46. letta/server/rest_api/routers/openai/assistants/threads.py +2 -3
  47. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -2
  48. letta/server/rest_api/routers/v1/__init__.py +4 -9
  49. letta/server/rest_api/routers/v1/agents.py +145 -61
  50. letta/server/rest_api/routers/v1/blocks.py +50 -5
  51. letta/server/rest_api/routers/v1/sandbox_configs.py +127 -0
  52. letta/server/rest_api/routers/v1/sources.py +8 -1
  53. letta/server/rest_api/routers/v1/tools.py +139 -13
  54. letta/server/rest_api/utils.py +6 -0
  55. letta/server/server.py +397 -340
  56. letta/server/static_files/assets/index-9fa459a2.js +1 -1
  57. letta/services/block_manager.py +23 -2
  58. letta/services/blocks_agents_manager.py +106 -0
  59. letta/services/per_agent_lock_manager.py +18 -0
  60. letta/services/sandbox_config_manager.py +256 -0
  61. letta/services/tool_execution_sandbox.py +352 -0
  62. letta/services/tool_manager.py +16 -22
  63. letta/services/tool_sandbox_env/.gitkeep +0 -0
  64. letta/settings.py +4 -0
  65. letta/utils.py +0 -7
  66. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/METADATA +8 -6
  67. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/RECORD +70 -60
  68. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/LICENSE +0 -0
  69. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/WHEEL +0 -0
  70. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/entry_points.txt +0 -0
letta/__init__.py CHANGED
@@ -1,10 +1,10 @@
1
- __version__ = "0.5.5"
1
+ __version__ = "0.6.0"
2
2
 
3
3
  # import clients
4
4
  from letta.client.client import LocalClient, RESTClient, create_client
5
5
 
6
6
  # imports for easier access
7
- from letta.schemas.agent import AgentState
7
+ from letta.schemas.agent import AgentState, PersistedAgentState
8
8
  from letta.schemas.block import Block
9
9
  from letta.schemas.embedding_config import EmbeddingConfig
10
10
  from letta.schemas.enums import JobStatus
letta/agent.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import datetime
2
2
  import inspect
3
+ import time
3
4
  import traceback
4
5
  import warnings
5
6
  from abc import ABC, abstractmethod
@@ -9,6 +10,7 @@ from tqdm import tqdm
9
10
 
10
11
  from letta.agent_store.storage import StorageConnector
11
12
  from letta.constants import (
13
+ BASE_TOOLS,
12
14
  CLI_WARNING_PREFIX,
13
15
  FIRST_MESSAGE_ATTEMPTS,
14
16
  FUNC_FAILED_HEARTBEAT_MESSAGE,
@@ -30,7 +32,7 @@ from letta.metadata import MetadataStore
30
32
  from letta.orm import User
31
33
  from letta.persistence_manager import LocalStateManager
32
34
  from letta.schemas.agent import AgentState, AgentStepResponse
33
- from letta.schemas.block import Block
35
+ from letta.schemas.block import BlockUpdate
34
36
  from letta.schemas.embedding_config import EmbeddingConfig
35
37
  from letta.schemas.enums import MessageRole
36
38
  from letta.schemas.memory import ContextWindowOverview, Memory
@@ -49,6 +51,7 @@ from letta.schemas.tool_rule import TerminalToolRule
49
51
  from letta.schemas.usage import LettaUsageStatistics
50
52
  from letta.services.block_manager import BlockManager
51
53
  from letta.services.source_manager import SourceManager
54
+ from letta.services.tool_execution_sandbox import ToolExecutionSandbox
52
55
  from letta.services.user_manager import UserManager
53
56
  from letta.streaming_interface import StreamingRefreshCLIInterface
54
57
  from letta.system import (
@@ -233,11 +236,8 @@ class Agent(BaseAgent):
233
236
  def __init__(
234
237
  self,
235
238
  interface: Optional[Union[AgentInterface, StreamingRefreshCLIInterface]],
236
- # agents can be created from providing agent_state
237
- agent_state: AgentState,
238
- tools: List[Tool],
239
+ agent_state: AgentState, # in-memory representation of the agent state (read from multiple tables)
239
240
  user: User,
240
- # memory: Memory,
241
241
  # extras
242
242
  messages_total: Optional[int] = None, # TODO remove?
243
243
  first_message_verify_mono: bool = True, # TODO move to config?
@@ -251,7 +251,7 @@ class Agent(BaseAgent):
251
251
  self.user = user
252
252
 
253
253
  # link tools
254
- self.link_tools(tools)
254
+ self.link_tools(agent_state.tools)
255
255
 
256
256
  # initialize a tool rules solver
257
257
  if agent_state.tool_rules:
@@ -263,26 +263,14 @@ class Agent(BaseAgent):
263
263
  # add default rule for having send_message be a terminal tool
264
264
  if agent_state.tool_rules is None:
265
265
  agent_state.tool_rules = []
266
- # Define the rule to add
267
- send_message_terminal_rule = TerminalToolRule(tool_name="send_message")
268
- # Check if an equivalent rule is already present
269
- if not any(
270
- isinstance(rule, TerminalToolRule) and rule.tool_name == send_message_terminal_rule.tool_name for rule in agent_state.tool_rules
271
- ):
272
- agent_state.tool_rules.append(send_message_terminal_rule)
273
266
 
274
267
  self.tool_rules_solver = ToolRulesSolver(tool_rules=agent_state.tool_rules)
275
268
 
276
269
  # gpt-4, gpt-3.5-turbo, ...
277
270
  self.model = self.agent_state.llm_config.model
278
271
 
279
- # Store the system instructions (used to rebuild memory)
280
- self.system = self.agent_state.system
281
-
282
- # Initialize the memory object
283
- self.memory = self.agent_state.memory
284
- assert isinstance(self.memory, Memory), f"Memory object is not of type Memory: {type(self.memory)}"
285
- printd("Initialized memory object", self.memory.compile())
272
+ # state managers
273
+ self.block_manager = BlockManager()
286
274
 
287
275
  # Interface must implement:
288
276
  # - internal_monologue
@@ -320,8 +308,8 @@ class Agent(BaseAgent):
320
308
  # Generate a sequence of initial messages to put in the buffer
321
309
  init_messages = initialize_message_sequence(
322
310
  model=self.model,
323
- system=self.system,
324
- memory=self.memory,
311
+ system=self.agent_state.system,
312
+ memory=self.agent_state.memory,
325
313
  archival_memory=None,
326
314
  recall_memory=None,
327
315
  memory_edit_timestamp=get_utc_time(),
@@ -343,8 +331,8 @@ class Agent(BaseAgent):
343
331
  # Basic "more human than human" initial message sequence
344
332
  init_messages = initialize_message_sequence(
345
333
  model=self.model,
346
- system=self.system,
347
- memory=self.memory,
334
+ system=self.agent_state.system,
335
+ memory=self.agent_state.memory,
348
336
  archival_memory=None,
349
337
  recall_memory=None,
350
338
  memory_edit_timestamp=get_utc_time(),
@@ -378,6 +366,76 @@ class Agent(BaseAgent):
378
366
  # Create the agent in the DB
379
367
  self.update_state()
380
368
 
369
+ def update_memory_if_change(self, new_memory: Memory) -> bool:
370
+ """
371
+ Update internal memory object and system prompt if there have been modifications.
372
+
373
+ Args:
374
+ new_memory (Memory): the new memory object to compare to the current memory object
375
+
376
+ Returns:
377
+ modified (bool): whether the memory was updated
378
+ """
379
+ if self.agent_state.memory.compile() != new_memory.compile():
380
+ # update the blocks (LRW) in the DB
381
+ for label in self.agent_state.memory.list_block_labels():
382
+ updated_value = new_memory.get_block(label).value
383
+ if updated_value != self.agent_state.memory.get_block(label).value:
384
+ # update the block if it's changed
385
+ block_id = self.agent_state.memory.get_block(label).id
386
+ block = self.block_manager.update_block(
387
+ block_id=block_id, block_update=BlockUpdate(value=updated_value), actor=self.user
388
+ )
389
+
390
+ # refresh memory from DB (using block ids)
391
+ self.agent_state.memory = Memory(
392
+ blocks=[self.block_manager.get_block_by_id(block.id, actor=self.user) for block in self.agent_state.memory.get_blocks()]
393
+ )
394
+
395
+ # NOTE: don't do this since re-buildin the memory is handled at the start of the step
396
+ # rebuild memory - this records the last edited timestamp of the memory
397
+ # TODO: pass in update timestamp from block edit time
398
+ self.rebuild_system_prompt()
399
+
400
+ return True
401
+ return False
402
+
403
+ def execute_tool_and_persist_state(self, function_name, function_to_call, function_args):
404
+ """
405
+ Execute tool modifications and persist the state of the agent.
406
+ Note: only some agent state modifications will be persisted, such as data in the AgentState ORM and block data
407
+ """
408
+ # TODO: add agent manager here
409
+ orig_memory_str = self.agent_state.memory.compile()
410
+
411
+ # TODO: need to have an AgentState object that actually has full access to the block data
412
+ # this is because the sandbox tools need to be able to access block.value to edit this data
413
+ try:
414
+ if function_name in BASE_TOOLS:
415
+ # base tools are allowed to access the `Agent` object and run on the database
416
+ function_args["self"] = self # need to attach self to arg since it's dynamically linked
417
+ function_response = function_to_call(**function_args)
418
+ else:
419
+ # execute tool in a sandbox
420
+ # TODO: allow agent_state to specify which sandbox to execute tools in
421
+ sandbox_run_result = ToolExecutionSandbox(function_name, function_args, self.agent_state.user_id).run(
422
+ agent_state=self.agent_state.__deepcopy__()
423
+ )
424
+ function_response, updated_agent_state = sandbox_run_result.func_return, sandbox_run_result.agent_state
425
+ assert orig_memory_str == self.agent_state.memory.compile(), "Memory should not be modified in a sandbox tool"
426
+ self.update_memory_if_change(updated_agent_state.memory)
427
+ except Exception as e:
428
+ # Need to catch error here, or else trunction wont happen
429
+ # TODO: modify to function execution error
430
+ from letta.constants import MAX_ERROR_MESSAGE_CHAR_LIMIT
431
+
432
+ error_msg = f"Error executing tool {function_name}: {e}"
433
+ if len(error_msg) > MAX_ERROR_MESSAGE_CHAR_LIMIT:
434
+ error_msg = error_msg[:MAX_ERROR_MESSAGE_CHAR_LIMIT]
435
+ raise ValueError(error_msg)
436
+
437
+ return function_response
438
+
381
439
  @property
382
440
  def messages(self) -> List[dict]:
383
441
  """Getter method that converts the internal Message list into OpenAI-style dicts"""
@@ -390,16 +448,6 @@ class Agent(BaseAgent):
390
448
  def link_tools(self, tools: List[Tool]):
391
449
  """Bind a tool object (schema + python function) to the agent object"""
392
450
 
393
- # tools
394
- for tool in tools:
395
- assert tool, f"Tool is None - must be error in querying tool from DB"
396
- assert tool.name in self.agent_state.tools, f"Tool {tool} not found in agent_state.tools"
397
- for tool_name in self.agent_state.tools:
398
- assert tool_name in [tool.name for tool in tools], f"Tool name {tool_name} not included in agent tool list"
399
-
400
- # Update tools
401
- self.tools = tools
402
-
403
451
  # Store the functions schemas (this is passed as an argument to ChatCompletion)
404
452
  self.functions = []
405
453
  self.functions_python = {}
@@ -414,9 +462,8 @@ class Agent(BaseAgent):
414
462
  exec(tool.source_code, env)
415
463
  self.functions_python[tool.json_schema["name"]] = env[tool.json_schema["name"]]
416
464
  self.functions.append(tool.json_schema)
417
- except Exception as e:
465
+ except Exception:
418
466
  warnings.warn(f"WARNING: tool {tool.name} failed to link")
419
- print(e)
420
467
  assert all([callable(f) for k, f in self.functions_python.items()]), self.functions_python
421
468
 
422
469
  def _load_messages_from_recall(self, message_ids: List[str]) -> List[Message]:
@@ -520,60 +567,60 @@ class Agent(BaseAgent):
520
567
  self,
521
568
  message_sequence: List[Message],
522
569
  function_call: str = "auto",
523
- first_message: bool = False, # hint
570
+ first_message: bool = False,
524
571
  stream: bool = False, # TODO move to config?
525
- fail_on_empty_response: bool = False,
526
572
  empty_response_retry_limit: int = 3,
573
+ backoff_factor: float = 0.5, # delay multiplier for exponential backoff
574
+ max_delay: float = 10.0, # max delay between retries
527
575
  ) -> ChatCompletionResponse:
528
- """Get response from LLM API"""
529
- # Get the allowed tools based on the ToolRulesSolver state
576
+ """Get response from LLM API with robust retry mechanism."""
577
+
530
578
  allowed_tool_names = self.tool_rules_solver.get_allowed_tool_names()
579
+ allowed_functions = (
580
+ self.functions if not allowed_tool_names else [func for func in self.functions if func["name"] in allowed_tool_names]
581
+ )
531
582
 
532
- if not allowed_tool_names:
533
- # if it's empty, any available tools are fair game
534
- allowed_functions = self.functions
535
- else:
536
- allowed_functions = [func for func in self.functions if func["name"] in allowed_tool_names]
583
+ for attempt in range(1, empty_response_retry_limit + 1):
584
+ try:
585
+ response = create(
586
+ llm_config=self.agent_state.llm_config,
587
+ messages=message_sequence,
588
+ user_id=self.agent_state.user_id,
589
+ functions=allowed_functions,
590
+ functions_python=self.functions_python,
591
+ function_call=function_call,
592
+ first_message=first_message,
593
+ stream=stream,
594
+ stream_interface=self.interface,
595
+ )
537
596
 
538
- try:
539
- response = create(
540
- # agent_state=self.agent_state,
541
- llm_config=self.agent_state.llm_config,
542
- messages=message_sequence,
543
- user_id=self.agent_state.user_id,
544
- functions=allowed_functions,
545
- functions_python=self.functions_python,
546
- function_call=function_call,
547
- # hint
548
- first_message=first_message,
549
- # streaming
550
- stream=stream,
551
- stream_interface=self.interface,
552
- )
597
+ # These bottom two are retryable
598
+ if len(response.choices) == 0 or response.choices[0] is None:
599
+ raise ValueError(f"API call returned an empty message: {response}")
553
600
 
554
- if len(response.choices) == 0 or response.choices[0] is None:
555
- empty_api_err_message = f"API call didn't return a message: {response}"
556
- if fail_on_empty_response or empty_response_retry_limit == 0:
557
- raise Exception(empty_api_err_message)
558
- else:
559
- # Decrement retry limit and try again
560
- warnings.warn(empty_api_err_message)
561
- return self._get_ai_reply(
562
- message_sequence, function_call, first_message, stream, fail_on_empty_response, empty_response_retry_limit - 1
563
- )
601
+ if response.choices[0].finish_reason not in ["stop", "function_call", "tool_calls"]:
602
+ if response.choices[0].finish_reason == "length":
603
+ # This is not retryable, hence RuntimeError v.s. ValueError
604
+ raise RuntimeError("Finish reason was length (maximum context length)")
605
+ else:
606
+ raise ValueError(f"Bad finish reason from API: {response.choices[0].finish_reason}")
564
607
 
565
- # special case for 'length'
566
- if response.choices[0].finish_reason == "length":
567
- raise Exception("Finish reason was length (maximum context length)")
608
+ return response
568
609
 
569
- # catches for soft errors
570
- if response.choices[0].finish_reason not in ["stop", "function_call", "tool_calls"]:
571
- raise Exception(f"API call finish with bad finish reason: {response}")
610
+ except ValueError as ve:
611
+ if attempt >= empty_response_retry_limit:
612
+ warnings.warn(f"Retry limit reached. Final error: {ve}")
613
+ break
614
+ else:
615
+ delay = min(backoff_factor * (2 ** (attempt - 1)), max_delay)
616
+ warnings.warn(f"Attempt {attempt} failed: {ve}. Retrying in {delay} seconds...")
617
+ time.sleep(delay)
572
618
 
573
- # unpack with response.choices[0].message.content
574
- return response
575
- except Exception as e:
576
- raise e
619
+ except Exception as e:
620
+ # For non-retryable errors, exit immediately
621
+ raise e
622
+
623
+ raise Exception("Retries exhausted and no valid response received.")
577
624
 
578
625
  def _handle_ai_response(
579
626
  self,
@@ -725,9 +772,10 @@ class Agent(BaseAgent):
725
772
  if isinstance(function_args[name], dict):
726
773
  function_args[name] = spec[name](**function_args[name])
727
774
 
728
- function_args["self"] = self # need to attach self to arg since it's dynamically linked
775
+ # handle tool execution (sandbox) and state updates
776
+ function_response = self.execute_tool_and_persist_state(function_name, function_to_call, function_args)
729
777
 
730
- function_response = function_to_call(**function_args)
778
+ # handle trunction
731
779
  if function_name in ["conversation_search", "conversation_search_date", "archival_memory_search"]:
732
780
  # with certain functions we rely on the paging mechanism to handle overflow
733
781
  truncate = False
@@ -747,6 +795,7 @@ class Agent(BaseAgent):
747
795
  error_msg_user = f"{error_msg}\n{traceback.format_exc()}"
748
796
  printd(error_msg_user)
749
797
  function_response = package_function_response(False, error_msg)
798
+ # TODO: truncate error message somehow
750
799
  messages.append(
751
800
  Message.dict_to_message(
752
801
  agent_id=self.agent_state.id,
@@ -799,7 +848,7 @@ class Agent(BaseAgent):
799
848
 
800
849
  # rebuild memory
801
850
  # TODO: @charles please check this
802
- self.rebuild_memory()
851
+ self.rebuild_system_prompt()
803
852
 
804
853
  # Update ToolRulesSolver state with last called function
805
854
  self.tool_rules_solver.update_tool_usage(function_name)
@@ -915,17 +964,10 @@ class Agent(BaseAgent):
915
964
 
916
965
  # Step 0: update core memory
917
966
  # only pulling latest block data if shared memory is being used
918
- # TODO: ensure we're passing in metadata store from all surfaces
919
- if ms is not None:
920
- should_update = False
921
- for block in self.agent_state.memory.to_dict()["memory"].values():
922
- if not block.get("template", False):
923
- should_update = True
924
- if should_update:
925
- # TODO: the force=True can be optimized away
926
- # once we ensure we're correctly comparing whether in-memory core
927
- # data is different than persisted core data.
928
- self.rebuild_memory(force=True, ms=ms)
967
+ current_persisted_memory = Memory(
968
+ blocks=[self.block_manager.get_block_by_id(block.id, actor=self.user) for block in self.agent_state.memory.get_blocks()]
969
+ ) # read blocks from DB
970
+ self.update_memory_if_change(current_persisted_memory)
929
971
 
930
972
  # Step 1: add user message
931
973
  if isinstance(messages, Message):
@@ -1208,43 +1250,10 @@ class Agent(BaseAgent):
1208
1250
  new_messages = [new_system_message_obj] + self._messages[1:] # swap index 0 (system)
1209
1251
  self._messages = new_messages
1210
1252
 
1211
- def update_memory_blocks_from_db(self):
1212
- for block in self.memory.to_dict()["memory"].values():
1213
- if block.get("templates", False):
1214
- # we don't expect to update shared memory blocks that
1215
- # are templates. this is something we could update in the
1216
- # future if we expect templates to change often.
1217
- continue
1218
- block_id = block.get("id")
1219
-
1220
- # TODO: This is really hacky and we should probably figure out how to
1221
- db_block = BlockManager().get_block_by_id(block_id=block_id, actor=self.user)
1222
- if db_block is None:
1223
- # this case covers if someone has deleted a shared block by interacting
1224
- # with some other agent.
1225
- # in that case we should remove this shared block from the agent currently being
1226
- # evaluated.
1227
- printd(f"removing block: {block_id=}")
1228
- continue
1229
- if not isinstance(db_block.value, str):
1230
- printd(f"skipping block update, unexpected value: {block_id=}")
1231
- continue
1232
- # TODO: we may want to update which columns we're updating from shared memory e.g. the limit
1233
- self.memory.update_block_value(label=block.get("label", ""), value=db_block.value)
1234
-
1235
- def rebuild_memory(self, force=False, update_timestamp=True, ms: Optional[MetadataStore] = None):
1253
+ def rebuild_system_prompt(self, force=False, update_timestamp=True):
1236
1254
  """Rebuilds the system message with the latest memory object and any shared memory block updates"""
1237
1255
  curr_system_message = self.messages[0] # this is the system + memory bank, not just the system prompt
1238
1256
 
1239
- # NOTE: This is a hacky way to check if the memory has changed
1240
- memory_repr = self.memory.compile()
1241
- if not force and memory_repr == curr_system_message["content"][-(len(memory_repr)) :]:
1242
- printd(f"Memory has not changed, not rebuilding system")
1243
- return
1244
-
1245
- if ms:
1246
- self.update_memory_blocks_from_db()
1247
-
1248
1257
  # If the memory didn't update, we probably don't want to update the timestamp inside
1249
1258
  # For example, if we're doing a system prompt swap, this should probably be False
1250
1259
  if update_timestamp:
@@ -1255,8 +1264,8 @@ class Agent(BaseAgent):
1255
1264
 
1256
1265
  # update memory (TODO: potentially update recall/archival stats seperately)
1257
1266
  new_system_message_str = compile_system_message(
1258
- system_prompt=self.system,
1259
- in_context_memory=self.memory,
1267
+ system_prompt=self.agent_state.system,
1268
+ in_context_memory=self.agent_state.memory,
1260
1269
  in_context_memory_last_edit=memory_edit_timestamp,
1261
1270
  archival_memory=self.persistence_manager.archival_memory,
1262
1271
  recall_memory=self.persistence_manager.recall_memory,
@@ -1283,14 +1292,13 @@ class Agent(BaseAgent):
1283
1292
  """Update the system prompt of the agent (requires rebuilding the memory block if there's a difference)"""
1284
1293
  assert isinstance(new_system_prompt, str)
1285
1294
 
1286
- if new_system_prompt == self.system:
1287
- input("same???")
1295
+ if new_system_prompt == self.agent_state.system:
1288
1296
  return
1289
1297
 
1290
- self.system = new_system_prompt
1298
+ self.agent_state.system = new_system_prompt
1291
1299
 
1292
1300
  # updating the system prompt requires rebuilding the memory block inside the compiled system message
1293
- self.rebuild_memory(force=True, update_timestamp=False)
1301
+ self.rebuild_system_prompt(force=True, update_timestamp=False)
1294
1302
 
1295
1303
  # make sure to persist the change
1296
1304
  _ = self.update_state()
@@ -1304,13 +1312,16 @@ class Agent(BaseAgent):
1304
1312
  raise NotImplementedError
1305
1313
 
1306
1314
  def update_state(self) -> AgentState:
1315
+ # TODO: this should be removed and self._messages should be moved into self.agent_state.in_context_messages
1307
1316
  message_ids = [msg.id for msg in self._messages]
1308
- assert isinstance(self.memory, Memory), f"Memory is not a Memory object: {type(self.memory)}"
1317
+
1318
+ # Assert that these are all strings
1319
+ if any(not isinstance(m_id, str) for m_id in message_ids):
1320
+ warnings.warn(f"Non-string message IDs found in agent state: {message_ids}")
1321
+ message_ids = [m_id for m_id in message_ids if isinstance(m_id, str)]
1309
1322
 
1310
1323
  # override any fields that may have been updated
1311
1324
  self.agent_state.message_ids = message_ids
1312
- self.agent_state.memory = self.memory
1313
- self.agent_state.system = self.system
1314
1325
 
1315
1326
  return self.agent_state
1316
1327
 
@@ -1511,7 +1522,7 @@ class Agent(BaseAgent):
1511
1522
 
1512
1523
  system_prompt = self.agent_state.system # TODO is this the current system or the initial system?
1513
1524
  num_tokens_system = count_tokens(system_prompt)
1514
- core_memory = self.memory.compile()
1525
+ core_memory = self.agent_state.memory.compile()
1515
1526
  num_tokens_core_memory = count_tokens(core_memory)
1516
1527
 
1517
1528
  # conversion of messages to OpenAI dict format, which is passed to the token counter
@@ -1603,37 +1614,15 @@ def save_agent(agent: Agent, ms: MetadataStore):
1603
1614
 
1604
1615
  agent.update_state()
1605
1616
  agent_state = agent.agent_state
1606
- agent_id = agent_state.id
1607
1617
  assert isinstance(agent_state.memory, Memory), f"Memory is not a Memory object: {type(agent_state.memory)}"
1608
1618
 
1609
- # NOTE: we're saving agent memory before persisting the agent to ensure
1610
- # that allocated block_ids for each memory block are present in the agent model
1611
- save_agent_memory(agent=agent)
1612
-
1613
- if ms.get_agent(agent_id=agent.agent_state.id):
1614
- ms.update_agent(agent_state)
1619
+ # TODO: move this to agent manager
1620
+ # convert to persisted model
1621
+ persisted_agent_state = agent.agent_state.to_persisted_agent_state()
1622
+ if ms.get_agent(agent_id=persisted_agent_state.id):
1623
+ ms.update_agent(persisted_agent_state)
1615
1624
  else:
1616
- ms.create_agent(agent_state)
1617
-
1618
- agent.agent_state = ms.get_agent(agent_id=agent_id)
1619
- assert isinstance(agent.agent_state.memory, Memory), f"Memory is not a Memory object: {type(agent_state.memory)}"
1620
-
1621
-
1622
- def save_agent_memory(agent: Agent):
1623
- """
1624
- Save agent memory to metadata store. Memory is a collection of blocks and each block is persisted to the block table.
1625
-
1626
- NOTE: we are assuming agent.update_state has already been called.
1627
- """
1628
-
1629
- for block_dict in agent.memory.to_dict()["memory"].values():
1630
- # TODO: block creation should happen in one place to enforce these sort of constraints consistently.
1631
- block = Block(**block_dict)
1632
- # FIXME: should we expect for block values to be None? If not, we need to figure out why that is
1633
- # the case in some tests, if so we should relax the DB constraint.
1634
- if block.value is None:
1635
- block.value = ""
1636
- BlockManager().create_or_update_block(block, actor=agent.user)
1625
+ ms.create_agent(persisted_agent_state)
1637
1626
 
1638
1627
 
1639
1628
  def strip_name_field_from_user_message(user_message_text: str) -> Tuple[str, Optional[str]]:
@@ -125,6 +125,8 @@ class ChromaStorageConnector(StorageConnector):
125
125
  ids, filters = self.get_filters(filters)
126
126
  if self.collection.count() == 0:
127
127
  return []
128
+ if ids == []:
129
+ ids = None
128
130
  if limit:
129
131
  results = self.collection.get(ids=ids, include=self.include, where=filters, limit=limit)
130
132
  else:
letta/agent_store/db.py CHANGED
@@ -433,7 +433,7 @@ class PostgresStorageConnector(SQLStorageConnector):
433
433
  else:
434
434
  db_record = self.db_model(**record.dict())
435
435
  session.add(db_record)
436
- print(f"Added record with id {record.id}")
436
+ # print(f"Added record with id {record.id}")
437
437
  session.commit()
438
438
 
439
439
  added_ids.append(record.id)
letta/cli/cli.py CHANGED
@@ -10,7 +10,12 @@ import letta.utils as utils
10
10
  from letta import create_client
11
11
  from letta.agent import Agent, save_agent
12
12
  from letta.config import LettaConfig
13
- from letta.constants import CLI_WARNING_PREFIX, LETTA_DIR, MIN_CONTEXT_WINDOW
13
+ from letta.constants import (
14
+ CLI_WARNING_PREFIX,
15
+ CORE_MEMORY_BLOCK_CHAR_LIMIT,
16
+ LETTA_DIR,
17
+ MIN_CONTEXT_WINDOW,
18
+ )
14
19
  from letta.local_llm.constants import ASSISTANT_MESSAGE_CLI_SYMBOL
15
20
  from letta.log import get_logger
16
21
  from letta.metadata import MetadataStore
@@ -91,7 +96,7 @@ def run(
91
96
  ] = None,
92
97
  core_memory_limit: Annotated[
93
98
  Optional[int], typer.Option(help="The character limit to each core-memory section (human/persona).")
94
- ] = 2000,
99
+ ] = CORE_MEMORY_BLOCK_CHAR_LIMIT,
95
100
  # other
96
101
  first: Annotated[bool, typer.Option(help="Use --first to send the first message in the sequence")] = False,
97
102
  strip_ui: Annotated[bool, typer.Option(help="Remove all the bells and whistles in CLI output (helpful for testing)")] = False,
@@ -219,8 +224,9 @@ def run(
219
224
  )
220
225
 
221
226
  # create agent
222
- tools = [server.tool_manager.get_tool_by_name(tool_name=tool_name, actor=client.user) for tool_name in agent_state.tools]
223
- letta_agent = Agent(agent_state=agent_state, interface=interface(), tools=tools, user=client.user)
227
+ tools = [server.tool_manager.get_tool_by_name(tool_name=tool_name, actor=client.user) for tool_name in agent_state.tool_names]
228
+ agent_state.tools = tools
229
+ letta_agent = Agent(agent_state=agent_state, interface=interface(), user=client.user)
224
230
 
225
231
  else: # create new agent
226
232
  # create new agent config: override defaults with args if provided
@@ -311,13 +317,11 @@ def run(
311
317
  metadata=metadata,
312
318
  )
313
319
  assert isinstance(agent_state.memory, Memory), f"Expected Memory, got {type(agent_state.memory)}"
314
- typer.secho(f"-> 🛠️ {len(agent_state.tools)} tools: {', '.join([t for t in agent_state.tools])}", fg=typer.colors.WHITE)
315
- tools = [server.tool_manager.get_tool_by_name(tool_name, actor=client.user) for tool_name in agent_state.tools]
320
+ typer.secho(f"-> 🛠️ {len(agent_state.tools)} tools: {', '.join([t for t in agent_state.tool_names])}", fg=typer.colors.WHITE)
316
321
 
317
322
  letta_agent = Agent(
318
323
  interface=interface(),
319
- agent_state=agent_state,
320
- tools=tools,
324
+ agent_state=client.get_agent(agent_state.id),
321
325
  # gpt-3.5-turbo tends to omit inner monologue, relax this requirement for now
322
326
  first_message_verify_mono=True if (model is not None and "gpt-4" in model) else False,
323
327
  user=client.user,
letta/cli/cli_config.py CHANGED
@@ -136,7 +136,7 @@ def add_tool(
136
136
  func = eval(func_def.name)
137
137
 
138
138
  # 4. Add or update the tool
139
- tool = client.create_tool(func=func, name=name, tags=tags, update=update)
139
+ tool = client.create_or_update_tool(func=func, name=name, tags=tags, update=update)
140
140
  print(f"Tool {tool.name} added successfully")
141
141
 
142
142