letta-nightly 0.6.4.dev20241213193437__py3-none-any.whl → 0.6.4.dev20241215104129__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 (62) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +54 -45
  3. letta/chat_only_agent.py +6 -8
  4. letta/cli/cli.py +2 -10
  5. letta/client/client.py +121 -138
  6. letta/config.py +0 -161
  7. letta/main.py +3 -8
  8. letta/memory.py +3 -14
  9. letta/o1_agent.py +1 -5
  10. letta/offline_memory_agent.py +2 -6
  11. letta/orm/__init__.py +2 -0
  12. letta/orm/agent.py +109 -0
  13. letta/orm/agents_tags.py +10 -18
  14. letta/orm/block.py +29 -4
  15. letta/orm/blocks_agents.py +5 -11
  16. letta/orm/custom_columns.py +152 -0
  17. letta/orm/message.py +3 -38
  18. letta/orm/organization.py +2 -7
  19. letta/orm/passage.py +10 -32
  20. letta/orm/source.py +5 -25
  21. letta/orm/sources_agents.py +13 -0
  22. letta/orm/sqlalchemy_base.py +54 -30
  23. letta/orm/tool.py +1 -19
  24. letta/orm/tools_agents.py +7 -24
  25. letta/orm/user.py +3 -4
  26. letta/schemas/agent.py +48 -65
  27. letta/schemas/memory.py +2 -1
  28. letta/schemas/sandbox_config.py +12 -1
  29. letta/server/rest_api/app.py +0 -5
  30. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +1 -1
  31. letta/server/rest_api/routers/v1/agents.py +99 -78
  32. letta/server/rest_api/routers/v1/blocks.py +22 -25
  33. letta/server/rest_api/routers/v1/jobs.py +4 -4
  34. letta/server/rest_api/routers/v1/sandbox_configs.py +10 -10
  35. letta/server/rest_api/routers/v1/sources.py +12 -12
  36. letta/server/rest_api/routers/v1/tools.py +35 -15
  37. letta/server/rest_api/routers/v1/users.py +0 -46
  38. letta/server/server.py +172 -716
  39. letta/server/ws_api/server.py +0 -5
  40. letta/services/agent_manager.py +405 -0
  41. letta/services/block_manager.py +13 -21
  42. letta/services/helpers/agent_manager_helper.py +90 -0
  43. letta/services/organization_manager.py +0 -1
  44. letta/services/passage_manager.py +62 -62
  45. letta/services/sandbox_config_manager.py +3 -3
  46. letta/services/source_manager.py +22 -1
  47. letta/services/user_manager.py +11 -6
  48. letta/utils.py +2 -2
  49. {letta_nightly-0.6.4.dev20241213193437.dist-info → letta_nightly-0.6.4.dev20241215104129.dist-info}/METADATA +1 -1
  50. {letta_nightly-0.6.4.dev20241213193437.dist-info → letta_nightly-0.6.4.dev20241215104129.dist-info}/RECORD +53 -57
  51. letta/metadata.py +0 -407
  52. letta/schemas/agents_tags.py +0 -33
  53. letta/schemas/api_key.py +0 -21
  54. letta/schemas/blocks_agents.py +0 -32
  55. letta/schemas/tools_agents.py +0 -32
  56. letta/server/rest_api/routers/openai/assistants/threads.py +0 -338
  57. letta/services/agents_tags_manager.py +0 -64
  58. letta/services/blocks_agents_manager.py +0 -106
  59. letta/services/tools_agents_manager.py +0 -94
  60. {letta_nightly-0.6.4.dev20241213193437.dist-info → letta_nightly-0.6.4.dev20241215104129.dist-info}/LICENSE +0 -0
  61. {letta_nightly-0.6.4.dev20241213193437.dist-info → letta_nightly-0.6.4.dev20241215104129.dist-info}/WHEEL +0 -0
  62. {letta_nightly-0.6.4.dev20241213193437.dist-info → letta_nightly-0.6.4.dev20241215104129.dist-info}/entry_points.txt +0 -0
letta/config.py CHANGED
@@ -1,12 +1,9 @@
1
1
  import configparser
2
- import inspect
3
- import json
4
2
  import os
5
3
  from dataclasses import dataclass
6
4
  from typing import Optional
7
5
 
8
6
  import letta
9
- import letta.utils as utils
10
7
  from letta.constants import (
11
8
  CORE_MEMORY_HUMAN_CHAR_LIMIT,
12
9
  CORE_MEMORY_PERSONA_CHAR_LIMIT,
@@ -16,7 +13,6 @@ from letta.constants import (
16
13
  LETTA_DIR,
17
14
  )
18
15
  from letta.log import get_logger
19
- from letta.schemas.agent import PersistedAgentState
20
16
  from letta.schemas.embedding_config import EmbeddingConfig
21
17
  from letta.schemas.llm_config import LLMConfig
22
18
 
@@ -312,160 +308,3 @@ class LettaConfig:
312
308
  for folder in folders:
313
309
  if not os.path.exists(os.path.join(LETTA_DIR, folder)):
314
310
  os.makedirs(os.path.join(LETTA_DIR, folder))
315
-
316
-
317
- @dataclass
318
- class AgentConfig:
319
- """
320
-
321
- NOTE: this is a deprecated class, use AgentState instead. This class is only used for backcompatibility.
322
- Configuration for a specific instance of an agent
323
- """
324
-
325
- def __init__(
326
- self,
327
- persona,
328
- human,
329
- # model info
330
- model=None,
331
- model_endpoint_type=None,
332
- model_endpoint=None,
333
- model_wrapper=None,
334
- context_window=None,
335
- # embedding info
336
- embedding_endpoint_type=None,
337
- embedding_endpoint=None,
338
- embedding_model=None,
339
- embedding_dim=None,
340
- embedding_chunk_size=None,
341
- # other
342
- preset=None,
343
- data_sources=None,
344
- # agent info
345
- agent_config_path=None,
346
- name=None,
347
- create_time=None,
348
- letta_version=None,
349
- # functions
350
- functions=None, # schema definitions ONLY (linked at runtime)
351
- ):
352
-
353
- assert name, f"Agent name must be provided"
354
- self.name = name
355
-
356
- config = LettaConfig.load() # get default values
357
- self.persona = config.persona if persona is None else persona
358
- self.human = config.human if human is None else human
359
- self.preset = config.preset if preset is None else preset
360
- self.context_window = config.default_llm_config.context_window if context_window is None else context_window
361
- self.model = config.default_llm_config.model if model is None else model
362
- self.model_endpoint_type = config.default_llm_config.model_endpoint_type if model_endpoint_type is None else model_endpoint_type
363
- self.model_endpoint = config.default_llm_config.model_endpoint if model_endpoint is None else model_endpoint
364
- self.model_wrapper = config.default_llm_config.model_wrapper if model_wrapper is None else model_wrapper
365
- self.llm_config = LLMConfig(
366
- model=self.model,
367
- model_endpoint_type=self.model_endpoint_type,
368
- model_endpoint=self.model_endpoint,
369
- model_wrapper=self.model_wrapper,
370
- context_window=self.context_window,
371
- )
372
- self.embedding_endpoint_type = (
373
- config.default_embedding_config.embedding_endpoint_type if embedding_endpoint_type is None else embedding_endpoint_type
374
- )
375
- self.embedding_endpoint = config.default_embedding_config.embedding_endpoint if embedding_endpoint is None else embedding_endpoint
376
- self.embedding_model = config.default_embedding_config.embedding_model if embedding_model is None else embedding_model
377
- self.embedding_dim = config.default_embedding_config.embedding_dim if embedding_dim is None else embedding_dim
378
- self.embedding_chunk_size = (
379
- config.default_embedding_config.embedding_chunk_size if embedding_chunk_size is None else embedding_chunk_size
380
- )
381
- self.embedding_config = EmbeddingConfig(
382
- embedding_endpoint_type=self.embedding_endpoint_type,
383
- embedding_endpoint=self.embedding_endpoint,
384
- embedding_model=self.embedding_model,
385
- embedding_dim=self.embedding_dim,
386
- embedding_chunk_size=self.embedding_chunk_size,
387
- )
388
-
389
- # agent metadata
390
- self.data_sources = data_sources if data_sources is not None else []
391
- self.create_time = create_time if create_time is not None else utils.get_local_time()
392
- if letta_version is None:
393
- import letta
394
-
395
- self.letta_version = letta.__version__
396
- else:
397
- self.letta_version = letta_version
398
-
399
- # functions
400
- self.functions = functions
401
-
402
- # save agent config
403
- self.agent_config_path = (
404
- os.path.join(LETTA_DIR, "agents", self.name, "config.json") if agent_config_path is None else agent_config_path
405
- )
406
-
407
- def attach_data_source(self, data_source: str):
408
- # TODO: add warning that only once source can be attached
409
- # i.e. previous source will be overriden
410
- self.data_sources.append(data_source)
411
- self.save()
412
-
413
- def save_dir(self):
414
- return os.path.join(LETTA_DIR, "agents", self.name)
415
-
416
- def save_state_dir(self):
417
- # directory to save agent state
418
- return os.path.join(LETTA_DIR, "agents", self.name, "agent_state")
419
-
420
- def save_persistence_manager_dir(self):
421
- # directory to save persistent manager state
422
- return os.path.join(LETTA_DIR, "agents", self.name, "persistence_manager")
423
-
424
- def save_agent_index_dir(self):
425
- # save llama index inside of persistent manager directory
426
- return os.path.join(self.save_persistence_manager_dir(), "index")
427
-
428
- def save(self):
429
- # save state of persistence manager
430
- os.makedirs(os.path.join(LETTA_DIR, "agents", self.name), exist_ok=True)
431
- # save version
432
- self.letta_version = letta.__version__
433
- with open(self.agent_config_path, "w", encoding="utf-8") as f:
434
- json.dump(vars(self), f, indent=4)
435
-
436
- def to_agent_state(self):
437
- return PersistedAgentState(
438
- name=self.name,
439
- preset=self.preset,
440
- persona=self.persona,
441
- human=self.human,
442
- llm_config=self.llm_config,
443
- embedding_config=self.embedding_config,
444
- create_time=self.create_time,
445
- )
446
-
447
- @staticmethod
448
- def exists(name: str):
449
- """Check if agent config exists"""
450
- agent_config_path = os.path.join(LETTA_DIR, "agents", name)
451
- return os.path.exists(agent_config_path)
452
-
453
- @classmethod
454
- def load(cls, name: str):
455
- """Load agent config from JSON file"""
456
- agent_config_path = os.path.join(LETTA_DIR, "agents", name, "config.json")
457
- assert os.path.exists(agent_config_path), f"Agent config file does not exist at {agent_config_path}"
458
- with open(agent_config_path, "r", encoding="utf-8") as f:
459
- agent_config = json.load(f)
460
- # allow compatibility accross versions
461
- try:
462
- class_args = inspect.getargspec(cls.__init__).args
463
- except AttributeError:
464
- # https://github.com/pytorch/pytorch/issues/15344
465
- class_args = inspect.getfullargspec(cls.__init__).args
466
- agent_fields = list(agent_config.keys())
467
- for key in agent_fields:
468
- if key not in class_args:
469
- utils.printd(f"Removing missing argument {key} from agent config")
470
- del agent_config[key]
471
- return cls(**agent_config)
letta/main.py CHANGED
@@ -19,7 +19,6 @@ from letta.cli.cli_config import add, add_tool, configure, delete, list, list_to
19
19
  from letta.cli.cli_load import app as load_app
20
20
  from letta.config import LettaConfig
21
21
  from letta.constants import FUNC_FAILED_HEARTBEAT_MESSAGE, REQ_HEARTBEAT_MESSAGE
22
- from letta.metadata import MetadataStore
23
22
 
24
23
  # from letta.interface import CLIInterface as interface # for printing to terminal
25
24
  from letta.streaming_interface import AgentRefreshStreamingInterface
@@ -62,7 +61,6 @@ def run_agent_loop(
62
61
  letta_agent: agent.Agent,
63
62
  config: LettaConfig,
64
63
  first: bool,
65
- ms: MetadataStore,
66
64
  no_verify: bool = False,
67
65
  strip_ui: bool = False,
68
66
  stream: bool = False,
@@ -92,7 +90,6 @@ def run_agent_loop(
92
90
 
93
91
  # create client
94
92
  client = create_client()
95
- ms = MetadataStore(config) # TODO: remove
96
93
 
97
94
  # run loops
98
95
  while True:
@@ -130,11 +127,11 @@ def run_agent_loop(
130
127
  # updated agent save functions
131
128
  if user_input.lower() == "/exit":
132
129
  # letta_agent.save()
133
- agent.save_agent(letta_agent, ms)
130
+ agent.save_agent(letta_agent)
134
131
  break
135
132
  elif user_input.lower() == "/save" or user_input.lower() == "/savechat":
136
133
  # letta_agent.save()
137
- agent.save_agent(letta_agent, ms)
134
+ agent.save_agent(letta_agent)
138
135
  continue
139
136
  elif user_input.lower() == "/attach":
140
137
  # TODO: check if agent already has it
@@ -378,7 +375,6 @@ def run_agent_loop(
378
375
  first_message=False,
379
376
  skip_verify=no_verify,
380
377
  stream=stream,
381
- ms=ms,
382
378
  )
383
379
  else:
384
380
  step_response = letta_agent.step_user_message(
@@ -386,7 +382,6 @@ def run_agent_loop(
386
382
  first_message=False,
387
383
  skip_verify=no_verify,
388
384
  stream=stream,
389
- ms=ms,
390
385
  )
391
386
  new_messages = step_response.messages
392
387
  heartbeat_request = step_response.heartbeat_request
@@ -394,7 +389,7 @@ def run_agent_loop(
394
389
  token_warning = step_response.in_context_memory_warning
395
390
  step_response.usage
396
391
 
397
- agent.save_agent(letta_agent, ms)
392
+ agent.save_agent(letta_agent)
398
393
  skip_next_user_input = False
399
394
  if token_warning:
400
395
  user_message = system.get_token_limit_warning()
letta/memory.py CHANGED
@@ -1,23 +1,13 @@
1
- import datetime
2
- from abc import ABC, abstractmethod
3
- from typing import Callable, Dict, List, Tuple, Union
1
+ from typing import Callable, Dict, List
4
2
 
5
3
  from letta.constants import MESSAGE_SUMMARY_REQUEST_ACK, MESSAGE_SUMMARY_WARNING_FRAC
6
- from letta.embeddings import embedding_model, parse_and_chunk_text, query_embedding
7
4
  from letta.llm_api.llm_api_tools import create
8
5
  from letta.prompts.gpt_summarize import SYSTEM as SUMMARY_PROMPT_SYSTEM
9
6
  from letta.schemas.agent import AgentState
10
7
  from letta.schemas.enums import MessageRole
11
8
  from letta.schemas.memory import Memory
12
9
  from letta.schemas.message import Message
13
- from letta.schemas.passage import Passage
14
- from letta.utils import (
15
- count_tokens,
16
- extract_date_from_timestamp,
17
- get_local_time,
18
- printd,
19
- validate_date_format,
20
- )
10
+ from letta.utils import count_tokens, printd
21
11
 
22
12
 
23
13
  def get_memory_functions(cls: Memory) -> Dict[str, Callable]:
@@ -67,7 +57,6 @@ def summarize_messages(
67
57
  + message_sequence_to_summarize[cutoff:]
68
58
  )
69
59
 
70
- agent_state.user_id
71
60
  dummy_agent_id = agent_state.id
72
61
  message_sequence = []
73
62
  message_sequence.append(Message(agent_id=dummy_agent_id, role=MessageRole.system, text=summary_prompt))
@@ -79,7 +68,7 @@ def summarize_messages(
79
68
  llm_config_no_inner_thoughts.put_inner_thoughts_in_kwargs = False
80
69
  response = create(
81
70
  llm_config=llm_config_no_inner_thoughts,
82
- user_id=agent_state.user_id,
71
+ user_id=agent_state.created_by_id,
83
72
  messages=message_sequence,
84
73
  stream=False,
85
74
  )
letta/o1_agent.py CHANGED
@@ -2,7 +2,6 @@ from typing import List, Optional, Union
2
2
 
3
3
  from letta.agent import Agent, save_agent
4
4
  from letta.interface import AgentInterface
5
- from letta.metadata import MetadataStore
6
5
  from letta.schemas.agent import AgentState
7
6
  from letta.schemas.message import Message
8
7
  from letta.schemas.openai.chat_completion_response import UsageStatistics
@@ -56,7 +55,6 @@ class O1Agent(Agent):
56
55
  messages: Union[Message, List[Message]],
57
56
  chaining: bool = True,
58
57
  max_chaining_steps: Optional[int] = None,
59
- ms: Optional[MetadataStore] = None,
60
58
  **kwargs,
61
59
  ) -> LettaUsageStatistics:
62
60
  """Run Agent.inner_step in a loop, terminate when final thinking message is sent or max_thinking_steps is reached"""
@@ -70,7 +68,6 @@ class O1Agent(Agent):
70
68
  if counter > 0:
71
69
  next_input_message = []
72
70
 
73
- kwargs["ms"] = ms
74
71
  kwargs["first_message"] = False
75
72
  step_response = self.inner_step(
76
73
  messages=next_input_message,
@@ -84,7 +81,6 @@ class O1Agent(Agent):
84
81
  # check if it is final thinking message
85
82
  if step_response.messages[-1].name == "send_final_message":
86
83
  break
87
- if ms:
88
- save_agent(self, ms)
84
+ save_agent(self)
89
85
 
90
86
  return LettaUsageStatistics(**total_usage.model_dump(), step_count=step_count)
@@ -2,7 +2,6 @@ from typing import List, Optional, Union
2
2
 
3
3
  from letta.agent import Agent, AgentState, save_agent
4
4
  from letta.interface import AgentInterface
5
- from letta.metadata import MetadataStore
6
5
  from letta.orm import User
7
6
  from letta.schemas.message import Message
8
7
  from letta.schemas.openai.chat_completion_response import UsageStatistics
@@ -130,7 +129,7 @@ class OfflineMemoryAgent(Agent):
130
129
  # extras
131
130
  first_message_verify_mono: bool = False,
132
131
  max_memory_rethinks: int = 10,
133
- initial_message_sequence: Optional[List[Message]] = None,
132
+ initial_message_sequence: Optional[List[Message]] = None,
134
133
  ):
135
134
  super().__init__(interface, agent_state, user, initial_message_sequence=initial_message_sequence)
136
135
  self.first_message_verify_mono = first_message_verify_mono
@@ -141,7 +140,6 @@ class OfflineMemoryAgent(Agent):
141
140
  messages: Union[Message, List[Message]],
142
141
  chaining: bool = True,
143
142
  max_chaining_steps: Optional[int] = None,
144
- ms: Optional[MetadataStore] = None,
145
143
  **kwargs,
146
144
  ) -> LettaUsageStatistics:
147
145
  """Go through what is currently in memory core memory and integrate information."""
@@ -153,7 +151,6 @@ class OfflineMemoryAgent(Agent):
153
151
  while counter < self.max_memory_rethinks:
154
152
  if counter > 0:
155
153
  next_input_message = []
156
- kwargs["ms"] = ms
157
154
  kwargs["first_message"] = False
158
155
  step_response = self.inner_step(
159
156
  messages=next_input_message,
@@ -172,7 +169,6 @@ class OfflineMemoryAgent(Agent):
172
169
  counter += 1
173
170
  self.interface.step_complete()
174
171
 
175
- if ms:
176
- save_agent(self, ms)
172
+ save_agent(self)
177
173
 
178
174
  return LettaUsageStatistics(**total_usage.model_dump(), step_count=step_count)
letta/orm/__init__.py CHANGED
@@ -1,3 +1,4 @@
1
+ from letta.orm.agent import Agent
1
2
  from letta.orm.agents_tags import AgentsTags
2
3
  from letta.orm.base import Base
3
4
  from letta.orm.block import Block
@@ -9,6 +10,7 @@ from letta.orm.organization import Organization
9
10
  from letta.orm.passage import Passage
10
11
  from letta.orm.sandbox_config import SandboxConfig, SandboxEnvironmentVariable
11
12
  from letta.orm.source import Source
13
+ from letta.orm.sources_agents import SourcesAgents
12
14
  from letta.orm.tool import Tool
13
15
  from letta.orm.tools_agents import ToolsAgents
14
16
  from letta.orm.user import User
letta/orm/agent.py ADDED
@@ -0,0 +1,109 @@
1
+ import uuid
2
+ from typing import TYPE_CHECKING, List, Optional
3
+
4
+ from sqlalchemy import JSON, String, UniqueConstraint
5
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
6
+
7
+ from letta.orm.block import Block
8
+ from letta.orm.custom_columns import (
9
+ EmbeddingConfigColumn,
10
+ LLMConfigColumn,
11
+ ToolRulesColumn,
12
+ )
13
+ from letta.orm.message import Message
14
+ from letta.orm.mixins import OrganizationMixin
15
+ from letta.orm.organization import Organization
16
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
17
+ from letta.schemas.agent import AgentState as PydanticAgentState
18
+ from letta.schemas.agent import AgentType
19
+ from letta.schemas.embedding_config import EmbeddingConfig
20
+ from letta.schemas.llm_config import LLMConfig
21
+ from letta.schemas.memory import Memory
22
+ from letta.schemas.tool_rule import ToolRule
23
+
24
+ if TYPE_CHECKING:
25
+ from letta.orm.agents_tags import AgentsTags
26
+ from letta.orm.organization import Organization
27
+ from letta.orm.source import Source
28
+ from letta.orm.tool import Tool
29
+
30
+
31
+ class Agent(SqlalchemyBase, OrganizationMixin):
32
+ __tablename__ = "agents"
33
+ __pydantic_model__ = PydanticAgentState
34
+ __table_args__ = (UniqueConstraint("organization_id", "name", name="unique_org_agent_name"),)
35
+
36
+ # agent generates its own id
37
+ # TODO: We want to migrate all the ORM models to do this, so we will need to move this to the SqlalchemyBase
38
+ # TODO: Move this in this PR? at the very end?
39
+ id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"agent-{uuid.uuid4()}")
40
+
41
+ # Descriptor fields
42
+ agent_type: Mapped[Optional[AgentType]] = mapped_column(String, nullable=True, doc="The type of Agent")
43
+ name: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="a human-readable identifier for an agent, non-unique.")
44
+ description: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The description of the agent.")
45
+
46
+ # System prompt
47
+ system: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The system prompt used by the agent.")
48
+
49
+ # In context memory
50
+ # TODO: This should be a separate mapping table
51
+ # This is dangerously flexible with the JSON type
52
+ message_ids: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True, doc="List of message IDs in in-context memory.")
53
+
54
+ # Metadata and configs
55
+ metadata_: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, doc="metadata for the agent.")
56
+ llm_config: Mapped[Optional[LLMConfig]] = mapped_column(
57
+ LLMConfigColumn, nullable=True, doc="the LLM backend configuration object for this agent."
58
+ )
59
+ embedding_config: Mapped[Optional[EmbeddingConfig]] = mapped_column(
60
+ EmbeddingConfigColumn, doc="the embedding configuration object for this agent."
61
+ )
62
+
63
+ # Tool rules
64
+ tool_rules: Mapped[Optional[List[ToolRule]]] = mapped_column(ToolRulesColumn, doc="the tool rules for this agent.")
65
+
66
+ # relationships
67
+ organization: Mapped["Organization"] = relationship("Organization", back_populates="agents")
68
+ tools: Mapped[List["Tool"]] = relationship("Tool", secondary="tools_agents", lazy="selectin", passive_deletes=True)
69
+ sources: Mapped[List["Source"]] = relationship("Source", secondary="sources_agents", lazy="selectin")
70
+ core_memory: Mapped[List["Block"]] = relationship("Block", secondary="blocks_agents", lazy="selectin")
71
+ messages: Mapped[List["Message"]] = relationship(
72
+ "Message",
73
+ back_populates="agent",
74
+ lazy="selectin",
75
+ cascade="all, delete-orphan", # Ensure messages are deleted when the agent is deleted
76
+ passive_deletes=True,
77
+ )
78
+ tags: Mapped[List["AgentsTags"]] = relationship(
79
+ "AgentsTags",
80
+ back_populates="agent",
81
+ cascade="all, delete-orphan",
82
+ lazy="selectin",
83
+ doc="Tags associated with the agent.",
84
+ )
85
+ # passages: Mapped[List["Passage"]] = relationship("Passage", back_populates="agent", lazy="selectin")
86
+
87
+ def to_pydantic(self) -> PydanticAgentState:
88
+ """converts to the basic pydantic model counterpart"""
89
+ state = {
90
+ "id": self.id,
91
+ "name": self.name,
92
+ "description": self.description,
93
+ "message_ids": self.message_ids,
94
+ "tools": self.tools,
95
+ "sources": self.sources,
96
+ "tags": [t.tag for t in self.tags],
97
+ "tool_rules": self.tool_rules,
98
+ "system": self.system,
99
+ "agent_type": self.agent_type,
100
+ "llm_config": self.llm_config,
101
+ "embedding_config": self.embedding_config,
102
+ "metadata_": self.metadata_,
103
+ "memory": Memory(blocks=[b.to_pydantic() for b in self.core_memory]),
104
+ "created_by_id": self.created_by_id,
105
+ "last_updated_by_id": self.last_updated_by_id,
106
+ "created_at": self.created_at,
107
+ "updated_at": self.updated_at,
108
+ }
109
+ return self.__pydantic_model__(**state)
letta/orm/agents_tags.py CHANGED
@@ -1,28 +1,20 @@
1
- from typing import TYPE_CHECKING
2
-
3
1
  from sqlalchemy import ForeignKey, String, UniqueConstraint
4
2
  from sqlalchemy.orm import Mapped, mapped_column, relationship
5
3
 
6
- from letta.orm.mixins import OrganizationMixin
7
- from letta.orm.sqlalchemy_base import SqlalchemyBase
8
- from letta.schemas.agents_tags import AgentsTags as PydanticAgentsTags
9
-
10
- if TYPE_CHECKING:
11
- from letta.orm.organization import Organization
12
-
4
+ from letta.orm.base import Base
13
5
 
14
- class AgentsTags(SqlalchemyBase, OrganizationMixin):
15
- """Associates tags with agents, allowing agents to have multiple tags and supporting tag-based filtering."""
16
6
 
7
+ class AgentsTags(Base):
17
8
  __tablename__ = "agents_tags"
18
- __pydantic_model__ = PydanticAgentsTags
19
9
  __table_args__ = (UniqueConstraint("agent_id", "tag", name="unique_agent_tag"),)
20
10
 
21
- # The agent associated with this tag
22
- agent_id = mapped_column(String, ForeignKey("agents.id"), primary_key=True)
11
+ # # agent generates its own id
12
+ # # TODO: We want to migrate all the ORM models to do this, so we will need to move this to the SqlalchemyBase
13
+ # # TODO: Move this in this PR? at the very end?
14
+ # id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"agents_tags-{uuid.uuid4()}")
23
15
 
24
- # The name of the tag
25
- tag: Mapped[str] = mapped_column(String, nullable=False, doc="The name of the tag associated with the agent.")
16
+ agent_id: Mapped[String] = mapped_column(String, ForeignKey("agents.id"), primary_key=True)
17
+ tag: Mapped[str] = mapped_column(String, doc="The name of the tag associated with the agent.", primary_key=True)
26
18
 
27
- # relationships
28
- organization: Mapped["Organization"] = relationship("Organization", back_populates="agents_tags")
19
+ # Relationships
20
+ agent: Mapped["Agent"] = relationship("Agent", back_populates="tags")
letta/orm/block.py CHANGED
@@ -1,16 +1,17 @@
1
1
  from typing import TYPE_CHECKING, Optional, Type
2
2
 
3
- from sqlalchemy import JSON, BigInteger, Integer, UniqueConstraint
4
- from sqlalchemy.orm import Mapped, mapped_column, relationship
3
+ from sqlalchemy import JSON, BigInteger, Integer, UniqueConstraint, event
4
+ from sqlalchemy.orm import Mapped, attributes, mapped_column, relationship
5
5
 
6
6
  from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
7
+ from letta.orm.blocks_agents import BlocksAgents
7
8
  from letta.orm.mixins import OrganizationMixin
8
9
  from letta.orm.sqlalchemy_base import SqlalchemyBase
9
10
  from letta.schemas.block import Block as PydanticBlock
10
11
  from letta.schemas.block import Human, Persona
11
12
 
12
13
  if TYPE_CHECKING:
13
- from letta.orm import BlocksAgents, Organization
14
+ from letta.orm import Organization
14
15
 
15
16
 
16
17
  class Block(OrganizationMixin, SqlalchemyBase):
@@ -35,7 +36,6 @@ class Block(OrganizationMixin, SqlalchemyBase):
35
36
 
36
37
  # relationships
37
38
  organization: Mapped[Optional["Organization"]] = relationship("Organization")
38
- blocks_agents: Mapped[list["BlocksAgents"]] = relationship("BlocksAgents", back_populates="block", cascade="all, delete")
39
39
 
40
40
  def to_pydantic(self) -> Type:
41
41
  match self.label:
@@ -46,3 +46,28 @@ class Block(OrganizationMixin, SqlalchemyBase):
46
46
  case _:
47
47
  Schema = PydanticBlock
48
48
  return Schema.model_validate(self)
49
+
50
+
51
+ @event.listens_for(Block, "after_update") # Changed from 'before_update'
52
+ def block_before_update(mapper, connection, target):
53
+ """Handle updating BlocksAgents when a block's label changes."""
54
+ label_history = attributes.get_history(target, "label")
55
+ if not label_history.has_changes():
56
+ return
57
+
58
+ blocks_agents = BlocksAgents.__table__
59
+ connection.execute(
60
+ blocks_agents.update()
61
+ .where(blocks_agents.c.block_id == target.id, blocks_agents.c.block_label == label_history.deleted[0])
62
+ .values(block_label=label_history.added[0])
63
+ )
64
+
65
+
66
+ @event.listens_for(Block, "before_insert")
67
+ @event.listens_for(Block, "before_update")
68
+ def validate_value_length(mapper, connection, target):
69
+ """Ensure the value length does not exceed the limit."""
70
+ if target.value and len(target.value) > target.limit:
71
+ raise ValueError(
72
+ f"Value length ({len(target.value)}) exceeds the limit ({target.limit}) for block with label '{target.label}' and id '{target.id}'."
73
+ )
@@ -1,15 +1,13 @@
1
1
  from sqlalchemy import ForeignKey, ForeignKeyConstraint, String, UniqueConstraint
2
- from sqlalchemy.orm import Mapped, mapped_column, relationship
2
+ from sqlalchemy.orm import Mapped, mapped_column
3
3
 
4
- from letta.orm.sqlalchemy_base import SqlalchemyBase
5
- from letta.schemas.blocks_agents import BlocksAgents as PydanticBlocksAgents
4
+ from letta.orm.base import Base
6
5
 
7
6
 
8
- class BlocksAgents(SqlalchemyBase):
7
+ class BlocksAgents(Base):
9
8
  """Agents must have one or many blocks to make up their core memory."""
10
9
 
11
10
  __tablename__ = "blocks_agents"
12
- __pydantic_model__ = PydanticBlocksAgents
13
11
  __table_args__ = (
14
12
  UniqueConstraint(
15
13
  "agent_id",
@@ -17,16 +15,12 @@ class BlocksAgents(SqlalchemyBase):
17
15
  name="unique_label_per_agent",
18
16
  ),
19
17
  ForeignKeyConstraint(
20
- ["block_id", "block_label"],
21
- ["block.id", "block.label"],
22
- name="fk_block_id_label",
18
+ ["block_id", "block_label"], ["block.id", "block.label"], name="fk_block_id_label", deferrable=True, initially="DEFERRED"
23
19
  ),
20
+ UniqueConstraint("agent_id", "block_id", name="unique_agent_block"),
24
21
  )
25
22
 
26
23
  # unique agent + block label
27
24
  agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id"), primary_key=True)
28
25
  block_id: Mapped[str] = mapped_column(String, primary_key=True)
29
26
  block_label: Mapped[str] = mapped_column(String, primary_key=True)
30
-
31
- # relationships
32
- block: Mapped["Block"] = relationship("Block", back_populates="blocks_agents")