letta-nightly 0.11.7.dev20250913103940__py3-none-any.whl → 0.11.7.dev20250915104130__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.
- letta/interfaces/openai_streaming_interface.py +14 -5
- letta/server/rest_api/interface.py +22 -75
- letta/server/rest_api/routers/v1/agents.py +6 -6
- letta/server/rest_api/routers/v1/health.py +2 -2
- letta/server/rest_api/routers/v1/messages.py +38 -21
- letta/server/rest_api/routers/v1/providers.py +7 -4
- letta/server/rest_api/routers/v1/steps.py +29 -3
- letta/server/rest_api/routers/v1/tags.py +17 -6
- letta/services/agent_manager.py +31 -7
- letta/services/step_manager.py +5 -1
- letta/streaming_utils.py +79 -18
- letta/utils.py +2 -0
- {letta_nightly-0.11.7.dev20250913103940.dist-info → letta_nightly-0.11.7.dev20250915104130.dist-info}/METADATA +1 -1
- {letta_nightly-0.11.7.dev20250913103940.dist-info → letta_nightly-0.11.7.dev20250915104130.dist-info}/RECORD +17 -17
- {letta_nightly-0.11.7.dev20250913103940.dist-info → letta_nightly-0.11.7.dev20250915104130.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20250913103940.dist-info → letta_nightly-0.11.7.dev20250915104130.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20250913103940.dist-info → letta_nightly-0.11.7.dev20250915104130.dist-info}/licenses/LICENSE +0 -0
@@ -24,7 +24,11 @@ from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
|
|
24
24
|
from letta.schemas.message import Message
|
25
25
|
from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall
|
26
26
|
from letta.server.rest_api.json_parser import OptimisticJSONParser
|
27
|
-
from letta.streaming_utils import
|
27
|
+
from letta.streaming_utils import (
|
28
|
+
FunctionArgumentsStreamHandler,
|
29
|
+
JSONInnerThoughtsExtractor,
|
30
|
+
sanitize_streamed_message_content,
|
31
|
+
)
|
28
32
|
from letta.utils import count_tokens
|
29
33
|
|
30
34
|
logger = get_logger(__name__)
|
@@ -278,8 +282,6 @@ class OpenAIStreamingInterface:
|
|
278
282
|
self.prev_assistant_message_id = self.function_id_buffer
|
279
283
|
# Reset message reader at the start of a new send_message stream
|
280
284
|
self.assistant_message_json_reader.reset()
|
281
|
-
self.assistant_message_json_reader.in_message = True
|
282
|
-
self.assistant_message_json_reader.message_started = True
|
283
285
|
|
284
286
|
else:
|
285
287
|
if prev_message_type and prev_message_type != "tool_call_message":
|
@@ -334,8 +336,15 @@ class OpenAIStreamingInterface:
|
|
334
336
|
self.last_flushed_function_name is not None
|
335
337
|
and self.last_flushed_function_name == self.assistant_message_tool_name
|
336
338
|
):
|
337
|
-
# Minimal, robust extraction: only emit the value of "message"
|
338
|
-
|
339
|
+
# Minimal, robust extraction: only emit the value of "message".
|
340
|
+
# If we buffered a prefix while name was streaming, feed it first.
|
341
|
+
if self.function_args_buffer:
|
342
|
+
payload = self.function_args_buffer + tool_call.function.arguments
|
343
|
+
self.function_args_buffer = None
|
344
|
+
else:
|
345
|
+
payload = tool_call.function.arguments
|
346
|
+
extracted = self.assistant_message_json_reader.process_json_chunk(payload)
|
347
|
+
extracted = sanitize_streamed_message_content(extracted or "")
|
339
348
|
if extracted:
|
340
349
|
if prev_message_type and prev_message_type != "assistant_message":
|
341
350
|
message_index += 1
|
@@ -808,86 +808,33 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
808
808
|
# If there was nothing in the name buffer, we can proceed to
|
809
809
|
# output the arguments chunk as a ToolCallMessage
|
810
810
|
else:
|
811
|
-
#
|
811
|
+
# use_assistant_message means we should emit only the value of "message"
|
812
812
|
if self.use_assistant_message and (
|
813
813
|
self.last_flushed_function_name is not None
|
814
814
|
and self.last_flushed_function_name == self.assistant_message_tool_name
|
815
815
|
):
|
816
|
-
#
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
updates_main_json = None
|
825
|
-
|
826
|
-
else:
|
827
|
-
# Some hardcoding to strip off the trailing "}"
|
828
|
-
if updates_main_json in ["}", '"}']:
|
829
|
-
updates_main_json = None
|
830
|
-
if updates_main_json and len(updates_main_json) > 0 and updates_main_json[-1:] == '"':
|
831
|
-
updates_main_json = updates_main_json[:-1]
|
832
|
-
|
833
|
-
if not updates_main_json:
|
834
|
-
# early exit to turn into content mode
|
816
|
+
# Feed any buffered prefix first to avoid missing the start of the value
|
817
|
+
payload = (self.function_args_buffer or "") + (updates_main_json or "")
|
818
|
+
self.function_args_buffer = None
|
819
|
+
cleaned = self.streaming_chat_completion_json_reader.process_json_chunk(payload)
|
820
|
+
from letta.streaming_utils import sanitize_streamed_message_content
|
821
|
+
|
822
|
+
cleaned = sanitize_streamed_message_content(cleaned or "")
|
823
|
+
if not cleaned:
|
835
824
|
return None
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
name=name,
|
850
|
-
otid=Message.generate_otid_from_id(message_id, message_index),
|
851
|
-
)
|
852
|
-
# Store the ID of the tool call so allow skipping the corresponding response
|
853
|
-
if self.function_id_buffer:
|
854
|
-
self.prev_assistant_message_id = self.function_id_buffer
|
855
|
-
# clear buffer
|
856
|
-
self.function_args_buffer = None
|
857
|
-
self.function_id_buffer = None
|
858
|
-
|
859
|
-
else:
|
860
|
-
# If there's no buffer to clear, just output a new chunk with new data
|
861
|
-
# TODO: THIS IS HORRIBLE
|
862
|
-
# TODO: WE USE THE OLD JSON PARSER EARLIER (WHICH DOES NOTHING) AND NOW THE NEW JSON PARSER
|
863
|
-
# TODO: THIS IS TOTALLY WRONG AND BAD, BUT SAVING FOR A LARGER REWRITE IN THE NEAR FUTURE
|
864
|
-
parsed_args = self.optimistic_json_parser.parse(self.current_function_arguments)
|
865
|
-
|
866
|
-
if parsed_args.get(self.assistant_message_tool_kwarg) and parsed_args.get(
|
867
|
-
self.assistant_message_tool_kwarg
|
868
|
-
) != self.current_json_parse_result.get(self.assistant_message_tool_kwarg):
|
869
|
-
new_content = parsed_args.get(self.assistant_message_tool_kwarg)
|
870
|
-
prev_content = self.current_json_parse_result.get(self.assistant_message_tool_kwarg, "")
|
871
|
-
# TODO: Assumes consistent state and that prev_content is subset of new_content
|
872
|
-
diff = new_content.replace(prev_content, "", 1)
|
873
|
-
self.current_json_parse_result = parsed_args
|
874
|
-
if prev_message_type and prev_message_type != "assistant_message":
|
875
|
-
message_index += 1
|
876
|
-
processed_chunk = AssistantMessage(
|
877
|
-
id=message_id,
|
878
|
-
date=message_date,
|
879
|
-
content=diff,
|
880
|
-
name=name,
|
881
|
-
otid=Message.generate_otid_from_id(message_id, message_index),
|
882
|
-
)
|
883
|
-
else:
|
884
|
-
return None
|
885
|
-
|
886
|
-
# Store the ID of the tool call so allow skipping the corresponding response
|
887
|
-
if self.function_id_buffer:
|
888
|
-
self.prev_assistant_message_id = self.function_id_buffer
|
889
|
-
# clear buffers
|
890
|
-
self.function_id_buffer = None
|
825
|
+
if prev_message_type and prev_message_type != "assistant_message":
|
826
|
+
message_index += 1
|
827
|
+
processed_chunk = AssistantMessage(
|
828
|
+
id=message_id,
|
829
|
+
date=message_date,
|
830
|
+
content=cleaned,
|
831
|
+
name=name,
|
832
|
+
otid=Message.generate_otid_from_id(message_id, message_index),
|
833
|
+
)
|
834
|
+
# Store the ID of the tool call so allow skipping the corresponding response
|
835
|
+
if self.function_id_buffer:
|
836
|
+
self.prev_assistant_message_id = self.function_id_buffer
|
837
|
+
# Do not clear function_id_buffer here — we may still need it
|
891
838
|
else:
|
892
839
|
# There may be a buffer from a previous chunk, for example
|
893
840
|
# if the previous chunk had arguments but we needed to flush name
|
@@ -162,8 +162,8 @@ class IndentedORJSONResponse(Response):
|
|
162
162
|
return orjson.dumps(content, option=orjson.OPT_INDENT_2)
|
163
163
|
|
164
164
|
|
165
|
-
@router.get("/{agent_id}/export", response_class=IndentedORJSONResponse, operation_id="
|
166
|
-
async def
|
165
|
+
@router.get("/{agent_id}/export", response_class=IndentedORJSONResponse, operation_id="export_agent")
|
166
|
+
async def export_agent(
|
167
167
|
agent_id: str,
|
168
168
|
max_steps: int = 100,
|
169
169
|
server: "SyncServer" = Depends(get_letta_server),
|
@@ -256,7 +256,7 @@ def import_agent_legacy(
|
|
256
256
|
raise HTTPException(status_code=500, detail=f"An unexpected error occurred while uploading the agent: {e!s}")
|
257
257
|
|
258
258
|
|
259
|
-
async def
|
259
|
+
async def _import_agent(
|
260
260
|
agent_file_json: dict,
|
261
261
|
server: "SyncServer",
|
262
262
|
actor: User,
|
@@ -313,8 +313,8 @@ async def import_agent(
|
|
313
313
|
raise HTTPException(status_code=500, detail=f"An unexpected error occurred while importing agents: {e!s}")
|
314
314
|
|
315
315
|
|
316
|
-
@router.post("/import", response_model=ImportedAgentsResponse, operation_id="
|
317
|
-
async def
|
316
|
+
@router.post("/import", response_model=ImportedAgentsResponse, operation_id="import_agent")
|
317
|
+
async def import_agent(
|
318
318
|
file: UploadFile = File(...),
|
319
319
|
server: "SyncServer" = Depends(get_letta_server),
|
320
320
|
actor_id: str | None = Header(None, alias="user_id"),
|
@@ -367,7 +367,7 @@ async def import_agent_serialized(
|
|
367
367
|
# TODO: This is kind of hacky, but should work as long as dont' change the schema
|
368
368
|
if "agents" in agent_json and isinstance(agent_json.get("agents"), list):
|
369
369
|
# This is an AgentFileSchema
|
370
|
-
agent_ids = await
|
370
|
+
agent_ids = await _import_agent(
|
371
371
|
agent_file_json=agent_json,
|
372
372
|
server=server,
|
373
373
|
actor=actor,
|
@@ -12,8 +12,8 @@ router = APIRouter(prefix="/health", tags=["health"])
|
|
12
12
|
|
13
13
|
|
14
14
|
# Health check
|
15
|
-
@router.get("/", response_model=Health, operation_id="
|
16
|
-
def
|
15
|
+
@router.get("/", response_model=Health, operation_id="check_health")
|
16
|
+
def check_health():
|
17
17
|
return Health(
|
18
18
|
version=__version__,
|
19
19
|
status="ok",
|
@@ -19,23 +19,22 @@ router = APIRouter(prefix="/messages", tags=["messages"])
|
|
19
19
|
logger = get_logger(__name__)
|
20
20
|
|
21
21
|
|
22
|
-
# Batch APIs
|
23
|
-
|
24
|
-
|
25
22
|
@router.post(
|
26
23
|
"/batches",
|
27
24
|
response_model=BatchJob,
|
28
|
-
operation_id="
|
25
|
+
operation_id="create_batch",
|
29
26
|
)
|
30
|
-
async def
|
27
|
+
async def create_batch(
|
31
28
|
request: Request,
|
32
29
|
payload: CreateBatch = Body(..., description="Messages and config for all agents"),
|
33
30
|
server: SyncServer = Depends(get_letta_server),
|
34
31
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
35
32
|
):
|
36
33
|
"""
|
37
|
-
Submit a batch of agent
|
34
|
+
Submit a batch of agent runs for asynchronous processing.
|
35
|
+
|
38
36
|
Creates a job that will fan out messages to all listed agents and process them in parallel.
|
37
|
+
The request will be rejected if it exceeds 256MB.
|
39
38
|
"""
|
40
39
|
# Reject requests greater than 256Mbs
|
41
40
|
max_bytes = 256 * 1024 * 1024
|
@@ -76,10 +75,7 @@ async def create_batch_run(
|
|
76
75
|
|
77
76
|
# TODO: update run metadata
|
78
77
|
except Exception as e:
|
79
|
-
|
80
|
-
|
81
|
-
print("Error creating batch job", e)
|
82
|
-
traceback.print_exc()
|
78
|
+
logger.error(f"Error creating batch job: {e}")
|
83
79
|
|
84
80
|
# mark job as failed
|
85
81
|
await server.job_manager.update_job_by_id_async(job_id=batch_job.id, job_update=JobUpdate(status=JobStatus.failed), actor=actor)
|
@@ -87,14 +83,14 @@ async def create_batch_run(
|
|
87
83
|
return batch_job
|
88
84
|
|
89
85
|
|
90
|
-
@router.get("/batches/{batch_id}", response_model=BatchJob, operation_id="
|
91
|
-
async def
|
86
|
+
@router.get("/batches/{batch_id}", response_model=BatchJob, operation_id="retrieve_batch")
|
87
|
+
async def retrieve_batch(
|
92
88
|
batch_id: str,
|
93
89
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
94
90
|
server: "SyncServer" = Depends(get_letta_server),
|
95
91
|
):
|
96
92
|
"""
|
97
|
-
|
93
|
+
Retrieve the status and details of a batch run.
|
98
94
|
"""
|
99
95
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
100
96
|
|
@@ -105,18 +101,36 @@ async def retrieve_batch_run(
|
|
105
101
|
raise HTTPException(status_code=404, detail="Batch not found")
|
106
102
|
|
107
103
|
|
108
|
-
@router.get("/batches", response_model=List[BatchJob], operation_id="
|
109
|
-
async def
|
104
|
+
@router.get("/batches", response_model=List[BatchJob], operation_id="list_batches")
|
105
|
+
async def list_batches(
|
106
|
+
before: Optional[str] = Query(
|
107
|
+
None, description="Job ID cursor for pagination. Returns jobs that come before this job ID in the specified sort order"
|
108
|
+
),
|
109
|
+
after: Optional[str] = Query(
|
110
|
+
None, description="Job ID cursor for pagination. Returns jobs that come after this job ID in the specified sort order"
|
111
|
+
),
|
112
|
+
limit: Optional[int] = Query(100, description="Maximum number of jobs to return"),
|
113
|
+
order: Literal["asc", "desc"] = Query(
|
114
|
+
"desc", description="Sort order for jobs by creation time. 'asc' for oldest first, 'desc' for newest first"
|
115
|
+
),
|
116
|
+
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
110
117
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
111
118
|
server: "SyncServer" = Depends(get_letta_server),
|
112
119
|
):
|
113
120
|
"""
|
114
121
|
List all batch runs.
|
115
122
|
"""
|
116
|
-
# TODO: filter
|
117
123
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
118
124
|
|
119
|
-
jobs = server.job_manager.list_jobs(
|
125
|
+
jobs = server.job_manager.list_jobs(
|
126
|
+
actor=actor,
|
127
|
+
statuses=[JobStatus.created, JobStatus.running],
|
128
|
+
job_type=JobType.BATCH,
|
129
|
+
before=before,
|
130
|
+
after=after,
|
131
|
+
limit=limit,
|
132
|
+
ascending=(order == "asc"),
|
133
|
+
)
|
120
134
|
return [BatchJob.from_job(job) for job in jobs]
|
121
135
|
|
122
136
|
|
@@ -137,14 +151,17 @@ async def list_batch_messages(
|
|
137
151
|
order: Literal["asc", "desc"] = Query(
|
138
152
|
"desc", description="Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first"
|
139
153
|
),
|
154
|
+
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
140
155
|
agent_id: Optional[str] = Query(None, description="Filter messages by agent ID"),
|
141
156
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
142
157
|
server: SyncServer = Depends(get_letta_server),
|
143
158
|
):
|
144
|
-
"""
|
159
|
+
"""
|
160
|
+
Get response messages for a specific batch job.
|
161
|
+
"""
|
145
162
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
146
163
|
|
147
|
-
#
|
164
|
+
# Verify the batch job exists and the user has access to it
|
148
165
|
try:
|
149
166
|
job = await server.job_manager.get_job_by_id_async(job_id=batch_id, actor=actor)
|
150
167
|
BatchJob.from_job(job)
|
@@ -159,8 +176,8 @@ async def list_batch_messages(
|
|
159
176
|
return LettaBatchMessages(messages=messages)
|
160
177
|
|
161
178
|
|
162
|
-
@router.patch("/batches/{batch_id}/cancel", operation_id="
|
163
|
-
async def
|
179
|
+
@router.patch("/batches/{batch_id}/cancel", operation_id="cancel_batch")
|
180
|
+
async def cancel_batch(
|
164
181
|
batch_id: str,
|
165
182
|
server: "SyncServer" = Depends(get_letta_server),
|
166
183
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
@@ -25,7 +25,7 @@ async def list_providers(
|
|
25
25
|
server: "SyncServer" = Depends(get_letta_server),
|
26
26
|
):
|
27
27
|
"""
|
28
|
-
Get a list of all custom providers
|
28
|
+
Get a list of all custom providers.
|
29
29
|
"""
|
30
30
|
try:
|
31
31
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
@@ -46,7 +46,7 @@ async def create_provider(
|
|
46
46
|
server: "SyncServer" = Depends(get_letta_server),
|
47
47
|
):
|
48
48
|
"""
|
49
|
-
Create a new custom provider
|
49
|
+
Create a new custom provider.
|
50
50
|
"""
|
51
51
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
52
52
|
for field_name in request.model_fields:
|
@@ -68,7 +68,7 @@ async def modify_provider(
|
|
68
68
|
server: "SyncServer" = Depends(get_letta_server),
|
69
69
|
):
|
70
70
|
"""
|
71
|
-
Update an existing custom provider
|
71
|
+
Update an existing custom provider.
|
72
72
|
"""
|
73
73
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
74
74
|
return await server.provider_manager.update_provider_async(provider_id=provider_id, provider_update=request, actor=actor)
|
@@ -79,6 +79,9 @@ async def check_provider(
|
|
79
79
|
request: ProviderCheck = Body(...),
|
80
80
|
server: "SyncServer" = Depends(get_letta_server),
|
81
81
|
):
|
82
|
+
"""
|
83
|
+
Verify the API key and additional parameters for a provider.
|
84
|
+
"""
|
82
85
|
try:
|
83
86
|
if request.base_url and len(request.base_url) == 0:
|
84
87
|
# set to null if empty string
|
@@ -100,7 +103,7 @@ async def delete_provider(
|
|
100
103
|
server: "SyncServer" = Depends(get_letta_server),
|
101
104
|
):
|
102
105
|
"""
|
103
|
-
Delete an existing custom provider
|
106
|
+
Delete an existing custom provider.
|
104
107
|
"""
|
105
108
|
try:
|
106
109
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
@@ -1,14 +1,17 @@
|
|
1
1
|
from datetime import datetime
|
2
2
|
from typing import List, Literal, Optional
|
3
3
|
|
4
|
-
from fastapi import APIRouter, Depends, Header, HTTPException, Query
|
4
|
+
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query
|
5
|
+
from pydantic import BaseModel, Field
|
5
6
|
|
6
7
|
from letta.orm.errors import NoResultFound
|
8
|
+
from letta.schemas.provider_trace import ProviderTrace
|
7
9
|
from letta.schemas.step import Step
|
8
10
|
from letta.schemas.step_metrics import StepMetrics
|
9
11
|
from letta.server.rest_api.utils import get_letta_server
|
10
12
|
from letta.server.server import SyncServer
|
11
13
|
from letta.services.step_manager import FeedbackType
|
14
|
+
from letta.settings import settings
|
12
15
|
|
13
16
|
router = APIRouter(prefix="/steps", tags=["steps"])
|
14
17
|
|
@@ -93,10 +96,33 @@ async def retrieve_step_metrics(
|
|
93
96
|
raise HTTPException(status_code=404, detail="Step metrics not found")
|
94
97
|
|
95
98
|
|
99
|
+
@router.get("/{step_id}/trace", response_model=Optional[ProviderTrace], operation_id="retrieve_step_trace")
|
100
|
+
async def retrieve_step_trace(
|
101
|
+
step_id: str,
|
102
|
+
server: SyncServer = Depends(get_letta_server),
|
103
|
+
actor_id: str | None = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
104
|
+
):
|
105
|
+
provider_trace = None
|
106
|
+
if settings.track_provider_trace:
|
107
|
+
try:
|
108
|
+
provider_trace = await server.telemetry_manager.get_provider_trace_by_step_id_async(
|
109
|
+
step_id=step_id, actor=await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
110
|
+
)
|
111
|
+
except:
|
112
|
+
pass
|
113
|
+
|
114
|
+
return provider_trace
|
115
|
+
|
116
|
+
|
117
|
+
class AddFeedbackRequest(BaseModel):
|
118
|
+
feedback: FeedbackType | None = Field(None, description="Whether this feedback is positive or negative")
|
119
|
+
tags: list[str] | None = Field(None, description="Feedback tags to add to the step")
|
120
|
+
|
121
|
+
|
96
122
|
@router.patch("/{step_id}/feedback", response_model=Step, operation_id="add_feedback")
|
97
123
|
async def add_feedback(
|
98
124
|
step_id: str,
|
99
|
-
|
125
|
+
request: AddFeedbackRequest = Body(...),
|
100
126
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
101
127
|
server: SyncServer = Depends(get_letta_server),
|
102
128
|
):
|
@@ -105,7 +131,7 @@ async def add_feedback(
|
|
105
131
|
"""
|
106
132
|
try:
|
107
133
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
108
|
-
return await server.step_manager.add_feedback_async(step_id=step_id, feedback=feedback, actor=actor)
|
134
|
+
return await server.step_manager.add_feedback_async(step_id=step_id, feedback=request.feedback, tags=request.tags, actor=actor)
|
109
135
|
except NoResultFound:
|
110
136
|
raise HTTPException(status_code=404, detail="Step not found")
|
111
137
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import TYPE_CHECKING, List, Optional
|
1
|
+
from typing import TYPE_CHECKING, List, Literal, Optional
|
2
2
|
|
3
3
|
from fastapi import APIRouter, Depends, Header, Query
|
4
4
|
|
@@ -13,15 +13,26 @@ router = APIRouter(prefix="/tags", tags=["tag", "admin"])
|
|
13
13
|
|
14
14
|
@router.get("/", tags=["admin"], response_model=List[str], operation_id="list_tags")
|
15
15
|
async def list_tags(
|
16
|
-
|
17
|
-
|
16
|
+
before: Optional[str] = Query(
|
17
|
+
None, description="Tag cursor for pagination. Returns tags that come before this tag in the specified sort order"
|
18
|
+
),
|
19
|
+
after: Optional[str] = Query(
|
20
|
+
None, description="Tag cursor for pagination. Returns tags that come after this tag in the specified sort order"
|
21
|
+
),
|
22
|
+
limit: Optional[int] = Query(50, description="Maximum number of tags to return"),
|
23
|
+
order: Literal["asc", "desc"] = Query(
|
24
|
+
"asc", description="Sort order for tags. 'asc' for alphabetical order, 'desc' for reverse alphabetical order"
|
25
|
+
),
|
26
|
+
order_by: Literal["name"] = Query("name", description="Field to sort by"),
|
27
|
+
query_text: Optional[str] = Query(None, description="Filter tags by text search"),
|
18
28
|
server: "SyncServer" = Depends(get_letta_server),
|
19
|
-
query_text: Optional[str] = Query(None),
|
20
29
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
21
30
|
):
|
22
31
|
"""
|
23
|
-
Get a list of all tags in the database
|
32
|
+
Get a list of all agent tags in the database.
|
24
33
|
"""
|
25
34
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
26
|
-
tags = await server.agent_manager.list_tags_async(
|
35
|
+
tags = await server.agent_manager.list_tags_async(
|
36
|
+
actor=actor, before=before, after=after, limit=limit, query_text=query_text, ascending=(order == "asc")
|
37
|
+
)
|
27
38
|
return tags
|
letta/services/agent_manager.py
CHANGED
@@ -3542,19 +3542,27 @@ class AgentManager:
|
|
3542
3542
|
@enforce_types
|
3543
3543
|
@trace_method
|
3544
3544
|
async def list_tags_async(
|
3545
|
-
self,
|
3545
|
+
self,
|
3546
|
+
actor: PydanticUser,
|
3547
|
+
before: Optional[str] = None,
|
3548
|
+
after: Optional[str] = None,
|
3549
|
+
limit: Optional[int] = 50,
|
3550
|
+
query_text: Optional[str] = None,
|
3551
|
+
ascending: bool = True,
|
3546
3552
|
) -> List[str]:
|
3547
3553
|
"""
|
3548
3554
|
Get all tags a user has created, ordered alphabetically.
|
3549
3555
|
|
3550
3556
|
Args:
|
3551
3557
|
actor: User performing the action.
|
3552
|
-
|
3553
|
-
|
3554
|
-
|
3558
|
+
before: Cursor for backward pagination (tags before this tag).
|
3559
|
+
after: Cursor for forward pagination (tags after this tag).
|
3560
|
+
limit: Maximum number of tags to return (default: 50).
|
3561
|
+
query_text: Filter tags by text search.
|
3562
|
+
ascending: Sort order - True for alphabetical, False for reverse (default: True).
|
3555
3563
|
|
3556
3564
|
Returns:
|
3557
|
-
List[str]: List of all tags.
|
3565
|
+
List[str]: List of all tags matching the criteria.
|
3558
3566
|
"""
|
3559
3567
|
async with db_registry.async_session() as session:
|
3560
3568
|
# Build the query using select() for async SQLAlchemy
|
@@ -3573,10 +3581,26 @@ class AgentManager:
|
|
3573
3581
|
# SQLite: Use LIKE with LOWER for case-insensitive search
|
3574
3582
|
query = query.where(func.lower(AgentsTags.tag).like(func.lower(f"%{query_text}%")))
|
3575
3583
|
|
3584
|
+
# Handle pagination cursors
|
3576
3585
|
if after:
|
3577
|
-
|
3586
|
+
if ascending:
|
3587
|
+
query = query.where(AgentsTags.tag > after)
|
3588
|
+
else:
|
3589
|
+
query = query.where(AgentsTags.tag < after)
|
3578
3590
|
|
3579
|
-
|
3591
|
+
if before:
|
3592
|
+
if ascending:
|
3593
|
+
query = query.where(AgentsTags.tag < before)
|
3594
|
+
else:
|
3595
|
+
query = query.where(AgentsTags.tag > before)
|
3596
|
+
|
3597
|
+
# Apply ordering based on ascending parameter
|
3598
|
+
if ascending:
|
3599
|
+
query = query.order_by(AgentsTags.tag.asc())
|
3600
|
+
else:
|
3601
|
+
query = query.order_by(AgentsTags.tag.desc())
|
3602
|
+
|
3603
|
+
query = query.limit(limit)
|
3580
3604
|
|
3581
3605
|
# Execute the query asynchronously
|
3582
3606
|
result = await session.execute(query)
|
letta/services/step_manager.py
CHANGED
@@ -197,12 +197,16 @@ class StepManager:
|
|
197
197
|
|
198
198
|
@enforce_types
|
199
199
|
@trace_method
|
200
|
-
async def add_feedback_async(
|
200
|
+
async def add_feedback_async(
|
201
|
+
self, step_id: str, feedback: FeedbackType | None, actor: PydanticUser, tags: list[str] | None = None
|
202
|
+
) -> PydanticStep:
|
201
203
|
async with db_registry.async_session() as session:
|
202
204
|
step = await StepModel.read_async(db_session=session, identifier=step_id, actor=actor)
|
203
205
|
if not step:
|
204
206
|
raise NoResultFound(f"Step with id {step_id} does not exist")
|
205
207
|
step.feedback = feedback
|
208
|
+
if tags:
|
209
|
+
step.tags = tags
|
206
210
|
step = await step.update_async(session)
|
207
211
|
return step.to_pydantic()
|
208
212
|
|
letta/streaming_utils.py
CHANGED
@@ -264,39 +264,100 @@ class FunctionArgumentsStreamHandler:
|
|
264
264
|
|
265
265
|
def process_json_chunk(self, chunk: str) -> Optional[str]:
|
266
266
|
"""Process a chunk from the function arguments and return the plaintext version"""
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
if
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
return None
|
267
|
+
clean_chunk = chunk.strip()
|
268
|
+
# Not in message yet: accumulate until we see '<json_key>': (robust to split fragments)
|
269
|
+
if not self.in_message:
|
270
|
+
if clean_chunk == "{":
|
271
|
+
self.key_buffer = ""
|
272
|
+
self.accumulating = True
|
273
|
+
return None
|
275
274
|
self.key_buffer += clean_chunk
|
275
|
+
if self.json_key in self.key_buffer and ":" in clean_chunk:
|
276
|
+
# Enter value mode; attempt to extract inline content if it exists in this same chunk
|
277
|
+
self.in_message = True
|
278
|
+
self.accumulating = False
|
279
|
+
# Try to find the first quote after the colon within the original (unstripped) chunk
|
280
|
+
s = chunk
|
281
|
+
colon_idx = s.find(":")
|
282
|
+
if colon_idx != -1:
|
283
|
+
q_idx = s.find('"', colon_idx + 1)
|
284
|
+
if q_idx != -1:
|
285
|
+
self.message_started = True
|
286
|
+
rem = s[q_idx + 1 :]
|
287
|
+
# Check if this same chunk also contains the terminating quote (and optional delimiter)
|
288
|
+
j = len(rem) - 1
|
289
|
+
while j >= 0 and rem[j] in " \t\r\n":
|
290
|
+
j -= 1
|
291
|
+
if j >= 1 and rem[j - 1] == '"' and rem[j] in ",}]":
|
292
|
+
out = rem[: j - 1]
|
293
|
+
self.in_message = False
|
294
|
+
self.message_started = False
|
295
|
+
return out
|
296
|
+
if j >= 0 and rem[j] == '"':
|
297
|
+
out = rem[:j]
|
298
|
+
self.in_message = False
|
299
|
+
self.message_started = False
|
300
|
+
return out
|
301
|
+
# No terminator yet; emit remainder as content
|
302
|
+
return rem
|
303
|
+
return None
|
304
|
+
if clean_chunk == "}":
|
305
|
+
self.in_message = False
|
306
|
+
self.message_started = False
|
307
|
+
self.key_buffer = ""
|
276
308
|
return None
|
277
309
|
|
310
|
+
# Inside message value
|
278
311
|
if self.in_message:
|
279
|
-
|
312
|
+
# Bare opening/closing quote tokens
|
313
|
+
if clean_chunk == '"' and self.message_started:
|
280
314
|
self.in_message = False
|
281
315
|
self.message_started = False
|
282
316
|
return None
|
283
|
-
if not self.message_started and
|
317
|
+
if not self.message_started and clean_chunk == '"':
|
284
318
|
self.message_started = True
|
285
319
|
return None
|
286
320
|
if self.message_started:
|
287
|
-
|
321
|
+
# Detect closing patterns: '"', '",', '"}' (with optional whitespace)
|
322
|
+
i = len(chunk) - 1
|
323
|
+
while i >= 0 and chunk[i] in " \t\r\n":
|
324
|
+
i -= 1
|
325
|
+
if i >= 1 and chunk[i - 1] == '"' and chunk[i] in ",}]":
|
326
|
+
out = chunk[: i - 1]
|
288
327
|
self.in_message = False
|
289
|
-
|
328
|
+
self.message_started = False
|
329
|
+
return out
|
330
|
+
if i >= 0 and chunk[i] == '"':
|
331
|
+
out = chunk[:i]
|
332
|
+
self.in_message = False
|
333
|
+
self.message_started = False
|
334
|
+
return out
|
335
|
+
# Otherwise, still mid-string
|
290
336
|
return chunk
|
291
337
|
|
292
|
-
if
|
293
|
-
self.key_buffer = ""
|
294
|
-
self.accumulating = True
|
295
|
-
return None
|
296
|
-
|
297
|
-
if chunk.strip() == "}":
|
338
|
+
if clean_chunk == "}":
|
298
339
|
self.in_message = False
|
299
340
|
self.message_started = False
|
341
|
+
self.key_buffer = ""
|
300
342
|
return None
|
301
343
|
|
302
344
|
return None
|
345
|
+
|
346
|
+
|
347
|
+
def sanitize_streamed_message_content(text: str) -> str:
|
348
|
+
"""Remove trailing JSON delimiters that can leak into assistant text.
|
349
|
+
|
350
|
+
Specifically handles cases where a message string is immediately followed
|
351
|
+
by a JSON delimiter in the stream (e.g., '"', '",', '"}', '" ]').
|
352
|
+
Internal commas inside the message are preserved.
|
353
|
+
"""
|
354
|
+
if not text:
|
355
|
+
return text
|
356
|
+
t = text.rstrip()
|
357
|
+
# strip trailing quote + delimiter
|
358
|
+
if len(t) >= 2 and t[-2] == '"' and t[-1] in ",}]":
|
359
|
+
return t[:-2]
|
360
|
+
# strip lone trailing quote
|
361
|
+
if t.endswith('"'):
|
362
|
+
return t[:-1]
|
363
|
+
return t
|
letta/utils.py
CHANGED
@@ -536,6 +536,8 @@ def enforce_types(func):
|
|
536
536
|
|
537
537
|
if origin is Union: # Handle Union types (including Optional)
|
538
538
|
return any(matches_type(value, arg) for arg in args)
|
539
|
+
elif hasattr(hint, "__class__") and hint.__class__.__name__ == "UnionType": # Handle Python 3.10+ X | Y syntax
|
540
|
+
return any(matches_type(value, arg) for arg in args)
|
539
541
|
elif origin is list and isinstance(value, list): # Handle List[T]
|
540
542
|
element_type = args[0] if args else None
|
541
543
|
return all(isinstance(v, element_type) for v in value) if element_type else True
|
@@ -11,9 +11,9 @@ letta/memory.py,sha256=l5iNhLAR_xzgTb0GBlQx4SVgH8kuZh8siJdC_CFPKEs,4278
|
|
11
11
|
letta/pytest.ini,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
12
|
letta/settings.py,sha256=QEjNUwRXGBgsQpQAs2kksQmGN5CbxKlxPPydrklx_Ms,15011
|
13
13
|
letta/streaming_interface.py,sha256=rPMfwUcjqITWk2tVqFQm1hmP99tU2IOHg9gU2dgPSo8,16400
|
14
|
-
letta/streaming_utils.py,sha256=
|
14
|
+
letta/streaming_utils.py,sha256=ZRFGFpQqn9ujCEbgZdLM7yTjiuNNvqQ47sNhV8ix-yQ,16553
|
15
15
|
letta/system.py,sha256=kHF7n3Viq7gV5UIUEXixod2gWa2jroUgztpEzMC1Sew,8925
|
16
|
-
letta/utils.py,sha256=
|
16
|
+
letta/utils.py,sha256=TwSAZKw3uCWAzmmEA156W4CYRDaEOiZmAO-zvzFdK6Q,43483
|
17
17
|
letta/adapters/letta_llm_adapter.py,sha256=11wkOkEQfPXUuJoJxbK22wCa-8gnWiDAb3UOXOxLt5U,3427
|
18
18
|
letta/adapters/letta_llm_request_adapter.py,sha256=wJhK5M_qOhRPAhgMmYI7EJcM8Op19tClnXe0kJ29a3Q,4831
|
19
19
|
letta/adapters/letta_llm_stream_adapter.py,sha256=G8IqtXor0LUuW-dKtGJWsUt6DfJreVCn5h6W2lHEPBg,7658
|
@@ -85,7 +85,7 @@ letta/humans/examples/cs_phd.txt,sha256=9C9ZAV_VuG7GB31ksy3-_NAyk8rjE6YtVOkhp08k
|
|
85
85
|
letta/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
86
86
|
letta/interfaces/anthropic_streaming_interface.py,sha256=0VyK8kTRgCLNDLQN6vX1gJ0dfJhqguL_NL1GYgFr6fU,25614
|
87
87
|
letta/interfaces/openai_chat_completions_streaming_interface.py,sha256=3xHXh8cW79EkiMUTYfvcH_s92nkLjxXfvtVOVC3bfLo,5050
|
88
|
-
letta/interfaces/openai_streaming_interface.py,sha256=
|
88
|
+
letta/interfaces/openai_streaming_interface.py,sha256=YLArar2ypOEaVt7suJxpg1QZr0ErwEmPSEVhzaP6JWc,24166
|
89
89
|
letta/interfaces/utils.py,sha256=c6jvO0dBYHh8DQnlN-B0qeNC64d3CSunhfqlFA4pJTY,278
|
90
90
|
letta/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
91
91
|
letta/jobs/helpers.py,sha256=kO4aj954xsQ1RAmkjY6LQQ7JEIGuhaxB1e9pzrYKHAY,914
|
@@ -340,7 +340,7 @@ letta/server/rest_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
340
340
|
letta/server/rest_api/app.py,sha256=T3LLveXRJmfWqR0uEzoaLY8LXwYrwCQGb80XMbSCDUo,21172
|
341
341
|
letta/server/rest_api/auth_token.py,sha256=725EFEIiNj4dh70hrSd94UysmFD8vcJLrTRfNHkzxDo,774
|
342
342
|
letta/server/rest_api/chat_completions_interface.py,sha256=-7wO7pNBWXMqblVkJpuZ8JPJ-LjudLTtT6BJu-q_XAM,11138
|
343
|
-
letta/server/rest_api/interface.py,sha256=
|
343
|
+
letta/server/rest_api/interface.py,sha256=_GQfKYUp9w4Wo2HSE_8Ff7QU16t1blspLaqmukpER9s,67099
|
344
344
|
letta/server/rest_api/json_parser.py,sha256=yoakaCkSMdf0Y_pyILoFKZlvzXeqF-E1KNeHzatLMDc,9157
|
345
345
|
letta/server/rest_api/redis_stream_manager.py,sha256=hz85CigFWdLkK1FWUmF-i6ObgoKkuoEgkiwshZ6QPKI,10764
|
346
346
|
letta/server/rest_api/static_files.py,sha256=NG8sN4Z5EJ8JVQdj19tkFa9iQ1kBPTab9f_CUxd_u4Q,3143
|
@@ -355,24 +355,24 @@ letta/server/rest_api/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
355
355
|
letta/server/rest_api/routers/openai/chat_completions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
356
356
|
letta/server/rest_api/routers/openai/chat_completions/chat_completions.py,sha256=ohM1i8BsNxTiw8duuRT5X_0tSUzBwctQM4fJ5DXURic,5157
|
357
357
|
letta/server/rest_api/routers/v1/__init__.py,sha256=9MnEA7CgtIxyU_dDNG0jm-Ziqu1somBml-e5gKjgd9I,1997
|
358
|
-
letta/server/rest_api/routers/v1/agents.py,sha256=
|
358
|
+
letta/server/rest_api/routers/v1/agents.py,sha256=rYCTQqlWHp0YgahElG1XOznD3OKT7zRr4R7mBo_ln8o,77729
|
359
359
|
letta/server/rest_api/routers/v1/blocks.py,sha256=ykI77xnmIxPLqdAy5kzGyGw0w0ZRyVXn-O5Xcdj6-70,7690
|
360
360
|
letta/server/rest_api/routers/v1/embeddings.py,sha256=PRaQlrmEXPiIdWsTbadrFsv3Afyv5oEFUdhgHA8FTi8,989
|
361
361
|
letta/server/rest_api/routers/v1/folders.py,sha256=8Yb-bw2JdXBxMfrJNIZQk9_FKN2fet9Ccp8T83_c2sc,23539
|
362
362
|
letta/server/rest_api/routers/v1/groups.py,sha256=PlCKfG1ZUubg-bNVRBmqJNBMvvZtHDvT50LUKKd0w9I,11466
|
363
|
-
letta/server/rest_api/routers/v1/health.py,sha256=
|
363
|
+
letta/server/rest_api/routers/v1/health.py,sha256=j43UoGJ7Yh5WzdwvqbKTEdWzlcKJBF6ZI5I1kslWim0,399
|
364
364
|
letta/server/rest_api/routers/v1/identities.py,sha256=KUfw6avQIVHNw2lWz4pXOyTOPVy1g19CJGG-zayORl8,7858
|
365
365
|
letta/server/rest_api/routers/v1/internal_templates.py,sha256=wY7tUmF7kZEVnjBVsw3_Tez4U2c8SABDJ2vplsKxhzM,11211
|
366
366
|
letta/server/rest_api/routers/v1/jobs.py,sha256=ZcP_cqxgixCEYNtKVMqN1FwErNY-945h7XZhQV4vcEE,4933
|
367
367
|
letta/server/rest_api/routers/v1/llms.py,sha256=0VJuuGW9_ta0cBnSDtXd3Ngw7GjsqEN2NBf5U3b6M3I,1920
|
368
|
-
letta/server/rest_api/routers/v1/messages.py,sha256=
|
368
|
+
letta/server/rest_api/routers/v1/messages.py,sha256=iXw59JTqpXs_I6JTxE5bNCh72EUExBOo2dewv68Lb94,8528
|
369
369
|
letta/server/rest_api/routers/v1/organizations.py,sha256=OnG2vMDZEmN4eEvj24CPwiV76ImHQuHi2ojrgwJnw7I,2925
|
370
|
-
letta/server/rest_api/routers/v1/providers.py,sha256=
|
370
|
+
letta/server/rest_api/routers/v1/providers.py,sha256=T3xvtiJO89p_a0wJ1fFmqPOc--kGbmOXI9eWdDW260c,4834
|
371
371
|
letta/server/rest_api/routers/v1/runs.py,sha256=WnYwoFNjHNZicTnCkvoXCxl0XiyVAEvF70TTaMCBhPw,12982
|
372
372
|
letta/server/rest_api/routers/v1/sandbox_configs.py,sha256=f0xEOwR3PXqCS2HOjEv7UKfMWTwEaTHx105HW_X-LI4,8852
|
373
373
|
letta/server/rest_api/routers/v1/sources.py,sha256=nXZxtHi40281VltWmx1RwGBbau_00UpzDS6teTLvt2w,22679
|
374
|
-
letta/server/rest_api/routers/v1/steps.py,sha256=
|
375
|
-
letta/server/rest_api/routers/v1/tags.py,sha256=
|
374
|
+
letta/server/rest_api/routers/v1/steps.py,sha256=t_RnOQR_dwthpPeE8Bko6hSXbW3GtMvJj-9wQYvDh6A,6670
|
375
|
+
letta/server/rest_api/routers/v1/tags.py,sha256=9VCZUc0YBZD07PvLPJl7iOaj2-foLaBJ5s5rZ8xzNHA,1608
|
376
376
|
letta/server/rest_api/routers/v1/telemetry.py,sha256=eSTg7mWbuwPb2OTHQxwRM0EUEl49wHzNB6i1xJtH8BQ,1036
|
377
377
|
letta/server/rest_api/routers/v1/tools.py,sha256=UMtJj3bX8fVe0VuuU5JS0TeaFimEzZ4YRyphSO2tQMU,51085
|
378
378
|
letta/server/rest_api/routers/v1/users.py,sha256=J1vaTbS1UrBMgnPya7GdZ2wr3L9XHmkm6qdGY6pWaOI,2366
|
@@ -389,7 +389,7 @@ letta/server/ws_api/protocol.py,sha256=5mDgpfNZn_kNwHnpt5Dsuw8gdNH298sgxTGed3etz
|
|
389
389
|
letta/server/ws_api/server.py,sha256=_16TQafm509rqRztZYqo0HKKZoe8ccBrNftd_kbIJTE,5833
|
390
390
|
letta/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
391
391
|
letta/services/agent_file_manager.py,sha256=bgYTyQA90Iqo3W-LprPtyyOKf2itoqivcRhh4EOUXss,30847
|
392
|
-
letta/services/agent_manager.py,sha256=
|
392
|
+
letta/services/agent_manager.py,sha256=C-k9S8_TwLRDSdCqu0YZXbwgSRn4_hPOpP2O62NkaMM,169791
|
393
393
|
letta/services/agent_serialization_manager.py,sha256=lWXTzYItqVxJMyy9ZYlcCDQwC3ZKk9XPCHvBkoVuszA,46388
|
394
394
|
letta/services/archive_manager.py,sha256=P10BjZ2PxLoIkCwJ8rx7qLzchNVBsqNG3_KzxTanCLQ,14060
|
395
395
|
letta/services/block_manager.py,sha256=mohj12QqHenSBbBx0Xmry1Rw25Gy5DSljOITzAwqMtw,33683
|
@@ -407,7 +407,7 @@ letta/services/per_agent_lock_manager.py,sha256=cMaW8r-qhucQbiK27jVqz8wzhlr2yuRN
|
|
407
407
|
letta/services/provider_manager.py,sha256=vysp_SgJDezn6YymOfTiNVKOF93EK_dLhsM7upzSjrM,10727
|
408
408
|
letta/services/sandbox_config_manager.py,sha256=BwN3bebiFvcliTJpRkbOwGxmV5dUJ8B64kFfXAgAqDw,25989
|
409
409
|
letta/services/source_manager.py,sha256=mH9l2KJ9R7yG1vdqhltOIVsAajQP4KbueKcB7ZgN0QA,18624
|
410
|
-
letta/services/step_manager.py,sha256=
|
410
|
+
letta/services/step_manager.py,sha256=vfXhE-cuE40dv2Uv6pICrpleJeXMjYeEOrkvGcY_sqI,20987
|
411
411
|
letta/services/telemetry_manager.py,sha256=zDdSsRrBYunmlemtUUL1Qh3bcKu5-nhL2n7AlAmVrgs,3297
|
412
412
|
letta/services/tool_manager.py,sha256=zh52n6StaFF5-v6nu0kdNSzJq4du5ACv5iGw5_Y9EDM,43192
|
413
413
|
letta/services/user_manager.py,sha256=XuG9eFrvax69sONx7t_D5kgpt5zNwyER-MhqLSDs8L4,9949
|
@@ -470,8 +470,8 @@ letta/templates/sandbox_code_file_async.py.j2,sha256=lb7nh_P2W9VZHzU_9TxSCEMUod7
|
|
470
470
|
letta/templates/summary_request_text.j2,sha256=ZttQwXonW2lk4pJLYzLK0pmo4EO4EtUUIXjgXKiizuc,842
|
471
471
|
letta/templates/template_helper.py,sha256=HkG3zwRc5NVGmSTQu5PUTpz7LevK43bzXVaQuN8urf0,1634
|
472
472
|
letta/types/__init__.py,sha256=hokKjCVFGEfR7SLMrtZsRsBfsC7yTIbgKPLdGg4K1eY,147
|
473
|
-
letta_nightly-0.11.7.
|
474
|
-
letta_nightly-0.11.7.
|
475
|
-
letta_nightly-0.11.7.
|
476
|
-
letta_nightly-0.11.7.
|
477
|
-
letta_nightly-0.11.7.
|
473
|
+
letta_nightly-0.11.7.dev20250915104130.dist-info/METADATA,sha256=mpHuQ8T7Dn58hWBd4OBq_IJhl-GIqXOmD1T4jt4SwSY,24424
|
474
|
+
letta_nightly-0.11.7.dev20250915104130.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
475
|
+
letta_nightly-0.11.7.dev20250915104130.dist-info/entry_points.txt,sha256=m-94Paj-kxiR6Ktu0us0_2qfhn29DzF2oVzqBE6cu8w,41
|
476
|
+
letta_nightly-0.11.7.dev20250915104130.dist-info/licenses/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
|
477
|
+
letta_nightly-0.11.7.dev20250915104130.dist-info/RECORD,,
|
File without changes
|
File without changes
|