letta-nightly 0.6.28.dev20250220163833__py3-none-any.whl → 0.6.29.dev20250221033538__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 +6 -1
- letta/llm_api/helpers.py +20 -10
- letta/llm_api/llm_api_tools.py +4 -1
- letta/llm_api/openai.py +3 -1
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +9 -11
- letta/orm/identities_agents.py +13 -0
- letta/orm/identity.py +26 -5
- letta/orm/sqlalchemy_base.py +4 -0
- letta/schemas/agent.py +3 -5
- letta/schemas/identity.py +26 -3
- letta/server/rest_api/chat_completions_interface.py +45 -21
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +98 -24
- letta/server/rest_api/routers/v1/agents.py +9 -4
- letta/server/rest_api/routers/v1/identities.py +20 -10
- letta/server/rest_api/utils.py +183 -4
- letta/server/server.py +10 -1
- letta/services/agent_manager.py +8 -9
- letta/services/helpers/agent_manager_helper.py +0 -15
- letta/services/identity_manager.py +37 -21
- letta/streaming_interface.py +6 -2
- {letta_nightly-0.6.28.dev20250220163833.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/METADATA +1 -1
- {letta_nightly-0.6.28.dev20250220163833.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/RECORD +27 -26
- {letta_nightly-0.6.28.dev20250220163833.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.28.dev20250220163833.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.28.dev20250220163833.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/entry_points.txt +0 -0
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from typing import TYPE_CHECKING,
|
|
2
|
+
from typing import TYPE_CHECKING, List, Optional, Union
|
|
3
3
|
|
|
4
|
+
import httpx
|
|
5
|
+
import openai
|
|
4
6
|
from fastapi import APIRouter, Body, Depends, Header, HTTPException
|
|
5
7
|
from fastapi.responses import StreamingResponse
|
|
6
|
-
from openai.types.chat import ChatCompletionMessageParam
|
|
7
8
|
from openai.types.chat.completion_create_params import CompletionCreateParams
|
|
9
|
+
from starlette.concurrency import run_in_threadpool
|
|
8
10
|
|
|
9
11
|
from letta.agent import Agent
|
|
10
12
|
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
|
|
11
13
|
from letta.log import get_logger
|
|
12
|
-
from letta.schemas.message import MessageCreate
|
|
13
|
-
from letta.schemas.openai.chat_completion_response import Message
|
|
14
|
+
from letta.schemas.message import Message, MessageCreate
|
|
14
15
|
from letta.schemas.user import User
|
|
15
16
|
from letta.server.rest_api.chat_completions_interface import ChatCompletionsStreamingInterface
|
|
16
17
|
|
|
17
18
|
# TODO this belongs in a controller!
|
|
18
|
-
from letta.server.rest_api.utils import
|
|
19
|
+
from letta.server.rest_api.utils import (
|
|
20
|
+
convert_letta_messages_to_openai,
|
|
21
|
+
create_assistant_message_from_openai_response,
|
|
22
|
+
create_user_message,
|
|
23
|
+
get_letta_server,
|
|
24
|
+
get_messages_from_completion_request,
|
|
25
|
+
sse_async_generator,
|
|
26
|
+
)
|
|
27
|
+
from letta.settings import model_settings
|
|
19
28
|
|
|
20
29
|
if TYPE_CHECKING:
|
|
21
30
|
from letta.server.server import SyncServer
|
|
@@ -25,6 +34,88 @@ router = APIRouter(prefix="/v1", tags=["chat_completions"])
|
|
|
25
34
|
logger = get_logger(__name__)
|
|
26
35
|
|
|
27
36
|
|
|
37
|
+
@router.post(
|
|
38
|
+
"/fast/chat/completions",
|
|
39
|
+
response_model=None,
|
|
40
|
+
operation_id="create_fast_chat_completions",
|
|
41
|
+
responses={
|
|
42
|
+
200: {
|
|
43
|
+
"description": "Successful response",
|
|
44
|
+
"content": {
|
|
45
|
+
"text/event-stream": {"description": "Server-Sent Events stream"},
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
async def create_fast_chat_completions(
|
|
51
|
+
completion_request: CompletionCreateParams = Body(...),
|
|
52
|
+
server: "SyncServer" = Depends(get_letta_server),
|
|
53
|
+
user_id: Optional[str] = Header(None, alias="user_id"),
|
|
54
|
+
):
|
|
55
|
+
# TODO: This is necessary, we need to factor out CompletionCreateParams due to weird behavior
|
|
56
|
+
agent_id = str(completion_request.get("user", None))
|
|
57
|
+
if agent_id is None:
|
|
58
|
+
error_msg = "Must pass agent_id in the 'user' field"
|
|
59
|
+
logger.error(error_msg)
|
|
60
|
+
raise HTTPException(status_code=400, detail=error_msg)
|
|
61
|
+
model = completion_request.get("model")
|
|
62
|
+
|
|
63
|
+
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
|
64
|
+
client = openai.AsyncClient(
|
|
65
|
+
api_key=model_settings.openai_api_key,
|
|
66
|
+
max_retries=0,
|
|
67
|
+
http_client=httpx.AsyncClient(
|
|
68
|
+
timeout=httpx.Timeout(connect=15.0, read=5.0, write=5.0, pool=5.0),
|
|
69
|
+
follow_redirects=True,
|
|
70
|
+
limits=httpx.Limits(
|
|
71
|
+
max_connections=50,
|
|
72
|
+
max_keepalive_connections=50,
|
|
73
|
+
keepalive_expiry=120,
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Magic message manipulating
|
|
79
|
+
input_message = get_messages_from_completion_request(completion_request)[-1]
|
|
80
|
+
completion_request.pop("messages")
|
|
81
|
+
|
|
82
|
+
# Get in context messages
|
|
83
|
+
in_context_messages = server.agent_manager.get_in_context_messages(agent_id=agent_id, actor=actor)
|
|
84
|
+
openai_dict_in_context_messages = convert_letta_messages_to_openai(in_context_messages)
|
|
85
|
+
openai_dict_in_context_messages.append(input_message)
|
|
86
|
+
|
|
87
|
+
async def event_stream():
|
|
88
|
+
# TODO: Factor this out into separate interface
|
|
89
|
+
response_accumulator = []
|
|
90
|
+
|
|
91
|
+
stream = await client.chat.completions.create(**completion_request, messages=openai_dict_in_context_messages)
|
|
92
|
+
|
|
93
|
+
async with stream:
|
|
94
|
+
async for chunk in stream:
|
|
95
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
96
|
+
# TODO: This does not support tool calling right now
|
|
97
|
+
response_accumulator.append(chunk.choices[0].delta.content)
|
|
98
|
+
yield f"data: {chunk.model_dump_json()}\n\n"
|
|
99
|
+
|
|
100
|
+
# Construct messages
|
|
101
|
+
user_message = create_user_message(input_message=input_message, agent_id=agent_id, actor=actor)
|
|
102
|
+
assistant_message = create_assistant_message_from_openai_response(
|
|
103
|
+
response_text="".join(response_accumulator), agent_id=agent_id, model=str(model), actor=actor
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Persist both in one synchronous DB call, done in a threadpool
|
|
107
|
+
await run_in_threadpool(
|
|
108
|
+
server.agent_manager.append_to_in_context_messages,
|
|
109
|
+
[user_message, assistant_message],
|
|
110
|
+
agent_id=agent_id,
|
|
111
|
+
actor=actor,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
yield "data: [DONE]\n\n"
|
|
115
|
+
|
|
116
|
+
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
|
117
|
+
|
|
118
|
+
|
|
28
119
|
@router.post(
|
|
29
120
|
"/chat/completions",
|
|
30
121
|
response_model=None,
|
|
@@ -44,26 +135,8 @@ async def create_chat_completions(
|
|
|
44
135
|
user_id: Optional[str] = Header(None, alias="user_id"),
|
|
45
136
|
):
|
|
46
137
|
# Validate and process fields
|
|
47
|
-
|
|
48
|
-
messages = list(cast(Iterable[ChatCompletionMessageParam], completion_request["messages"]))
|
|
49
|
-
except KeyError:
|
|
50
|
-
# Handle the case where "messages" is not present in the request
|
|
51
|
-
raise HTTPException(status_code=400, detail="The 'messages' field is missing in the request.")
|
|
52
|
-
except TypeError:
|
|
53
|
-
# Handle the case where "messages" is not iterable
|
|
54
|
-
raise HTTPException(status_code=400, detail="The 'messages' field must be an iterable.")
|
|
55
|
-
except Exception as e:
|
|
56
|
-
# Catch any other unexpected errors and include the exception message
|
|
57
|
-
raise HTTPException(status_code=400, detail=f"An error occurred while processing 'messages': {str(e)}")
|
|
58
|
-
|
|
59
|
-
if messages[-1]["role"] != "user":
|
|
60
|
-
logger.error(f"The last message does not have a `user` role: {messages}")
|
|
61
|
-
raise HTTPException(status_code=400, detail="'messages[-1].role' must be a 'user'")
|
|
62
|
-
|
|
138
|
+
messages = get_messages_from_completion_request(completion_request)
|
|
63
139
|
input_message = messages[-1]
|
|
64
|
-
if not isinstance(input_message["content"], str):
|
|
65
|
-
logger.error(f"The input message does not have valid content: {input_message}")
|
|
66
|
-
raise HTTPException(status_code=400, detail="'messages[-1].content' must be a 'string'")
|
|
67
140
|
|
|
68
141
|
# Process remaining fields
|
|
69
142
|
if not completion_request["stream"]:
|
|
@@ -138,6 +211,7 @@ async def send_message_to_agent_chat_completions(
|
|
|
138
211
|
agent_id=letta_agent.agent_state.id,
|
|
139
212
|
messages=messages,
|
|
140
213
|
interface=streaming_interface,
|
|
214
|
+
put_inner_thoughts_first=False,
|
|
141
215
|
)
|
|
142
216
|
)
|
|
143
217
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import traceback
|
|
1
2
|
from datetime import datetime
|
|
2
3
|
from typing import Annotated, List, Optional
|
|
3
4
|
|
|
@@ -51,7 +52,7 @@ def list_agents(
|
|
|
51
52
|
project_id: Optional[str] = Query(None, description="Search agents by project id"),
|
|
52
53
|
template_id: Optional[str] = Query(None, description="Search agents by template id"),
|
|
53
54
|
base_template_id: Optional[str] = Query(None, description="Search agents by base template id"),
|
|
54
|
-
|
|
55
|
+
identifier_keys: Optional[List[str]] = Query(None, description="Search agents by identifier keys"),
|
|
55
56
|
):
|
|
56
57
|
"""
|
|
57
58
|
List all agents associated with a given user.
|
|
@@ -67,7 +68,6 @@ def list_agents(
|
|
|
67
68
|
"project_id": project_id,
|
|
68
69
|
"template_id": template_id,
|
|
69
70
|
"base_template_id": base_template_id,
|
|
70
|
-
"identifier_key": identifier_key,
|
|
71
71
|
}.items()
|
|
72
72
|
if value is not None
|
|
73
73
|
}
|
|
@@ -81,6 +81,7 @@ def list_agents(
|
|
|
81
81
|
query_text=query_text,
|
|
82
82
|
tags=tags,
|
|
83
83
|
match_all_tags=match_all_tags,
|
|
84
|
+
identifier_keys=identifier_keys,
|
|
84
85
|
**kwargs,
|
|
85
86
|
)
|
|
86
87
|
return agents
|
|
@@ -119,8 +120,12 @@ def create_agent(
|
|
|
119
120
|
"""
|
|
120
121
|
Create a new agent with the specified configuration.
|
|
121
122
|
"""
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
try:
|
|
124
|
+
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
|
125
|
+
return server.create_agent(agent, actor=actor)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
traceback.print_exc()
|
|
128
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
124
129
|
|
|
125
130
|
|
|
126
131
|
@router.patch("/{agent_id}", response_model=AgentState, operation_id="modify_agent")
|
|
@@ -16,6 +16,7 @@ router = APIRouter(prefix="/identities", tags=["identities"])
|
|
|
16
16
|
def list_identities(
|
|
17
17
|
name: Optional[str] = Query(None),
|
|
18
18
|
project_id: Optional[str] = Query(None),
|
|
19
|
+
identifier_key: Optional[str] = Query(None),
|
|
19
20
|
identity_type: Optional[IdentityType] = Query(None),
|
|
20
21
|
before: Optional[str] = Query(None),
|
|
21
22
|
after: Optional[str] = Query(None),
|
|
@@ -30,7 +31,14 @@ def list_identities(
|
|
|
30
31
|
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
|
31
32
|
|
|
32
33
|
identities = server.identity_manager.list_identities(
|
|
33
|
-
name=name,
|
|
34
|
+
name=name,
|
|
35
|
+
project_id=project_id,
|
|
36
|
+
identifier_key=identifier_key,
|
|
37
|
+
identity_type=identity_type,
|
|
38
|
+
before=before,
|
|
39
|
+
after=after,
|
|
40
|
+
limit=limit,
|
|
41
|
+
actor=actor,
|
|
34
42
|
)
|
|
35
43
|
except HTTPException:
|
|
36
44
|
raise
|
|
@@ -39,13 +47,15 @@ def list_identities(
|
|
|
39
47
|
return identities
|
|
40
48
|
|
|
41
49
|
|
|
42
|
-
@router.get("/{
|
|
50
|
+
@router.get("/{identity_id}", tags=["identities"], response_model=Identity, operation_id="retrieve_identity")
|
|
43
51
|
def retrieve_identity(
|
|
44
|
-
|
|
52
|
+
identity_id: str,
|
|
45
53
|
server: "SyncServer" = Depends(get_letta_server),
|
|
54
|
+
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
46
55
|
):
|
|
47
56
|
try:
|
|
48
|
-
|
|
57
|
+
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
|
58
|
+
return server.identity_manager.get_identity(identity_id=identity_id, actor=actor)
|
|
49
59
|
except NoResultFound as e:
|
|
50
60
|
raise HTTPException(status_code=404, detail=str(e))
|
|
51
61
|
|
|
@@ -82,25 +92,25 @@ def upsert_identity(
|
|
|
82
92
|
raise HTTPException(status_code=500, detail=f"{e}")
|
|
83
93
|
|
|
84
94
|
|
|
85
|
-
@router.patch("/{
|
|
95
|
+
@router.patch("/{identity_id}", tags=["identities"], response_model=Identity, operation_id="update_identity")
|
|
86
96
|
def modify_identity(
|
|
87
|
-
|
|
97
|
+
identity_id: str,
|
|
88
98
|
identity: IdentityUpdate = Body(...),
|
|
89
99
|
server: "SyncServer" = Depends(get_letta_server),
|
|
90
100
|
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
91
101
|
):
|
|
92
102
|
try:
|
|
93
103
|
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
|
94
|
-
return server.identity_manager.
|
|
104
|
+
return server.identity_manager.update_identity(identity_id=identity_id, identity=identity, actor=actor)
|
|
95
105
|
except HTTPException:
|
|
96
106
|
raise
|
|
97
107
|
except Exception as e:
|
|
98
108
|
raise HTTPException(status_code=500, detail=f"{e}")
|
|
99
109
|
|
|
100
110
|
|
|
101
|
-
@router.delete("/{
|
|
111
|
+
@router.delete("/{identity_id}", tags=["identities"], operation_id="delete_identity")
|
|
102
112
|
def delete_identity(
|
|
103
|
-
|
|
113
|
+
identity_id: str,
|
|
104
114
|
server: "SyncServer" = Depends(get_letta_server),
|
|
105
115
|
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
106
116
|
):
|
|
@@ -108,4 +118,4 @@ def delete_identity(
|
|
|
108
118
|
Delete an identity by its identifier key
|
|
109
119
|
"""
|
|
110
120
|
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
|
111
|
-
server.identity_manager.
|
|
121
|
+
server.identity_manager.delete_identity(identity_id=identity_id, actor=actor)
|
letta/server/rest_api/utils.py
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
+
import uuid
|
|
4
5
|
import warnings
|
|
6
|
+
from datetime import datetime, timezone
|
|
5
7
|
from enum import Enum
|
|
6
|
-
from typing import TYPE_CHECKING, AsyncGenerator, Optional, Union
|
|
8
|
+
from typing import TYPE_CHECKING, AsyncGenerator, Dict, Iterable, List, Optional, Union, cast
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
import pytz
|
|
11
|
+
from fastapi import Header, HTTPException
|
|
12
|
+
from openai.types.chat import ChatCompletionMessageParam
|
|
13
|
+
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
|
|
14
|
+
from openai.types.chat.chat_completion_message_tool_call import Function as OpenAIFunction
|
|
15
|
+
from openai.types.chat.completion_create_params import CompletionCreateParams
|
|
9
16
|
from pydantic import BaseModel
|
|
10
17
|
|
|
18
|
+
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
|
|
11
19
|
from letta.errors import ContextWindowExceededError, RateLimitExceededError
|
|
12
20
|
from letta.log import get_logger
|
|
21
|
+
from letta.schemas.enums import MessageRole
|
|
22
|
+
from letta.schemas.letta_message import TextContent
|
|
23
|
+
from letta.schemas.message import Message
|
|
13
24
|
from letta.schemas.usage import LettaUsageStatistics
|
|
25
|
+
from letta.schemas.user import User
|
|
14
26
|
from letta.server.rest_api.interface import StreamingServerInterface
|
|
15
27
|
|
|
16
28
|
if TYPE_CHECKING:
|
|
17
29
|
from letta.server.server import SyncServer
|
|
18
30
|
|
|
19
|
-
# from letta.orm.user import User
|
|
20
|
-
# from letta.orm.utilities import get_db_session
|
|
21
31
|
|
|
22
32
|
SSE_PREFIX = "data: "
|
|
23
33
|
SSE_SUFFIX = "\n\n"
|
|
@@ -128,3 +138,172 @@ def log_error_to_sentry(e):
|
|
|
128
138
|
import sentry_sdk
|
|
129
139
|
|
|
130
140
|
sentry_sdk.capture_exception(e)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def create_user_message(input_message: dict, agent_id: str, actor: User) -> Message:
|
|
144
|
+
"""
|
|
145
|
+
Converts a user input message into the internal structured format.
|
|
146
|
+
"""
|
|
147
|
+
# Generate timestamp in the correct format
|
|
148
|
+
now = datetime.now(pytz.timezone("US/Pacific")).strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
|
|
149
|
+
|
|
150
|
+
# Format message as structured JSON
|
|
151
|
+
structured_message = {"type": "user_message", "message": input_message["content"], "time": now}
|
|
152
|
+
|
|
153
|
+
# Construct the Message object
|
|
154
|
+
user_message = Message(
|
|
155
|
+
id=f"message-{uuid.uuid4()}",
|
|
156
|
+
role=MessageRole.user,
|
|
157
|
+
content=[TextContent(text=json.dumps(structured_message, indent=2))], # Store structured JSON
|
|
158
|
+
organization_id=actor.organization_id,
|
|
159
|
+
agent_id=agent_id,
|
|
160
|
+
model=None,
|
|
161
|
+
tool_calls=None,
|
|
162
|
+
tool_call_id=None,
|
|
163
|
+
created_at=datetime.now(timezone.utc),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return user_message
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def create_assistant_message_from_openai_response(
|
|
170
|
+
response_text: str,
|
|
171
|
+
agent_id: str,
|
|
172
|
+
model: str,
|
|
173
|
+
actor: User,
|
|
174
|
+
) -> Message:
|
|
175
|
+
"""
|
|
176
|
+
Converts an OpenAI response into a Message that follows the internal
|
|
177
|
+
paradigm where LLM responses are structured as tool calls instead of content.
|
|
178
|
+
"""
|
|
179
|
+
tool_call_id = str(uuid.uuid4())
|
|
180
|
+
|
|
181
|
+
# Construct the tool call with the assistant's message
|
|
182
|
+
tool_call = OpenAIToolCall(
|
|
183
|
+
id=tool_call_id,
|
|
184
|
+
function=OpenAIFunction(
|
|
185
|
+
name=DEFAULT_MESSAGE_TOOL,
|
|
186
|
+
arguments='{\n "' + DEFAULT_MESSAGE_TOOL_KWARG + '": ' + f'"{response_text}",\n "request_heartbeat": true\n' + "}",
|
|
187
|
+
),
|
|
188
|
+
type="function",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Construct the Message object
|
|
192
|
+
assistant_message = Message(
|
|
193
|
+
id=f"message-{uuid.uuid4()}",
|
|
194
|
+
role=MessageRole.assistant,
|
|
195
|
+
content=[],
|
|
196
|
+
organization_id=actor.organization_id,
|
|
197
|
+
agent_id=agent_id,
|
|
198
|
+
model=model,
|
|
199
|
+
tool_calls=[tool_call],
|
|
200
|
+
tool_call_id=None,
|
|
201
|
+
created_at=datetime.now(timezone.utc),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return assistant_message
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def convert_letta_messages_to_openai(messages: List[Message]) -> List[dict]:
|
|
208
|
+
"""
|
|
209
|
+
Flattens Letta's messages (with system, user, assistant, tool roles, etc.)
|
|
210
|
+
into standard OpenAI chat messages (system, user, assistant).
|
|
211
|
+
|
|
212
|
+
Transformation rules:
|
|
213
|
+
1. Assistant + send_message tool_call => content = tool_call's "message"
|
|
214
|
+
2. Tool (role=tool) referencing send_message => skip
|
|
215
|
+
3. User messages might store actual text inside JSON => parse that into content
|
|
216
|
+
4. System => pass through as normal
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
openai_messages = []
|
|
220
|
+
|
|
221
|
+
for msg in messages:
|
|
222
|
+
# 1. Assistant + 'send_message' tool_calls => flatten
|
|
223
|
+
if msg.role == MessageRole.assistant and msg.tool_calls:
|
|
224
|
+
# Find any 'send_message' tool_calls
|
|
225
|
+
send_message_calls = [tc for tc in msg.tool_calls if tc.function.name == "send_message"]
|
|
226
|
+
if send_message_calls:
|
|
227
|
+
# If we have multiple calls, just pick the first or merge them
|
|
228
|
+
# Typically there's only one.
|
|
229
|
+
tc = send_message_calls[0]
|
|
230
|
+
arguments = json.loads(tc.function.arguments)
|
|
231
|
+
# Extract the "message" string
|
|
232
|
+
extracted_text = arguments.get("message", "")
|
|
233
|
+
|
|
234
|
+
# Create a new content with the extracted text
|
|
235
|
+
msg = Message(
|
|
236
|
+
id=msg.id,
|
|
237
|
+
role=msg.role,
|
|
238
|
+
content=[TextContent(text=extracted_text)],
|
|
239
|
+
organization_id=msg.organization_id,
|
|
240
|
+
agent_id=msg.agent_id,
|
|
241
|
+
model=msg.model,
|
|
242
|
+
name=msg.name,
|
|
243
|
+
tool_calls=None, # no longer needed
|
|
244
|
+
tool_call_id=None,
|
|
245
|
+
created_at=msg.created_at,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# 2. If role=tool and it's referencing send_message => skip
|
|
249
|
+
if msg.role == MessageRole.tool and msg.name == "send_message":
|
|
250
|
+
# Usually 'tool' messages with `send_message` are just status/OK messages
|
|
251
|
+
# that OpenAI doesn't need to see. So skip them.
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# 3. User messages might store text in JSON => parse it
|
|
255
|
+
if msg.role == MessageRole.user:
|
|
256
|
+
# Example: content=[TextContent(text='{"type": "user_message","message":"Hello"}')]
|
|
257
|
+
# Attempt to parse JSON and extract "message"
|
|
258
|
+
if msg.content and msg.content[0].text.strip().startswith("{"):
|
|
259
|
+
try:
|
|
260
|
+
parsed = json.loads(msg.content[0].text)
|
|
261
|
+
# If there's a "message" field, use that as the content
|
|
262
|
+
if "message" in parsed:
|
|
263
|
+
actual_user_text = parsed["message"]
|
|
264
|
+
msg = Message(
|
|
265
|
+
id=msg.id,
|
|
266
|
+
role=msg.role,
|
|
267
|
+
content=[TextContent(text=actual_user_text)],
|
|
268
|
+
organization_id=msg.organization_id,
|
|
269
|
+
agent_id=msg.agent_id,
|
|
270
|
+
model=msg.model,
|
|
271
|
+
name=msg.name,
|
|
272
|
+
tool_calls=msg.tool_calls,
|
|
273
|
+
tool_call_id=msg.tool_call_id,
|
|
274
|
+
created_at=msg.created_at,
|
|
275
|
+
)
|
|
276
|
+
except json.JSONDecodeError:
|
|
277
|
+
pass # It's not JSON, leave as-is
|
|
278
|
+
|
|
279
|
+
# 4. System is left as-is (or any other role that doesn't need special handling)
|
|
280
|
+
#
|
|
281
|
+
# Finally, convert to dict using your existing method
|
|
282
|
+
openai_messages.append(msg.to_openai_dict())
|
|
283
|
+
|
|
284
|
+
return openai_messages
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def get_messages_from_completion_request(completion_request: CompletionCreateParams) -> List[Dict]:
|
|
288
|
+
try:
|
|
289
|
+
messages = list(cast(Iterable[ChatCompletionMessageParam], completion_request["messages"]))
|
|
290
|
+
except KeyError:
|
|
291
|
+
# Handle the case where "messages" is not present in the request
|
|
292
|
+
raise HTTPException(status_code=400, detail="The 'messages' field is missing in the request.")
|
|
293
|
+
except TypeError:
|
|
294
|
+
# Handle the case where "messages" is not iterable
|
|
295
|
+
raise HTTPException(status_code=400, detail="The 'messages' field must be an iterable.")
|
|
296
|
+
except Exception as e:
|
|
297
|
+
# Catch any other unexpected errors and include the exception message
|
|
298
|
+
raise HTTPException(status_code=400, detail=f"An error occurred while processing 'messages': {str(e)}")
|
|
299
|
+
|
|
300
|
+
if messages[-1]["role"] != "user":
|
|
301
|
+
logger.error(f"The last message does not have a `user` role: {messages}")
|
|
302
|
+
raise HTTPException(status_code=400, detail="'messages[-1].role' must be a 'user'")
|
|
303
|
+
|
|
304
|
+
input_message = messages[-1]
|
|
305
|
+
if not isinstance(input_message["content"], str):
|
|
306
|
+
logger.error(f"The input message does not have valid content: {input_message}")
|
|
307
|
+
raise HTTPException(status_code=400, detail="'messages[-1].content' must be a 'string'")
|
|
308
|
+
|
|
309
|
+
return messages
|
letta/server/server.py
CHANGED
|
@@ -336,6 +336,7 @@ class SyncServer(Server):
|
|
|
336
336
|
agent_id: str,
|
|
337
337
|
input_messages: Union[Message, List[Message]],
|
|
338
338
|
interface: Union[AgentInterface, None] = None, # needed to getting responses
|
|
339
|
+
put_inner_thoughts_first: bool = True,
|
|
339
340
|
# timestamp: Optional[datetime],
|
|
340
341
|
) -> LettaUsageStatistics:
|
|
341
342
|
"""Send the input message through the agent"""
|
|
@@ -368,6 +369,7 @@ class SyncServer(Server):
|
|
|
368
369
|
stream=token_streaming,
|
|
369
370
|
skip_verify=True,
|
|
370
371
|
metadata=metadata,
|
|
372
|
+
put_inner_thoughts_first=put_inner_thoughts_first,
|
|
371
373
|
)
|
|
372
374
|
|
|
373
375
|
except Exception as e:
|
|
@@ -625,6 +627,7 @@ class SyncServer(Server):
|
|
|
625
627
|
wrap_system_message: bool = True,
|
|
626
628
|
interface: Union[AgentInterface, ChatCompletionsStreamingInterface, None] = None, # needed to getting responses
|
|
627
629
|
metadata: Optional[dict] = None, # Pass through metadata to interface
|
|
630
|
+
put_inner_thoughts_first: bool = True,
|
|
628
631
|
) -> LettaUsageStatistics:
|
|
629
632
|
"""Send a list of messages to the agent
|
|
630
633
|
|
|
@@ -675,7 +678,13 @@ class SyncServer(Server):
|
|
|
675
678
|
interface.metadata = metadata
|
|
676
679
|
|
|
677
680
|
# Run the agent state forward
|
|
678
|
-
return self._step(
|
|
681
|
+
return self._step(
|
|
682
|
+
actor=actor,
|
|
683
|
+
agent_id=agent_id,
|
|
684
|
+
input_messages=message_objects,
|
|
685
|
+
interface=interface,
|
|
686
|
+
put_inner_thoughts_first=put_inner_thoughts_first,
|
|
687
|
+
)
|
|
679
688
|
|
|
680
689
|
# @LockingServer.agent_lock_decorator
|
|
681
690
|
def run_command(self, user_id: str, agent_id: str, command: str) -> LettaUsageStatistics:
|
letta/services/agent_manager.py
CHANGED
|
@@ -11,6 +11,7 @@ from letta.log import get_logger
|
|
|
11
11
|
from letta.orm import Agent as AgentModel
|
|
12
12
|
from letta.orm import AgentPassage, AgentsTags
|
|
13
13
|
from letta.orm import Block as BlockModel
|
|
14
|
+
from letta.orm import Identity as IdentityModel
|
|
14
15
|
from letta.orm import Source as SourceModel
|
|
15
16
|
from letta.orm import SourcePassage, SourcesAgents
|
|
16
17
|
from letta.orm import Tool as ToolModel
|
|
@@ -34,7 +35,6 @@ from letta.schemas.user import User as PydanticUser
|
|
|
34
35
|
from letta.serialize_schemas import SerializedAgentSchema
|
|
35
36
|
from letta.services.block_manager import BlockManager
|
|
36
37
|
from letta.services.helpers.agent_manager_helper import (
|
|
37
|
-
_process_identity,
|
|
38
38
|
_process_relationship,
|
|
39
39
|
_process_tags,
|
|
40
40
|
check_supports_structured_output,
|
|
@@ -138,6 +138,7 @@ class AgentManager:
|
|
|
138
138
|
tool_ids=tool_ids,
|
|
139
139
|
source_ids=agent_create.source_ids or [],
|
|
140
140
|
tags=agent_create.tags or [],
|
|
141
|
+
identity_ids=agent_create.identity_ids or [],
|
|
141
142
|
description=agent_create.description,
|
|
142
143
|
metadata=agent_create.metadata,
|
|
143
144
|
tool_rules=tool_rules,
|
|
@@ -145,7 +146,6 @@ class AgentManager:
|
|
|
145
146
|
project_id=agent_create.project_id,
|
|
146
147
|
template_id=agent_create.template_id,
|
|
147
148
|
base_template_id=agent_create.base_template_id,
|
|
148
|
-
identifier_key=agent_create.identifier_key,
|
|
149
149
|
message_buffer_autoclear=agent_create.message_buffer_autoclear,
|
|
150
150
|
)
|
|
151
151
|
|
|
@@ -203,13 +203,13 @@ class AgentManager:
|
|
|
203
203
|
tool_ids: List[str],
|
|
204
204
|
source_ids: List[str],
|
|
205
205
|
tags: List[str],
|
|
206
|
+
identity_ids: List[str],
|
|
206
207
|
description: Optional[str] = None,
|
|
207
208
|
metadata: Optional[Dict] = None,
|
|
208
209
|
tool_rules: Optional[List[PydanticToolRule]] = None,
|
|
209
210
|
project_id: Optional[str] = None,
|
|
210
211
|
template_id: Optional[str] = None,
|
|
211
212
|
base_template_id: Optional[str] = None,
|
|
212
|
-
identifier_key: Optional[str] = None,
|
|
213
213
|
message_buffer_autoclear: bool = False,
|
|
214
214
|
) -> PydanticAgentState:
|
|
215
215
|
"""Create a new agent."""
|
|
@@ -237,9 +237,7 @@ class AgentManager:
|
|
|
237
237
|
_process_relationship(session, new_agent, "sources", SourceModel, source_ids, replace=True)
|
|
238
238
|
_process_relationship(session, new_agent, "core_memory", BlockModel, block_ids, replace=True)
|
|
239
239
|
_process_tags(new_agent, tags, replace=True)
|
|
240
|
-
|
|
241
|
-
identity = self.identity_manager.get_identity_from_identifier_key(identifier_key)
|
|
242
|
-
_process_identity(new_agent, identifier_key, identity)
|
|
240
|
+
_process_relationship(session, new_agent, "identities", IdentityModel, identity_ids, replace=True)
|
|
243
241
|
|
|
244
242
|
new_agent.create(session, actor=actor)
|
|
245
243
|
|
|
@@ -313,9 +311,8 @@ class AgentManager:
|
|
|
313
311
|
_process_relationship(session, agent, "core_memory", BlockModel, agent_update.block_ids, replace=True)
|
|
314
312
|
if agent_update.tags is not None:
|
|
315
313
|
_process_tags(agent, agent_update.tags, replace=True)
|
|
316
|
-
if agent_update.
|
|
317
|
-
|
|
318
|
-
_process_identity(agent, agent_update.identifier_key, identity)
|
|
314
|
+
if agent_update.identity_ids is not None:
|
|
315
|
+
_process_relationship(session, agent, "identities", IdentityModel, agent_update.identity_ids, replace=True)
|
|
319
316
|
|
|
320
317
|
# Commit and refresh the agent
|
|
321
318
|
agent.update(session, actor=actor)
|
|
@@ -333,6 +330,7 @@ class AgentManager:
|
|
|
333
330
|
tags: Optional[List[str]] = None,
|
|
334
331
|
match_all_tags: bool = False,
|
|
335
332
|
query_text: Optional[str] = None,
|
|
333
|
+
identifier_keys: Optional[List[str]] = None,
|
|
336
334
|
**kwargs,
|
|
337
335
|
) -> List[PydanticAgentState]:
|
|
338
336
|
"""
|
|
@@ -348,6 +346,7 @@ class AgentManager:
|
|
|
348
346
|
match_all_tags=match_all_tags,
|
|
349
347
|
organization_id=actor.organization_id if actor else None,
|
|
350
348
|
query_text=query_text,
|
|
349
|
+
identifier_keys=identifier_keys,
|
|
351
350
|
**kwargs,
|
|
352
351
|
)
|
|
353
352
|
|
|
@@ -11,7 +11,6 @@ from letta.orm.errors import NoResultFound
|
|
|
11
11
|
from letta.prompts import gpt_system
|
|
12
12
|
from letta.schemas.agent import AgentState, AgentType
|
|
13
13
|
from letta.schemas.enums import MessageRole
|
|
14
|
-
from letta.schemas.identity import Identity
|
|
15
14
|
from letta.schemas.memory import Memory
|
|
16
15
|
from letta.schemas.message import Message, MessageCreate, TextContent
|
|
17
16
|
from letta.schemas.tool_rule import ToolRule
|
|
@@ -85,20 +84,6 @@ def _process_tags(agent: AgentModel, tags: List[str], replace=True):
|
|
|
85
84
|
agent.tags.extend([tag for tag in new_tags if tag.tag not in existing_tags])
|
|
86
85
|
|
|
87
86
|
|
|
88
|
-
def _process_identity(agent: AgentModel, identifier_key: str, identity: Identity):
|
|
89
|
-
"""
|
|
90
|
-
Handles identity for an agent.
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
agent: The AgentModel instance.
|
|
94
|
-
identifier_key: The identifier key of the identity to set or update.
|
|
95
|
-
identity: The Identity object to set or update.
|
|
96
|
-
"""
|
|
97
|
-
agent.identifier_key = identifier_key
|
|
98
|
-
agent.identity = identity
|
|
99
|
-
agent.identity_id = identity.id
|
|
100
|
-
|
|
101
|
-
|
|
102
87
|
def derive_system_message(agent_type: AgentType, system: Optional[str] = None):
|
|
103
88
|
if system is None:
|
|
104
89
|
# TODO: don't hardcode
|