letta-nightly 0.6.13.dev20250122185528__py3-none-any.whl → 0.6.14.dev20250123041709__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +2 -2
- letta/agent.py +69 -100
- letta/chat_only_agent.py +1 -1
- letta/client/client.py +153 -137
- letta/constants.py +1 -8
- letta/data_sources/connectors.py +1 -1
- letta/functions/helpers.py +29 -4
- letta/functions/schema_generator.py +55 -0
- letta/llm_api/helpers.py +51 -1
- letta/memory.py +9 -7
- letta/orm/agent.py +2 -2
- letta/orm/block.py +3 -1
- letta/orm/custom_columns.py +5 -4
- letta/orm/enums.py +1 -0
- letta/orm/message.py +2 -2
- letta/orm/sqlalchemy_base.py +5 -0
- letta/schemas/agent.py +3 -3
- letta/schemas/block.py +2 -2
- letta/schemas/environment_variables.py +1 -1
- letta/schemas/job.py +1 -1
- letta/schemas/letta_base.py +6 -0
- letta/schemas/letta_message.py +6 -6
- letta/schemas/memory.py +3 -2
- letta/schemas/message.py +21 -13
- letta/schemas/passage.py +1 -1
- letta/schemas/source.py +4 -4
- letta/schemas/tool.py +38 -43
- letta/server/rest_api/app.py +1 -16
- letta/server/rest_api/routers/v1/agents.py +101 -84
- letta/server/rest_api/routers/v1/blocks.py +8 -46
- letta/server/rest_api/routers/v1/jobs.py +4 -4
- letta/server/rest_api/routers/v1/providers.py +2 -2
- letta/server/rest_api/routers/v1/runs.py +6 -6
- letta/server/rest_api/routers/v1/sources.py +8 -38
- letta/server/rest_api/routers/v1/tags.py +1 -1
- letta/server/rest_api/routers/v1/tools.py +6 -7
- letta/server/server.py +3 -3
- letta/services/agent_manager.py +43 -9
- letta/services/block_manager.py +3 -3
- letta/services/job_manager.py +5 -3
- letta/services/organization_manager.py +1 -1
- letta/services/passage_manager.py +3 -3
- letta/services/provider_manager.py +2 -2
- letta/services/sandbox_config_manager.py +2 -2
- letta/services/source_manager.py +3 -3
- letta/services/tool_execution_sandbox.py +3 -1
- letta/services/tool_manager.py +8 -3
- letta/services/user_manager.py +2 -2
- letta/settings.py +29 -0
- letta/system.py +2 -2
- {letta_nightly-0.6.13.dev20250122185528.dist-info → letta_nightly-0.6.14.dev20250123041709.dist-info}/METADATA +1 -1
- {letta_nightly-0.6.13.dev20250122185528.dist-info → letta_nightly-0.6.14.dev20250123041709.dist-info}/RECORD +55 -61
- letta/server/rest_api/routers/openai/__init__.py +0 -0
- letta/server/rest_api/routers/openai/assistants/__init__.py +0 -0
- letta/server/rest_api/routers/openai/assistants/assistants.py +0 -115
- letta/server/rest_api/routers/openai/assistants/schemas.py +0 -115
- letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -120
- {letta_nightly-0.6.13.dev20250122185528.dist-info → letta_nightly-0.6.14.dev20250123041709.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.13.dev20250122185528.dist-info → letta_nightly-0.6.14.dev20250123041709.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.13.dev20250122185528.dist-info → letta_nightly-0.6.14.dev20250123041709.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
|
|
58
|
+
def refresh_source_code_and_json_schema(self):
|
|
56
59
|
"""
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
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(
|
|
127
|
-
assert
|
|
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
|
-
|
|
149
|
+
composio_action_schema = composio_action_schemas[0]
|
|
130
150
|
|
|
131
|
-
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 =
|
|
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.")
|
letta/server/rest_api/app.py
CHANGED
|
@@ -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
|
-
|
|
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="
|
|
74
|
-
def
|
|
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="
|
|
110
|
-
def
|
|
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="
|
|
122
|
-
def
|
|
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.
|
|
129
|
+
return server.agent_manager.list_attached_tools(agent_id=agent_id, actor=actor)
|
|
130
130
|
|
|
131
131
|
|
|
132
|
-
@router.patch("/{agent_id}/
|
|
133
|
-
def
|
|
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"),
|
|
137
|
+
user_id: Optional[str] = Header(None, alias="user_id"),
|
|
138
138
|
):
|
|
139
|
-
"""
|
|
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}/
|
|
145
|
-
def
|
|
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"),
|
|
151
|
+
user_id: Optional[str] = Header(None, alias="user_id"),
|
|
150
152
|
):
|
|
151
|
-
"""
|
|
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}/
|
|
157
|
-
def
|
|
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
|
-
|
|
163
|
+
source_id: str,
|
|
160
164
|
server: "SyncServer" = Depends(get_letta_server),
|
|
161
|
-
user_id: Optional[str] = Header(None, alias="user_id"),
|
|
165
|
+
user_id: Optional[str] = Header(None, alias="user_id"),
|
|
162
166
|
):
|
|
163
|
-
"""
|
|
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.
|
|
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="
|
|
169
|
-
def
|
|
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="
|
|
203
|
-
def
|
|
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}/
|
|
217
|
-
def
|
|
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}/
|
|
232
|
-
def
|
|
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}/
|
|
250
|
-
def
|
|
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.
|
|
267
|
-
def
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
#
|
|
285
|
-
|
|
286
|
-
|
|
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.
|
|
290
|
-
def
|
|
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
|
-
|
|
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"),
|
|
313
|
+
user_id: Optional[str] = Header(None, alias="user_id"),
|
|
297
314
|
):
|
|
298
315
|
"""
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"),
|
|
327
|
+
user_id: Optional[str] = Header(None, alias="user_id"),
|
|
316
328
|
):
|
|
317
329
|
"""
|
|
318
|
-
|
|
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
|
-
|
|
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}/
|
|
358
|
-
def
|
|
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}/
|
|
375
|
-
def
|
|
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/
|
|
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="
|
|
405
|
-
def
|
|
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="
|
|
440
|
-
def
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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="
|
|
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="
|
|
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="
|
|
41
|
-
def
|
|
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="
|
|
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="
|
|
62
|
-
def
|
|
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
|
|
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.
|
|
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="
|
|
49
|
-
def
|
|
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),
|