letta-nightly 0.12.1.dev20251024104217__py3-none-any.whl → 0.13.0.dev20251024223017__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 -3
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/simple_llm_request_adapter.py +8 -5
- letta/adapters/simple_llm_stream_adapter.py +22 -6
- letta/agents/agent_loop.py +10 -3
- letta/agents/base_agent.py +4 -1
- letta/agents/helpers.py +41 -9
- letta/agents/letta_agent.py +11 -10
- letta/agents/letta_agent_v2.py +47 -37
- letta/agents/letta_agent_v3.py +395 -300
- letta/agents/voice_agent.py +8 -6
- letta/agents/voice_sleeptime_agent.py +3 -3
- letta/constants.py +30 -7
- letta/errors.py +20 -0
- letta/functions/function_sets/base.py +55 -3
- letta/functions/mcp_client/types.py +33 -57
- letta/functions/schema_generator.py +135 -23
- letta/groups/sleeptime_multi_agent_v3.py +6 -11
- letta/groups/sleeptime_multi_agent_v4.py +227 -0
- letta/helpers/converters.py +78 -4
- letta/helpers/crypto_utils.py +6 -2
- letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
- letta/interfaces/anthropic_streaming_interface.py +3 -4
- letta/interfaces/gemini_streaming_interface.py +4 -6
- letta/interfaces/openai_streaming_interface.py +63 -28
- letta/llm_api/anthropic_client.py +7 -4
- letta/llm_api/deepseek_client.py +6 -4
- letta/llm_api/google_ai_client.py +3 -12
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +90 -61
- letta/llm_api/llm_api_tools.py +4 -1
- letta/llm_api/openai.py +12 -12
- letta/llm_api/openai_client.py +53 -16
- letta/local_llm/constants.py +4 -3
- letta/local_llm/json_parser.py +5 -2
- letta/local_llm/utils.py +2 -3
- letta/log.py +171 -7
- letta/orm/agent.py +43 -9
- letta/orm/archive.py +4 -0
- letta/orm/custom_columns.py +15 -0
- letta/orm/identity.py +11 -11
- letta/orm/mcp_server.py +9 -0
- letta/orm/message.py +6 -1
- letta/orm/run_metrics.py +7 -2
- letta/orm/sqlalchemy_base.py +2 -2
- letta/orm/tool.py +3 -0
- letta/otel/tracing.py +2 -0
- letta/prompts/prompt_generator.py +7 -2
- letta/schemas/agent.py +41 -10
- letta/schemas/agent_file.py +3 -0
- letta/schemas/archive.py +4 -2
- letta/schemas/block.py +2 -1
- letta/schemas/enums.py +36 -3
- letta/schemas/file.py +3 -3
- letta/schemas/folder.py +2 -1
- letta/schemas/group.py +2 -1
- letta/schemas/identity.py +18 -9
- letta/schemas/job.py +3 -1
- letta/schemas/letta_message.py +71 -12
- letta/schemas/letta_request.py +7 -3
- letta/schemas/letta_stop_reason.py +0 -25
- letta/schemas/llm_config.py +8 -2
- letta/schemas/mcp.py +80 -83
- letta/schemas/mcp_server.py +349 -0
- letta/schemas/memory.py +20 -8
- letta/schemas/message.py +212 -67
- letta/schemas/providers/anthropic.py +13 -6
- letta/schemas/providers/azure.py +6 -4
- letta/schemas/providers/base.py +8 -4
- letta/schemas/providers/bedrock.py +6 -2
- letta/schemas/providers/cerebras.py +7 -3
- letta/schemas/providers/deepseek.py +2 -1
- letta/schemas/providers/google_gemini.py +15 -6
- letta/schemas/providers/groq.py +2 -1
- letta/schemas/providers/lmstudio.py +9 -6
- letta/schemas/providers/mistral.py +2 -1
- letta/schemas/providers/openai.py +7 -2
- letta/schemas/providers/together.py +9 -3
- letta/schemas/providers/xai.py +7 -3
- letta/schemas/run.py +7 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +2 -2
- letta/schemas/secret.py +3 -158
- letta/schemas/source.py +2 -2
- letta/schemas/step.py +2 -2
- letta/schemas/tool.py +24 -1
- letta/schemas/usage.py +0 -1
- letta/server/rest_api/app.py +123 -7
- letta/server/rest_api/dependencies.py +3 -0
- letta/server/rest_api/interface.py +7 -4
- letta/server/rest_api/redis_stream_manager.py +16 -1
- letta/server/rest_api/routers/v1/__init__.py +7 -0
- letta/server/rest_api/routers/v1/agents.py +332 -322
- letta/server/rest_api/routers/v1/archives.py +127 -40
- letta/server/rest_api/routers/v1/blocks.py +54 -6
- letta/server/rest_api/routers/v1/chat_completions.py +146 -0
- letta/server/rest_api/routers/v1/folders.py +27 -35
- letta/server/rest_api/routers/v1/groups.py +23 -35
- letta/server/rest_api/routers/v1/identities.py +24 -10
- letta/server/rest_api/routers/v1/internal_runs.py +107 -0
- letta/server/rest_api/routers/v1/internal_templates.py +162 -179
- letta/server/rest_api/routers/v1/jobs.py +15 -27
- letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
- letta/server/rest_api/routers/v1/messages.py +23 -34
- letta/server/rest_api/routers/v1/organizations.py +6 -27
- letta/server/rest_api/routers/v1/providers.py +35 -62
- letta/server/rest_api/routers/v1/runs.py +30 -43
- letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
- letta/server/rest_api/routers/v1/sources.py +26 -42
- letta/server/rest_api/routers/v1/steps.py +16 -29
- letta/server/rest_api/routers/v1/tools.py +17 -13
- letta/server/rest_api/routers/v1/users.py +5 -17
- letta/server/rest_api/routers/v1/voice.py +18 -27
- letta/server/rest_api/streaming_response.py +5 -2
- letta/server/rest_api/utils.py +187 -25
- letta/server/server.py +27 -22
- letta/server/ws_api/server.py +5 -4
- letta/services/agent_manager.py +148 -26
- letta/services/agent_serialization_manager.py +6 -1
- letta/services/archive_manager.py +168 -15
- letta/services/block_manager.py +14 -4
- letta/services/file_manager.py +33 -29
- letta/services/group_manager.py +10 -0
- letta/services/helpers/agent_manager_helper.py +65 -11
- letta/services/identity_manager.py +105 -4
- letta/services/job_manager.py +11 -1
- letta/services/mcp/base_client.py +2 -2
- letta/services/mcp/oauth_utils.py +33 -8
- letta/services/mcp_manager.py +174 -78
- letta/services/mcp_server_manager.py +1331 -0
- letta/services/message_manager.py +109 -4
- letta/services/organization_manager.py +4 -4
- letta/services/passage_manager.py +9 -25
- letta/services/provider_manager.py +91 -15
- letta/services/run_manager.py +72 -15
- letta/services/sandbox_config_manager.py +45 -3
- letta/services/source_manager.py +15 -8
- letta/services/step_manager.py +24 -1
- letta/services/streaming_service.py +581 -0
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +111 -0
- letta/services/tool_executor/files_tool_executor.py +5 -3
- letta/services/tool_executor/sandbox_tool_executor.py +2 -2
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_manager.py +10 -3
- letta/services/tool_sandbox/base.py +61 -1
- letta/services/tool_sandbox/local_sandbox.py +1 -3
- letta/services/user_manager.py +2 -2
- letta/settings.py +49 -5
- letta/system.py +14 -5
- letta/utils.py +73 -1
- letta/validators.py +105 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/RECORD +157 -151
- letta/schemas/letta_ping.py +0 -28
- letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
from datetime import datetime
|
|
1
2
|
from typing import List, Literal, Optional
|
|
2
3
|
|
|
3
|
-
from fastapi import APIRouter, Body, Depends,
|
|
4
|
-
from pydantic import BaseModel
|
|
4
|
+
from fastapi import APIRouter, Body, Depends, Query
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
5
6
|
|
|
6
|
-
from letta
|
|
7
|
-
from letta.schemas.
|
|
7
|
+
from letta import AgentState
|
|
8
|
+
from letta.schemas.agent import AgentRelationships
|
|
9
|
+
from letta.schemas.archive import Archive as PydanticArchive, ArchiveBase
|
|
10
|
+
from letta.schemas.embedding_config import EmbeddingConfig
|
|
11
|
+
from letta.schemas.passage import Passage as PydanticPassage
|
|
8
12
|
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
|
9
13
|
from letta.server.server import SyncServer
|
|
14
|
+
from letta.validators import AgentId, ArchiveId, PassageId
|
|
10
15
|
|
|
11
16
|
router = APIRouter(prefix="/archives", tags=["archives"])
|
|
12
17
|
|
|
@@ -18,6 +23,7 @@ class ArchiveCreateRequest(BaseModel):
|
|
|
18
23
|
"""
|
|
19
24
|
|
|
20
25
|
name: str
|
|
26
|
+
embedding_config: EmbeddingConfig = Field(..., description="Embedding configuration for the archive")
|
|
21
27
|
description: Optional[str] = None
|
|
22
28
|
|
|
23
29
|
|
|
@@ -40,15 +46,13 @@ async def create_archive(
|
|
|
40
46
|
"""
|
|
41
47
|
Create a new archive.
|
|
42
48
|
"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
except Exception as e:
|
|
51
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
49
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
50
|
+
return await server.archive_manager.create_archive_async(
|
|
51
|
+
name=archive.name,
|
|
52
|
+
embedding_config=archive.embedding_config,
|
|
53
|
+
description=archive.description,
|
|
54
|
+
actor=actor,
|
|
55
|
+
)
|
|
52
56
|
|
|
53
57
|
|
|
54
58
|
@router.get("/", response_model=List[PydanticArchive], operation_id="list_archives")
|
|
@@ -65,6 +69,7 @@ async def list_archives(
|
|
|
65
69
|
order: Literal["asc", "desc"] = Query(
|
|
66
70
|
"desc", description="Sort order for archives by creation time. 'asc' for oldest first, 'desc' for newest first"
|
|
67
71
|
),
|
|
72
|
+
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
|
68
73
|
name: Optional[str] = Query(None, description="Filter by archive name (exact match)"),
|
|
69
74
|
agent_id: Optional[str] = Query(None, description="Only archives attached to this agent ID"),
|
|
70
75
|
server: "SyncServer" = Depends(get_letta_server),
|
|
@@ -73,25 +78,38 @@ async def list_archives(
|
|
|
73
78
|
"""
|
|
74
79
|
Get a list of all archives for the current organization with optional filters and pagination.
|
|
75
80
|
"""
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
82
|
+
archives = await server.archive_manager.list_archives_async(
|
|
83
|
+
actor=actor,
|
|
84
|
+
before=before,
|
|
85
|
+
after=after,
|
|
86
|
+
limit=limit,
|
|
87
|
+
ascending=(order == "asc"),
|
|
88
|
+
name=name,
|
|
89
|
+
agent_id=agent_id,
|
|
90
|
+
)
|
|
91
|
+
return archives
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@router.get("/{archive_id}", response_model=PydanticArchive, operation_id="get_archive_by_id")
|
|
95
|
+
async def get_archive_by_id(
|
|
96
|
+
archive_id: ArchiveId,
|
|
97
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
98
|
+
headers: HeaderParams = Depends(get_headers),
|
|
99
|
+
):
|
|
100
|
+
"""
|
|
101
|
+
Get a single archive by its ID.
|
|
102
|
+
"""
|
|
103
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
104
|
+
return await server.archive_manager.get_archive_by_id_async(
|
|
105
|
+
archive_id=archive_id,
|
|
106
|
+
actor=actor,
|
|
107
|
+
)
|
|
90
108
|
|
|
91
109
|
|
|
92
110
|
@router.patch("/{archive_id}", response_model=PydanticArchive, operation_id="modify_archive")
|
|
93
111
|
async def modify_archive(
|
|
94
|
-
archive_id:
|
|
112
|
+
archive_id: ArchiveId,
|
|
95
113
|
archive: ArchiveUpdateRequest = Body(...),
|
|
96
114
|
server: "SyncServer" = Depends(get_letta_server),
|
|
97
115
|
headers: HeaderParams = Depends(get_headers),
|
|
@@ -99,15 +117,84 @@ async def modify_archive(
|
|
|
99
117
|
"""
|
|
100
118
|
Update an existing archive's name and/or description.
|
|
101
119
|
"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
121
|
+
return await server.archive_manager.update_archive_async(
|
|
122
|
+
archive_id=archive_id,
|
|
123
|
+
name=archive.name,
|
|
124
|
+
description=archive.description,
|
|
125
|
+
actor=actor,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@router.delete("/{archive_id}", response_model=PydanticArchive, operation_id="delete_archive")
|
|
130
|
+
async def delete_archive(
|
|
131
|
+
archive_id: ArchiveId,
|
|
132
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
133
|
+
headers: HeaderParams = Depends(get_headers),
|
|
134
|
+
):
|
|
135
|
+
"""
|
|
136
|
+
Delete an archive by its ID.
|
|
137
|
+
"""
|
|
138
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
139
|
+
return await server.archive_manager.delete_archive_async(
|
|
140
|
+
archive_id=archive_id,
|
|
141
|
+
actor=actor,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@router.get("/{archive_id}/agents", response_model=List[AgentState], operation_id="list_agents_for_archive")
|
|
146
|
+
async def list_agents_for_archive(
|
|
147
|
+
archive_id: ArchiveId,
|
|
148
|
+
before: Optional[str] = Query(
|
|
149
|
+
None,
|
|
150
|
+
description="Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order",
|
|
151
|
+
),
|
|
152
|
+
after: Optional[str] = Query(
|
|
153
|
+
None,
|
|
154
|
+
description="Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order",
|
|
155
|
+
),
|
|
156
|
+
limit: Optional[int] = Query(50, description="Maximum number of agents to return"),
|
|
157
|
+
order: Literal["asc", "desc"] = Query(
|
|
158
|
+
"desc", description="Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first"
|
|
159
|
+
),
|
|
160
|
+
include: List[AgentRelationships] = Query(
|
|
161
|
+
[],
|
|
162
|
+
description=("Specify which relational fields to include in the response. No relationships are included by default."),
|
|
163
|
+
),
|
|
164
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
165
|
+
headers: HeaderParams = Depends(get_headers),
|
|
166
|
+
):
|
|
167
|
+
"""
|
|
168
|
+
Get a list of agents that have access to an archive with pagination support.
|
|
169
|
+
"""
|
|
170
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
171
|
+
return await server.archive_manager.get_agents_for_archive_async(
|
|
172
|
+
archive_id=archive_id,
|
|
173
|
+
actor=actor,
|
|
174
|
+
before=before,
|
|
175
|
+
after=after,
|
|
176
|
+
limit=limit,
|
|
177
|
+
include=include,
|
|
178
|
+
ascending=(order == "asc"),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@router.delete("/{archive_id}/passages/{passage_id}", status_code=204, operation_id="delete_passage_from_archive")
|
|
183
|
+
async def delete_passage_from_archive(
|
|
184
|
+
archive_id: ArchiveId,
|
|
185
|
+
passage_id: PassageId,
|
|
186
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
187
|
+
headers: HeaderParams = Depends(get_headers),
|
|
188
|
+
):
|
|
189
|
+
"""
|
|
190
|
+
Delete a passage from an archive.
|
|
191
|
+
|
|
192
|
+
This permanently removes the passage from both the database and vector storage (if applicable).
|
|
193
|
+
"""
|
|
194
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
195
|
+
await server.archive_manager.delete_passage_from_archive_async(
|
|
196
|
+
archive_id=archive_id,
|
|
197
|
+
passage_id=passage_id,
|
|
198
|
+
actor=actor,
|
|
199
|
+
)
|
|
200
|
+
return None
|
|
@@ -3,10 +3,12 @@ from typing import TYPE_CHECKING, List, Literal, Optional
|
|
|
3
3
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
|
4
4
|
|
|
5
5
|
from letta.orm.errors import NoResultFound
|
|
6
|
-
from letta.schemas.agent import AgentState
|
|
7
|
-
from letta.schemas.block import Block, BlockUpdate, CreateBlock
|
|
6
|
+
from letta.schemas.agent import AgentRelationships, AgentState
|
|
7
|
+
from letta.schemas.block import BaseBlock, Block, BlockUpdate, CreateBlock
|
|
8
8
|
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
|
9
9
|
from letta.server.server import SyncServer
|
|
10
|
+
from letta.utils import is_1_0_sdk_version
|
|
11
|
+
from letta.validators import BlockId
|
|
10
12
|
|
|
11
13
|
if TYPE_CHECKING:
|
|
12
14
|
pass
|
|
@@ -128,7 +130,7 @@ async def create_block(
|
|
|
128
130
|
|
|
129
131
|
@router.patch("/{block_id}", response_model=Block, operation_id="modify_block")
|
|
130
132
|
async def modify_block(
|
|
131
|
-
block_id:
|
|
133
|
+
block_id: BlockId,
|
|
132
134
|
block_update: BlockUpdate = Body(...),
|
|
133
135
|
server: SyncServer = Depends(get_letta_server),
|
|
134
136
|
headers: HeaderParams = Depends(get_headers),
|
|
@@ -139,7 +141,7 @@ async def modify_block(
|
|
|
139
141
|
|
|
140
142
|
@router.delete("/{block_id}", operation_id="delete_block")
|
|
141
143
|
async def delete_block(
|
|
142
|
-
block_id:
|
|
144
|
+
block_id: BlockId,
|
|
143
145
|
server: SyncServer = Depends(get_letta_server),
|
|
144
146
|
headers: HeaderParams = Depends(get_headers),
|
|
145
147
|
):
|
|
@@ -149,7 +151,7 @@ async def delete_block(
|
|
|
149
151
|
|
|
150
152
|
@router.get("/{block_id}", response_model=Block, operation_id="retrieve_block")
|
|
151
153
|
async def retrieve_block(
|
|
152
|
-
block_id:
|
|
154
|
+
block_id: BlockId,
|
|
153
155
|
server: SyncServer = Depends(get_letta_server),
|
|
154
156
|
headers: HeaderParams = Depends(get_headers),
|
|
155
157
|
):
|
|
@@ -162,7 +164,7 @@ async def retrieve_block(
|
|
|
162
164
|
|
|
163
165
|
@router.get("/{block_id}/agents", response_model=List[AgentState], operation_id="list_agents_for_block")
|
|
164
166
|
async def list_agents_for_block(
|
|
165
|
-
block_id:
|
|
167
|
+
block_id: BlockId,
|
|
166
168
|
before: Optional[str] = Query(
|
|
167
169
|
None,
|
|
168
170
|
description="Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order",
|
|
@@ -182,8 +184,13 @@ async def list_agents_for_block(
|
|
|
182
184
|
"Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. "
|
|
183
185
|
"If not provided, all relationships are loaded by default. "
|
|
184
186
|
"Using this can optimize performance by reducing unnecessary joins."
|
|
187
|
+
"This is a legacy parameter, and no longer supported after 1.0.0 SDK versions."
|
|
185
188
|
),
|
|
186
189
|
),
|
|
190
|
+
include: List[AgentRelationships] = Query(
|
|
191
|
+
[],
|
|
192
|
+
description=("Specify which relational fields to include in the response. No relationships are included by default."),
|
|
193
|
+
),
|
|
187
194
|
server: SyncServer = Depends(get_letta_server),
|
|
188
195
|
headers: HeaderParams = Depends(get_headers),
|
|
189
196
|
):
|
|
@@ -192,6 +199,8 @@ async def list_agents_for_block(
|
|
|
192
199
|
Raises a 404 if the block does not exist.
|
|
193
200
|
"""
|
|
194
201
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
202
|
+
if include_relationships is None and is_1_0_sdk_version(headers):
|
|
203
|
+
include_relationships = [] # don't default include all if using new SDK version
|
|
195
204
|
agents = await server.block_manager.get_agents_for_block_async(
|
|
196
205
|
block_id=block_id,
|
|
197
206
|
before=before,
|
|
@@ -199,6 +208,45 @@ async def list_agents_for_block(
|
|
|
199
208
|
limit=limit,
|
|
200
209
|
ascending=(order == "asc"),
|
|
201
210
|
include_relationships=include_relationships,
|
|
211
|
+
include=include,
|
|
202
212
|
actor=actor,
|
|
203
213
|
)
|
|
204
214
|
return agents
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@router.patch("/{block_id}/identities/attach/{identity_id}", response_model=Block, operation_id="attach_identity_to_block")
|
|
218
|
+
async def attach_identity_to_block(
|
|
219
|
+
identity_id: str,
|
|
220
|
+
block_id: BlockId,
|
|
221
|
+
server: SyncServer = Depends(get_letta_server),
|
|
222
|
+
headers: HeaderParams = Depends(get_headers),
|
|
223
|
+
):
|
|
224
|
+
"""
|
|
225
|
+
Attach an identity to a block.
|
|
226
|
+
"""
|
|
227
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
228
|
+
await server.identity_manager.attach_block_async(
|
|
229
|
+
identity_id=identity_id,
|
|
230
|
+
block_id=block_id,
|
|
231
|
+
actor=actor,
|
|
232
|
+
)
|
|
233
|
+
return await server.block_manager.get_block_by_id_async(block_id=block_id, actor=actor)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@router.patch("/{block_id}/identities/detach/{identity_id}", response_model=Block, operation_id="detach_identity_from_block")
|
|
237
|
+
async def detach_identity_from_block(
|
|
238
|
+
identity_id: str,
|
|
239
|
+
block_id: BlockId,
|
|
240
|
+
server: SyncServer = Depends(get_letta_server),
|
|
241
|
+
headers: HeaderParams = Depends(get_headers),
|
|
242
|
+
):
|
|
243
|
+
"""
|
|
244
|
+
Detach an identity from a block.
|
|
245
|
+
"""
|
|
246
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
247
|
+
await server.identity_manager.detach_block_async(
|
|
248
|
+
identity_id=identity_id,
|
|
249
|
+
block_id=block_id,
|
|
250
|
+
actor=actor,
|
|
251
|
+
)
|
|
252
|
+
return await server.block_manager.get_block_by_id_async(block_id=block_id, actor=actor)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from typing import Optional, Union
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Body, Depends
|
|
4
|
+
from fastapi.responses import StreamingResponse
|
|
5
|
+
from openai.types.chat import ChatCompletion
|
|
6
|
+
from openai.types.chat.chat_completion_message_param import ChatCompletionMessageParam
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from letta.errors import LettaInvalidArgumentError
|
|
10
|
+
from letta.log import get_logger
|
|
11
|
+
from letta.schemas.enums import MessageRole
|
|
12
|
+
from letta.schemas.letta_request import LettaStreamingRequest
|
|
13
|
+
from letta.schemas.message import MessageCreate
|
|
14
|
+
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
|
15
|
+
from letta.server.server import SyncServer
|
|
16
|
+
from letta.services.streaming_service import StreamingService
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
router = APIRouter(tags=["chat"])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChatCompletionRequest(BaseModel):
|
|
24
|
+
"""OpenAI-compatible chat completion request - exactly matching OpenAI's schema."""
|
|
25
|
+
|
|
26
|
+
model: str = Field(..., description="ID of the model to use")
|
|
27
|
+
messages: list[ChatCompletionMessageParam] = Field(..., description="Messages comprising the conversation so far")
|
|
28
|
+
|
|
29
|
+
# optional parameters
|
|
30
|
+
temperature: Optional[float] = Field(None, ge=0, le=2, description="Sampling temperature")
|
|
31
|
+
top_p: Optional[float] = Field(None, ge=0, le=1, description="Nucleus sampling parameter")
|
|
32
|
+
n: Optional[int] = Field(1, ge=1, description="Number of chat completion choices to generate")
|
|
33
|
+
stream: Optional[bool] = Field(False, description="Whether to stream back partial progress")
|
|
34
|
+
stop: Optional[Union[str, list[str]]] = Field(None, description="Sequences where the API will stop generating")
|
|
35
|
+
max_tokens: Optional[int] = Field(None, description="Maximum number of tokens to generate")
|
|
36
|
+
presence_penalty: Optional[float] = Field(None, ge=-2, le=2, description="Presence penalty")
|
|
37
|
+
frequency_penalty: Optional[float] = Field(None, ge=-2, le=2, description="Frequency penalty")
|
|
38
|
+
user: Optional[str] = Field(None, description="A unique identifier representing your end-user")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def _handle_chat_completion(
|
|
42
|
+
request: ChatCompletionRequest,
|
|
43
|
+
server: SyncServer,
|
|
44
|
+
headers: HeaderParams,
|
|
45
|
+
) -> Union[ChatCompletion, StreamingResponse]:
|
|
46
|
+
"""
|
|
47
|
+
Internal handler for chat completion logic.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
request: OpenAI-compatible chat completion request
|
|
51
|
+
server: Letta server instance
|
|
52
|
+
headers: Request headers with user info
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Streaming or non-streaming chat completion response
|
|
56
|
+
"""
|
|
57
|
+
if request.user:
|
|
58
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=request.user)
|
|
59
|
+
else:
|
|
60
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
61
|
+
|
|
62
|
+
resolved_agent_id = request.model
|
|
63
|
+
if not resolved_agent_id.startswith("agent-"):
|
|
64
|
+
raise LettaInvalidArgumentError(
|
|
65
|
+
f"For this endpoint, the 'model' field should contain an agent ID (format: 'agent-...'). Received: '{resolved_agent_id}'",
|
|
66
|
+
argument_name="model",
|
|
67
|
+
)
|
|
68
|
+
await server.agent_manager.validate_agent_exists_async(resolved_agent_id, actor)
|
|
69
|
+
|
|
70
|
+
# convert OpenAI messages to Letta MessageCreate format
|
|
71
|
+
# NOTE: we only process the last user message
|
|
72
|
+
if len(request.messages) > 1:
|
|
73
|
+
logger.warning(
|
|
74
|
+
f"Chat completions endpoint received {len(request.messages)} messages. "
|
|
75
|
+
"Letta maintains conversation state internally, so only the last user message will be processed. "
|
|
76
|
+
"Previous messages are already stored in the agent's memory."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
last_user_message = None
|
|
80
|
+
for msg in reversed(request.messages):
|
|
81
|
+
role = msg.get("role", "user")
|
|
82
|
+
if role == "user":
|
|
83
|
+
last_user_message = msg
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
if not last_user_message:
|
|
87
|
+
raise LettaInvalidArgumentError(
|
|
88
|
+
"No user message found in the request. Please include at least one message with role='user'.",
|
|
89
|
+
argument_name="messages",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
letta_messages = [
|
|
93
|
+
MessageCreate(
|
|
94
|
+
role=MessageRole.user,
|
|
95
|
+
content=last_user_message.get("content", ""),
|
|
96
|
+
)
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
letta_request = LettaStreamingRequest(
|
|
100
|
+
messages=letta_messages,
|
|
101
|
+
stream_tokens=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if request.stream:
|
|
105
|
+
streaming_service = StreamingService(server)
|
|
106
|
+
return await streaming_service.create_agent_stream_openai_chat_completions(
|
|
107
|
+
agent_id=resolved_agent_id,
|
|
108
|
+
actor=actor,
|
|
109
|
+
request=letta_request,
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
raise LettaInvalidArgumentError(
|
|
113
|
+
"Non-streaming chat completions not yet implemented. Please set stream=true.",
|
|
114
|
+
argument_name="stream",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@router.post(
|
|
119
|
+
"/chat/completions",
|
|
120
|
+
response_model=ChatCompletion,
|
|
121
|
+
responses={
|
|
122
|
+
200: {
|
|
123
|
+
"description": "Successful response",
|
|
124
|
+
"content": {
|
|
125
|
+
"application/json": {"schema": {"$ref": "#/components/schemas/ChatCompletion"}},
|
|
126
|
+
"text/event-stream": {"description": "Server-Sent Events stream (when stream=true)"},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
operation_id="create_chat_completion",
|
|
131
|
+
)
|
|
132
|
+
async def create_chat_completion(
|
|
133
|
+
request: ChatCompletionRequest = Body(...),
|
|
134
|
+
server: SyncServer = Depends(get_letta_server),
|
|
135
|
+
headers: HeaderParams = Depends(get_headers),
|
|
136
|
+
) -> Union[ChatCompletion, StreamingResponse]:
|
|
137
|
+
"""
|
|
138
|
+
Create a chat completion using a Letta agent (OpenAI-compatible).
|
|
139
|
+
|
|
140
|
+
This endpoint provides full OpenAI API compatibility. The agent is selected based on:
|
|
141
|
+
- The 'model' parameter in the request (should contain an agent ID in format 'agent-...')
|
|
142
|
+
|
|
143
|
+
When streaming is enabled (stream=true), the response will be Server-Sent Events
|
|
144
|
+
with ChatCompletionChunk objects.
|
|
145
|
+
"""
|
|
146
|
+
return await _handle_chat_completion(request, server, headers)
|