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.

Files changed (105) hide show
  1. letta/__init__.py +1 -1
  2. letta/adapters/simple_llm_stream_adapter.py +1 -0
  3. letta/agents/letta_agent_v2.py +8 -0
  4. letta/agents/letta_agent_v3.py +127 -27
  5. letta/agents/temporal/activities/__init__.py +25 -0
  6. letta/agents/temporal/activities/create_messages.py +26 -0
  7. letta/agents/temporal/activities/create_step.py +57 -0
  8. letta/agents/temporal/activities/example_activity.py +9 -0
  9. letta/agents/temporal/activities/execute_tool.py +130 -0
  10. letta/agents/temporal/activities/llm_request.py +114 -0
  11. letta/agents/temporal/activities/prepare_messages.py +27 -0
  12. letta/agents/temporal/activities/refresh_context.py +160 -0
  13. letta/agents/temporal/activities/summarize_conversation_history.py +77 -0
  14. letta/agents/temporal/activities/update_message_ids.py +25 -0
  15. letta/agents/temporal/activities/update_run.py +43 -0
  16. letta/agents/temporal/constants.py +59 -0
  17. letta/agents/temporal/temporal_agent_workflow.py +704 -0
  18. letta/agents/temporal/types.py +275 -0
  19. letta/constants.py +11 -0
  20. letta/errors.py +4 -0
  21. letta/functions/function_sets/base.py +0 -11
  22. letta/groups/helpers.py +7 -1
  23. letta/groups/sleeptime_multi_agent_v4.py +4 -3
  24. letta/interfaces/anthropic_streaming_interface.py +0 -1
  25. letta/interfaces/openai_streaming_interface.py +103 -100
  26. letta/llm_api/anthropic_client.py +57 -12
  27. letta/llm_api/bedrock_client.py +1 -0
  28. letta/llm_api/deepseek_client.py +3 -2
  29. letta/llm_api/google_vertex_client.py +5 -4
  30. letta/llm_api/groq_client.py +1 -0
  31. letta/llm_api/llm_client_base.py +15 -1
  32. letta/llm_api/openai.py +2 -2
  33. letta/llm_api/openai_client.py +17 -3
  34. letta/llm_api/xai_client.py +1 -0
  35. letta/orm/agent.py +3 -0
  36. letta/orm/organization.py +4 -0
  37. letta/orm/sqlalchemy_base.py +7 -0
  38. letta/otel/tracing.py +131 -4
  39. letta/schemas/agent.py +108 -40
  40. letta/schemas/agent_file.py +10 -10
  41. letta/schemas/block.py +22 -3
  42. letta/schemas/enums.py +21 -0
  43. letta/schemas/environment_variables.py +3 -2
  44. letta/schemas/group.py +3 -3
  45. letta/schemas/letta_response.py +36 -4
  46. letta/schemas/llm_batch_job.py +3 -3
  47. letta/schemas/llm_config.py +123 -4
  48. letta/schemas/mcp.py +3 -2
  49. letta/schemas/mcp_server.py +3 -2
  50. letta/schemas/message.py +167 -49
  51. letta/schemas/model.py +265 -0
  52. letta/schemas/organization.py +2 -1
  53. letta/schemas/passage.py +2 -1
  54. letta/schemas/provider_trace.py +2 -1
  55. letta/schemas/providers/openrouter.py +1 -2
  56. letta/schemas/run_metrics.py +2 -1
  57. letta/schemas/sandbox_config.py +3 -1
  58. letta/schemas/step_metrics.py +2 -1
  59. letta/schemas/tool_rule.py +2 -2
  60. letta/schemas/user.py +2 -1
  61. letta/server/rest_api/app.py +5 -1
  62. letta/server/rest_api/routers/v1/__init__.py +4 -0
  63. letta/server/rest_api/routers/v1/agents.py +71 -9
  64. letta/server/rest_api/routers/v1/blocks.py +7 -7
  65. letta/server/rest_api/routers/v1/groups.py +40 -0
  66. letta/server/rest_api/routers/v1/identities.py +2 -2
  67. letta/server/rest_api/routers/v1/internal_agents.py +31 -0
  68. letta/server/rest_api/routers/v1/internal_blocks.py +177 -0
  69. letta/server/rest_api/routers/v1/internal_runs.py +25 -1
  70. letta/server/rest_api/routers/v1/runs.py +2 -22
  71. letta/server/rest_api/routers/v1/tools.py +12 -1
  72. letta/server/server.py +20 -4
  73. letta/services/agent_manager.py +4 -4
  74. letta/services/archive_manager.py +16 -0
  75. letta/services/group_manager.py +44 -0
  76. letta/services/helpers/run_manager_helper.py +2 -2
  77. letta/services/lettuce/lettuce_client.py +148 -0
  78. letta/services/mcp/base_client.py +9 -3
  79. letta/services/run_manager.py +148 -37
  80. letta/services/source_manager.py +91 -3
  81. letta/services/step_manager.py +2 -3
  82. letta/services/streaming_service.py +52 -13
  83. letta/services/summarizer/summarizer.py +28 -2
  84. letta/services/tool_executor/builtin_tool_executor.py +1 -1
  85. letta/services/tool_executor/core_tool_executor.py +2 -117
  86. letta/services/tool_sandbox/e2b_sandbox.py +4 -1
  87. letta/services/tool_schema_generator.py +2 -2
  88. letta/validators.py +21 -0
  89. {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/METADATA +1 -1
  90. {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/RECORD +93 -87
  91. letta/agent.py +0 -1758
  92. letta/cli/cli_load.py +0 -16
  93. letta/client/__init__.py +0 -0
  94. letta/client/streaming.py +0 -95
  95. letta/client/utils.py +0 -78
  96. letta/functions/async_composio_toolset.py +0 -109
  97. letta/functions/composio_helpers.py +0 -96
  98. letta/helpers/composio_helpers.py +0 -38
  99. letta/orm/job_messages.py +0 -33
  100. letta/schemas/providers.py +0 -1617
  101. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -132
  102. letta/services/tool_executor/composio_tool_executor.py +0 -57
  103. {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/WHEEL +0 -0
  104. {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/entry_points.txt +0 -0
  105. {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[Block], operation_id="list_blocks_for_identity")
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.delete_run_by_id(run_id=run_id, actor=actor)
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 "anthropic/claude-3-5-sonnet-20240620")
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
- request.model = settings.default_llm_handle
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": request.model,
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
@@ -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
- ValueError: If either agent or source doesn't exist
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(
@@ -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=not ascending,
58
+ forward=ascending,
59
59
  nulls_last=sort_nulls_last,
60
60
  )
61
61
  )