letta-nightly 0.13.0.dev20251031104146__py3-none-any.whl → 0.13.1.dev20251101010313__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 +1 -1
- letta/adapters/simple_llm_stream_adapter.py +1 -0
- letta/agents/letta_agent_v2.py +8 -0
- letta/agents/letta_agent_v3.py +127 -27
- letta/agents/temporal/activities/__init__.py +25 -0
- letta/agents/temporal/activities/create_messages.py +26 -0
- letta/agents/temporal/activities/create_step.py +57 -0
- letta/agents/temporal/activities/example_activity.py +9 -0
- letta/agents/temporal/activities/execute_tool.py +130 -0
- letta/agents/temporal/activities/llm_request.py +114 -0
- letta/agents/temporal/activities/prepare_messages.py +27 -0
- letta/agents/temporal/activities/refresh_context.py +160 -0
- letta/agents/temporal/activities/summarize_conversation_history.py +77 -0
- letta/agents/temporal/activities/update_message_ids.py +25 -0
- letta/agents/temporal/activities/update_run.py +43 -0
- letta/agents/temporal/constants.py +59 -0
- letta/agents/temporal/temporal_agent_workflow.py +704 -0
- letta/agents/temporal/types.py +275 -0
- letta/constants.py +11 -0
- letta/errors.py +4 -0
- letta/functions/function_sets/base.py +0 -11
- letta/groups/helpers.py +7 -1
- letta/groups/sleeptime_multi_agent_v4.py +4 -3
- letta/interfaces/anthropic_streaming_interface.py +0 -1
- letta/interfaces/openai_streaming_interface.py +103 -100
- letta/llm_api/anthropic_client.py +57 -12
- letta/llm_api/bedrock_client.py +1 -0
- letta/llm_api/deepseek_client.py +3 -2
- letta/llm_api/google_vertex_client.py +5 -4
- letta/llm_api/groq_client.py +1 -0
- letta/llm_api/llm_client_base.py +15 -1
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +17 -3
- letta/llm_api/xai_client.py +1 -0
- letta/orm/agent.py +3 -0
- letta/orm/organization.py +4 -0
- letta/orm/sqlalchemy_base.py +7 -0
- letta/otel/tracing.py +131 -4
- letta/schemas/agent.py +108 -40
- letta/schemas/agent_file.py +10 -10
- letta/schemas/block.py +22 -3
- letta/schemas/enums.py +21 -0
- letta/schemas/environment_variables.py +3 -2
- letta/schemas/group.py +3 -3
- letta/schemas/letta_response.py +36 -4
- letta/schemas/llm_batch_job.py +3 -3
- letta/schemas/llm_config.py +123 -4
- letta/schemas/mcp.py +3 -2
- letta/schemas/mcp_server.py +3 -2
- letta/schemas/message.py +167 -49
- letta/schemas/model.py +265 -0
- letta/schemas/organization.py +2 -1
- letta/schemas/passage.py +2 -1
- letta/schemas/provider_trace.py +2 -1
- letta/schemas/providers/openrouter.py +1 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +3 -1
- letta/schemas/step_metrics.py +2 -1
- letta/schemas/tool_rule.py +2 -2
- letta/schemas/user.py +2 -1
- letta/server/rest_api/app.py +5 -1
- letta/server/rest_api/routers/v1/__init__.py +4 -0
- letta/server/rest_api/routers/v1/agents.py +71 -9
- letta/server/rest_api/routers/v1/blocks.py +7 -7
- letta/server/rest_api/routers/v1/groups.py +40 -0
- letta/server/rest_api/routers/v1/identities.py +2 -2
- letta/server/rest_api/routers/v1/internal_agents.py +31 -0
- letta/server/rest_api/routers/v1/internal_blocks.py +177 -0
- letta/server/rest_api/routers/v1/internal_runs.py +25 -1
- letta/server/rest_api/routers/v1/runs.py +2 -22
- letta/server/rest_api/routers/v1/tools.py +12 -1
- letta/server/server.py +20 -4
- letta/services/agent_manager.py +4 -4
- letta/services/archive_manager.py +16 -0
- letta/services/group_manager.py +44 -0
- letta/services/helpers/run_manager_helper.py +2 -2
- letta/services/lettuce/lettuce_client.py +148 -0
- letta/services/mcp/base_client.py +9 -3
- letta/services/run_manager.py +148 -37
- letta/services/source_manager.py +91 -3
- letta/services/step_manager.py +2 -3
- letta/services/streaming_service.py +52 -13
- letta/services/summarizer/summarizer.py +28 -2
- letta/services/tool_executor/builtin_tool_executor.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +2 -117
- letta/services/tool_sandbox/e2b_sandbox.py +4 -1
- letta/services/tool_schema_generator.py +2 -2
- letta/validators.py +21 -0
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/METADATA +1 -1
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/RECORD +93 -87
- letta/agent.py +0 -1758
- letta/cli/cli_load.py +0 -16
- letta/client/__init__.py +0 -0
- letta/client/streaming.py +0 -95
- letta/client/utils.py +0 -78
- letta/functions/async_composio_toolset.py +0 -109
- letta/functions/composio_helpers.py +0 -96
- letta/helpers/composio_helpers.py +0 -38
- letta/orm/job_messages.py +0 -33
- letta/schemas/providers.py +0 -1617
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -132
- letta/services/tool_executor/composio_tool_executor.py +0 -57
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/WHEEL +0 -0
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/licenses/LICENSE +0 -0
|
@@ -284,3 +284,43 @@ async def reset_group_messages(
|
|
|
284
284
|
"""
|
|
285
285
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
286
286
|
await server.group_manager.reset_messages_async(group_id=group_id, actor=actor)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@router.patch("/{group_id}/blocks/attach/{block_id}", response_model=None, operation_id="attach_block_to_group")
|
|
290
|
+
async def attach_block_to_group(
|
|
291
|
+
block_id: str,
|
|
292
|
+
group_id: GroupId,
|
|
293
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
294
|
+
headers: HeaderParams = Depends(get_headers),
|
|
295
|
+
):
|
|
296
|
+
"""
|
|
297
|
+
Attach a block to a group.
|
|
298
|
+
This will add the block to the group and all agents within the group.
|
|
299
|
+
"""
|
|
300
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
301
|
+
await server.group_manager.attach_block_async(
|
|
302
|
+
group_id=group_id,
|
|
303
|
+
block_id=block_id,
|
|
304
|
+
actor=actor,
|
|
305
|
+
)
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@router.patch("/{group_id}/blocks/detach/{block_id}", response_model=None, operation_id="detach_block_from_group")
|
|
310
|
+
async def detach_block_from_group(
|
|
311
|
+
block_id: str,
|
|
312
|
+
group_id: GroupId,
|
|
313
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
314
|
+
headers: HeaderParams = Depends(get_headers),
|
|
315
|
+
):
|
|
316
|
+
"""
|
|
317
|
+
Detach a block from a group.
|
|
318
|
+
This will remove the block from the group and all agents within the group.
|
|
319
|
+
"""
|
|
320
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
321
|
+
await server.group_manager.detach_block_async(
|
|
322
|
+
group_id=group_id,
|
|
323
|
+
block_id=block_id,
|
|
324
|
+
actor=actor,
|
|
325
|
+
)
|
|
326
|
+
return None
|
|
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Body, Depends, Header, Query
|
|
|
4
4
|
|
|
5
5
|
from letta.orm.errors import NoResultFound, UniqueConstraintViolationError
|
|
6
6
|
from letta.schemas.agent import AgentRelationships, AgentState
|
|
7
|
-
from letta.schemas.block import Block
|
|
7
|
+
from letta.schemas.block import Block, BlockResponse
|
|
8
8
|
from letta.schemas.identity import (
|
|
9
9
|
Identity,
|
|
10
10
|
IdentityCreate,
|
|
@@ -188,7 +188,7 @@ async def list_agents_for_identity(
|
|
|
188
188
|
)
|
|
189
189
|
|
|
190
190
|
|
|
191
|
-
@router.get("/{identity_id}/blocks", response_model=List[
|
|
191
|
+
@router.get("/{identity_id}/blocks", response_model=List[BlockResponse], operation_id="list_blocks_for_identity")
|
|
192
192
|
async def list_blocks_for_identity(
|
|
193
193
|
identity_id: IdentityId,
|
|
194
194
|
before: Optional[str] = Query(
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from fastapi import APIRouter, Body, Depends
|
|
2
|
+
|
|
3
|
+
from letta.schemas.block import Block, BlockUpdate
|
|
4
|
+
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
|
5
|
+
from letta.server.server import SyncServer
|
|
6
|
+
from letta.validators import AgentId
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/_internal_agents", tags=["_internal_agents"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@router.patch("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="modify_internal_core_memory_block")
|
|
12
|
+
async def modify_block_for_agent(
|
|
13
|
+
block_label: str,
|
|
14
|
+
agent_id: AgentId,
|
|
15
|
+
block_update: BlockUpdate = Body(...),
|
|
16
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
17
|
+
headers: HeaderParams = Depends(get_headers),
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Updates a core memory block of an agent.
|
|
21
|
+
"""
|
|
22
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
23
|
+
|
|
24
|
+
block = await server.agent_manager.modify_block_by_label_async(
|
|
25
|
+
agent_id=agent_id, block_label=block_label, block_update=block_update, actor=actor
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# This should also trigger a system prompt change in the agent
|
|
29
|
+
await server.agent_manager.rebuild_system_prompt_async(agent_id=agent_id, actor=actor, force=True, update_timestamp=False)
|
|
30
|
+
|
|
31
|
+
return block
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, List, Literal, Optional
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Body, Depends, Query
|
|
4
|
+
|
|
5
|
+
from letta.schemas.agent import AgentState
|
|
6
|
+
from letta.schemas.block import Block, CreateBlock
|
|
7
|
+
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
|
8
|
+
from letta.server.server import SyncServer
|
|
9
|
+
from letta.utils import is_1_0_sdk_version
|
|
10
|
+
from letta.validators import BlockId
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
router = APIRouter(prefix="/_internal_blocks", tags=["_internal_blocks"])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.get("/", response_model=List[Block], operation_id="list_internal_blocks")
|
|
19
|
+
async def list_blocks(
|
|
20
|
+
# query parameters
|
|
21
|
+
label: Optional[str] = Query(None, description="Labels to include (e.g. human, persona)"),
|
|
22
|
+
templates_only: bool = Query(False, description="Whether to include only templates"),
|
|
23
|
+
name: Optional[str] = Query(None, description="Name of the block"),
|
|
24
|
+
identity_id: Optional[str] = Query(None, description="Search agents by identifier id"),
|
|
25
|
+
identifier_keys: Optional[List[str]] = Query(None, description="Search agents by identifier keys"),
|
|
26
|
+
project_id: Optional[str] = Query(None, description="Search blocks by project id"),
|
|
27
|
+
limit: Optional[int] = Query(50, description="Number of blocks to return"),
|
|
28
|
+
before: Optional[str] = Query(
|
|
29
|
+
None,
|
|
30
|
+
description="Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order",
|
|
31
|
+
),
|
|
32
|
+
after: Optional[str] = Query(
|
|
33
|
+
None,
|
|
34
|
+
description="Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order",
|
|
35
|
+
),
|
|
36
|
+
order: Literal["asc", "desc"] = Query(
|
|
37
|
+
"asc", description="Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first"
|
|
38
|
+
),
|
|
39
|
+
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
|
40
|
+
label_search: Optional[str] = Query(
|
|
41
|
+
None,
|
|
42
|
+
description=("Search blocks by label. If provided, returns blocks that match this label. This is a full-text search on labels."),
|
|
43
|
+
),
|
|
44
|
+
description_search: Optional[str] = Query(
|
|
45
|
+
None,
|
|
46
|
+
description=(
|
|
47
|
+
"Search blocks by description. If provided, returns blocks that match this description. "
|
|
48
|
+
"This is a full-text search on block descriptions."
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
value_search: Optional[str] = Query(
|
|
52
|
+
None,
|
|
53
|
+
description=("Search blocks by value. If provided, returns blocks that match this value."),
|
|
54
|
+
),
|
|
55
|
+
connected_to_agents_count_gt: Optional[int] = Query(
|
|
56
|
+
None,
|
|
57
|
+
description=(
|
|
58
|
+
"Filter blocks by the number of connected agents. "
|
|
59
|
+
"If provided, returns blocks that have more than this number of connected agents."
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
connected_to_agents_count_lt: Optional[int] = Query(
|
|
63
|
+
None,
|
|
64
|
+
description=(
|
|
65
|
+
"Filter blocks by the number of connected agents. "
|
|
66
|
+
"If provided, returns blocks that have less than this number of connected agents."
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
connected_to_agents_count_eq: Optional[List[int]] = Query(
|
|
70
|
+
None,
|
|
71
|
+
description=(
|
|
72
|
+
"Filter blocks by the exact number of connected agents. "
|
|
73
|
+
"If provided, returns blocks that have exactly this number of connected agents."
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
show_hidden_blocks: bool | None = Query(
|
|
77
|
+
False,
|
|
78
|
+
include_in_schema=False,
|
|
79
|
+
description="If set to True, include blocks marked as hidden in the results.",
|
|
80
|
+
),
|
|
81
|
+
server: SyncServer = Depends(get_letta_server),
|
|
82
|
+
headers: HeaderParams = Depends(get_headers),
|
|
83
|
+
):
|
|
84
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
85
|
+
return await server.block_manager.get_blocks_async(
|
|
86
|
+
actor=actor,
|
|
87
|
+
label=label,
|
|
88
|
+
is_template=templates_only,
|
|
89
|
+
value_search=value_search,
|
|
90
|
+
label_search=label_search,
|
|
91
|
+
description_search=description_search,
|
|
92
|
+
template_name=name,
|
|
93
|
+
identity_id=identity_id,
|
|
94
|
+
identifier_keys=identifier_keys,
|
|
95
|
+
project_id=project_id,
|
|
96
|
+
before=before,
|
|
97
|
+
connected_to_agents_count_gt=connected_to_agents_count_gt,
|
|
98
|
+
connected_to_agents_count_lt=connected_to_agents_count_lt,
|
|
99
|
+
connected_to_agents_count_eq=connected_to_agents_count_eq,
|
|
100
|
+
limit=limit,
|
|
101
|
+
after=after,
|
|
102
|
+
ascending=(order == "asc"),
|
|
103
|
+
show_hidden_blocks=show_hidden_blocks,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.post("/", response_model=Block, operation_id="create_internal_block")
|
|
108
|
+
async def create_block(
|
|
109
|
+
create_block: CreateBlock = Body(...),
|
|
110
|
+
server: SyncServer = Depends(get_letta_server),
|
|
111
|
+
headers: HeaderParams = Depends(get_headers),
|
|
112
|
+
):
|
|
113
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
114
|
+
block = Block(**create_block.model_dump())
|
|
115
|
+
return await server.block_manager.create_or_update_block_async(actor=actor, block=block)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@router.delete("/{block_id}", operation_id="delete_internal_block")
|
|
119
|
+
async def delete_block(
|
|
120
|
+
block_id: BlockId,
|
|
121
|
+
server: SyncServer = Depends(get_letta_server),
|
|
122
|
+
headers: HeaderParams = Depends(get_headers),
|
|
123
|
+
):
|
|
124
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
125
|
+
await server.block_manager.delete_block_async(block_id=block_id, actor=actor)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@router.get("/{block_id}/agents", response_model=List[AgentState], operation_id="list_agents_for_internal_block")
|
|
129
|
+
async def list_agents_for_block(
|
|
130
|
+
block_id: BlockId,
|
|
131
|
+
before: Optional[str] = Query(
|
|
132
|
+
None,
|
|
133
|
+
description="Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order",
|
|
134
|
+
),
|
|
135
|
+
after: Optional[str] = Query(
|
|
136
|
+
None,
|
|
137
|
+
description="Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order",
|
|
138
|
+
),
|
|
139
|
+
limit: Optional[int] = Query(50, description="Maximum number of agents to return"),
|
|
140
|
+
order: Literal["asc", "desc"] = Query(
|
|
141
|
+
"desc", description="Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first"
|
|
142
|
+
),
|
|
143
|
+
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
|
144
|
+
include_relationships: list[str] | None = Query(
|
|
145
|
+
None,
|
|
146
|
+
description=(
|
|
147
|
+
"Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. "
|
|
148
|
+
"If not provided, all relationships are loaded by default. "
|
|
149
|
+
"Using this can optimize performance by reducing unnecessary joins."
|
|
150
|
+
"This is a legacy parameter, and no longer supported after 1.0.0 SDK versions."
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
include: List[str] = Query(
|
|
154
|
+
[],
|
|
155
|
+
description=("Specify which relational fields to include in the response. No relationships are included by default."),
|
|
156
|
+
),
|
|
157
|
+
server: SyncServer = Depends(get_letta_server),
|
|
158
|
+
headers: HeaderParams = Depends(get_headers),
|
|
159
|
+
):
|
|
160
|
+
"""
|
|
161
|
+
Retrieves all agents associated with the specified block.
|
|
162
|
+
Raises a 404 if the block does not exist.
|
|
163
|
+
"""
|
|
164
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
165
|
+
if include_relationships is None and is_1_0_sdk_version(headers):
|
|
166
|
+
include_relationships = [] # don't default include all if using new SDK version
|
|
167
|
+
agents = await server.block_manager.get_agents_for_block_async(
|
|
168
|
+
block_id=block_id,
|
|
169
|
+
before=before,
|
|
170
|
+
after=after,
|
|
171
|
+
limit=limit,
|
|
172
|
+
ascending=(order == "asc"),
|
|
173
|
+
include_relationships=include_relationships,
|
|
174
|
+
include=include,
|
|
175
|
+
actor=actor,
|
|
176
|
+
)
|
|
177
|
+
return agents
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from datetime import datetime
|
|
1
2
|
from typing import List, Literal, Optional
|
|
2
3
|
|
|
3
4
|
from fastapi import APIRouter, Depends, Query
|
|
@@ -55,13 +56,25 @@ async def list_runs(
|
|
|
55
56
|
order: Literal["asc", "desc"] = Query(
|
|
56
57
|
"desc", description="Sort order for runs by creation time. 'asc' for oldest first, 'desc' for newest first"
|
|
57
58
|
),
|
|
58
|
-
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
|
59
|
+
order_by: Literal["created_at", "duration"] = Query("created_at", description="Field to sort by"),
|
|
59
60
|
active: bool = Query(False, description="Filter for active runs."),
|
|
60
61
|
ascending: bool = Query(
|
|
61
62
|
False,
|
|
62
63
|
description="Whether to sort agents oldest to newest (True) or newest to oldest (False, default). Deprecated in favor of order field.",
|
|
63
64
|
deprecated=True,
|
|
64
65
|
),
|
|
66
|
+
project_id: Optional[str] = Query(None, description="Filter runs by project ID."),
|
|
67
|
+
duration_percentile: Optional[int] = Query(
|
|
68
|
+
None, description="Filter runs by duration percentile (1-100). Returns runs slower than this percentile."
|
|
69
|
+
),
|
|
70
|
+
duration_value: Optional[int] = Query(
|
|
71
|
+
None, description="Duration value in nanoseconds for filtering. Must be used with duration_operator."
|
|
72
|
+
),
|
|
73
|
+
duration_operator: Optional[Literal["gt", "lt", "eq"]] = Query(
|
|
74
|
+
None, description="Comparison operator for duration filter: 'gt' (greater than), 'lt' (less than), 'eq' (equals)."
|
|
75
|
+
),
|
|
76
|
+
start_date: Optional[datetime] = Query(None, description="Filter runs created on or after this date (ISO 8601 format)."),
|
|
77
|
+
end_date: Optional[datetime] = Query(None, description="Filter runs created on or before this date (ISO 8601 format)."),
|
|
65
78
|
headers: HeaderParams = Depends(get_headers),
|
|
66
79
|
):
|
|
67
80
|
"""
|
|
@@ -89,6 +102,11 @@ async def list_runs(
|
|
|
89
102
|
# Convert string statuses to RunStatus enum
|
|
90
103
|
parsed_statuses = convert_statuses_to_enum(statuses)
|
|
91
104
|
|
|
105
|
+
# Create duration filter dict if both parameters provided
|
|
106
|
+
duration_filter = None
|
|
107
|
+
if duration_value is not None and duration_operator is not None:
|
|
108
|
+
duration_filter = {"value": duration_value, "operator": duration_operator}
|
|
109
|
+
|
|
92
110
|
runs = await runs_manager.list_runs(
|
|
93
111
|
actor=actor,
|
|
94
112
|
agent_ids=agent_ids,
|
|
@@ -103,5 +121,11 @@ async def list_runs(
|
|
|
103
121
|
step_count=step_count,
|
|
104
122
|
step_count_operator=step_count_operator,
|
|
105
123
|
tools_used=tools_used,
|
|
124
|
+
project_id=project_id,
|
|
125
|
+
order_by=order_by,
|
|
126
|
+
duration_percentile=duration_percentile,
|
|
127
|
+
duration_filter=duration_filter,
|
|
128
|
+
start_date=start_date,
|
|
129
|
+
end_date=end_date,
|
|
106
130
|
)
|
|
107
131
|
return runs
|
|
@@ -23,7 +23,6 @@ from letta.server.rest_api.streaming_response import (
|
|
|
23
23
|
cancellation_aware_stream_wrapper,
|
|
24
24
|
)
|
|
25
25
|
from letta.server.server import SyncServer
|
|
26
|
-
from letta.services.lettuce import LettuceClient
|
|
27
26
|
from letta.services.run_manager import RunManager
|
|
28
27
|
from letta.settings import settings
|
|
29
28
|
|
|
@@ -150,26 +149,7 @@ async def retrieve_run(
|
|
|
150
149
|
"""
|
|
151
150
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
152
151
|
runs_manager = RunManager()
|
|
153
|
-
|
|
154
|
-
run = await runs_manager.get_run_by_id(run_id=run_id, actor=actor)
|
|
155
|
-
|
|
156
|
-
use_lettuce = run.metadata and run.metadata.get("lettuce")
|
|
157
|
-
if use_lettuce and run.status not in [RunStatus.completed, RunStatus.failed, RunStatus.cancelled]:
|
|
158
|
-
lettuce_client = await LettuceClient.create()
|
|
159
|
-
status = await lettuce_client.get_status(run_id=run_id)
|
|
160
|
-
|
|
161
|
-
# Map the status to our enum
|
|
162
|
-
run_status = run.status
|
|
163
|
-
if status == "RUNNING":
|
|
164
|
-
run_status = RunStatus.running
|
|
165
|
-
elif status == "COMPLETED":
|
|
166
|
-
run_status = RunStatus.completed
|
|
167
|
-
elif status == "FAILED":
|
|
168
|
-
run_status = RunStatus.failed
|
|
169
|
-
elif status == "CANCELLED":
|
|
170
|
-
run_status = RunStatus.cancelled
|
|
171
|
-
run.status = run_status
|
|
172
|
-
return run
|
|
152
|
+
return await runs_manager.get_run_with_status(run_id=run_id, actor=actor)
|
|
173
153
|
|
|
174
154
|
|
|
175
155
|
RunMessagesResponse = Annotated[
|
|
@@ -276,7 +256,7 @@ async def delete_run(
|
|
|
276
256
|
"""
|
|
277
257
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
278
258
|
runs_manager = RunManager()
|
|
279
|
-
return await runs_manager.
|
|
259
|
+
return await runs_manager.delete_run(run_id=run_id, actor=actor)
|
|
280
260
|
|
|
281
261
|
|
|
282
262
|
@router.post(
|
|
@@ -7,6 +7,7 @@ from httpx import ConnectError, HTTPStatusError
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
from starlette.responses import StreamingResponse
|
|
9
9
|
|
|
10
|
+
from letta.constants import DEFAULT_GENERATE_TOOL_MODEL_HANDLE
|
|
10
11
|
from letta.errors import (
|
|
11
12
|
LettaInvalidArgumentError,
|
|
12
13
|
LettaInvalidMCPSchemaError,
|
|
@@ -817,7 +818,7 @@ async def generate_tool_from_prompt(
|
|
|
817
818
|
Generate a tool from the given user prompt.
|
|
818
819
|
"""
|
|
819
820
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
820
|
-
llm_config = await server.get_cached_llm_config_async(actor=actor, handle=request.handle or
|
|
821
|
+
llm_config = await server.get_cached_llm_config_async(actor=actor, handle=request.handle or DEFAULT_GENERATE_TOOL_MODEL_HANDLE)
|
|
821
822
|
formatted_prompt = (
|
|
822
823
|
f"Generate a python function named {request.tool_name} using the instructions below "
|
|
823
824
|
+ (f"based on this starter code: \n\n```\n{request.starter_code}\n```\n\n" if request.starter_code else "\n")
|
|
@@ -867,12 +868,22 @@ async def generate_tool_from_prompt(
|
|
|
867
868
|
response = llm_client.convert_response_to_chat_completion(response_data, input_messages, llm_config)
|
|
868
869
|
output = json.loads(response.choices[0].message.tool_calls[0].function.arguments)
|
|
869
870
|
pip_requirements = [PipRequirement(name=k, version=v or None) for k, v in json.loads(output["pip_requirements_json"]).items()]
|
|
871
|
+
|
|
872
|
+
# Derive JSON schema from the generated source code
|
|
873
|
+
try:
|
|
874
|
+
json_schema = derive_openai_json_schema(source_code=output["raw_source_code"])
|
|
875
|
+
except Exception as e:
|
|
876
|
+
raise LettaInvalidArgumentError(
|
|
877
|
+
message=f"Failed to generate JSON schema for tool '{request.tool_name}': {e}", argument_name="tool_name"
|
|
878
|
+
)
|
|
879
|
+
|
|
870
880
|
return GenerateToolOutput(
|
|
871
881
|
tool=Tool(
|
|
872
882
|
name=request.tool_name,
|
|
873
883
|
source_type="python",
|
|
874
884
|
source_code=output["raw_source_code"],
|
|
875
885
|
pip_requirements=pip_requirements,
|
|
886
|
+
json_schema=json_schema,
|
|
876
887
|
),
|
|
877
888
|
sample_args=json.loads(output["sample_args_json"]),
|
|
878
889
|
response=response.choices[0].message.content,
|
letta/server/server.py
CHANGED
|
@@ -304,9 +304,8 @@ class SyncServer(object):
|
|
|
304
304
|
if model_settings.openrouter_api_key:
|
|
305
305
|
self._enabled_providers.append(
|
|
306
306
|
OpenRouterProvider(
|
|
307
|
-
name="openrouter",
|
|
307
|
+
name=model_settings.openrouter_handle_base if model_settings.openrouter_handle_base else "openrouter",
|
|
308
308
|
api_key=model_settings.openrouter_api_key,
|
|
309
|
-
handle_base=model_settings.openrouter_handle_base,
|
|
310
309
|
)
|
|
311
310
|
)
|
|
312
311
|
|
|
@@ -415,21 +414,38 @@ class SyncServer(object):
|
|
|
415
414
|
actor: User,
|
|
416
415
|
) -> AgentState:
|
|
417
416
|
if request.llm_config is None:
|
|
417
|
+
additional_config_params = {}
|
|
418
418
|
if request.model is None:
|
|
419
419
|
if settings.default_llm_handle is None:
|
|
420
420
|
raise LettaInvalidArgumentError("Must specify either model or llm_config in request", argument_name="model")
|
|
421
421
|
else:
|
|
422
|
-
|
|
422
|
+
handle = settings.default_llm_handle
|
|
423
|
+
else:
|
|
424
|
+
if isinstance(request.model, str):
|
|
425
|
+
handle = request.model
|
|
426
|
+
elif isinstance(request.model, list):
|
|
427
|
+
raise LettaInvalidArgumentError("Multiple models are not supported yet")
|
|
428
|
+
else:
|
|
429
|
+
# EXTREMELEY HACKY, TEMPORARY WORKAROUND
|
|
430
|
+
handle = f"{request.model.provider}/{request.model.model}"
|
|
431
|
+
# TODO: figure out how to override various params
|
|
432
|
+
additional_config_params = request.model._to_legacy_config_params()
|
|
433
|
+
|
|
423
434
|
config_params = {
|
|
424
|
-
"handle":
|
|
435
|
+
"handle": handle,
|
|
425
436
|
"context_window_limit": request.context_window_limit,
|
|
426
437
|
"max_tokens": request.max_tokens,
|
|
427
438
|
"max_reasoning_tokens": request.max_reasoning_tokens,
|
|
428
439
|
"enable_reasoner": request.enable_reasoner,
|
|
429
440
|
}
|
|
441
|
+
config_params.update(additional_config_params)
|
|
430
442
|
log_event(name="start get_cached_llm_config", attributes=config_params)
|
|
431
443
|
request.llm_config = await self.get_cached_llm_config_async(actor=actor, **config_params)
|
|
432
444
|
log_event(name="end get_cached_llm_config", attributes=config_params)
|
|
445
|
+
if request.model and isinstance(request.model, str):
|
|
446
|
+
assert request.llm_config.handle == request.model, (
|
|
447
|
+
f"LLM config handle {request.llm_config.handle} does not match request handle {request.model}"
|
|
448
|
+
)
|
|
433
449
|
|
|
434
450
|
if request.reasoning is None:
|
|
435
451
|
request.reasoning = request.llm_config.enable_reasoner or request.llm_config.put_inner_thoughts_in_kwargs
|
letta/services/agent_manager.py
CHANGED
|
@@ -411,9 +411,6 @@ class AgentManager:
|
|
|
411
411
|
if agent_create.include_multi_agent_tools:
|
|
412
412
|
tool_names |= calculate_multi_agent_tools()
|
|
413
413
|
|
|
414
|
-
# take out the deprecated tool names
|
|
415
|
-
tool_names.difference_update(set(DEPRECATED_LETTA_TOOLS))
|
|
416
|
-
|
|
417
414
|
supplied_ids = set(agent_create.tool_ids or [])
|
|
418
415
|
|
|
419
416
|
source_ids = agent_create.source_ids or []
|
|
@@ -1568,7 +1565,7 @@ class AgentManager:
|
|
|
1568
1565
|
actor: User performing the action
|
|
1569
1566
|
|
|
1570
1567
|
Raises:
|
|
1571
|
-
|
|
1568
|
+
NoResultFound: If either agent or source doesn't exist or actor lacks permission to access them
|
|
1572
1569
|
IntegrityError: If the source is already attached to the agent
|
|
1573
1570
|
"""
|
|
1574
1571
|
|
|
@@ -1576,6 +1573,9 @@ class AgentManager:
|
|
|
1576
1573
|
# Verify both agent and source exist and user has permission to access them
|
|
1577
1574
|
agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
|
1578
1575
|
|
|
1576
|
+
# Verify the actor has permission to access the source
|
|
1577
|
+
await SourceModel.read_async(db_session=session, identifier=source_id, actor=actor)
|
|
1578
|
+
|
|
1579
1579
|
# The _process_relationship helper already handles duplicate checking via unique constraint
|
|
1580
1580
|
await _process_relationship_async(
|
|
1581
1581
|
session=session,
|
|
@@ -14,6 +14,7 @@ from letta.schemas.embedding_config import EmbeddingConfig
|
|
|
14
14
|
from letta.schemas.enums import PrimitiveType, VectorDBProvider
|
|
15
15
|
from letta.schemas.user import User as PydanticUser
|
|
16
16
|
from letta.server.db import db_registry
|
|
17
|
+
from letta.services.helpers.agent_manager_helper import validate_agent_exists_async
|
|
17
18
|
from letta.settings import DatabaseChoice, settings
|
|
18
19
|
from letta.utils import enforce_types
|
|
19
20
|
from letta.validators import raise_on_invalid_id
|
|
@@ -130,6 +131,9 @@ class ArchiveManager:
|
|
|
130
131
|
]
|
|
131
132
|
|
|
132
133
|
async with db_registry.async_session() as session:
|
|
134
|
+
if agent_id:
|
|
135
|
+
await validate_agent_exists_async(session, agent_id, actor)
|
|
136
|
+
|
|
133
137
|
archives = await ArchiveModel.list_async(
|
|
134
138
|
db_session=session,
|
|
135
139
|
before=before,
|
|
@@ -157,6 +161,12 @@ class ArchiveManager:
|
|
|
157
161
|
) -> None:
|
|
158
162
|
"""Attach an agent to an archive."""
|
|
159
163
|
async with db_registry.async_session() as session:
|
|
164
|
+
# Verify agent exists and user has access to it
|
|
165
|
+
await validate_agent_exists_async(session, agent_id, actor)
|
|
166
|
+
|
|
167
|
+
# Verify archive exists and user has access to it
|
|
168
|
+
await ArchiveModel.read_async(db_session=session, identifier=archive_id, actor=actor)
|
|
169
|
+
|
|
160
170
|
# Check if relationship already exists
|
|
161
171
|
existing = await session.execute(
|
|
162
172
|
select(ArchivesAgents).where(
|
|
@@ -194,6 +204,12 @@ class ArchiveManager:
|
|
|
194
204
|
) -> None:
|
|
195
205
|
"""Detach an agent from an archive."""
|
|
196
206
|
async with db_registry.async_session() as session:
|
|
207
|
+
# Verify agent exists and user has access to it
|
|
208
|
+
await validate_agent_exists_async(session, agent_id, actor)
|
|
209
|
+
|
|
210
|
+
# Verify archive exists and user has access to it
|
|
211
|
+
await ArchiveModel.read_async(db_session=session, identifier=archive_id, actor=actor)
|
|
212
|
+
|
|
197
213
|
# Delete the relationship directly
|
|
198
214
|
result = await session.execute(
|
|
199
215
|
delete(ArchivesAgents).where(
|
letta/services/group_manager.py
CHANGED
|
@@ -5,8 +5,10 @@ from sqlalchemy import and_, asc, delete, desc, or_, select
|
|
|
5
5
|
from sqlalchemy.orm import Session
|
|
6
6
|
|
|
7
7
|
from letta.orm.agent import Agent as AgentModel
|
|
8
|
+
from letta.orm.block import Block
|
|
8
9
|
from letta.orm.errors import NoResultFound
|
|
9
10
|
from letta.orm.group import Group as GroupModel
|
|
11
|
+
from letta.orm.groups_blocks import GroupsBlocks
|
|
10
12
|
from letta.orm.message import Message as MessageModel
|
|
11
13
|
from letta.otel.tracing import trace_method
|
|
12
14
|
from letta.schemas.enums import PrimitiveType
|
|
@@ -410,6 +412,48 @@ class GroupManager:
|
|
|
410
412
|
for block in blocks:
|
|
411
413
|
session.add(BlocksAgents(agent_id=manager_agent.id, block_id=block.id, block_label=block.label))
|
|
412
414
|
|
|
415
|
+
@enforce_types
|
|
416
|
+
@trace_method
|
|
417
|
+
@raise_on_invalid_id(param_name="group_id", expected_prefix=PrimitiveType.GROUP)
|
|
418
|
+
@raise_on_invalid_id(param_name="block_id", expected_prefix=PrimitiveType.BLOCK)
|
|
419
|
+
async def attach_block_async(self, group_id: str, block_id: str, actor: PydanticUser) -> None:
|
|
420
|
+
"""Attach a block to a group."""
|
|
421
|
+
async with db_registry.async_session() as session:
|
|
422
|
+
# Verify group exists and user has access
|
|
423
|
+
await GroupModel.read_async(db_session=session, identifier=group_id, actor=actor)
|
|
424
|
+
|
|
425
|
+
# Verify block exists AND user has access to it
|
|
426
|
+
await Block.read_async(db_session=session, identifier=block_id, actor=actor)
|
|
427
|
+
|
|
428
|
+
# Check if block is already attached to the group
|
|
429
|
+
check_query = select(GroupsBlocks).where(and_(GroupsBlocks.group_id == group_id, GroupsBlocks.block_id == block_id))
|
|
430
|
+
result = await session.execute(check_query)
|
|
431
|
+
if result.scalar_one_or_none():
|
|
432
|
+
# Block already attached, no-op
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
# Add block to group
|
|
436
|
+
session.add(GroupsBlocks(group_id=group_id, block_id=block_id))
|
|
437
|
+
await session.commit()
|
|
438
|
+
|
|
439
|
+
@enforce_types
|
|
440
|
+
@trace_method
|
|
441
|
+
@raise_on_invalid_id(param_name="group_id", expected_prefix=PrimitiveType.GROUP)
|
|
442
|
+
@raise_on_invalid_id(param_name="block_id", expected_prefix=PrimitiveType.BLOCK)
|
|
443
|
+
async def detach_block_async(self, group_id: str, block_id: str, actor: PydanticUser) -> None:
|
|
444
|
+
"""Detach a block from a group."""
|
|
445
|
+
async with db_registry.async_session() as session:
|
|
446
|
+
# Verify group exists and user has access
|
|
447
|
+
await GroupModel.read_async(db_session=session, identifier=group_id, actor=actor)
|
|
448
|
+
|
|
449
|
+
# Verify block exists AND user has access to it
|
|
450
|
+
await Block.read_async(db_session=session, identifier=block_id, actor=actor)
|
|
451
|
+
|
|
452
|
+
# Remove block from group
|
|
453
|
+
delete_group_block = delete(GroupsBlocks).where(and_(GroupsBlocks.group_id == group_id, GroupsBlocks.block_id == block_id))
|
|
454
|
+
await session.execute(delete_group_block)
|
|
455
|
+
await session.commit()
|
|
456
|
+
|
|
413
457
|
@staticmethod
|
|
414
458
|
def ensure_buffer_length_range_valid(
|
|
415
459
|
max_value: Optional[int],
|
|
@@ -37,7 +37,7 @@ async def _apply_pagination_async(
|
|
|
37
37
|
RunModel.id,
|
|
38
38
|
after_sort_value,
|
|
39
39
|
after_id,
|
|
40
|
-
forward=ascending,
|
|
40
|
+
forward=not ascending,
|
|
41
41
|
nulls_last=sort_nulls_last,
|
|
42
42
|
)
|
|
43
43
|
)
|
|
@@ -55,7 +55,7 @@ async def _apply_pagination_async(
|
|
|
55
55
|
RunModel.id,
|
|
56
56
|
before_sort_value,
|
|
57
57
|
before_id,
|
|
58
|
-
forward=
|
|
58
|
+
forward=ascending,
|
|
59
59
|
nulls_last=sort_nulls_last,
|
|
60
60
|
)
|
|
61
61
|
)
|