letta-nightly 0.5.1.dev20241104104148__py3-none-any.whl → 0.5.1.dev20241106104104__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 (33) hide show
  1. letta/agent.py +37 -5
  2. letta/agent_store/db.py +19 -19
  3. letta/cli/cli.py +3 -3
  4. letta/cli/cli_config.py +2 -2
  5. letta/client/client.py +36 -19
  6. letta/functions/schema_generator.py +48 -7
  7. letta/metadata.py +10 -10
  8. letta/orm/base.py +5 -2
  9. letta/orm/mixins.py +2 -53
  10. letta/orm/organization.py +3 -1
  11. letta/orm/sqlalchemy_base.py +6 -45
  12. letta/orm/tool.py +3 -2
  13. letta/orm/user.py +3 -1
  14. letta/schemas/agent.py +6 -2
  15. letta/schemas/block.py +1 -1
  16. letta/schemas/letta_base.py +2 -0
  17. letta/schemas/memory.py +4 -0
  18. letta/schemas/organization.py +4 -4
  19. letta/schemas/tool.py +14 -11
  20. letta/schemas/user.py +1 -1
  21. letta/server/rest_api/routers/v1/agents.py +6 -1
  22. letta/server/rest_api/routers/v1/organizations.py +2 -1
  23. letta/server/rest_api/routers/v1/tools.py +2 -1
  24. letta/server/rest_api/routers/v1/users.py +2 -2
  25. letta/server/server.py +20 -11
  26. letta/services/organization_manager.py +4 -4
  27. letta/services/tool_manager.py +17 -16
  28. letta/services/user_manager.py +3 -3
  29. {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/METADATA +3 -3
  30. {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/RECORD +33 -33
  31. {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/LICENSE +0 -0
  32. {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/WHEEL +0 -0
  33. {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/entry_points.txt +0 -0
letta/agent.py CHANGED
@@ -3,6 +3,7 @@ import inspect
3
3
  import traceback
4
4
  import warnings
5
5
  from abc import ABC, abstractmethod
6
+ from lib2to3.fixer_util import is_list
6
7
  from typing import List, Literal, Optional, Tuple, Union
7
8
 
8
9
  from tqdm import tqdm
@@ -235,6 +236,7 @@ class Agent(BaseAgent):
235
236
  # extras
236
237
  messages_total: Optional[int] = None, # TODO remove?
237
238
  first_message_verify_mono: bool = True, # TODO move to config?
239
+ initial_message_sequence: Optional[List[Message]] = None,
238
240
  ):
239
241
  assert isinstance(agent_state.memory, Memory), f"Memory object is not of type Memory: {type(agent_state.memory)}"
240
242
  # Hold a copy of the state that was used to init the agent
@@ -249,6 +251,9 @@ class Agent(BaseAgent):
249
251
  # if there are tool rules, print out a warning
250
252
  warnings.warn("Tool rules only work reliably for the latest OpenAI models that support structured outputs.")
251
253
  # add default rule for having send_message be a terminal tool
254
+
255
+ if not is_list(agent_state.tool_rules):
256
+ agent_state.tool_rules = []
252
257
  agent_state.tool_rules.append(TerminalToolRule(tool_name="send_message"))
253
258
  self.tool_rules_solver = ToolRulesSolver(tool_rules=agent_state.tool_rules)
254
259
 
@@ -294,6 +299,7 @@ class Agent(BaseAgent):
294
299
 
295
300
  else:
296
301
  printd(f"Agent.__init__ :: creating, state={agent_state.message_ids}")
302
+ assert self.agent_state.id is not None and self.agent_state.user_id is not None
297
303
 
298
304
  # Generate a sequence of initial messages to put in the buffer
299
305
  init_messages = initialize_message_sequence(
@@ -306,14 +312,40 @@ class Agent(BaseAgent):
306
312
  include_initial_boot_message=True,
307
313
  )
308
314
 
309
- # Cast the messages to actual Message objects to be synced to the DB
310
- init_messages_objs = []
311
- for msg in init_messages:
312
- init_messages_objs.append(
315
+ if initial_message_sequence is not None:
316
+ # We always need the system prompt up front
317
+ system_message_obj = Message.dict_to_message(
318
+ agent_id=self.agent_state.id,
319
+ user_id=self.agent_state.user_id,
320
+ model=self.model,
321
+ openai_message_dict=init_messages[0],
322
+ )
323
+ # Don't use anything else in the pregen sequence, instead use the provided sequence
324
+ init_messages = [system_message_obj] + initial_message_sequence
325
+
326
+ else:
327
+ # Basic "more human than human" initial message sequence
328
+ init_messages = initialize_message_sequence(
329
+ model=self.model,
330
+ system=self.system,
331
+ memory=self.memory,
332
+ archival_memory=None,
333
+ recall_memory=None,
334
+ memory_edit_timestamp=get_utc_time(),
335
+ include_initial_boot_message=True,
336
+ )
337
+ # Cast to Message objects
338
+ init_messages = [
313
339
  Message.dict_to_message(
314
340
  agent_id=self.agent_state.id, user_id=self.agent_state.user_id, model=self.model, openai_message_dict=msg
315
341
  )
316
- )
342
+ for msg in init_messages
343
+ ]
344
+
345
+ # Cast the messages to actual Message objects to be synced to the DB
346
+ init_messages_objs = []
347
+ for msg in init_messages:
348
+ init_messages_objs.append(msg)
317
349
  assert all([isinstance(msg, Message) for msg in init_messages_objs]), (init_messages_objs, init_messages)
318
350
 
319
351
  # Put the messages inside the message buffer
letta/agent_store/db.py CHANGED
@@ -358,26 +358,26 @@ class PostgresStorageConnector(SQLStorageConnector):
358
358
  # construct URI from enviornment variables
359
359
  if settings.pg_uri:
360
360
  self.uri = settings.pg_uri
361
+
362
+ # use config URI
363
+ # TODO: remove this eventually (config should NOT contain URI)
364
+ if table_type == TableType.ARCHIVAL_MEMORY or table_type == TableType.PASSAGES:
365
+ self.uri = self.config.archival_storage_uri
366
+ self.db_model = PassageModel
367
+ if self.config.archival_storage_uri is None:
368
+ raise ValueError(f"Must specify archival_storage_uri in config {self.config.config_path}")
369
+ elif table_type == TableType.RECALL_MEMORY:
370
+ self.uri = self.config.recall_storage_uri
371
+ self.db_model = MessageModel
372
+ if self.config.recall_storage_uri is None:
373
+ raise ValueError(f"Must specify recall_storage_uri in config {self.config.config_path}")
374
+ elif table_type == TableType.FILES:
375
+ self.uri = self.config.metadata_storage_uri
376
+ self.db_model = FileMetadataModel
377
+ if self.config.metadata_storage_uri is None:
378
+ raise ValueError(f"Must specify metadata_storage_uri in config {self.config.config_path}")
361
379
  else:
362
- # use config URI
363
- # TODO: remove this eventually (config should NOT contain URI)
364
- if table_type == TableType.ARCHIVAL_MEMORY or table_type == TableType.PASSAGES:
365
- self.uri = self.config.archival_storage_uri
366
- self.db_model = PassageModel
367
- if self.config.archival_storage_uri is None:
368
- raise ValueError(f"Must specify archival_storage_uri in config {self.config.config_path}")
369
- elif table_type == TableType.RECALL_MEMORY:
370
- self.uri = self.config.recall_storage_uri
371
- self.db_model = MessageModel
372
- if self.config.recall_storage_uri is None:
373
- raise ValueError(f"Must specify recall_storage_uri in config {self.config.config_path}")
374
- elif table_type == TableType.FILES:
375
- self.uri = self.config.metadata_storage_uri
376
- self.db_model = FileMetadataModel
377
- if self.config.metadata_storage_uri is None:
378
- raise ValueError(f"Must specify metadata_storage_uri in config {self.config.config_path}")
379
- else:
380
- raise ValueError(f"Table type {table_type} not implemented")
380
+ raise ValueError(f"Table type {table_type} not implemented")
381
381
 
382
382
  for c in self.db_model.__table__.columns:
383
383
  if c.name == "embedding":
letta/cli/cli.py CHANGED
@@ -282,10 +282,10 @@ def run(
282
282
  system_prompt = system if system else None
283
283
 
284
284
  memory = ChatMemory(human=human_obj.value, persona=persona_obj.value, limit=core_memory_limit)
285
- metadata = {"human": human_obj.name, "persona": persona_obj.name}
285
+ metadata = {"human": human_obj.template_name, "persona": persona_obj.template_name}
286
286
 
287
- typer.secho(f"-> {ASSISTANT_MESSAGE_CLI_SYMBOL} Using persona profile: '{persona_obj.name}'", fg=typer.colors.WHITE)
288
- typer.secho(f"-> 🧑 Using human profile: '{human_obj.name}'", fg=typer.colors.WHITE)
287
+ typer.secho(f"-> {ASSISTANT_MESSAGE_CLI_SYMBOL} Using persona profile: '{persona_obj.template_name}'", fg=typer.colors.WHITE)
288
+ typer.secho(f"-> 🧑 Using human profile: '{human_obj.template_name}'", fg=typer.colors.WHITE)
289
289
 
290
290
  # add tools
291
291
  agent_state = client.create_agent(
letta/cli/cli_config.py CHANGED
@@ -59,13 +59,13 @@ def list(arg: Annotated[ListChoice, typer.Argument]):
59
59
  """List all humans"""
60
60
  table.field_names = ["Name", "Text"]
61
61
  for human in client.list_humans():
62
- table.add_row([human.name, human.value.replace("\n", "")[:100]])
62
+ table.add_row([human.template_name, human.value.replace("\n", "")[:100]])
63
63
  print(table)
64
64
  elif arg == ListChoice.personas:
65
65
  """List all personas"""
66
66
  table.field_names = ["Name", "Text"]
67
67
  for persona in client.list_personas():
68
- table.add_row([persona.name, persona.value.replace("\n", "")[:100]])
68
+ table.add_row([persona.template_name, persona.value.replace("\n", "")[:100]])
69
69
  print(table)
70
70
  elif arg == ListChoice.sources:
71
71
  """List all data sources"""
letta/client/client.py CHANGED
@@ -376,6 +376,7 @@ class RESTClient(AbstractClient):
376
376
  # metadata
377
377
  metadata: Optional[Dict] = {"human:": DEFAULT_HUMAN, "persona": DEFAULT_PERSONA},
378
378
  description: Optional[str] = None,
379
+ initial_message_sequence: Optional[List[Message]] = None,
379
380
  ) -> AgentState:
380
381
  """Create an agent
381
382
 
@@ -428,9 +429,18 @@ class RESTClient(AbstractClient):
428
429
  agent_type=agent_type,
429
430
  llm_config=llm_config if llm_config else self._default_llm_config,
430
431
  embedding_config=embedding_config if embedding_config else self._default_embedding_config,
432
+ initial_message_sequence=initial_message_sequence,
433
+ )
434
+
435
+ # Use model_dump_json() instead of model_dump()
436
+ # If we use model_dump(), the datetime objects will not be serialized correctly
437
+ # response = requests.post(f"{self.base_url}/{self.api_prefix}/agents", json=request.model_dump(), headers=self.headers)
438
+ response = requests.post(
439
+ f"{self.base_url}/{self.api_prefix}/agents",
440
+ data=request.model_dump_json(), # Use model_dump_json() instead of json=model_dump()
441
+ headers={"Content-Type": "application/json", **self.headers},
431
442
  )
432
443
 
433
- response = requests.post(f"{self.base_url}/{self.api_prefix}/agents", json=request.model_dump(), headers=self.headers)
434
444
  if response.status_code != 200:
435
445
  raise ValueError(f"Status {response.status_code} - Failed to create agent: {response.text}")
436
446
  return AgentState(**response.json())
@@ -602,7 +612,12 @@ class RESTClient(AbstractClient):
602
612
  agent_id (str): ID of the agent
603
613
  """
604
614
  # TODO: implement this
605
- raise NotImplementedError
615
+ response = requests.get(f"{self.base_url}/{self.api_prefix}/agents", headers=self.headers, params={"name": agent_name})
616
+ agents = [AgentState(**agent) for agent in response.json()]
617
+ if len(agents) == 0:
618
+ return None
619
+ assert len(agents) == 1, f"Multiple agents with the same name: {agents}"
620
+ return agents[0].id
606
621
 
607
622
  # memory
608
623
  def get_in_context_memory(self, agent_id: str) -> Memory:
@@ -859,8 +874,8 @@ class RESTClient(AbstractClient):
859
874
  else:
860
875
  return [Block(**block) for block in response.json()]
861
876
 
862
- def create_block(self, label: str, text: str, name: Optional[str] = None, template: bool = False) -> Block: #
863
- request = CreateBlock(label=label, value=text, template=template, name=name)
877
+ def create_block(self, label: str, text: str, template_name: Optional[str] = None, template: bool = False) -> Block: #
878
+ request = CreateBlock(label=label, value=text, template=template, template_name=template_name)
864
879
  response = requests.post(f"{self.base_url}/{self.api_prefix}/blocks", json=request.model_dump(), headers=self.headers)
865
880
  if response.status_code != 200:
866
881
  raise ValueError(f"Failed to create block: {response.text}")
@@ -872,7 +887,7 @@ class RESTClient(AbstractClient):
872
887
  return Block(**response.json())
873
888
 
874
889
  def update_block(self, block_id: str, name: Optional[str] = None, text: Optional[str] = None) -> Block:
875
- request = UpdateBlock(id=block_id, name=name, value=text)
890
+ request = UpdateBlock(id=block_id, template_name=name, value=text)
876
891
  response = requests.post(f"{self.base_url}/{self.api_prefix}/blocks/{block_id}", json=request.model_dump(), headers=self.headers)
877
892
  if response.status_code != 200:
878
893
  raise ValueError(f"Failed to update block: {response.text}")
@@ -926,7 +941,7 @@ class RESTClient(AbstractClient):
926
941
  Returns:
927
942
  human (Human): Human block
928
943
  """
929
- return self.create_block(label="human", name=name, text=text, template=True)
944
+ return self.create_block(label="human", template_name=name, text=text, template=True)
930
945
 
931
946
  def update_human(self, human_id: str, name: Optional[str] = None, text: Optional[str] = None) -> Human:
932
947
  """
@@ -939,7 +954,7 @@ class RESTClient(AbstractClient):
939
954
  Returns:
940
955
  human (Human): Updated human block
941
956
  """
942
- request = UpdateHuman(id=human_id, name=name, value=text)
957
+ request = UpdateHuman(id=human_id, template_name=name, value=text)
943
958
  response = requests.post(f"{self.base_url}/{self.api_prefix}/blocks/{human_id}", json=request.model_dump(), headers=self.headers)
944
959
  if response.status_code != 200:
945
960
  raise ValueError(f"Failed to update human: {response.text}")
@@ -966,7 +981,7 @@ class RESTClient(AbstractClient):
966
981
  Returns:
967
982
  persona (Persona): Persona block
968
983
  """
969
- return self.create_block(label="persona", name=name, text=text, template=True)
984
+ return self.create_block(label="persona", template_name=name, text=text, template=True)
970
985
 
971
986
  def update_persona(self, persona_id: str, name: Optional[str] = None, text: Optional[str] = None) -> Persona:
972
987
  """
@@ -979,7 +994,7 @@ class RESTClient(AbstractClient):
979
994
  Returns:
980
995
  persona (Persona): Updated persona block
981
996
  """
982
- request = UpdatePersona(id=persona_id, name=name, value=text)
997
+ request = UpdatePersona(id=persona_id, template_name=name, value=text)
983
998
  response = requests.post(f"{self.base_url}/{self.api_prefix}/blocks/{persona_id}", json=request.model_dump(), headers=self.headers)
984
999
  if response.status_code != 200:
985
1000
  raise ValueError(f"Failed to update persona: {response.text}")
@@ -1648,6 +1663,7 @@ class LocalClient(AbstractClient):
1648
1663
  # metadata
1649
1664
  metadata: Optional[Dict] = {"human:": DEFAULT_HUMAN, "persona": DEFAULT_PERSONA},
1650
1665
  description: Optional[str] = None,
1666
+ initial_message_sequence: Optional[List[Message]] = None,
1651
1667
  ) -> AgentState:
1652
1668
  """Create an agent
1653
1669
 
@@ -1702,6 +1718,7 @@ class LocalClient(AbstractClient):
1702
1718
  agent_type=agent_type,
1703
1719
  llm_config=llm_config if llm_config else self._default_llm_config,
1704
1720
  embedding_config=embedding_config if embedding_config else self._default_embedding_config,
1721
+ initial_message_sequence=initial_message_sequence,
1705
1722
  ),
1706
1723
  actor=self.user,
1707
1724
  )
@@ -2116,7 +2133,7 @@ class LocalClient(AbstractClient):
2116
2133
  Returns:
2117
2134
  human (Human): Human block
2118
2135
  """
2119
- return self.server.create_block(CreateHuman(name=name, value=text, user_id=self.user_id), user_id=self.user_id)
2136
+ return self.server.create_block(CreateHuman(template_name=name, value=text, user_id=self.user_id), user_id=self.user_id)
2120
2137
 
2121
2138
  def create_persona(self, name: str, text: str):
2122
2139
  """
@@ -2129,7 +2146,7 @@ class LocalClient(AbstractClient):
2129
2146
  Returns:
2130
2147
  persona (Persona): Persona block
2131
2148
  """
2132
- return self.server.create_block(CreatePersona(name=name, value=text, user_id=self.user_id), user_id=self.user_id)
2149
+ return self.server.create_block(CreatePersona(template_name=name, value=text, user_id=self.user_id), user_id=self.user_id)
2133
2150
 
2134
2151
  def list_humans(self):
2135
2152
  """
@@ -2255,18 +2272,18 @@ class LocalClient(AbstractClient):
2255
2272
  langchain_tool=langchain_tool,
2256
2273
  additional_imports_module_attr_map=additional_imports_module_attr_map,
2257
2274
  )
2258
- return self.server.tool_manager.create_or_update_tool(tool_create, actor=self.user)
2275
+ return self.server.tool_manager.create_or_update_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=self.user)
2259
2276
 
2260
2277
  def load_crewai_tool(self, crewai_tool: "CrewAIBaseTool", additional_imports_module_attr_map: dict[str, str] = None) -> Tool:
2261
2278
  tool_create = ToolCreate.from_crewai(
2262
2279
  crewai_tool=crewai_tool,
2263
2280
  additional_imports_module_attr_map=additional_imports_module_attr_map,
2264
2281
  )
2265
- return self.server.tool_manager.create_or_update_tool(tool_create, actor=self.user)
2282
+ return self.server.tool_manager.create_or_update_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=self.user)
2266
2283
 
2267
2284
  def load_composio_tool(self, action: "ActionType") -> Tool:
2268
2285
  tool_create = ToolCreate.from_composio(action=action)
2269
- return self.server.tool_manager.create_or_update_tool(tool_create, actor=self.user)
2286
+ return self.server.tool_manager.create_or_update_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=self.user)
2270
2287
 
2271
2288
  # TODO: Use the above function `add_tool` here as there is duplicate logic
2272
2289
  def create_tool(
@@ -2298,7 +2315,7 @@ class LocalClient(AbstractClient):
2298
2315
 
2299
2316
  # call server function
2300
2317
  return self.server.tool_manager.create_or_update_tool(
2301
- ToolCreate(
2318
+ Tool(
2302
2319
  source_type=source_type,
2303
2320
  source_code=source_code,
2304
2321
  name=name,
@@ -2635,7 +2652,7 @@ class LocalClient(AbstractClient):
2635
2652
  """
2636
2653
  return self.server.get_blocks(label=label, template=templates_only)
2637
2654
 
2638
- def create_block(self, label: str, text: str, name: Optional[str] = None, template: bool = False) -> Block: #
2655
+ def create_block(self, label: str, text: str, template_name: Optional[str] = None, template: bool = False) -> Block: #
2639
2656
  """
2640
2657
  Create a block
2641
2658
 
@@ -2648,7 +2665,7 @@ class LocalClient(AbstractClient):
2648
2665
  block (Block): Created block
2649
2666
  """
2650
2667
  return self.server.create_block(
2651
- CreateBlock(label=label, name=name, value=text, user_id=self.user_id, template=template), user_id=self.user_id
2668
+ CreateBlock(label=label, template_name=template_name, value=text, user_id=self.user_id, template=template), user_id=self.user_id
2652
2669
  )
2653
2670
 
2654
2671
  def update_block(self, block_id: str, name: Optional[str] = None, text: Optional[str] = None) -> Block:
@@ -2663,7 +2680,7 @@ class LocalClient(AbstractClient):
2663
2680
  Returns:
2664
2681
  block (Block): Updated block
2665
2682
  """
2666
- return self.server.update_block(UpdateBlock(id=block_id, name=name, value=text))
2683
+ return self.server.update_block(UpdateBlock(id=block_id, template_name=name, value=text))
2667
2684
 
2668
2685
  def get_block(self, block_id: str) -> Block:
2669
2686
  """
@@ -2726,7 +2743,7 @@ class LocalClient(AbstractClient):
2726
2743
  return self.server.list_embedding_models()
2727
2744
 
2728
2745
  def create_org(self, name: Optional[str] = None) -> Organization:
2729
- return self.server.organization_manager.create_organization(name=name)
2746
+ return self.server.organization_manager.create_organization(pydantic_org=Organization(name=name))
2730
2747
 
2731
2748
  def list_orgs(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[Organization]:
2732
2749
  return self.server.organization_manager.list_organizations(cursor=cursor, limit=limit)
@@ -139,32 +139,73 @@ def generate_schema(function, name: Optional[str] = None, description: Optional[
139
139
  return schema
140
140
 
141
141
 
142
- def generate_schema_from_args_schema(
142
+ def generate_schema_from_args_schema_v1(
143
143
  args_schema: Type[V1BaseModel], name: Optional[str] = None, description: Optional[str] = None, append_heartbeat: bool = True
144
144
  ) -> Dict[str, Any]:
145
145
  properties = {}
146
146
  required = []
147
147
  for field_name, field in args_schema.__fields__.items():
148
- if field.type_.__name__ == "str":
148
+ if field.type_ == str:
149
149
  field_type = "string"
150
- elif field.type_.__name__ == "int":
150
+ elif field.type_ == int:
151
151
  field_type = "integer"
152
- elif field.type_.__name__ == "bool":
152
+ elif field.type_ == bool:
153
153
  field_type = "boolean"
154
154
  else:
155
155
  field_type = field.type_.__name__
156
- properties[field_name] = {"type": field_type, "description": field.field_info.description}
156
+
157
+ properties[field_name] = {
158
+ "type": field_type,
159
+ "description": field.field_info.description,
160
+ }
157
161
  if field.required:
158
162
  required.append(field_name)
159
163
 
160
- # Construct the OpenAI function call JSON object
161
164
  function_call_json = {
162
165
  "name": name,
163
166
  "description": description,
164
167
  "parameters": {"type": "object", "properties": properties, "required": required},
165
168
  }
166
169
 
167
- # append heartbeat (necessary for triggering another reasoning step after this tool call)
170
+ if append_heartbeat:
171
+ function_call_json["parameters"]["properties"]["request_heartbeat"] = {
172
+ "type": "boolean",
173
+ "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.",
174
+ }
175
+ function_call_json["parameters"]["required"].append("request_heartbeat")
176
+
177
+ return function_call_json
178
+
179
+
180
+ def generate_schema_from_args_schema_v2(
181
+ args_schema: Type[BaseModel], name: Optional[str] = None, description: Optional[str] = None, append_heartbeat: bool = True
182
+ ) -> Dict[str, Any]:
183
+ properties = {}
184
+ required = []
185
+ for field_name, field in args_schema.model_fields.items():
186
+ field_type_annotation = field.annotation
187
+ if field_type_annotation == str:
188
+ field_type = "string"
189
+ elif field_type_annotation == int:
190
+ field_type = "integer"
191
+ elif field_type_annotation == bool:
192
+ field_type = "boolean"
193
+ else:
194
+ field_type = field_type_annotation.__name__
195
+
196
+ properties[field_name] = {
197
+ "type": field_type,
198
+ "description": field.description,
199
+ }
200
+ if field.is_required():
201
+ required.append(field_name)
202
+
203
+ function_call_json = {
204
+ "name": name,
205
+ "description": description,
206
+ "parameters": {"type": "object", "properties": properties, "required": required},
207
+ }
208
+
168
209
  if append_heartbeat:
169
210
  function_call_json["parameters"]["properties"]["request_heartbeat"] = {
170
211
  "type": "boolean",
letta/metadata.py CHANGED
@@ -348,7 +348,7 @@ class BlockModel(Base):
348
348
  id = Column(String, primary_key=True, nullable=False)
349
349
  value = Column(String, nullable=False)
350
350
  limit = Column(BIGINT)
351
- name = Column(String)
351
+ template_name = Column(String, nullable=True, default=None)
352
352
  template = Column(Boolean, default=False) # True: listed as possible human/persona
353
353
  label = Column(String, nullable=False)
354
354
  metadata_ = Column(JSON)
@@ -357,7 +357,7 @@ class BlockModel(Base):
357
357
  Index(__tablename__ + "_idx_user", user_id),
358
358
 
359
359
  def __repr__(self) -> str:
360
- return f"<Block(id='{self.id}', name='{self.name}', template='{self.template}', label='{self.label}', user_id='{self.user_id}')>"
360
+ return f"<Block(id='{self.id}', template_name='{self.template_name}', template='{self.template_name}', label='{self.label}', user_id='{self.user_id}')>"
361
361
 
362
362
  def to_record(self) -> Block:
363
363
  if self.label == "persona":
@@ -365,7 +365,7 @@ class BlockModel(Base):
365
365
  id=self.id,
366
366
  value=self.value,
367
367
  limit=self.limit,
368
- name=self.name,
368
+ template_name=self.template_name,
369
369
  template=self.template,
370
370
  label=self.label,
371
371
  metadata_=self.metadata_,
@@ -377,7 +377,7 @@ class BlockModel(Base):
377
377
  id=self.id,
378
378
  value=self.value,
379
379
  limit=self.limit,
380
- name=self.name,
380
+ template_name=self.template_name,
381
381
  template=self.template,
382
382
  label=self.label,
383
383
  metadata_=self.metadata_,
@@ -389,7 +389,7 @@ class BlockModel(Base):
389
389
  id=self.id,
390
390
  value=self.value,
391
391
  limit=self.limit,
392
- name=self.name,
392
+ template_name=self.template_name,
393
393
  template=self.template,
394
394
  label=self.label,
395
395
  metadata_=self.metadata_,
@@ -512,7 +512,7 @@ class MetadataStore:
512
512
  # with a given name doesn't exist.
513
513
  if (
514
514
  session.query(BlockModel)
515
- .filter(BlockModel.name == block.name)
515
+ .filter(BlockModel.template_name == block.template_name)
516
516
  .filter(BlockModel.user_id == block.user_id)
517
517
  .filter(BlockModel.template == True)
518
518
  .filter(BlockModel.label == block.label)
@@ -520,7 +520,7 @@ class MetadataStore:
520
520
  > 0
521
521
  ):
522
522
 
523
- raise ValueError(f"Block with name {block.name} already exists")
523
+ raise ValueError(f"Block with name {block.template_name} already exists")
524
524
  session.add(BlockModel(**vars(block)))
525
525
  session.commit()
526
526
 
@@ -658,7 +658,7 @@ class MetadataStore:
658
658
  user_id: Optional[str],
659
659
  label: Optional[str] = None,
660
660
  template: Optional[bool] = None,
661
- name: Optional[str] = None,
661
+ template_name: Optional[str] = None,
662
662
  id: Optional[str] = None,
663
663
  ) -> Optional[List[Block]]:
664
664
  """List available blocks"""
@@ -671,8 +671,8 @@ class MetadataStore:
671
671
  if label:
672
672
  query = query.filter(BlockModel.label == label)
673
673
 
674
- if name:
675
- query = query.filter(BlockModel.name == name)
674
+ if template_name:
675
+ query = query.filter(BlockModel.template_name == template_name)
676
676
 
677
677
  if id:
678
678
  query = query.filter(BlockModel.id == id)
letta/orm/base.py CHANGED
@@ -67,7 +67,7 @@ class CommonSqlalchemyMetaMixins(Base):
67
67
  prop_value = getattr(self, full_prop, None)
68
68
  if not prop_value:
69
69
  return
70
- return f"user-{prop_value}"
70
+ return prop_value
71
71
 
72
72
  def _user_id_setter(self, prop: str, value: str) -> None:
73
73
  """returns the user id for the specified property"""
@@ -75,6 +75,9 @@ class CommonSqlalchemyMetaMixins(Base):
75
75
  if not value:
76
76
  setattr(self, full_prop, None)
77
77
  return
78
+ # Safety check
78
79
  prefix, id_ = value.split("-", 1)
79
80
  assert prefix == "user", f"{prefix} is not a valid id prefix for a user id"
80
- setattr(self, full_prop, id_)
81
+
82
+ # Set the full value
83
+ setattr(self, full_prop, value)
letta/orm/mixins.py CHANGED
@@ -1,11 +1,9 @@
1
- from typing import Optional
2
1
  from uuid import UUID
3
2
 
4
3
  from sqlalchemy import ForeignKey, String
5
4
  from sqlalchemy.orm import Mapped, mapped_column
6
5
 
7
6
  from letta.orm.base import Base
8
- from letta.orm.errors import MalformedIdError
9
7
 
10
8
 
11
9
  def is_valid_uuid4(uuid_string: str) -> bool:
@@ -17,53 +15,12 @@ def is_valid_uuid4(uuid_string: str) -> bool:
17
15
  return False
18
16
 
19
17
 
20
- def _relation_getter(instance: "Base", prop: str) -> Optional[str]:
21
- """Get relation and return id with prefix as a string."""
22
- prefix = prop.replace("_", "")
23
- formatted_prop = f"_{prop}_id"
24
- try:
25
- id_ = getattr(instance, formatted_prop) # Get the string id directly
26
- return f"{prefix}-{id_}"
27
- except AttributeError:
28
- return None
29
-
30
-
31
- def _relation_setter(instance: "Base", prop: str, value: str) -> None:
32
- """Set relation using the id with prefix, ensuring the id is a valid UUIDv4."""
33
- formatted_prop = f"_{prop}_id"
34
- prefix = prop.replace("_", "")
35
- if not value:
36
- setattr(instance, formatted_prop, None)
37
- return
38
- try:
39
- found_prefix, id_ = value.split("-", 1)
40
- except ValueError as e:
41
- raise MalformedIdError(f"{value} is not a valid ID.") from e
42
-
43
- # Ensure prefix matches
44
- assert found_prefix == prefix, f"{found_prefix} is not a valid id prefix, expecting {prefix}"
45
-
46
- # Validate that the id is a valid UUID4 string
47
- if not is_valid_uuid4(id_):
48
- raise MalformedIdError(f"Hash segment of {value} is not a valid UUID4")
49
-
50
- setattr(instance, formatted_prop, id_) # Store id as a string
51
-
52
-
53
18
  class OrganizationMixin(Base):
54
19
  """Mixin for models that belong to an organization."""
55
20
 
56
21
  __abstract__ = True
57
22
 
58
- _organization_id: Mapped[str] = mapped_column(String, ForeignKey("organization._id"))
59
-
60
- @property
61
- def organization_id(self) -> str:
62
- return _relation_getter(self, "organization")
63
-
64
- @organization_id.setter
65
- def organization_id(self, value: str) -> None:
66
- _relation_setter(self, "organization", value)
23
+ organization_id: Mapped[str] = mapped_column(String, ForeignKey("organizations.id"))
67
24
 
68
25
 
69
26
  class UserMixin(Base):
@@ -71,12 +28,4 @@ class UserMixin(Base):
71
28
 
72
29
  __abstract__ = True
73
30
 
74
- _user_id: Mapped[str] = mapped_column(String, ForeignKey("user._id"))
75
-
76
- @property
77
- def user_id(self) -> str:
78
- return _relation_getter(self, "user")
79
-
80
- @user_id.setter
81
- def user_id(self, value: str) -> None:
82
- _relation_setter(self, "user", value)
31
+ user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"))
letta/orm/organization.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from typing import TYPE_CHECKING, List
2
2
 
3
+ from sqlalchemy import String
3
4
  from sqlalchemy.orm import Mapped, mapped_column, relationship
4
5
 
5
6
  from letta.orm.sqlalchemy_base import SqlalchemyBase
@@ -14,9 +15,10 @@ if TYPE_CHECKING:
14
15
  class Organization(SqlalchemyBase):
15
16
  """The highest level of the object tree. All Entities belong to one and only one Organization."""
16
17
 
17
- __tablename__ = "organization"
18
+ __tablename__ = "organizations"
18
19
  __pydantic_model__ = PydanticOrganization
19
20
 
21
+ id: Mapped[str] = mapped_column(String, primary_key=True)
20
22
  name: Mapped[str] = mapped_column(doc="The display name of the organization.")
21
23
 
22
24
  users: Mapped[List["User"]] = relationship("User", back_populates="organization", cascade="all, delete-orphan")