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.

Files changed (159) hide show
  1. letta/__init__.py +2 -3
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/simple_llm_request_adapter.py +8 -5
  4. letta/adapters/simple_llm_stream_adapter.py +22 -6
  5. letta/agents/agent_loop.py +10 -3
  6. letta/agents/base_agent.py +4 -1
  7. letta/agents/helpers.py +41 -9
  8. letta/agents/letta_agent.py +11 -10
  9. letta/agents/letta_agent_v2.py +47 -37
  10. letta/agents/letta_agent_v3.py +395 -300
  11. letta/agents/voice_agent.py +8 -6
  12. letta/agents/voice_sleeptime_agent.py +3 -3
  13. letta/constants.py +30 -7
  14. letta/errors.py +20 -0
  15. letta/functions/function_sets/base.py +55 -3
  16. letta/functions/mcp_client/types.py +33 -57
  17. letta/functions/schema_generator.py +135 -23
  18. letta/groups/sleeptime_multi_agent_v3.py +6 -11
  19. letta/groups/sleeptime_multi_agent_v4.py +227 -0
  20. letta/helpers/converters.py +78 -4
  21. letta/helpers/crypto_utils.py +6 -2
  22. letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
  23. letta/interfaces/anthropic_streaming_interface.py +3 -4
  24. letta/interfaces/gemini_streaming_interface.py +4 -6
  25. letta/interfaces/openai_streaming_interface.py +63 -28
  26. letta/llm_api/anthropic_client.py +7 -4
  27. letta/llm_api/deepseek_client.py +6 -4
  28. letta/llm_api/google_ai_client.py +3 -12
  29. letta/llm_api/google_vertex_client.py +1 -1
  30. letta/llm_api/helpers.py +90 -61
  31. letta/llm_api/llm_api_tools.py +4 -1
  32. letta/llm_api/openai.py +12 -12
  33. letta/llm_api/openai_client.py +53 -16
  34. letta/local_llm/constants.py +4 -3
  35. letta/local_llm/json_parser.py +5 -2
  36. letta/local_llm/utils.py +2 -3
  37. letta/log.py +171 -7
  38. letta/orm/agent.py +43 -9
  39. letta/orm/archive.py +4 -0
  40. letta/orm/custom_columns.py +15 -0
  41. letta/orm/identity.py +11 -11
  42. letta/orm/mcp_server.py +9 -0
  43. letta/orm/message.py +6 -1
  44. letta/orm/run_metrics.py +7 -2
  45. letta/orm/sqlalchemy_base.py +2 -2
  46. letta/orm/tool.py +3 -0
  47. letta/otel/tracing.py +2 -0
  48. letta/prompts/prompt_generator.py +7 -2
  49. letta/schemas/agent.py +41 -10
  50. letta/schemas/agent_file.py +3 -0
  51. letta/schemas/archive.py +4 -2
  52. letta/schemas/block.py +2 -1
  53. letta/schemas/enums.py +36 -3
  54. letta/schemas/file.py +3 -3
  55. letta/schemas/folder.py +2 -1
  56. letta/schemas/group.py +2 -1
  57. letta/schemas/identity.py +18 -9
  58. letta/schemas/job.py +3 -1
  59. letta/schemas/letta_message.py +71 -12
  60. letta/schemas/letta_request.py +7 -3
  61. letta/schemas/letta_stop_reason.py +0 -25
  62. letta/schemas/llm_config.py +8 -2
  63. letta/schemas/mcp.py +80 -83
  64. letta/schemas/mcp_server.py +349 -0
  65. letta/schemas/memory.py +20 -8
  66. letta/schemas/message.py +212 -67
  67. letta/schemas/providers/anthropic.py +13 -6
  68. letta/schemas/providers/azure.py +6 -4
  69. letta/schemas/providers/base.py +8 -4
  70. letta/schemas/providers/bedrock.py +6 -2
  71. letta/schemas/providers/cerebras.py +7 -3
  72. letta/schemas/providers/deepseek.py +2 -1
  73. letta/schemas/providers/google_gemini.py +15 -6
  74. letta/schemas/providers/groq.py +2 -1
  75. letta/schemas/providers/lmstudio.py +9 -6
  76. letta/schemas/providers/mistral.py +2 -1
  77. letta/schemas/providers/openai.py +7 -2
  78. letta/schemas/providers/together.py +9 -3
  79. letta/schemas/providers/xai.py +7 -3
  80. letta/schemas/run.py +7 -2
  81. letta/schemas/run_metrics.py +2 -1
  82. letta/schemas/sandbox_config.py +2 -2
  83. letta/schemas/secret.py +3 -158
  84. letta/schemas/source.py +2 -2
  85. letta/schemas/step.py +2 -2
  86. letta/schemas/tool.py +24 -1
  87. letta/schemas/usage.py +0 -1
  88. letta/server/rest_api/app.py +123 -7
  89. letta/server/rest_api/dependencies.py +3 -0
  90. letta/server/rest_api/interface.py +7 -4
  91. letta/server/rest_api/redis_stream_manager.py +16 -1
  92. letta/server/rest_api/routers/v1/__init__.py +7 -0
  93. letta/server/rest_api/routers/v1/agents.py +332 -322
  94. letta/server/rest_api/routers/v1/archives.py +127 -40
  95. letta/server/rest_api/routers/v1/blocks.py +54 -6
  96. letta/server/rest_api/routers/v1/chat_completions.py +146 -0
  97. letta/server/rest_api/routers/v1/folders.py +27 -35
  98. letta/server/rest_api/routers/v1/groups.py +23 -35
  99. letta/server/rest_api/routers/v1/identities.py +24 -10
  100. letta/server/rest_api/routers/v1/internal_runs.py +107 -0
  101. letta/server/rest_api/routers/v1/internal_templates.py +162 -179
  102. letta/server/rest_api/routers/v1/jobs.py +15 -27
  103. letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
  104. letta/server/rest_api/routers/v1/messages.py +23 -34
  105. letta/server/rest_api/routers/v1/organizations.py +6 -27
  106. letta/server/rest_api/routers/v1/providers.py +35 -62
  107. letta/server/rest_api/routers/v1/runs.py +30 -43
  108. letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
  109. letta/server/rest_api/routers/v1/sources.py +26 -42
  110. letta/server/rest_api/routers/v1/steps.py +16 -29
  111. letta/server/rest_api/routers/v1/tools.py +17 -13
  112. letta/server/rest_api/routers/v1/users.py +5 -17
  113. letta/server/rest_api/routers/v1/voice.py +18 -27
  114. letta/server/rest_api/streaming_response.py +5 -2
  115. letta/server/rest_api/utils.py +187 -25
  116. letta/server/server.py +27 -22
  117. letta/server/ws_api/server.py +5 -4
  118. letta/services/agent_manager.py +148 -26
  119. letta/services/agent_serialization_manager.py +6 -1
  120. letta/services/archive_manager.py +168 -15
  121. letta/services/block_manager.py +14 -4
  122. letta/services/file_manager.py +33 -29
  123. letta/services/group_manager.py +10 -0
  124. letta/services/helpers/agent_manager_helper.py +65 -11
  125. letta/services/identity_manager.py +105 -4
  126. letta/services/job_manager.py +11 -1
  127. letta/services/mcp/base_client.py +2 -2
  128. letta/services/mcp/oauth_utils.py +33 -8
  129. letta/services/mcp_manager.py +174 -78
  130. letta/services/mcp_server_manager.py +1331 -0
  131. letta/services/message_manager.py +109 -4
  132. letta/services/organization_manager.py +4 -4
  133. letta/services/passage_manager.py +9 -25
  134. letta/services/provider_manager.py +91 -15
  135. letta/services/run_manager.py +72 -15
  136. letta/services/sandbox_config_manager.py +45 -3
  137. letta/services/source_manager.py +15 -8
  138. letta/services/step_manager.py +24 -1
  139. letta/services/streaming_service.py +581 -0
  140. letta/services/summarizer/summarizer.py +1 -1
  141. letta/services/tool_executor/core_tool_executor.py +111 -0
  142. letta/services/tool_executor/files_tool_executor.py +5 -3
  143. letta/services/tool_executor/sandbox_tool_executor.py +2 -2
  144. letta/services/tool_executor/tool_execution_manager.py +1 -1
  145. letta/services/tool_manager.py +10 -3
  146. letta/services/tool_sandbox/base.py +61 -1
  147. letta/services/tool_sandbox/local_sandbox.py +1 -3
  148. letta/services/user_manager.py +2 -2
  149. letta/settings.py +49 -5
  150. letta/system.py +14 -5
  151. letta/utils.py +73 -1
  152. letta/validators.py +105 -0
  153. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
  154. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/RECORD +157 -151
  155. letta/schemas/letta_ping.py +0 -28
  156. letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
  157. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
  158. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
  159. {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, HTTPException, Query
4
- from pydantic import BaseModel
4
+ from fastapi import APIRouter, Body, Depends, Query
5
+ from pydantic import BaseModel, Field
5
6
 
6
- from letta.orm.errors import NoResultFound
7
- from letta.schemas.archive import Archive as PydanticArchive
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
- try:
44
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
45
- return await server.archive_manager.create_archive_async(
46
- name=archive.name,
47
- description=archive.description,
48
- actor=actor,
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
- try:
77
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
78
- archives = await server.archive_manager.list_archives_async(
79
- actor=actor,
80
- before=before,
81
- after=after,
82
- limit=limit,
83
- ascending=(order == "asc"),
84
- name=name,
85
- agent_id=agent_id,
86
- )
87
- return archives
88
- except Exception as e:
89
- raise HTTPException(status_code=500, detail=str(e))
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: str,
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
- try:
103
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
104
- return await server.archive_manager.update_archive_async(
105
- archive_id=archive_id,
106
- name=archive.name,
107
- description=archive.description,
108
- actor=actor,
109
- )
110
- except NoResultFound as e:
111
- raise HTTPException(status_code=404, detail=str(e))
112
- except Exception as e:
113
- raise HTTPException(status_code=500, detail=str(e))
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: str,
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: str,
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: str,
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: str,
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)