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
letta/__init__.py
CHANGED
letta/agent.py
CHANGED
|
@@ -322,6 +322,7 @@ class Agent(BaseAgent):
|
|
|
322
322
|
max_delay: float = 10.0, # max delay between retries
|
|
323
323
|
step_count: Optional[int] = None,
|
|
324
324
|
last_function_failed: bool = False,
|
|
325
|
+
put_inner_thoughts_first: bool = True,
|
|
325
326
|
) -> ChatCompletionResponse:
|
|
326
327
|
"""Get response from LLM API with robust retry mechanism."""
|
|
327
328
|
log_telemetry(self.logger, "_get_ai_reply start")
|
|
@@ -367,6 +368,7 @@ class Agent(BaseAgent):
|
|
|
367
368
|
force_tool_call=force_tool_call,
|
|
368
369
|
stream=stream,
|
|
369
370
|
stream_interface=self.interface,
|
|
371
|
+
put_inner_thoughts_first=put_inner_thoughts_first,
|
|
370
372
|
)
|
|
371
373
|
log_telemetry(self.logger, "_get_ai_reply create finish")
|
|
372
374
|
|
|
@@ -648,6 +650,7 @@ class Agent(BaseAgent):
|
|
|
648
650
|
# additional args
|
|
649
651
|
chaining: bool = True,
|
|
650
652
|
max_chaining_steps: Optional[int] = None,
|
|
653
|
+
put_inner_thoughts_first: bool = True,
|
|
651
654
|
**kwargs,
|
|
652
655
|
) -> LettaUsageStatistics:
|
|
653
656
|
"""Run Agent.step in a loop, handling chaining via heartbeat requests and function failures"""
|
|
@@ -662,6 +665,7 @@ class Agent(BaseAgent):
|
|
|
662
665
|
kwargs["last_function_failed"] = function_failed
|
|
663
666
|
step_response = self.inner_step(
|
|
664
667
|
messages=next_input_message,
|
|
668
|
+
put_inner_thoughts_first=put_inner_thoughts_first,
|
|
665
669
|
**kwargs,
|
|
666
670
|
)
|
|
667
671
|
|
|
@@ -743,9 +747,9 @@ class Agent(BaseAgent):
|
|
|
743
747
|
metadata: Optional[dict] = None,
|
|
744
748
|
summarize_attempt_count: int = 0,
|
|
745
749
|
last_function_failed: bool = False,
|
|
750
|
+
put_inner_thoughts_first: bool = True,
|
|
746
751
|
) -> AgentStepResponse:
|
|
747
752
|
"""Runs a single step in the agent loop (generates at most one LLM call)"""
|
|
748
|
-
|
|
749
753
|
try:
|
|
750
754
|
|
|
751
755
|
# Extract job_id from metadata if present
|
|
@@ -778,6 +782,7 @@ class Agent(BaseAgent):
|
|
|
778
782
|
stream=stream,
|
|
779
783
|
step_count=step_count,
|
|
780
784
|
last_function_failed=last_function_failed,
|
|
785
|
+
put_inner_thoughts_first=put_inner_thoughts_first,
|
|
781
786
|
)
|
|
782
787
|
if not response:
|
|
783
788
|
# EDGE CASE: Function call failed AND there's no tools left for agent to call -> return early
|
letta/llm_api/helpers.py
CHANGED
|
@@ -202,21 +202,29 @@ def add_inner_thoughts_to_functions(
|
|
|
202
202
|
inner_thoughts_key: str,
|
|
203
203
|
inner_thoughts_description: str,
|
|
204
204
|
inner_thoughts_required: bool = True,
|
|
205
|
+
put_inner_thoughts_first: bool = True,
|
|
205
206
|
) -> List[dict]:
|
|
206
207
|
"""Add an inner_thoughts kwarg to every function in the provided list, ensuring it's the first parameter"""
|
|
207
208
|
new_functions = []
|
|
208
209
|
for function_object in functions:
|
|
209
210
|
new_function_object = copy.deepcopy(function_object)
|
|
210
|
-
|
|
211
|
-
# Create a new OrderedDict with inner_thoughts as the first item
|
|
212
211
|
new_properties = OrderedDict()
|
|
213
|
-
new_properties[inner_thoughts_key] = {
|
|
214
|
-
"type": "string",
|
|
215
|
-
"description": inner_thoughts_description,
|
|
216
|
-
}
|
|
217
212
|
|
|
218
|
-
#
|
|
219
|
-
|
|
213
|
+
# For chat completions, we want inner thoughts to come later
|
|
214
|
+
if put_inner_thoughts_first:
|
|
215
|
+
# Create with inner_thoughts as the first item
|
|
216
|
+
new_properties[inner_thoughts_key] = {
|
|
217
|
+
"type": "string",
|
|
218
|
+
"description": inner_thoughts_description,
|
|
219
|
+
}
|
|
220
|
+
# Add the rest of the properties
|
|
221
|
+
new_properties.update(function_object["parameters"]["properties"])
|
|
222
|
+
else:
|
|
223
|
+
new_properties.update(function_object["parameters"]["properties"])
|
|
224
|
+
new_properties[inner_thoughts_key] = {
|
|
225
|
+
"type": "string",
|
|
226
|
+
"description": inner_thoughts_description,
|
|
227
|
+
}
|
|
220
228
|
|
|
221
229
|
# Cast OrderedDict back to a regular dict
|
|
222
230
|
new_function_object["parameters"]["properties"] = dict(new_properties)
|
|
@@ -225,9 +233,11 @@ def add_inner_thoughts_to_functions(
|
|
|
225
233
|
if inner_thoughts_required:
|
|
226
234
|
required_params = new_function_object["parameters"].get("required", [])
|
|
227
235
|
if inner_thoughts_key not in required_params:
|
|
228
|
-
|
|
236
|
+
if put_inner_thoughts_first:
|
|
237
|
+
required_params.insert(0, inner_thoughts_key)
|
|
238
|
+
else:
|
|
239
|
+
required_params.append(inner_thoughts_key)
|
|
229
240
|
new_function_object["parameters"]["required"] = required_params
|
|
230
|
-
|
|
231
241
|
new_functions.append(new_function_object)
|
|
232
242
|
|
|
233
243
|
return new_functions
|
letta/llm_api/llm_api_tools.py
CHANGED
|
@@ -140,6 +140,7 @@ def create(
|
|
|
140
140
|
stream: bool = False,
|
|
141
141
|
stream_interface: Optional[Union[AgentRefreshStreamingInterface, AgentChunkStreamingInterface]] = None,
|
|
142
142
|
model_settings: Optional[dict] = None, # TODO: eventually pass from server
|
|
143
|
+
put_inner_thoughts_first: bool = True,
|
|
143
144
|
) -> ChatCompletionResponse:
|
|
144
145
|
"""Return response to chat completion with backoff"""
|
|
145
146
|
from letta.utils import printd
|
|
@@ -185,7 +186,9 @@ def create(
|
|
|
185
186
|
else:
|
|
186
187
|
function_call = "required"
|
|
187
188
|
|
|
188
|
-
data = build_openai_chat_completions_request(
|
|
189
|
+
data = build_openai_chat_completions_request(
|
|
190
|
+
llm_config, messages, user_id, functions, function_call, use_tool_naming, put_inner_thoughts_first=put_inner_thoughts_first
|
|
191
|
+
)
|
|
189
192
|
if stream: # Client requested token streaming
|
|
190
193
|
data.stream = True
|
|
191
194
|
assert isinstance(stream_interface, AgentChunkStreamingInterface) or isinstance(
|
letta/llm_api/openai.py
CHANGED
|
@@ -94,6 +94,7 @@ def build_openai_chat_completions_request(
|
|
|
94
94
|
functions: Optional[list],
|
|
95
95
|
function_call: Optional[str],
|
|
96
96
|
use_tool_naming: bool,
|
|
97
|
+
put_inner_thoughts_first: bool = True,
|
|
97
98
|
) -> ChatCompletionRequest:
|
|
98
99
|
if functions and llm_config.put_inner_thoughts_in_kwargs:
|
|
99
100
|
# Special case for LM Studio backend since it needs extra guidance to force out the thoughts first
|
|
@@ -105,6 +106,7 @@ def build_openai_chat_completions_request(
|
|
|
105
106
|
functions=functions,
|
|
106
107
|
inner_thoughts_key=INNER_THOUGHTS_KWARG,
|
|
107
108
|
inner_thoughts_description=inner_thoughts_desc,
|
|
109
|
+
put_inner_thoughts_first=put_inner_thoughts_first,
|
|
108
110
|
)
|
|
109
111
|
|
|
110
112
|
openai_message_list = [
|
|
@@ -390,7 +392,7 @@ def openai_chat_completions_process_stream(
|
|
|
390
392
|
chat_completion_response.usage.completion_tokens = n_chunks
|
|
391
393
|
chat_completion_response.usage.total_tokens = prompt_tokens + n_chunks
|
|
392
394
|
|
|
393
|
-
assert len(chat_completion_response.choices) > 0, chat_completion_response
|
|
395
|
+
assert len(chat_completion_response.choices) > 0, f"No response from provider {chat_completion_response}"
|
|
394
396
|
|
|
395
397
|
# printd(chat_completion_response)
|
|
396
398
|
return chat_completion_response
|
letta/orm/__init__.py
CHANGED
|
@@ -4,6 +4,7 @@ from letta.orm.base import Base
|
|
|
4
4
|
from letta.orm.block import Block
|
|
5
5
|
from letta.orm.blocks_agents import BlocksAgents
|
|
6
6
|
from letta.orm.file import FileMetadata
|
|
7
|
+
from letta.orm.identities_agents import IdentitiesAgents
|
|
7
8
|
from letta.orm.identity import Identity
|
|
8
9
|
from letta.orm.job import Job
|
|
9
10
|
from letta.orm.job_messages import JobMessage
|
letta/orm/agent.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import uuid
|
|
2
2
|
from typing import TYPE_CHECKING, List, Optional
|
|
3
3
|
|
|
4
|
-
from sqlalchemy import JSON, Boolean,
|
|
4
|
+
from sqlalchemy import JSON, Boolean, Index, String
|
|
5
5
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
6
6
|
|
|
7
7
|
from letta.orm.block import Block
|
|
@@ -61,14 +61,6 @@ class Agent(SqlalchemyBase, OrganizationMixin):
|
|
|
61
61
|
template_id: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The id of the template the agent belongs to.")
|
|
62
62
|
base_template_id: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The base template id of the agent.")
|
|
63
63
|
|
|
64
|
-
# Identity
|
|
65
|
-
identity_id: Mapped[Optional[str]] = mapped_column(
|
|
66
|
-
String, ForeignKey("identities.id", ondelete="CASCADE"), nullable=True, doc="The id of the identity the agent belongs to."
|
|
67
|
-
)
|
|
68
|
-
identifier_key: Mapped[Optional[str]] = mapped_column(
|
|
69
|
-
String, nullable=True, doc="The identifier key of the identity the agent belongs to."
|
|
70
|
-
)
|
|
71
|
-
|
|
72
64
|
# Tool rules
|
|
73
65
|
tool_rules: Mapped[Optional[List[ToolRule]]] = mapped_column(ToolRulesColumn, doc="the tool rules for this agent.")
|
|
74
66
|
|
|
@@ -79,7 +71,6 @@ class Agent(SqlalchemyBase, OrganizationMixin):
|
|
|
79
71
|
|
|
80
72
|
# relationships
|
|
81
73
|
organization: Mapped["Organization"] = relationship("Organization", back_populates="agents")
|
|
82
|
-
identity: Mapped["Identity"] = relationship("Identity", back_populates="agents")
|
|
83
74
|
tool_exec_environment_variables: Mapped[List["AgentEnvironmentVariable"]] = relationship(
|
|
84
75
|
"AgentEnvironmentVariable",
|
|
85
76
|
back_populates="agent",
|
|
@@ -130,7 +121,13 @@ class Agent(SqlalchemyBase, OrganizationMixin):
|
|
|
130
121
|
viewonly=True, # Ensures SQLAlchemy doesn't attempt to manage this relationship
|
|
131
122
|
doc="All passages derived created by this agent.",
|
|
132
123
|
)
|
|
133
|
-
|
|
124
|
+
identities: Mapped[List["Identity"]] = relationship(
|
|
125
|
+
"Identity",
|
|
126
|
+
secondary="identities_agents",
|
|
127
|
+
lazy="selectin",
|
|
128
|
+
back_populates="agents",
|
|
129
|
+
passive_deletes=True,
|
|
130
|
+
)
|
|
134
131
|
|
|
135
132
|
def to_pydantic(self) -> PydanticAgentState:
|
|
136
133
|
"""converts to the basic pydantic model counterpart"""
|
|
@@ -160,6 +157,7 @@ class Agent(SqlalchemyBase, OrganizationMixin):
|
|
|
160
157
|
"project_id": self.project_id,
|
|
161
158
|
"template_id": self.template_id,
|
|
162
159
|
"base_template_id": self.base_template_id,
|
|
160
|
+
"identity_ids": [identity.id for identity in self.identities],
|
|
163
161
|
"message_buffer_autoclear": self.message_buffer_autoclear,
|
|
164
162
|
}
|
|
165
163
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from sqlalchemy import ForeignKey, String
|
|
2
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
3
|
+
|
|
4
|
+
from letta.orm.base import Base
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class IdentitiesAgents(Base):
|
|
8
|
+
"""Identities may have one or many agents associated with them."""
|
|
9
|
+
|
|
10
|
+
__tablename__ = "identities_agents"
|
|
11
|
+
|
|
12
|
+
identity_id: Mapped[str] = mapped_column(String, ForeignKey("identities.id", ondelete="CASCADE"), primary_key=True)
|
|
13
|
+
agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id", ondelete="CASCADE"), primary_key=True)
|
letta/orm/identity.py
CHANGED
|
@@ -2,11 +2,13 @@ import uuid
|
|
|
2
2
|
from typing import List, Optional
|
|
3
3
|
|
|
4
4
|
from sqlalchemy import String, UniqueConstraint
|
|
5
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
5
6
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
6
7
|
|
|
7
8
|
from letta.orm.mixins import OrganizationMixin
|
|
8
9
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
9
10
|
from letta.schemas.identity import Identity as PydanticIdentity
|
|
11
|
+
from letta.schemas.identity import IdentityProperty
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class Identity(SqlalchemyBase, OrganizationMixin):
|
|
@@ -14,17 +16,35 @@ class Identity(SqlalchemyBase, OrganizationMixin):
|
|
|
14
16
|
|
|
15
17
|
__tablename__ = "identities"
|
|
16
18
|
__pydantic_model__ = PydanticIdentity
|
|
17
|
-
__table_args__ = (
|
|
19
|
+
__table_args__ = (
|
|
20
|
+
UniqueConstraint(
|
|
21
|
+
"identifier_key",
|
|
22
|
+
"project_id",
|
|
23
|
+
"organization_id",
|
|
24
|
+
name="unique_identifier_without_project",
|
|
25
|
+
postgresql_nulls_not_distinct=True,
|
|
26
|
+
),
|
|
27
|
+
)
|
|
18
28
|
|
|
19
29
|
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"identity-{uuid.uuid4()}")
|
|
20
30
|
identifier_key: Mapped[str] = mapped_column(nullable=False, doc="External, user-generated identifier key of the identity.")
|
|
21
31
|
name: Mapped[str] = mapped_column(nullable=False, doc="The name of the identity.")
|
|
22
32
|
identity_type: Mapped[str] = mapped_column(nullable=False, doc="The type of the identity.")
|
|
23
33
|
project_id: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The project id of the identity.")
|
|
34
|
+
properties: Mapped[List["IdentityProperty"]] = mapped_column(
|
|
35
|
+
JSONB, nullable=False, default=list, doc="List of properties associated with the identity"
|
|
36
|
+
)
|
|
24
37
|
|
|
25
38
|
# relationships
|
|
26
39
|
organization: Mapped["Organization"] = relationship("Organization", back_populates="identities")
|
|
27
|
-
agents: Mapped[List["Agent"]] = relationship(
|
|
40
|
+
agents: Mapped[List["Agent"]] = relationship(
|
|
41
|
+
"Agent", secondary="identities_agents", lazy="selectin", passive_deletes=True, back_populates="identities"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def agent_ids(self) -> List[str]:
|
|
46
|
+
"""Get just the agent IDs without loading the full agent objects"""
|
|
47
|
+
return [agent.id for agent in self.agents]
|
|
28
48
|
|
|
29
49
|
def to_pydantic(self) -> PydanticIdentity:
|
|
30
50
|
state = {
|
|
@@ -33,7 +53,8 @@ class Identity(SqlalchemyBase, OrganizationMixin):
|
|
|
33
53
|
"name": self.name,
|
|
34
54
|
"identity_type": self.identity_type,
|
|
35
55
|
"project_id": self.project_id,
|
|
36
|
-
"
|
|
56
|
+
"agent_ids": self.agent_ids,
|
|
57
|
+
"organization_id": self.organization_id,
|
|
58
|
+
"properties": self.properties,
|
|
37
59
|
}
|
|
38
|
-
|
|
39
|
-
return self.__pydantic_model__(**state)
|
|
60
|
+
return PydanticIdentity(**state)
|
letta/orm/sqlalchemy_base.py
CHANGED
|
@@ -68,6 +68,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
68
68
|
access_type: AccessType = AccessType.ORGANIZATION,
|
|
69
69
|
join_model: Optional[Base] = None,
|
|
70
70
|
join_conditions: Optional[Union[Tuple, List]] = None,
|
|
71
|
+
identifier_keys: Optional[List[str]] = None,
|
|
71
72
|
**kwargs,
|
|
72
73
|
) -> List["SqlalchemyBase"]:
|
|
73
74
|
"""
|
|
@@ -143,6 +144,9 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
143
144
|
# Group by primary key and all necessary columns to avoid JSON comparison
|
|
144
145
|
query = query.group_by(cls.id)
|
|
145
146
|
|
|
147
|
+
if identifier_keys and hasattr(cls, "identities"):
|
|
148
|
+
query = query.join(cls.identities).filter(cls.identities.property.mapper.class_.identifier_key.in_(identifier_keys))
|
|
149
|
+
|
|
146
150
|
# Apply filtering logic from kwargs
|
|
147
151
|
for key, value in kwargs.items():
|
|
148
152
|
if "." in key:
|
letta/schemas/agent.py
CHANGED
|
@@ -83,9 +83,7 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
|
|
|
83
83
|
project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
|
|
84
84
|
template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
|
|
85
85
|
base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
|
|
86
|
-
|
|
87
|
-
# Identity
|
|
88
|
-
identifier_key: Optional[str] = Field(None, description="The identifier key belonging to the identity associated with this agent.")
|
|
86
|
+
identity_ids: List[str] = Field([], description="The ids of the identities associated with this agent.")
|
|
89
87
|
|
|
90
88
|
# An advanced configuration that makes it so this agent does not remember any previous messages
|
|
91
89
|
message_buffer_autoclear: bool = Field(
|
|
@@ -161,7 +159,7 @@ class CreateAgent(BaseModel, validate_assignment=True): #
|
|
|
161
159
|
project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
|
|
162
160
|
template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
|
|
163
161
|
base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
|
|
164
|
-
|
|
162
|
+
identity_ids: Optional[List[str]] = Field(None, description="The ids of the identities associated with this agent.")
|
|
165
163
|
message_buffer_autoclear: bool = Field(
|
|
166
164
|
False,
|
|
167
165
|
description="If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case.",
|
|
@@ -236,7 +234,7 @@ class UpdateAgent(BaseModel):
|
|
|
236
234
|
project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
|
|
237
235
|
template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
|
|
238
236
|
base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
|
|
239
|
-
|
|
237
|
+
identity_ids: Optional[List[str]] = Field(None, description="The ids of the identities associated with this agent.")
|
|
240
238
|
message_buffer_autoclear: Optional[bool] = Field(
|
|
241
239
|
None,
|
|
242
240
|
description="If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case.",
|
letta/schemas/identity.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import List, Optional
|
|
2
|
+
from typing import List, Optional, Union
|
|
3
3
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
|
|
6
|
-
from letta.schemas.agent import AgentState
|
|
7
6
|
from letta.schemas.letta_base import LettaBase
|
|
8
7
|
|
|
9
8
|
|
|
@@ -17,17 +16,38 @@ class IdentityType(str, Enum):
|
|
|
17
16
|
other = "other"
|
|
18
17
|
|
|
19
18
|
|
|
19
|
+
class IdentityPropertyType(str, Enum):
|
|
20
|
+
"""
|
|
21
|
+
Enum to represent the type of the identity property.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
string = "string"
|
|
25
|
+
number = "number"
|
|
26
|
+
boolean = "boolean"
|
|
27
|
+
json = "json"
|
|
28
|
+
|
|
29
|
+
|
|
20
30
|
class IdentityBase(LettaBase):
|
|
21
31
|
__id_prefix__ = "identity"
|
|
22
32
|
|
|
23
33
|
|
|
34
|
+
class IdentityProperty(LettaBase):
|
|
35
|
+
"""A property of an identity"""
|
|
36
|
+
|
|
37
|
+
key: str = Field(..., description="The key of the property")
|
|
38
|
+
value: Union[str, int, float, bool, dict] = Field(..., description="The value of the property")
|
|
39
|
+
type: IdentityPropertyType = Field(..., description="The type of the property")
|
|
40
|
+
|
|
41
|
+
|
|
24
42
|
class Identity(IdentityBase):
|
|
25
43
|
id: str = IdentityBase.generate_id_field()
|
|
26
44
|
identifier_key: str = Field(..., description="External, user-generated identifier key of the identity.")
|
|
27
45
|
name: str = Field(..., description="The name of the identity.")
|
|
28
46
|
identity_type: IdentityType = Field(..., description="The type of the identity.")
|
|
29
47
|
project_id: Optional[str] = Field(None, description="The project id of the identity, if applicable.")
|
|
30
|
-
|
|
48
|
+
agent_ids: List[str] = Field(..., description="The IDs of the agents associated with the identity.")
|
|
49
|
+
organization_id: Optional[str] = Field(None, description="The organization id of the user")
|
|
50
|
+
properties: List[IdentityProperty] = Field(default_factory=list, description="List of properties associated with the identity")
|
|
31
51
|
|
|
32
52
|
|
|
33
53
|
class IdentityCreate(LettaBase):
|
|
@@ -36,9 +56,12 @@ class IdentityCreate(LettaBase):
|
|
|
36
56
|
identity_type: IdentityType = Field(..., description="The type of the identity.")
|
|
37
57
|
project_id: Optional[str] = Field(None, description="The project id of the identity, if applicable.")
|
|
38
58
|
agent_ids: Optional[List[str]] = Field(None, description="The agent ids that are associated with the identity.")
|
|
59
|
+
properties: Optional[List[IdentityProperty]] = Field(None, description="List of properties associated with the identity.")
|
|
39
60
|
|
|
40
61
|
|
|
41
62
|
class IdentityUpdate(LettaBase):
|
|
63
|
+
identifier_key: Optional[str] = Field(None, description="External, user-generated identifier key of the identity.")
|
|
42
64
|
name: Optional[str] = Field(None, description="The name of the identity.")
|
|
43
65
|
identity_type: Optional[IdentityType] = Field(None, description="The type of the identity.")
|
|
44
66
|
agent_ids: Optional[List[str]] = Field(None, description="The agent ids that are associated with the identity.")
|
|
67
|
+
properties: Optional[List[IdentityProperty]] = Field(None, description="List of properties associated with the identity.")
|
|
@@ -56,6 +56,7 @@ class ChatCompletionsStreamingInterface(AgentChunkStreamingInterface):
|
|
|
56
56
|
self.current_function_name = ""
|
|
57
57
|
self.current_function_arguments = []
|
|
58
58
|
self.current_json_parse_result = {}
|
|
59
|
+
self._found_message_tool_kwarg = False
|
|
59
60
|
|
|
60
61
|
# Internal chunk buffer and event for async notification
|
|
61
62
|
self._chunks = deque()
|
|
@@ -153,12 +154,13 @@ class ChatCompletionsStreamingInterface(AgentChunkStreamingInterface):
|
|
|
153
154
|
"""No-op retained for interface compatibility."""
|
|
154
155
|
return
|
|
155
156
|
|
|
156
|
-
def process_chunk(
|
|
157
|
+
def process_chunk(
|
|
158
|
+
self, chunk: ChatCompletionChunkResponse, message_id: str, message_date: datetime, expect_reasoning_content: bool = False
|
|
159
|
+
) -> None:
|
|
157
160
|
"""
|
|
158
161
|
Called externally with a ChatCompletionChunkResponse. Transforms
|
|
159
162
|
it if necessary, then enqueues partial messages for streaming back.
|
|
160
163
|
"""
|
|
161
|
-
# print("RECEIVED CHUNK...")
|
|
162
164
|
processed_chunk = self._process_chunk_to_openai_style(chunk)
|
|
163
165
|
if processed_chunk is not None:
|
|
164
166
|
self._push_to_buffer(processed_chunk)
|
|
@@ -197,6 +199,10 @@ class ChatCompletionsStreamingInterface(AgentChunkStreamingInterface):
|
|
|
197
199
|
content (especially from a 'send_message' tool) is exposed as text
|
|
198
200
|
deltas in 'content'. Otherwise, pass through or yield finish reasons.
|
|
199
201
|
"""
|
|
202
|
+
# If we've already sent the final chunk, ignore everything.
|
|
203
|
+
if self._found_message_tool_kwarg:
|
|
204
|
+
return None
|
|
205
|
+
|
|
200
206
|
choice = chunk.choices[0]
|
|
201
207
|
delta = choice.delta
|
|
202
208
|
|
|
@@ -219,25 +225,43 @@ class ChatCompletionsStreamingInterface(AgentChunkStreamingInterface):
|
|
|
219
225
|
combined_args = "".join(self.current_function_arguments)
|
|
220
226
|
parsed_args = OptimisticJSONParser().parse(combined_args)
|
|
221
227
|
|
|
222
|
-
#
|
|
223
|
-
# This is
|
|
224
|
-
if parsed_args
|
|
225
|
-
self.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
228
|
+
# TODO: Make this less brittle! This depends on `message` coming first!
|
|
229
|
+
# This is a heuristic we use to know if we're done with the `message` part of `send_message`
|
|
230
|
+
if len(parsed_args.keys()) > 1:
|
|
231
|
+
self._found_message_tool_kwarg = True
|
|
232
|
+
return ChatCompletionChunk(
|
|
233
|
+
id=chunk.id,
|
|
234
|
+
object=chunk.object,
|
|
235
|
+
created=chunk.created.timestamp(),
|
|
236
|
+
model=chunk.model,
|
|
237
|
+
choices=[
|
|
238
|
+
Choice(
|
|
239
|
+
index=choice.index,
|
|
240
|
+
delta=ChoiceDelta(),
|
|
241
|
+
finish_reason="stop",
|
|
242
|
+
)
|
|
243
|
+
],
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
# If the parsed result is different
|
|
247
|
+
# This is an edge case we need to consider. E.g. if the last streamed token is '}', we shouldn't stream that out
|
|
248
|
+
if parsed_args != self.current_json_parse_result:
|
|
249
|
+
self.current_json_parse_result = parsed_args
|
|
250
|
+
# If we can see a "message" field, return it as partial content
|
|
251
|
+
if self.assistant_message_tool_kwarg in parsed_args and parsed_args[self.assistant_message_tool_kwarg]:
|
|
252
|
+
return ChatCompletionChunk(
|
|
253
|
+
id=chunk.id,
|
|
254
|
+
object=chunk.object,
|
|
255
|
+
created=chunk.created.timestamp(),
|
|
256
|
+
model=chunk.model,
|
|
257
|
+
choices=[
|
|
258
|
+
Choice(
|
|
259
|
+
index=choice.index,
|
|
260
|
+
delta=ChoiceDelta(content=self.current_function_arguments[-1], role=self.ASSISTANT_STR),
|
|
261
|
+
finish_reason=None,
|
|
262
|
+
)
|
|
263
|
+
],
|
|
264
|
+
)
|
|
241
265
|
|
|
242
266
|
# If there's a finish reason, pass that along
|
|
243
267
|
if choice.finish_reason is not None:
|