letta-nightly 0.6.13.dev20250122185528__py3-none-any.whl → 0.6.14.dev20250123104106__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.
Files changed (61) hide show
  1. letta/__init__.py +2 -2
  2. letta/agent.py +69 -100
  3. letta/chat_only_agent.py +1 -1
  4. letta/client/client.py +153 -137
  5. letta/constants.py +1 -8
  6. letta/data_sources/connectors.py +1 -1
  7. letta/functions/helpers.py +29 -4
  8. letta/functions/schema_generator.py +55 -0
  9. letta/llm_api/helpers.py +51 -1
  10. letta/memory.py +9 -7
  11. letta/orm/agent.py +2 -2
  12. letta/orm/block.py +3 -1
  13. letta/orm/custom_columns.py +5 -4
  14. letta/orm/enums.py +1 -0
  15. letta/orm/message.py +2 -2
  16. letta/orm/sqlalchemy_base.py +5 -0
  17. letta/schemas/agent.py +3 -3
  18. letta/schemas/block.py +2 -2
  19. letta/schemas/environment_variables.py +1 -1
  20. letta/schemas/job.py +1 -1
  21. letta/schemas/letta_base.py +6 -0
  22. letta/schemas/letta_message.py +6 -6
  23. letta/schemas/memory.py +3 -2
  24. letta/schemas/message.py +21 -13
  25. letta/schemas/passage.py +1 -1
  26. letta/schemas/source.py +4 -4
  27. letta/schemas/tool.py +38 -43
  28. letta/server/rest_api/app.py +1 -16
  29. letta/server/rest_api/routers/v1/agents.py +101 -84
  30. letta/server/rest_api/routers/v1/blocks.py +8 -46
  31. letta/server/rest_api/routers/v1/jobs.py +4 -4
  32. letta/server/rest_api/routers/v1/providers.py +2 -2
  33. letta/server/rest_api/routers/v1/runs.py +6 -6
  34. letta/server/rest_api/routers/v1/sources.py +8 -38
  35. letta/server/rest_api/routers/v1/tags.py +1 -1
  36. letta/server/rest_api/routers/v1/tools.py +6 -7
  37. letta/server/server.py +3 -3
  38. letta/services/agent_manager.py +43 -9
  39. letta/services/block_manager.py +3 -3
  40. letta/services/job_manager.py +5 -3
  41. letta/services/organization_manager.py +1 -1
  42. letta/services/passage_manager.py +3 -3
  43. letta/services/provider_manager.py +2 -2
  44. letta/services/sandbox_config_manager.py +2 -2
  45. letta/services/source_manager.py +3 -3
  46. letta/services/tool_execution_sandbox.py +3 -1
  47. letta/services/tool_manager.py +8 -3
  48. letta/services/user_manager.py +2 -2
  49. letta/settings.py +29 -0
  50. letta/system.py +2 -2
  51. {letta_nightly-0.6.13.dev20250122185528.dist-info → letta_nightly-0.6.14.dev20250123104106.dist-info}/METADATA +1 -1
  52. {letta_nightly-0.6.13.dev20250122185528.dist-info → letta_nightly-0.6.14.dev20250123104106.dist-info}/RECORD +55 -61
  53. letta/server/rest_api/routers/openai/__init__.py +0 -0
  54. letta/server/rest_api/routers/openai/assistants/__init__.py +0 -0
  55. letta/server/rest_api/routers/openai/assistants/assistants.py +0 -115
  56. letta/server/rest_api/routers/openai/assistants/schemas.py +0 -115
  57. letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
  58. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -120
  59. {letta_nightly-0.6.13.dev20250122185528.dist-info → letta_nightly-0.6.14.dev20250123104106.dist-info}/LICENSE +0 -0
  60. {letta_nightly-0.6.13.dev20250122185528.dist-info → letta_nightly-0.6.14.dev20250123104106.dist-info}/WHEEL +0 -0
  61. {letta_nightly-0.6.13.dev20250122185528.dist-info → letta_nightly-0.6.14.dev20250123104106.dist-info}/entry_points.txt +0 -0
letta/schemas/tool.py CHANGED
@@ -9,11 +9,14 @@ from letta.constants import (
9
9
  LETTA_MULTI_AGENT_TOOL_MODULE_NAME,
10
10
  )
11
11
  from letta.functions.functions import derive_openai_json_schema, get_json_schema_from_module
12
- from letta.functions.helpers import generate_composio_tool_wrapper, generate_langchain_tool_wrapper
13
- from letta.functions.schema_generator import generate_schema_from_args_schema_v2
12
+ from letta.functions.helpers import generate_composio_action_from_func_name, generate_composio_tool_wrapper, generate_langchain_tool_wrapper
13
+ from letta.functions.schema_generator import generate_schema_from_args_schema_v2, generate_tool_schema_for_composio
14
+ from letta.log import get_logger
14
15
  from letta.orm.enums import ToolType
15
16
  from letta.schemas.letta_base import LettaBase
16
17
 
18
+ logger = get_logger(__name__)
19
+
17
20
 
18
21
  class BaseTool(LettaBase):
19
22
  __id_prefix__ = "tool"
@@ -52,14 +55,16 @@ class Tool(BaseTool):
52
55
  last_updated_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
53
56
 
54
57
  @model_validator(mode="after")
55
- def populate_missing_fields(self):
58
+ def refresh_source_code_and_json_schema(self):
56
59
  """
57
- Populate missing fields: name, description, and json_schema.
60
+ Refresh name, description, source_code, and json_schema.
58
61
  """
59
62
  if self.tool_type == ToolType.CUSTOM:
60
63
  # If it's a custom tool, we need to ensure source_code is present
61
64
  if not self.source_code:
62
- raise ValueError(f"Custom tool with id={self.id} is missing source_code field.")
65
+ error_msg = f"Custom tool with id={self.id} is missing source_code field."
66
+ logger.error(error_msg)
67
+ raise ValueError(error_msg)
63
68
 
64
69
  # Always derive json_schema for freshest possible json_schema
65
70
  # TODO: Instead of checking the tag, we should having `COMPOSIO` as a specific ToolType
@@ -72,6 +77,24 @@ class Tool(BaseTool):
72
77
  elif self.tool_type in {ToolType.LETTA_MULTI_AGENT_CORE}:
73
78
  # If it's letta multi-agent tool, we also generate the json_schema on the fly here
74
79
  self.json_schema = get_json_schema_from_module(module_name=LETTA_MULTI_AGENT_TOOL_MODULE_NAME, function_name=self.name)
80
+ elif self.tool_type == ToolType.EXTERNAL_COMPOSIO:
81
+ # If it is a composio tool, we generate both the source code and json schema on the fly here
82
+ # TODO: This is brittle, need to think long term about how to improve this
83
+ try:
84
+ composio_action = generate_composio_action_from_func_name(self.name)
85
+ tool_create = ToolCreate.from_composio(composio_action)
86
+ self.source_code = tool_create.source_code
87
+ self.json_schema = tool_create.json_schema
88
+ self.description = tool_create.description
89
+ self.tags = tool_create.tags
90
+ except Exception as e:
91
+ logger.error(f"Encountered exception while attempting to refresh source_code and json_schema for composio_tool: {e}")
92
+
93
+ # At this point, we need to validate that at least json_schema is populated
94
+ if not self.json_schema:
95
+ error_msg = f"Tool with id={self.id} name={self.name} tool_type={self.tool_type} is missing a json_schema."
96
+ logger.error(error_msg)
97
+ raise ValueError(error_msg)
75
98
 
76
99
  # Derive name from the JSON schema if not provided
77
100
  if not self.name:
@@ -100,7 +123,7 @@ class ToolCreate(LettaBase):
100
123
  return_char_limit: int = Field(FUNCTION_RETURN_CHAR_LIMIT, description="The maximum number of characters in the response.")
101
124
 
102
125
  @classmethod
103
- def from_composio(cls, action_name: str, api_key: Optional[str] = None) -> "ToolCreate":
126
+ def from_composio(cls, action_name: str) -> "ToolCreate":
104
127
  """
105
128
  Class method to create an instance of Letta-compatible Composio Tool.
106
129
  Check https://docs.composio.dev/introduction/intro/overview to look at options for from_composio
@@ -115,24 +138,21 @@ class ToolCreate(LettaBase):
115
138
  from composio import LogLevel
116
139
  from composio_langchain import ComposioToolSet
117
140
 
118
- if api_key:
119
- # Pass in an external API key
120
- composio_toolset = ComposioToolSet(logging_level=LogLevel.ERROR, api_key=api_key)
121
- else:
122
- # Use environmental variable
123
- composio_toolset = ComposioToolSet(logging_level=LogLevel.ERROR)
124
- composio_tools = composio_toolset.get_tools(actions=[action_name])
141
+ composio_toolset = ComposioToolSet(logging_level=LogLevel.ERROR)
142
+ composio_action_schemas = composio_toolset.get_action_schemas(actions=[action_name], check_connected_accounts=False)
125
143
 
126
- assert len(composio_tools) > 0, "User supplied parameters do not match any Composio tools"
127
- assert len(composio_tools) == 1, f"User supplied parameters match too many Composio tools; {len(composio_tools)} > 1"
144
+ assert len(composio_action_schemas) > 0, "User supplied parameters do not match any Composio tools"
145
+ assert (
146
+ len(composio_action_schemas) == 1
147
+ ), f"User supplied parameters match too many Composio tools; {len(composio_action_schemas)} > 1"
128
148
 
129
- composio_tool = composio_tools[0]
149
+ composio_action_schema = composio_action_schemas[0]
130
150
 
131
- description = composio_tool.description
151
+ description = composio_action_schema.description
132
152
  source_type = "python"
133
153
  tags = [COMPOSIO_TOOL_TAG_NAME]
134
154
  wrapper_func_name, wrapper_function_str = generate_composio_tool_wrapper(action_name)
135
- json_schema = generate_schema_from_args_schema_v2(composio_tool.args_schema, name=wrapper_func_name, description=description)
155
+ json_schema = generate_tool_schema_for_composio(composio_action_schema.parameters, name=wrapper_func_name, description=description)
136
156
 
137
157
  return cls(
138
158
  name=wrapper_func_name,
@@ -175,31 +195,6 @@ class ToolCreate(LettaBase):
175
195
  json_schema=json_schema,
176
196
  )
177
197
 
178
- @classmethod
179
- def load_default_langchain_tools(cls) -> List["ToolCreate"]:
180
- # For now, we only support wikipedia tool
181
- from langchain_community.tools import WikipediaQueryRun
182
- from langchain_community.utilities import WikipediaAPIWrapper
183
-
184
- wikipedia_tool = ToolCreate.from_langchain(
185
- WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper()), {"langchain_community.utilities": "WikipediaAPIWrapper"}
186
- )
187
-
188
- return [wikipedia_tool]
189
-
190
- @classmethod
191
- def load_default_composio_tools(cls) -> List["ToolCreate"]:
192
- pass
193
-
194
- # TODO: Disable composio tools for now
195
- # TODO: Naming is causing issues
196
- # calculator = ToolCreate.from_composio(action_name=Action.MATHEMATICAL_CALCULATOR.name)
197
- # serp_news = ToolCreate.from_composio(action_name=Action.SERPAPI_NEWS_SEARCH.name)
198
- # serp_google_search = ToolCreate.from_composio(action_name=Action.SERPAPI_SEARCH.name)
199
- # serp_google_maps = ToolCreate.from_composio(action_name=Action.SERPAPI_GOOGLE_MAPS_SEARCH.name)
200
-
201
- return []
202
-
203
198
 
204
199
  class ToolUpdate(LettaBase):
205
200
  description: Optional[str] = Field(None, description="The description of the tool.")
@@ -22,8 +22,6 @@ from letta.server.constants import REST_DEFAULT_PORT
22
22
  # NOTE(charles): these are extra routes that are not part of v1 but we still need to mount to pass tests
23
23
  from letta.server.rest_api.auth.index import setup_auth_router # TODO: probably remove right?
24
24
  from letta.server.rest_api.interface import StreamingServerInterface
25
- from letta.server.rest_api.routers.openai.assistants.assistants import router as openai_assistants_router
26
- from letta.server.rest_api.routers.openai.chat_completions.chat_completions import router as openai_chat_completions_router
27
25
 
28
26
  # from letta.orm.utilities import get_db_session # TODO(ethan) reenable once we merge ORM
29
27
  from letta.server.rest_api.routers.v1 import ROUTERS as v1_routes
@@ -62,20 +60,11 @@ def generate_openapi_schema(app: FastAPI):
62
60
  if not app.openapi_schema:
63
61
  app.openapi_schema = app.openapi()
64
62
 
65
- openai_docs, letta_docs = [app.openapi_schema.copy() for _ in range(2)]
66
-
67
- openai_docs["paths"] = {k: v for k, v in openai_docs["paths"].items() if k.startswith("/openai")}
68
- openai_docs["info"]["title"] = "OpenAI Assistants API"
63
+ letta_docs = app.openapi_schema.copy()
69
64
  letta_docs["paths"] = {k: v for k, v in letta_docs["paths"].items() if not k.startswith("/openai")}
70
65
  letta_docs["info"]["title"] = "Letta API"
71
66
  letta_docs["components"]["schemas"]["LettaMessageUnion"] = create_letta_message_union_schema()
72
-
73
- # Split the API docs into Letta API, and OpenAI Assistants compatible API
74
67
  for name, docs in [
75
- (
76
- "openai",
77
- openai_docs,
78
- ),
79
68
  (
80
69
  "letta",
81
70
  letta_docs,
@@ -249,10 +238,6 @@ def create_application() -> "FastAPI":
249
238
  app.include_router(users_router, prefix=ADMIN_PREFIX)
250
239
  app.include_router(organizations_router, prefix=ADMIN_PREFIX)
251
240
 
252
- # openai
253
- app.include_router(openai_assistants_router, prefix=OPENAI_API_PREFIX)
254
- app.include_router(openai_chat_completions_router, prefix=OPENAI_API_PREFIX)
255
-
256
241
  # /api/auth endpoints
257
242
  app.include_router(setup_auth_router(server, interface, password), prefix=API_PREFIX)
258
243
 
@@ -70,8 +70,8 @@ def list_agents(
70
70
  return agents
71
71
 
72
72
 
73
- @router.get("/{agent_id}/context", response_model=ContextWindowOverview, operation_id="get_agent_context_window")
74
- def get_agent_context_window(
73
+ @router.get("/{agent_id}/context", response_model=ContextWindowOverview, operation_id="retrieve_agent_context_window")
74
+ def retrieve_agent_context_window(
75
75
  agent_id: str,
76
76
  server: "SyncServer" = Depends(get_letta_server),
77
77
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -106,8 +106,8 @@ def create_agent(
106
106
  return server.create_agent(agent, actor=actor)
107
107
 
108
108
 
109
- @router.patch("/{agent_id}", response_model=AgentState, operation_id="update_agent")
110
- def update_agent(
109
+ @router.patch("/{agent_id}", response_model=AgentState, operation_id="modify_agent")
110
+ def modify_agent(
111
111
  agent_id: str,
112
112
  update_agent: UpdateAgent = Body(...),
113
113
  server: "SyncServer" = Depends(get_letta_server),
@@ -118,55 +118,75 @@ def update_agent(
118
118
  return server.agent_manager.update_agent(agent_id=agent_id, agent_update=update_agent, actor=actor)
119
119
 
120
120
 
121
- @router.get("/{agent_id}/tools", response_model=List[Tool], operation_id="get_tools_from_agent")
122
- def get_tools_from_agent(
121
+ @router.get("/{agent_id}/tools", response_model=List[Tool], operation_id="list_agent_tools")
122
+ def list_agent_tools(
123
123
  agent_id: str,
124
124
  server: "SyncServer" = Depends(get_letta_server),
125
125
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
126
126
  ):
127
127
  """Get tools from an existing agent"""
128
128
  actor = server.user_manager.get_user_or_default(user_id=user_id)
129
- return server.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor).tools
129
+ return server.agent_manager.list_attached_tools(agent_id=agent_id, actor=actor)
130
130
 
131
131
 
132
- @router.patch("/{agent_id}/add-tool/{tool_id}", response_model=AgentState, operation_id="add_tool_to_agent")
133
- def add_tool_to_agent(
132
+ @router.patch("/{agent_id}/tools/attach/{tool_id}", response_model=AgentState, operation_id="attach_tool")
133
+ def attach_tool(
134
134
  agent_id: str,
135
135
  tool_id: str,
136
136
  server: "SyncServer" = Depends(get_letta_server),
137
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
137
+ user_id: Optional[str] = Header(None, alias="user_id"),
138
138
  ):
139
- """Add tools to an existing agent"""
139
+ """
140
+ Attach a tool to an agent.
141
+ """
140
142
  actor = server.user_manager.get_user_or_default(user_id=user_id)
141
143
  return server.agent_manager.attach_tool(agent_id=agent_id, tool_id=tool_id, actor=actor)
142
144
 
143
145
 
144
- @router.patch("/{agent_id}/remove-tool/{tool_id}", response_model=AgentState, operation_id="remove_tool_from_agent")
145
- def remove_tool_from_agent(
146
+ @router.patch("/{agent_id}/tools/detach/{tool_id}", response_model=AgentState, operation_id="detach_tool")
147
+ def detach_tool(
146
148
  agent_id: str,
147
149
  tool_id: str,
148
150
  server: "SyncServer" = Depends(get_letta_server),
149
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
151
+ user_id: Optional[str] = Header(None, alias="user_id"),
150
152
  ):
151
- """Add tools to an existing agent"""
153
+ """
154
+ Detach a tool from an agent.
155
+ """
152
156
  actor = server.user_manager.get_user_or_default(user_id=user_id)
153
157
  return server.agent_manager.detach_tool(agent_id=agent_id, tool_id=tool_id, actor=actor)
154
158
 
155
159
 
156
- @router.patch("/{agent_id}/reset-messages", response_model=AgentState, operation_id="reset_messages")
157
- def reset_messages(
160
+ @router.patch("/{agent_id}/sources/attach/{source_id}", response_model=AgentState, operation_id="attach_source_to_agent")
161
+ def attach_source(
158
162
  agent_id: str,
159
- add_default_initial_messages: bool = Query(default=False, description="If true, adds the default initial messages after resetting."),
163
+ source_id: str,
160
164
  server: "SyncServer" = Depends(get_letta_server),
161
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
165
+ user_id: Optional[str] = Header(None, alias="user_id"),
162
166
  ):
163
- """Resets the messages for an agent"""
167
+ """
168
+ Attach a source to an agent.
169
+ """
164
170
  actor = server.user_manager.get_user_or_default(user_id=user_id)
165
- return server.agent_manager.reset_messages(agent_id=agent_id, actor=actor, add_default_initial_messages=add_default_initial_messages)
171
+ return server.agent_manager.attach_source(agent_id=agent_id, source_id=source_id, actor=actor)
172
+
173
+
174
+ @router.patch("/{agent_id}/sources/detach/{source_id}", response_model=AgentState, operation_id="detach_source_from_agent")
175
+ def detach_source(
176
+ agent_id: str,
177
+ source_id: str,
178
+ server: "SyncServer" = Depends(get_letta_server),
179
+ user_id: Optional[str] = Header(None, alias="user_id"),
180
+ ):
181
+ """
182
+ Detach a source from an agent.
183
+ """
184
+ actor = server.user_manager.get_user_or_default(user_id=user_id)
185
+ return server.agent_manager.detach_source(agent_id=agent_id, source_id=source_id, actor=actor)
166
186
 
167
187
 
168
- @router.get("/{agent_id}", response_model=AgentState, operation_id="get_agent")
169
- def get_agent_state(
188
+ @router.get("/{agent_id}", response_model=AgentState, operation_id="retrieve_agent")
189
+ def retrieve_agent(
170
190
  agent_id: str,
171
191
  server: "SyncServer" = Depends(get_letta_server),
172
192
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -199,8 +219,8 @@ def delete_agent(
199
219
  raise HTTPException(status_code=404, detail=f"Agent agent_id={agent_id} not found for user_id={actor.id}.")
200
220
 
201
221
 
202
- @router.get("/{agent_id}/sources", response_model=List[Source], operation_id="get_agent_sources")
203
- def get_agent_sources(
222
+ @router.get("/{agent_id}/sources", response_model=List[Source], operation_id="list_agent_sources")
223
+ def list_agent_sources(
204
224
  agent_id: str,
205
225
  server: "SyncServer" = Depends(get_letta_server),
206
226
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -213,8 +233,8 @@ def get_agent_sources(
213
233
 
214
234
 
215
235
  # TODO: remove? can also get with agent blocks
216
- @router.get("/{agent_id}/core_memory", response_model=Memory, operation_id="get_agent_memory")
217
- def get_agent_memory(
236
+ @router.get("/{agent_id}/core-memory", response_model=Memory, operation_id="retrieve_agent_memory")
237
+ def retrieve_agent_memory(
218
238
  agent_id: str,
219
239
  server: "SyncServer" = Depends(get_letta_server),
220
240
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -228,8 +248,8 @@ def get_agent_memory(
228
248
  return server.get_agent_memory(agent_id=agent_id, actor=actor)
229
249
 
230
250
 
231
- @router.get("/{agent_id}/core_memory/blocks/{block_label}", response_model=Block, operation_id="get_agent_memory_block")
232
- def get_agent_memory_block(
251
+ @router.get("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="retrieve_core_memory_block")
252
+ def retrieve_core_memory_block(
233
253
  agent_id: str,
234
254
  block_label: str,
235
255
  server: "SyncServer" = Depends(get_letta_server),
@@ -246,8 +266,8 @@ def get_agent_memory_block(
246
266
  raise HTTPException(status_code=404, detail=str(e))
247
267
 
248
268
 
249
- @router.get("/{agent_id}/core_memory/blocks", response_model=List[Block], operation_id="list_agent_memory_blocks")
250
- def list_agent_memory_blocks(
269
+ @router.get("/{agent_id}/core-memory/blocks", response_model=List[Block], operation_id="list_core_memory_blocks")
270
+ def list_core_memory_blocks(
251
271
  agent_id: str,
252
272
  server: "SyncServer" = Depends(get_letta_server),
253
273
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -263,73 +283,58 @@ def list_agent_memory_blocks(
263
283
  raise HTTPException(status_code=404, detail=str(e))
264
284
 
265
285
 
266
- @router.post("/{agent_id}/core_memory/blocks", response_model=Memory, operation_id="add_agent_memory_block")
267
- def add_agent_memory_block(
286
+ @router.patch("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="modify_core_memory_block")
287
+ def modify_core_memory_block(
268
288
  agent_id: str,
269
- create_block: CreateBlock = Body(...),
289
+ block_label: str,
290
+ block_update: BlockUpdate = Body(...),
270
291
  server: "SyncServer" = Depends(get_letta_server),
271
292
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
272
293
  ):
273
294
  """
274
- Creates a memory block and links it to the agent.
295
+ Updates a memory block of an agent.
275
296
  """
276
297
  actor = server.user_manager.get_user_or_default(user_id=user_id)
277
298
 
278
- # Copied from POST /blocks
279
- # TODO: Should have block_manager accept only CreateBlock
280
- # TODO: This will be possible once we move ID creation to the ORM
281
- block_req = Block(**create_block.model_dump())
282
- block = server.block_manager.create_or_update_block(actor=actor, block=block_req)
299
+ block = server.agent_manager.get_block_with_label(agent_id=agent_id, block_label=block_label, actor=actor)
300
+ block = server.block_manager.update_block(block.id, block_update=block_update, actor=actor)
283
301
 
284
- # Link the block to the agent
285
- agent = server.agent_manager.attach_block(agent_id=agent_id, block_id=block.id, actor=actor)
286
- return agent.memory
302
+ # This should also trigger a system prompt change in the agent
303
+ server.agent_manager.rebuild_system_prompt(agent_id=agent_id, actor=actor, force=True, update_timestamp=False)
304
+
305
+ return block
287
306
 
288
307
 
289
- @router.delete("/{agent_id}/core_memory/blocks/{block_label}", response_model=Memory, operation_id="remove_agent_memory_block_by_label")
290
- def remove_agent_memory_block(
308
+ @router.patch("/{agent_id}/core-memory/blocks/attach/{block_id}", response_model=AgentState, operation_id="attach_core_memory_block")
309
+ def attach_core_memory_block(
291
310
  agent_id: str,
292
- # TODO should this be block_id, or the label?
293
- # I think label is OK since it's user-friendly + guaranteed to be unique within a Memory object
294
- block_label: str,
311
+ block_id: str,
295
312
  server: "SyncServer" = Depends(get_letta_server),
296
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
313
+ user_id: Optional[str] = Header(None, alias="user_id"),
297
314
  ):
298
315
  """
299
- Removes a memory block from an agent by unlnking it. If the block is not linked to any other agent, it is deleted.
316
+ Attach a block to an agent.
300
317
  """
301
318
  actor = server.user_manager.get_user_or_default(user_id=user_id)
319
+ return server.agent_manager.attach_block(agent_id=agent_id, block_id=block_id, actor=actor)
302
320
 
303
- # Unlink the block from the agent
304
- agent = server.agent_manager.detach_block_with_label(agent_id=agent_id, block_label=block_label, actor=actor)
305
-
306
- return agent.memory
307
321
 
308
-
309
- @router.patch("/{agent_id}/core_memory/blocks/{block_label}", response_model=Block, operation_id="update_agent_memory_block_by_label")
310
- def update_agent_memory_block(
322
+ @router.patch("/{agent_id}/core-memory/blocks/detach/{block_id}", response_model=AgentState, operation_id="detach_core_memory_block")
323
+ def detach_core_memory_block(
311
324
  agent_id: str,
312
- block_label: str,
313
- block_update: BlockUpdate = Body(...),
325
+ block_id: str,
314
326
  server: "SyncServer" = Depends(get_letta_server),
315
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
327
+ user_id: Optional[str] = Header(None, alias="user_id"),
316
328
  ):
317
329
  """
318
- Removes a memory block from an agent by unlnking it. If the block is not linked to any other agent, it is deleted.
330
+ Detach a block from an agent.
319
331
  """
320
332
  actor = server.user_manager.get_user_or_default(user_id=user_id)
333
+ return server.agent_manager.detach_block(agent_id=agent_id, block_id=block_id, actor=actor)
321
334
 
322
- block = server.agent_manager.get_block_with_label(agent_id=agent_id, block_label=block_label, actor=actor)
323
- block = server.block_manager.update_block(block.id, block_update=block_update, actor=actor)
324
-
325
- # This should also trigger a system prompt change in the agent
326
- server.agent_manager.rebuild_system_prompt(agent_id=agent_id, actor=actor, force=True, update_timestamp=False)
327
335
 
328
- return block
329
-
330
-
331
- @router.get("/{agent_id}/archival_memory", response_model=List[Passage], operation_id="list_agent_archival_memory")
332
- def get_agent_archival_memory(
336
+ @router.get("/{agent_id}/archival-memory", response_model=List[Passage], operation_id="list_archival_memory")
337
+ def list_archival_memory(
333
338
  agent_id: str,
334
339
  server: "SyncServer" = Depends(get_letta_server),
335
340
  after: Optional[int] = Query(None, description="Unique ID of the memory to start the query range at."),
@@ -354,8 +359,8 @@ def get_agent_archival_memory(
354
359
  )
355
360
 
356
361
 
357
- @router.post("/{agent_id}/archival_memory", response_model=List[Passage], operation_id="create_agent_archival_memory")
358
- def insert_agent_archival_memory(
362
+ @router.post("/{agent_id}/archival-memory", response_model=List[Passage], operation_id="create_archival_memory")
363
+ def create_archival_memory(
359
364
  agent_id: str,
360
365
  request: CreateArchivalMemory = Body(...),
361
366
  server: "SyncServer" = Depends(get_letta_server),
@@ -371,8 +376,8 @@ def insert_agent_archival_memory(
371
376
 
372
377
  # TODO(ethan): query or path parameter for memory_id?
373
378
  # @router.delete("/{agent_id}/archival")
374
- @router.delete("/{agent_id}/archival_memory/{memory_id}", response_model=None, operation_id="delete_agent_archival_memory")
375
- def delete_agent_archival_memory(
379
+ @router.delete("/{agent_id}/archival-memory/{memory_id}", response_model=None, operation_id="delete_archival_memory")
380
+ def delete_archival_memory(
376
381
  agent_id: str,
377
382
  memory_id: str,
378
383
  # memory_id: str = Query(..., description="Unique ID of the memory to be deleted."),
@@ -393,7 +398,7 @@ AgentMessagesResponse = Annotated[
393
398
  Field(
394
399
  json_schema_extra={
395
400
  "anyOf": [
396
- {"type": "array", "items": {"$ref": "#/components/schemas/letta__schemas__message__Message"}},
401
+ {"type": "array", "items": {"$ref": "#/components/schemas/Message"}},
397
402
  {"type": "array", "items": {"$ref": "#/components/schemas/LettaMessageUnion"}},
398
403
  ]
399
404
  }
@@ -401,8 +406,8 @@ AgentMessagesResponse = Annotated[
401
406
  ]
402
407
 
403
408
 
404
- @router.get("/{agent_id}/messages", response_model=AgentMessagesResponse, operation_id="list_agent_messages")
405
- def get_agent_messages(
409
+ @router.get("/{agent_id}/messages", response_model=AgentMessagesResponse, operation_id="list_messages")
410
+ def list_messages(
406
411
  agent_id: str,
407
412
  server: "SyncServer" = Depends(get_letta_server),
408
413
  before: Optional[str] = Query(None, description="Message before which to retrieve the returned messages."),
@@ -436,8 +441,8 @@ def get_agent_messages(
436
441
  )
437
442
 
438
443
 
439
- @router.patch("/{agent_id}/messages/{message_id}", response_model=Message, operation_id="update_agent_message")
440
- def update_message(
444
+ @router.patch("/{agent_id}/messages/{message_id}", response_model=Message, operation_id="modify_message")
445
+ def modify_message(
441
446
  agent_id: str,
442
447
  message_id: str,
443
448
  request: MessageUpdate = Body(...),
@@ -455,7 +460,7 @@ def update_message(
455
460
  @router.post(
456
461
  "/{agent_id}/messages",
457
462
  response_model=LettaResponse,
458
- operation_id="create_agent_message",
463
+ operation_id="send_message",
459
464
  )
460
465
  async def send_message(
461
466
  agent_id: str,
@@ -551,7 +556,7 @@ async def process_message_background(
551
556
  job_update = JobUpdate(
552
557
  status=JobStatus.completed,
553
558
  completed_at=datetime.utcnow(),
554
- metadata_={"result": result.model_dump()}, # Store the result in metadata
559
+ metadata={"result": result.model_dump()}, # Store the result in metadata
555
560
  )
556
561
  server.job_manager.update_job_by_id(job_id=job_id, job_update=job_update, actor=actor)
557
562
 
@@ -563,7 +568,7 @@ async def process_message_background(
563
568
  job_update = JobUpdate(
564
569
  status=JobStatus.failed,
565
570
  completed_at=datetime.utcnow(),
566
- metadata_={"error": str(e)},
571
+ metadata={"error": str(e)},
567
572
  )
568
573
  server.job_manager.update_job_by_id(job_id=job_id, job_update=job_update, actor=actor)
569
574
  raise
@@ -591,7 +596,7 @@ async def send_message_async(
591
596
  run = Run(
592
597
  user_id=actor.id,
593
598
  status=JobStatus.created,
594
- metadata_={
599
+ metadata={
595
600
  "job_type": "send_message_async",
596
601
  "agent_id": agent_id,
597
602
  },
@@ -613,3 +618,15 @@ async def send_message_async(
613
618
  )
614
619
 
615
620
  return run
621
+
622
+
623
+ @router.patch("/{agent_id}/reset-messages", response_model=AgentState, operation_id="reset_messages")
624
+ def reset_messages(
625
+ agent_id: str,
626
+ add_default_initial_messages: bool = Query(default=False, description="If true, adds the default initial messages after resetting."),
627
+ server: "SyncServer" = Depends(get_letta_server),
628
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
629
+ ):
630
+ """Resets the messages for an agent"""
631
+ actor = server.user_manager.get_user_or_default(user_id=user_id)
632
+ return server.agent_manager.reset_messages(agent_id=agent_id, actor=actor, add_default_initial_messages=add_default_initial_messages)
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING, List, Optional
2
2
 
3
- from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query, Response
3
+ from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query
4
4
 
5
5
  from letta.orm.errors import NoResultFound
6
6
  from letta.schemas.block import Block, BlockUpdate, CreateBlock
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
13
13
  router = APIRouter(prefix="/blocks", tags=["blocks"])
14
14
 
15
15
 
16
- @router.get("/", response_model=List[Block], operation_id="list_memory_blocks")
16
+ @router.get("/", response_model=List[Block], operation_id="list_blocks")
17
17
  def list_blocks(
18
18
  # query parameters
19
19
  label: Optional[str] = Query(None, description="Labels to include (e.g. human, persona)"),
@@ -26,7 +26,7 @@ def list_blocks(
26
26
  return server.block_manager.get_blocks(actor=actor, label=label, is_template=templates_only, template_name=name)
27
27
 
28
28
 
29
- @router.post("/", response_model=Block, operation_id="create_memory_block")
29
+ @router.post("/", response_model=Block, operation_id="create_block")
30
30
  def create_block(
31
31
  create_block: CreateBlock = Body(...),
32
32
  server: SyncServer = Depends(get_letta_server),
@@ -37,8 +37,8 @@ def create_block(
37
37
  return server.block_manager.create_or_update_block(actor=actor, block=block)
38
38
 
39
39
 
40
- @router.patch("/{block_id}", response_model=Block, operation_id="update_memory_block")
41
- def update_block(
40
+ @router.patch("/{block_id}", response_model=Block, operation_id="modify_block")
41
+ def modify_block(
42
42
  block_id: str,
43
43
  block_update: BlockUpdate = Body(...),
44
44
  server: SyncServer = Depends(get_letta_server),
@@ -48,7 +48,7 @@ def update_block(
48
48
  return server.block_manager.update_block(block_id=block_id, block_update=block_update, actor=actor)
49
49
 
50
50
 
51
- @router.delete("/{block_id}", response_model=Block, operation_id="delete_memory_block")
51
+ @router.delete("/{block_id}", response_model=Block, operation_id="delete_block")
52
52
  def delete_block(
53
53
  block_id: str,
54
54
  server: SyncServer = Depends(get_letta_server),
@@ -58,8 +58,8 @@ def delete_block(
58
58
  return server.block_manager.delete_block(block_id=block_id, actor=actor)
59
59
 
60
60
 
61
- @router.get("/{block_id}", response_model=Block, operation_id="get_memory_block")
62
- def get_block(
61
+ @router.get("/{block_id}", response_model=Block, operation_id="retrieve_block")
62
+ def retrieve_block(
63
63
  block_id: str,
64
64
  server: SyncServer = Depends(get_letta_server),
65
65
  user_id: Optional[str] = Header(None, alias="user_id"),
@@ -73,41 +73,3 @@ def get_block(
73
73
  return block
74
74
  except NoResultFound:
75
75
  raise HTTPException(status_code=404, detail="Block not found")
76
-
77
-
78
- @router.patch("/{block_id}/attach", response_model=None, status_code=204, operation_id="link_agent_memory_block")
79
- def link_agent_memory_block(
80
- block_id: str,
81
- agent_id: str = Query(..., description="The unique identifier of the agent to attach the source to."),
82
- server: "SyncServer" = Depends(get_letta_server),
83
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
84
- ):
85
- """
86
- Link a memory block to an agent.
87
- """
88
- actor = server.user_manager.get_user_or_default(user_id=user_id)
89
-
90
- try:
91
- server.agent_manager.attach_block(agent_id=agent_id, block_id=block_id, actor=actor)
92
- return Response(status_code=204)
93
- except NoResultFound as e:
94
- raise HTTPException(status_code=404, detail=str(e))
95
-
96
-
97
- @router.patch("/{block_id}/detach", response_model=None, status_code=204, operation_id="unlink_agent_memory_block")
98
- def unlink_agent_memory_block(
99
- block_id: str,
100
- agent_id: str = Query(..., description="The unique identifier of the agent to attach the source to."),
101
- server: "SyncServer" = Depends(get_letta_server),
102
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
103
- ):
104
- """
105
- Unlink a memory block from an agent
106
- """
107
- actor = server.user_manager.get_user_or_default(user_id=user_id)
108
-
109
- try:
110
- server.agent_manager.detach_block(agent_id=agent_id, block_id=block_id, actor=actor)
111
- return Response(status_code=204)
112
- except NoResultFound as e:
113
- raise HTTPException(status_code=404, detail=str(e))
@@ -26,9 +26,9 @@ def list_jobs(
26
26
  jobs = server.job_manager.list_jobs(actor=actor)
27
27
 
28
28
  if source_id:
29
- # can't be in the ORM since we have source_id stored in the metadata_
29
+ # can't be in the ORM since we have source_id stored in the metadata
30
30
  # TODO: Probably change this
31
- jobs = [job for job in jobs if job.metadata_.get("source_id") == source_id]
31
+ jobs = [job for job in jobs if job.metadata.get("source_id") == source_id]
32
32
  return jobs
33
33
 
34
34
 
@@ -45,8 +45,8 @@ def list_active_jobs(
45
45
  return server.job_manager.list_jobs(actor=actor, statuses=[JobStatus.created, JobStatus.running])
46
46
 
47
47
 
48
- @router.get("/{job_id}", response_model=Job, operation_id="get_job")
49
- def get_job(
48
+ @router.get("/{job_id}", response_model=Job, operation_id="retrieve_job")
49
+ def retrieve_job(
50
50
  job_id: str,
51
51
  user_id: Optional[str] = Header(None, alias="user_id"),
52
52
  server: "SyncServer" = Depends(get_letta_server),