letta-nightly 0.6.27.dev20250220104103__py3-none-any.whl → 0.6.28.dev20250220163833__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +1 -1
- letta/agent.py +13 -1
- letta/client/client.py +2 -0
- letta/constants.py +2 -0
- letta/functions/schema_generator.py +6 -6
- letta/helpers/converters.py +153 -0
- letta/helpers/tool_rule_solver.py +11 -1
- letta/llm_api/anthropic.py +10 -5
- letta/llm_api/aws_bedrock.py +1 -1
- letta/llm_api/deepseek.py +303 -0
- letta/llm_api/llm_api_tools.py +81 -1
- letta/llm_api/openai.py +13 -0
- letta/local_llm/chat_completion_proxy.py +15 -2
- letta/local_llm/lmstudio/api.py +75 -1
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +14 -5
- letta/orm/custom_columns.py +31 -110
- letta/orm/identity.py +39 -0
- letta/orm/organization.py +2 -0
- letta/schemas/agent.py +13 -1
- letta/schemas/identity.py +44 -0
- letta/schemas/llm_config.py +2 -0
- letta/schemas/message.py +1 -1
- letta/schemas/openai/chat_completion_response.py +2 -0
- letta/schemas/providers.py +72 -1
- letta/schemas/tool_rule.py +9 -1
- letta/serialize_schemas/__init__.py +1 -0
- letta/serialize_schemas/agent.py +36 -0
- letta/serialize_schemas/base.py +12 -0
- letta/serialize_schemas/custom_fields.py +69 -0
- letta/serialize_schemas/message.py +15 -0
- letta/server/db.py +111 -0
- letta/server/rest_api/app.py +8 -0
- letta/server/rest_api/interface.py +114 -9
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +7 -1
- letta/server/rest_api/routers/v1/identities.py +111 -0
- letta/server/server.py +13 -116
- letta/services/agent_manager.py +54 -6
- letta/services/block_manager.py +1 -1
- letta/services/helpers/agent_manager_helper.py +15 -0
- letta/services/identity_manager.py +140 -0
- letta/services/job_manager.py +1 -1
- letta/services/message_manager.py +1 -1
- letta/services/organization_manager.py +1 -1
- letta/services/passage_manager.py +1 -1
- letta/services/provider_manager.py +1 -1
- letta/services/sandbox_config_manager.py +1 -1
- letta/services/source_manager.py +1 -1
- letta/services/step_manager.py +1 -1
- letta/services/tool_manager.py +1 -1
- letta/services/user_manager.py +1 -1
- letta/settings.py +3 -0
- letta/tracing.py +205 -0
- letta/utils.py +4 -0
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.28.dev20250220163833.dist-info}/METADATA +9 -2
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.28.dev20250220163833.dist-info}/RECORD +60 -47
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.28.dev20250220163833.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.28.dev20250220163833.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.28.dev20250220163833.dist-info}/entry_points.txt +0 -0
|
@@ -317,6 +317,9 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
317
317
|
self.debug = False
|
|
318
318
|
self.timeout = 10 * 60 # 10 minute timeout
|
|
319
319
|
|
|
320
|
+
# for expect_reasoning_content, we should accumulate `content`
|
|
321
|
+
self.expect_reasoning_content_buffer = None
|
|
322
|
+
|
|
320
323
|
def _reset_inner_thoughts_json_reader(self):
|
|
321
324
|
# A buffer for accumulating function arguments (we want to buffer keys and run checks on each one)
|
|
322
325
|
self.function_args_reader = JSONInnerThoughtsExtractor(inner_thoughts_key=self.inner_thoughts_kwarg, wait_for_first_key=True)
|
|
@@ -387,6 +390,39 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
387
390
|
# Wipe the inner thoughts buffers
|
|
388
391
|
self._reset_inner_thoughts_json_reader()
|
|
389
392
|
|
|
393
|
+
# If we were in reasoning mode and accumulated a json block, attempt to release it as chunks
|
|
394
|
+
# if self.expect_reasoning_content_buffer is not None:
|
|
395
|
+
# try:
|
|
396
|
+
# # NOTE: this is hardcoded for our DeepSeek API integration
|
|
397
|
+
# json_reasoning_content = json.loads(self.expect_reasoning_content_buffer)
|
|
398
|
+
|
|
399
|
+
# if "name" in json_reasoning_content:
|
|
400
|
+
# self._push_to_buffer(
|
|
401
|
+
# ToolCallMessage(
|
|
402
|
+
# id=message_id,
|
|
403
|
+
# date=message_date,
|
|
404
|
+
# tool_call=ToolCallDelta(
|
|
405
|
+
# name=json_reasoning_content["name"],
|
|
406
|
+
# arguments=None,
|
|
407
|
+
# tool_call_id=None,
|
|
408
|
+
# ),
|
|
409
|
+
# )
|
|
410
|
+
# )
|
|
411
|
+
# if "arguments" in json_reasoning_content:
|
|
412
|
+
# self._push_to_buffer(
|
|
413
|
+
# ToolCallMessage(
|
|
414
|
+
# id=message_id,
|
|
415
|
+
# date=message_date,
|
|
416
|
+
# tool_call=ToolCallDelta(
|
|
417
|
+
# name=None,
|
|
418
|
+
# arguments=json_reasoning_content["arguments"],
|
|
419
|
+
# tool_call_id=None,
|
|
420
|
+
# ),
|
|
421
|
+
# )
|
|
422
|
+
# )
|
|
423
|
+
# except Exception as e:
|
|
424
|
+
# print(f"Failed to interpret reasoning content ({self.expect_reasoning_content_buffer}) as JSON: {e}")
|
|
425
|
+
|
|
390
426
|
def step_complete(self):
|
|
391
427
|
"""Signal from the agent that one 'step' finished (step = LLM response + tool execution)"""
|
|
392
428
|
if not self.multi_step:
|
|
@@ -410,7 +446,13 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
410
446
|
return
|
|
411
447
|
|
|
412
448
|
def _process_chunk_to_letta_style(
|
|
413
|
-
self,
|
|
449
|
+
self,
|
|
450
|
+
chunk: ChatCompletionChunkResponse,
|
|
451
|
+
message_id: str,
|
|
452
|
+
message_date: datetime,
|
|
453
|
+
# if we expect `reasoning_content``, then that's what gets mapped to ReasoningMessage
|
|
454
|
+
# and `content` needs to be handled outside the interface
|
|
455
|
+
expect_reasoning_content: bool = False,
|
|
414
456
|
) -> Optional[Union[ReasoningMessage, ToolCallMessage, AssistantMessage]]:
|
|
415
457
|
"""
|
|
416
458
|
Example data from non-streaming response looks like:
|
|
@@ -426,6 +468,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
426
468
|
|
|
427
469
|
if (
|
|
428
470
|
message_delta.content is None
|
|
471
|
+
and (expect_reasoning_content and message_delta.reasoning_content is None)
|
|
429
472
|
and message_delta.tool_calls is None
|
|
430
473
|
and message_delta.function_call is None
|
|
431
474
|
and choice.finish_reason is None
|
|
@@ -435,17 +478,68 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
435
478
|
return None
|
|
436
479
|
|
|
437
480
|
# inner thoughts
|
|
438
|
-
if message_delta.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
481
|
+
if expect_reasoning_content and message_delta.reasoning_content is not None:
|
|
482
|
+
processed_chunk = ReasoningMessage(
|
|
483
|
+
id=message_id,
|
|
484
|
+
date=message_date,
|
|
485
|
+
reasoning=message_delta.reasoning_content,
|
|
486
|
+
)
|
|
487
|
+
elif expect_reasoning_content and message_delta.content is not None:
|
|
488
|
+
# "ignore" content if we expect reasoning content
|
|
489
|
+
if self.expect_reasoning_content_buffer is None:
|
|
490
|
+
self.expect_reasoning_content_buffer = message_delta.content
|
|
442
491
|
else:
|
|
443
|
-
|
|
492
|
+
self.expect_reasoning_content_buffer += message_delta.content
|
|
493
|
+
|
|
494
|
+
# we expect this to be pure JSON
|
|
495
|
+
# OptimisticJSONParser
|
|
496
|
+
|
|
497
|
+
# If we can pull a name out, pull it
|
|
498
|
+
|
|
499
|
+
try:
|
|
500
|
+
# NOTE: this is hardcoded for our DeepSeek API integration
|
|
501
|
+
json_reasoning_content = json.loads(self.expect_reasoning_content_buffer)
|
|
502
|
+
print(f"json_reasoning_content: {json_reasoning_content}")
|
|
503
|
+
|
|
504
|
+
processed_chunk = ToolCallMessage(
|
|
444
505
|
id=message_id,
|
|
445
506
|
date=message_date,
|
|
446
|
-
|
|
507
|
+
tool_call=ToolCallDelta(
|
|
508
|
+
name=json_reasoning_content.get("name"),
|
|
509
|
+
arguments=json.dumps(json_reasoning_content.get("arguments")),
|
|
510
|
+
tool_call_id=None,
|
|
511
|
+
),
|
|
447
512
|
)
|
|
448
513
|
|
|
514
|
+
except json.JSONDecodeError as e:
|
|
515
|
+
print(f"Failed to interpret reasoning content ({self.expect_reasoning_content_buffer}) as JSON: {e}")
|
|
516
|
+
|
|
517
|
+
return None
|
|
518
|
+
# Else,
|
|
519
|
+
# return None
|
|
520
|
+
# processed_chunk = ToolCallMessage(
|
|
521
|
+
# id=message_id,
|
|
522
|
+
# date=message_date,
|
|
523
|
+
# tool_call=ToolCallDelta(
|
|
524
|
+
# # name=tool_call_delta.get("name"),
|
|
525
|
+
# name=None,
|
|
526
|
+
# arguments=message_delta.content,
|
|
527
|
+
# # tool_call_id=tool_call_delta.get("id"),
|
|
528
|
+
# tool_call_id=None,
|
|
529
|
+
# ),
|
|
530
|
+
# )
|
|
531
|
+
# return processed_chunk
|
|
532
|
+
|
|
533
|
+
# TODO eventually output as tool call outputs?
|
|
534
|
+
# print(f"Hiding content delta stream: '{message_delta.content}'")
|
|
535
|
+
# return None
|
|
536
|
+
elif message_delta.content is not None:
|
|
537
|
+
processed_chunk = ReasoningMessage(
|
|
538
|
+
id=message_id,
|
|
539
|
+
date=message_date,
|
|
540
|
+
reasoning=message_delta.content,
|
|
541
|
+
)
|
|
542
|
+
|
|
449
543
|
# tool calls
|
|
450
544
|
elif message_delta.tool_calls is not None and len(message_delta.tool_calls) > 0:
|
|
451
545
|
tool_call = message_delta.tool_calls[0]
|
|
@@ -890,7 +984,13 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
890
984
|
|
|
891
985
|
return processed_chunk
|
|
892
986
|
|
|
893
|
-
def process_chunk(
|
|
987
|
+
def process_chunk(
|
|
988
|
+
self,
|
|
989
|
+
chunk: ChatCompletionChunkResponse,
|
|
990
|
+
message_id: str,
|
|
991
|
+
message_date: datetime,
|
|
992
|
+
expect_reasoning_content: bool = False,
|
|
993
|
+
):
|
|
894
994
|
"""Process a streaming chunk from an OpenAI-compatible server.
|
|
895
995
|
|
|
896
996
|
Example data from non-streaming response looks like:
|
|
@@ -910,7 +1010,12 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
|
910
1010
|
# processed_chunk = self._process_chunk_to_openai_style(chunk)
|
|
911
1011
|
raise NotImplementedError("OpenAI proxy streaming temporarily disabled")
|
|
912
1012
|
else:
|
|
913
|
-
processed_chunk = self._process_chunk_to_letta_style(
|
|
1013
|
+
processed_chunk = self._process_chunk_to_letta_style(
|
|
1014
|
+
chunk=chunk,
|
|
1015
|
+
message_id=message_id,
|
|
1016
|
+
message_date=message_date,
|
|
1017
|
+
expect_reasoning_content=expect_reasoning_content,
|
|
1018
|
+
)
|
|
914
1019
|
if processed_chunk is None:
|
|
915
1020
|
return
|
|
916
1021
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from letta.server.rest_api.routers.v1.agents import router as agents_router
|
|
2
2
|
from letta.server.rest_api.routers.v1.blocks import router as blocks_router
|
|
3
3
|
from letta.server.rest_api.routers.v1.health import router as health_router
|
|
4
|
+
from letta.server.rest_api.routers.v1.identities import router as identities_router
|
|
4
5
|
from letta.server.rest_api.routers.v1.jobs import router as jobs_router
|
|
5
6
|
from letta.server.rest_api.routers.v1.llms import router as llm_router
|
|
6
7
|
from letta.server.rest_api.routers.v1.providers import router as providers_router
|
|
@@ -15,6 +16,7 @@ ROUTERS = [
|
|
|
15
16
|
tools_router,
|
|
16
17
|
sources_router,
|
|
17
18
|
agents_router,
|
|
19
|
+
identities_router,
|
|
18
20
|
llm_router,
|
|
19
21
|
blocks_router,
|
|
20
22
|
jobs_router,
|
|
@@ -23,6 +23,7 @@ from letta.schemas.tool import Tool
|
|
|
23
23
|
from letta.schemas.user import User
|
|
24
24
|
from letta.server.rest_api.utils import get_letta_server
|
|
25
25
|
from letta.server.server import SyncServer
|
|
26
|
+
from letta.tracing import trace_method
|
|
26
27
|
|
|
27
28
|
# These can be forward refs, but because Fastapi needs them at runtime the must be imported normally
|
|
28
29
|
|
|
@@ -50,6 +51,7 @@ def list_agents(
|
|
|
50
51
|
project_id: Optional[str] = Query(None, description="Search agents by project id"),
|
|
51
52
|
template_id: Optional[str] = Query(None, description="Search agents by template id"),
|
|
52
53
|
base_template_id: Optional[str] = Query(None, description="Search agents by base template id"),
|
|
54
|
+
identifier_key: Optional[str] = Query(None, description="Search agents by identifier key"),
|
|
53
55
|
):
|
|
54
56
|
"""
|
|
55
57
|
List all agents associated with a given user.
|
|
@@ -65,6 +67,7 @@ def list_agents(
|
|
|
65
67
|
"project_id": project_id,
|
|
66
68
|
"template_id": template_id,
|
|
67
69
|
"base_template_id": base_template_id,
|
|
70
|
+
"identifier_key": identifier_key,
|
|
68
71
|
}.items()
|
|
69
72
|
if value is not None
|
|
70
73
|
}
|
|
@@ -111,6 +114,7 @@ def create_agent(
|
|
|
111
114
|
agent: CreateAgentRequest = Body(...),
|
|
112
115
|
server: "SyncServer" = Depends(get_letta_server),
|
|
113
116
|
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
117
|
+
x_project: Optional[str] = Header(None, alias="X-Project"), # Only handled by next js middleware
|
|
114
118
|
):
|
|
115
119
|
"""
|
|
116
120
|
Create a new agent with the specified configuration.
|
|
@@ -460,6 +464,7 @@ def modify_message(
|
|
|
460
464
|
response_model=LettaResponse,
|
|
461
465
|
operation_id="send_message",
|
|
462
466
|
)
|
|
467
|
+
@trace_method("POST /v1/agents/{agent_id}/messages")
|
|
463
468
|
async def send_message(
|
|
464
469
|
agent_id: str,
|
|
465
470
|
server: SyncServer = Depends(get_letta_server),
|
|
@@ -498,6 +503,7 @@ async def send_message(
|
|
|
498
503
|
}
|
|
499
504
|
},
|
|
500
505
|
)
|
|
506
|
+
@trace_method("POST /v1/agents/{agent_id}/messages/stream")
|
|
501
507
|
async def send_message_streaming(
|
|
502
508
|
agent_id: str,
|
|
503
509
|
server: SyncServer = Depends(get_letta_server),
|
|
@@ -509,7 +515,6 @@ async def send_message_streaming(
|
|
|
509
515
|
This endpoint accepts a message from a user and processes it through the agent.
|
|
510
516
|
It will stream the steps of the response always, and stream the tokens if 'stream_tokens' is set to True.
|
|
511
517
|
"""
|
|
512
|
-
|
|
513
518
|
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
|
514
519
|
result = await server.send_message_to_agent(
|
|
515
520
|
agent_id=agent_id,
|
|
@@ -574,6 +579,7 @@ async def process_message_background(
|
|
|
574
579
|
response_model=Run,
|
|
575
580
|
operation_id="create_agent_message_async",
|
|
576
581
|
)
|
|
582
|
+
@trace_method("POST /v1/agents/{agent_id}/messages/async")
|
|
577
583
|
async def send_message_async(
|
|
578
584
|
agent_id: str,
|
|
579
585
|
background_tasks: BackgroundTasks,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query
|
|
4
|
+
|
|
5
|
+
from letta.orm.errors import NoResultFound
|
|
6
|
+
from letta.schemas.identity import Identity, IdentityCreate, IdentityType, IdentityUpdate
|
|
7
|
+
from letta.server.rest_api.utils import get_letta_server
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from letta.server.server import SyncServer
|
|
11
|
+
|
|
12
|
+
router = APIRouter(prefix="/identities", tags=["identities"])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.get("/", tags=["identities"], response_model=List[Identity], operation_id="list_identities")
|
|
16
|
+
def list_identities(
|
|
17
|
+
name: Optional[str] = Query(None),
|
|
18
|
+
project_id: Optional[str] = Query(None),
|
|
19
|
+
identity_type: Optional[IdentityType] = Query(None),
|
|
20
|
+
before: Optional[str] = Query(None),
|
|
21
|
+
after: Optional[str] = Query(None),
|
|
22
|
+
limit: Optional[int] = Query(50),
|
|
23
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
24
|
+
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Get a list of all identities in the database
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
|
31
|
+
|
|
32
|
+
identities = server.identity_manager.list_identities(
|
|
33
|
+
name=name, project_id=project_id, identity_type=identity_type, before=before, after=after, limit=limit, actor=actor
|
|
34
|
+
)
|
|
35
|
+
except HTTPException:
|
|
36
|
+
raise
|
|
37
|
+
except Exception as e:
|
|
38
|
+
raise HTTPException(status_code=500, detail=f"{e}")
|
|
39
|
+
return identities
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.get("/{identifier_key}", tags=["identities"], response_model=Identity, operation_id="get_identity_from_identifier_key")
|
|
43
|
+
def retrieve_identity(
|
|
44
|
+
identifier_key: str,
|
|
45
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
46
|
+
):
|
|
47
|
+
try:
|
|
48
|
+
return server.identity_manager.get_identity_from_identifier_key(identifier_key=identifier_key)
|
|
49
|
+
except NoResultFound as e:
|
|
50
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.post("/", tags=["identities"], response_model=Identity, operation_id="create_identity")
|
|
54
|
+
def create_identity(
|
|
55
|
+
identity: IdentityCreate = Body(...),
|
|
56
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
57
|
+
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
58
|
+
x_project: Optional[str] = Header(None, alias="X-Project"), # Only handled by next js middleware
|
|
59
|
+
):
|
|
60
|
+
try:
|
|
61
|
+
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
|
62
|
+
return server.identity_manager.create_identity(identity=identity, actor=actor)
|
|
63
|
+
except HTTPException:
|
|
64
|
+
raise
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise HTTPException(status_code=500, detail=f"{e}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@router.put("/", tags=["identities"], response_model=Identity, operation_id="upsert_identity")
|
|
70
|
+
def upsert_identity(
|
|
71
|
+
identity: IdentityCreate = Body(...),
|
|
72
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
73
|
+
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
74
|
+
x_project: Optional[str] = Header(None, alias="X-Project"), # Only handled by next js middleware
|
|
75
|
+
):
|
|
76
|
+
try:
|
|
77
|
+
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
|
78
|
+
return server.identity_manager.upsert_identity(identity=identity, actor=actor)
|
|
79
|
+
except HTTPException:
|
|
80
|
+
raise
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise HTTPException(status_code=500, detail=f"{e}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.patch("/{identifier_key}", tags=["identities"], response_model=Identity, operation_id="update_identity")
|
|
86
|
+
def modify_identity(
|
|
87
|
+
identifier_key: str,
|
|
88
|
+
identity: IdentityUpdate = Body(...),
|
|
89
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
90
|
+
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
91
|
+
):
|
|
92
|
+
try:
|
|
93
|
+
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
|
94
|
+
return server.identity_manager.update_identity_by_key(identifier_key=identifier_key, identity=identity, actor=actor)
|
|
95
|
+
except HTTPException:
|
|
96
|
+
raise
|
|
97
|
+
except Exception as e:
|
|
98
|
+
raise HTTPException(status_code=500, detail=f"{e}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.delete("/{identifier_key}", tags=["identities"], operation_id="delete_identity")
|
|
102
|
+
def delete_identity(
|
|
103
|
+
identifier_key: str,
|
|
104
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
105
|
+
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
106
|
+
):
|
|
107
|
+
"""
|
|
108
|
+
Delete an identity by its identifier key
|
|
109
|
+
"""
|
|
110
|
+
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
|
111
|
+
server.identity_manager.delete_identity_by_key(identifier_key=identifier_key, actor=actor)
|
letta/server/server.py
CHANGED
|
@@ -18,6 +18,7 @@ import letta.server.utils as server_utils
|
|
|
18
18
|
import letta.system as system
|
|
19
19
|
from letta.agent import Agent, save_agent
|
|
20
20
|
from letta.chat_only_agent import ChatOnlyAgent
|
|
21
|
+
from letta.config import LettaConfig
|
|
21
22
|
from letta.data_sources.connectors import DataConnector, load_data
|
|
22
23
|
from letta.helpers.datetime_helpers import get_utc_time
|
|
23
24
|
from letta.helpers.json_helpers import json_dumps, json_loads
|
|
@@ -27,7 +28,6 @@ from letta.interface import AgentInterface # abstract
|
|
|
27
28
|
from letta.interface import CLIInterface # for printing to terminal
|
|
28
29
|
from letta.log import get_logger
|
|
29
30
|
from letta.offline_memory_agent import OfflineMemoryAgent
|
|
30
|
-
from letta.orm import Base
|
|
31
31
|
from letta.orm.errors import NoResultFound
|
|
32
32
|
from letta.schemas.agent import AgentState, AgentType, CreateAgent
|
|
33
33
|
from letta.schemas.block import BlockUpdate
|
|
@@ -48,6 +48,7 @@ from letta.schemas.providers import (
|
|
|
48
48
|
AnthropicBedrockProvider,
|
|
49
49
|
AnthropicProvider,
|
|
50
50
|
AzureProvider,
|
|
51
|
+
DeepSeekProvider,
|
|
51
52
|
GoogleAIProvider,
|
|
52
53
|
GoogleVertexProvider,
|
|
53
54
|
GroqProvider,
|
|
@@ -70,6 +71,7 @@ from letta.server.rest_api.interface import StreamingServerInterface
|
|
|
70
71
|
from letta.server.rest_api.utils import sse_async_generator
|
|
71
72
|
from letta.services.agent_manager import AgentManager
|
|
72
73
|
from letta.services.block_manager import BlockManager
|
|
74
|
+
from letta.services.identity_manager import IdentityManager
|
|
73
75
|
from letta.services.job_manager import JobManager
|
|
74
76
|
from letta.services.message_manager import MessageManager
|
|
75
77
|
from letta.services.organization_manager import OrganizationManager
|
|
@@ -82,8 +84,11 @@ from letta.services.step_manager import StepManager
|
|
|
82
84
|
from letta.services.tool_execution_sandbox import ToolExecutionSandbox
|
|
83
85
|
from letta.services.tool_manager import ToolManager
|
|
84
86
|
from letta.services.user_manager import UserManager
|
|
87
|
+
from letta.settings import model_settings, settings, tool_settings
|
|
88
|
+
from letta.tracing import trace_method
|
|
85
89
|
from letta.utils import get_friendly_error_msg
|
|
86
90
|
|
|
91
|
+
config = LettaConfig.load()
|
|
87
92
|
logger = get_logger(__name__)
|
|
88
93
|
|
|
89
94
|
|
|
@@ -145,118 +150,6 @@ class Server(object):
|
|
|
145
150
|
raise NotImplementedError
|
|
146
151
|
|
|
147
152
|
|
|
148
|
-
from contextlib import contextmanager
|
|
149
|
-
|
|
150
|
-
from rich.console import Console
|
|
151
|
-
from rich.panel import Panel
|
|
152
|
-
from rich.text import Text
|
|
153
|
-
from sqlalchemy import create_engine
|
|
154
|
-
from sqlalchemy.orm import sessionmaker
|
|
155
|
-
|
|
156
|
-
from letta.config import LettaConfig
|
|
157
|
-
|
|
158
|
-
# NOTE: hack to see if single session management works
|
|
159
|
-
from letta.settings import model_settings, settings, tool_settings
|
|
160
|
-
|
|
161
|
-
config = LettaConfig.load()
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def print_sqlite_schema_error():
|
|
165
|
-
"""Print a formatted error message for SQLite schema issues"""
|
|
166
|
-
console = Console()
|
|
167
|
-
error_text = Text()
|
|
168
|
-
error_text.append("Existing SQLite DB schema is invalid, and schema migrations are not supported for SQLite. ", style="bold red")
|
|
169
|
-
error_text.append("To have migrations supported between Letta versions, please run Letta with Docker (", style="white")
|
|
170
|
-
error_text.append("https://docs.letta.com/server/docker", style="blue underline")
|
|
171
|
-
error_text.append(") or use Postgres by setting ", style="white")
|
|
172
|
-
error_text.append("LETTA_PG_URI", style="yellow")
|
|
173
|
-
error_text.append(".\n\n", style="white")
|
|
174
|
-
error_text.append("If you wish to keep using SQLite, you can reset your database by removing the DB file with ", style="white")
|
|
175
|
-
error_text.append("rm ~/.letta/sqlite.db", style="yellow")
|
|
176
|
-
error_text.append(" or downgrade to your previous version of Letta.", style="white")
|
|
177
|
-
|
|
178
|
-
console.print(Panel(error_text, border_style="red"))
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
@contextmanager
|
|
182
|
-
def db_error_handler():
|
|
183
|
-
"""Context manager for handling database errors"""
|
|
184
|
-
try:
|
|
185
|
-
yield
|
|
186
|
-
except Exception as e:
|
|
187
|
-
# Handle other SQLAlchemy errors
|
|
188
|
-
print(e)
|
|
189
|
-
print_sqlite_schema_error()
|
|
190
|
-
# raise ValueError(f"SQLite DB error: {str(e)}")
|
|
191
|
-
exit(1)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if settings.letta_pg_uri_no_default:
|
|
195
|
-
print("Creating postgres engine")
|
|
196
|
-
config.recall_storage_type = "postgres"
|
|
197
|
-
config.recall_storage_uri = settings.letta_pg_uri_no_default
|
|
198
|
-
config.archival_storage_type = "postgres"
|
|
199
|
-
config.archival_storage_uri = settings.letta_pg_uri_no_default
|
|
200
|
-
|
|
201
|
-
# create engine
|
|
202
|
-
engine = create_engine(
|
|
203
|
-
settings.letta_pg_uri,
|
|
204
|
-
pool_size=settings.pg_pool_size,
|
|
205
|
-
max_overflow=settings.pg_max_overflow,
|
|
206
|
-
pool_timeout=settings.pg_pool_timeout,
|
|
207
|
-
pool_recycle=settings.pg_pool_recycle,
|
|
208
|
-
echo=settings.pg_echo,
|
|
209
|
-
)
|
|
210
|
-
else:
|
|
211
|
-
# TODO: don't rely on config storage
|
|
212
|
-
engine_path = "sqlite:///" + os.path.join(config.recall_storage_path, "sqlite.db")
|
|
213
|
-
logger.info("Creating sqlite engine " + engine_path)
|
|
214
|
-
|
|
215
|
-
engine = create_engine(engine_path)
|
|
216
|
-
|
|
217
|
-
# Store the original connect method
|
|
218
|
-
original_connect = engine.connect
|
|
219
|
-
|
|
220
|
-
def wrapped_connect(*args, **kwargs):
|
|
221
|
-
with db_error_handler():
|
|
222
|
-
# Get the connection
|
|
223
|
-
connection = original_connect(*args, **kwargs)
|
|
224
|
-
|
|
225
|
-
# Store the original execution method
|
|
226
|
-
original_execute = connection.execute
|
|
227
|
-
|
|
228
|
-
# Wrap the execute method of the connection
|
|
229
|
-
def wrapped_execute(*args, **kwargs):
|
|
230
|
-
with db_error_handler():
|
|
231
|
-
return original_execute(*args, **kwargs)
|
|
232
|
-
|
|
233
|
-
# Replace the connection's execute method
|
|
234
|
-
connection.execute = wrapped_execute
|
|
235
|
-
|
|
236
|
-
return connection
|
|
237
|
-
|
|
238
|
-
# Replace the engine's connect method
|
|
239
|
-
engine.connect = wrapped_connect
|
|
240
|
-
|
|
241
|
-
Base.metadata.create_all(bind=engine)
|
|
242
|
-
|
|
243
|
-
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
# Dependency
|
|
247
|
-
def get_db():
|
|
248
|
-
db = SessionLocal()
|
|
249
|
-
try:
|
|
250
|
-
yield db
|
|
251
|
-
finally:
|
|
252
|
-
db.close()
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
from contextlib import contextmanager
|
|
256
|
-
|
|
257
|
-
db_context = contextmanager(get_db)
|
|
258
|
-
|
|
259
|
-
|
|
260
153
|
class SyncServer(Server):
|
|
261
154
|
"""Simple single-threaded / blocking server process"""
|
|
262
155
|
|
|
@@ -304,6 +197,7 @@ class SyncServer(Server):
|
|
|
304
197
|
self.agent_manager = AgentManager()
|
|
305
198
|
self.provider_manager = ProviderManager()
|
|
306
199
|
self.step_manager = StepManager()
|
|
200
|
+
self.identity_manager = IdentityManager()
|
|
307
201
|
|
|
308
202
|
# Managers that interface with parallelism
|
|
309
203
|
self.per_agent_lock_manager = PerAgentLockManager()
|
|
@@ -415,6 +309,8 @@ class SyncServer(Server):
|
|
|
415
309
|
else model_settings.lmstudio_base_url + "/v1"
|
|
416
310
|
)
|
|
417
311
|
self._enabled_providers.append(LMStudioOpenAIProvider(base_url=lmstudio_url))
|
|
312
|
+
if model_settings.deepseek_api_key:
|
|
313
|
+
self._enabled_providers.append(DeepSeekProvider(api_key=model_settings.deepseek_api_key))
|
|
418
314
|
|
|
419
315
|
def load_agent(self, agent_id: str, actor: User, interface: Union[AgentInterface, None] = None) -> Agent:
|
|
420
316
|
"""Updated method to load agents from persisted storage"""
|
|
@@ -1256,6 +1152,7 @@ class SyncServer(Server):
|
|
|
1256
1152
|
actions = self.get_composio_client(api_key=api_key).actions.get(apps=[composio_app_name])
|
|
1257
1153
|
return actions
|
|
1258
1154
|
|
|
1155
|
+
@trace_method("Send Message")
|
|
1259
1156
|
async def send_message_to_agent(
|
|
1260
1157
|
self,
|
|
1261
1158
|
agent_id: str,
|
|
@@ -1273,7 +1170,6 @@ class SyncServer(Server):
|
|
|
1273
1170
|
metadata: Optional[dict] = None,
|
|
1274
1171
|
) -> Union[StreamingResponse, LettaResponse]:
|
|
1275
1172
|
"""Split off into a separate function so that it can be imported in the /chat/completion proxy."""
|
|
1276
|
-
|
|
1277
1173
|
# TODO: @charles is this the correct way to handle?
|
|
1278
1174
|
include_final_message = True
|
|
1279
1175
|
|
|
@@ -1292,11 +1188,12 @@ class SyncServer(Server):
|
|
|
1292
1188
|
# Disable token streaming if not OpenAI or Anthropic
|
|
1293
1189
|
# TODO: cleanup this logic
|
|
1294
1190
|
llm_config = letta_agent.agent_state.llm_config
|
|
1191
|
+
supports_token_streaming = ["openai", "anthropic", "deepseek"]
|
|
1295
1192
|
if stream_tokens and (
|
|
1296
|
-
llm_config.model_endpoint_type not in
|
|
1193
|
+
llm_config.model_endpoint_type not in supports_token_streaming or "inference.memgpt.ai" in llm_config.model_endpoint
|
|
1297
1194
|
):
|
|
1298
1195
|
warnings.warn(
|
|
1299
|
-
f"Token streaming is only supported for models with type '
|
|
1196
|
+
f"Token streaming is only supported for models with type {' or '.join(supports_token_streaming)} in the model_endpoint: agent has endpoint type {llm_config.model_endpoint_type} and {llm_config.model_endpoint}. Setting stream_tokens to False."
|
|
1300
1197
|
)
|
|
1301
1198
|
stream_tokens = False
|
|
1302
1199
|
|