letta-nightly 0.6.27.dev20250220104103__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 +19 -2
- 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/helpers.py +20 -10
- letta/llm_api/llm_api_tools.py +85 -2
- letta/llm_api/openai.py +16 -1
- letta/local_llm/chat_completion_proxy.py +15 -2
- letta/local_llm/lmstudio/api.py +75 -1
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +11 -4
- letta/orm/custom_columns.py +31 -110
- letta/orm/identities_agents.py +13 -0
- letta/orm/identity.py +60 -0
- letta/orm/organization.py +2 -0
- letta/orm/sqlalchemy_base.py +4 -0
- letta/schemas/agent.py +11 -1
- letta/schemas/identity.py +67 -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/chat_completions_interface.py +45 -21
- letta/server/rest_api/interface.py +114 -9
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +98 -24
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +14 -3
- letta/server/rest_api/routers/v1/identities.py +121 -0
- letta/server/rest_api/utils.py +183 -4
- letta/server/server.py +23 -117
- letta/services/agent_manager.py +53 -6
- letta/services/block_manager.py +1 -1
- letta/services/identity_manager.py +156 -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/streaming_interface.py +6 -2
- letta/tracing.py +205 -0
- letta/utils.py +4 -0
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/METADATA +9 -2
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/RECORD +66 -52
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/entry_points.txt +0 -0
letta/local_llm/lmstudio/api.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from urllib.parse import urljoin
|
|
2
3
|
|
|
3
4
|
from letta.local_llm.settings.settings import get_completions_settings
|
|
@@ -6,6 +7,73 @@ from letta.utils import count_tokens
|
|
|
6
7
|
|
|
7
8
|
LMSTUDIO_API_CHAT_SUFFIX = "/v1/chat/completions"
|
|
8
9
|
LMSTUDIO_API_COMPLETIONS_SUFFIX = "/v1/completions"
|
|
10
|
+
LMSTUDIO_API_CHAT_COMPLETIONS_SUFFIX = "/v1/chat/completions"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_lmstudio_completion_chatcompletions(endpoint, auth_type, auth_key, model, messages):
|
|
14
|
+
"""
|
|
15
|
+
This is the request we need to send
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
"model": "deepseek-r1-distill-qwen-7b",
|
|
19
|
+
"messages": [
|
|
20
|
+
{ "role": "system", "content": "Always answer in rhymes. Today is Thursday" },
|
|
21
|
+
{ "role": "user", "content": "What day is it today?" },
|
|
22
|
+
{ "role": "user", "content": "What day is it today?" }],
|
|
23
|
+
"temperature": 0.7,
|
|
24
|
+
"max_tokens": -1,
|
|
25
|
+
"stream": false
|
|
26
|
+
"""
|
|
27
|
+
from letta.utils import printd
|
|
28
|
+
|
|
29
|
+
URI = endpoint + LMSTUDIO_API_CHAT_COMPLETIONS_SUFFIX
|
|
30
|
+
request = {"model": model, "messages": messages}
|
|
31
|
+
|
|
32
|
+
response = post_json_auth_request(uri=URI, json_payload=request, auth_type=auth_type, auth_key=auth_key)
|
|
33
|
+
|
|
34
|
+
# Get the reasoning from the model
|
|
35
|
+
if response.status_code == 200:
|
|
36
|
+
result_full = response.json()
|
|
37
|
+
result_reasoning = result_full["choices"][0]["message"].get("reasoning_content")
|
|
38
|
+
result = result_full["choices"][0]["message"]["content"]
|
|
39
|
+
usage = result_full["usage"]
|
|
40
|
+
|
|
41
|
+
# See if result is json
|
|
42
|
+
try:
|
|
43
|
+
function_call = json.loads(result)
|
|
44
|
+
if "function" in function_call and "params" in function_call:
|
|
45
|
+
return result, usage, result_reasoning
|
|
46
|
+
else:
|
|
47
|
+
print("Did not get json on without json constraint, attempting with json decoding")
|
|
48
|
+
except Exception as e:
|
|
49
|
+
print(f"Did not get json on without json constraint, attempting with json decoding: {e}")
|
|
50
|
+
|
|
51
|
+
request["messages"].append({"role": "assistant", "content": result_reasoning})
|
|
52
|
+
request["messages"].append({"role": "user", "content": ""}) # last message must be user
|
|
53
|
+
# Now run with json decoding to get the function
|
|
54
|
+
request["response_format"] = {
|
|
55
|
+
"type": "json_schema",
|
|
56
|
+
"json_schema": {
|
|
57
|
+
"name": "function_call",
|
|
58
|
+
"strict": "true",
|
|
59
|
+
"schema": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {"function": {"type": "string"}, "params": {"type": "object"}},
|
|
62
|
+
"required": ["function", "params"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
response = post_json_auth_request(uri=URI, json_payload=request, auth_type=auth_type, auth_key=auth_key)
|
|
68
|
+
if response.status_code == 200:
|
|
69
|
+
result_full = response.json()
|
|
70
|
+
printd(f"JSON API response:\n{result_full}")
|
|
71
|
+
result = result_full["choices"][0]["message"]["content"]
|
|
72
|
+
# add usage with previous call, merge with prev usage
|
|
73
|
+
for key, value in result_full["usage"].items():
|
|
74
|
+
usage[key] += value
|
|
75
|
+
|
|
76
|
+
return result, usage, result_reasoning
|
|
9
77
|
|
|
10
78
|
|
|
11
79
|
def get_lmstudio_completion(endpoint, auth_type, auth_key, prompt, context_window, api="completions"):
|
|
@@ -24,7 +92,8 @@ def get_lmstudio_completion(endpoint, auth_type, auth_key, prompt, context_windo
|
|
|
24
92
|
# This controls how LM studio handles context overflow
|
|
25
93
|
# In Letta we handle this ourselves, so this should be disabled
|
|
26
94
|
# "context_overflow_policy": 0,
|
|
27
|
-
"lmstudio": {"context_overflow_policy": 0}, # 0 = stop at limit
|
|
95
|
+
# "lmstudio": {"context_overflow_policy": 0}, # 0 = stop at limit
|
|
96
|
+
# "lmstudio": {"context_overflow_policy": "stopAtLimit"}, # https://github.com/letta-ai/letta/issues/1782
|
|
28
97
|
"stream": False,
|
|
29
98
|
"model": "local model",
|
|
30
99
|
}
|
|
@@ -72,6 +141,11 @@ def get_lmstudio_completion(endpoint, auth_type, auth_key, prompt, context_windo
|
|
|
72
141
|
elif api == "completions":
|
|
73
142
|
result = result_full["choices"][0]["text"]
|
|
74
143
|
usage = result_full.get("usage", None)
|
|
144
|
+
elif api == "chat/completions":
|
|
145
|
+
result = result_full["choices"][0]["content"]
|
|
146
|
+
result_full["choices"][0]["reasoning_content"]
|
|
147
|
+
usage = result_full.get("usage", None)
|
|
148
|
+
|
|
75
149
|
else:
|
|
76
150
|
# Example error: msg={"error":"Context length exceeded. Tokens in context: 8000, Context length: 8000"}
|
|
77
151
|
if "context length" in str(response.text).lower():
|
letta/orm/__init__.py
CHANGED
|
@@ -4,6 +4,8 @@ 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
|
|
8
|
+
from letta.orm.identity import Identity
|
|
7
9
|
from letta.orm.job import Job
|
|
8
10
|
from letta.orm.job_messages import JobMessage
|
|
9
11
|
from letta.orm.message import Message
|
letta/orm/agent.py
CHANGED
|
@@ -6,6 +6,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
6
6
|
|
|
7
7
|
from letta.orm.block import Block
|
|
8
8
|
from letta.orm.custom_columns import EmbeddingConfigColumn, LLMConfigColumn, ToolRulesColumn
|
|
9
|
+
from letta.orm.identity import Identity
|
|
9
10
|
from letta.orm.message import Message
|
|
10
11
|
from letta.orm.mixins import OrganizationMixin
|
|
11
12
|
from letta.orm.organization import Organization
|
|
@@ -15,10 +16,11 @@ from letta.schemas.agent import AgentType
|
|
|
15
16
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
16
17
|
from letta.schemas.llm_config import LLMConfig
|
|
17
18
|
from letta.schemas.memory import Memory
|
|
18
|
-
from letta.schemas.tool_rule import
|
|
19
|
+
from letta.schemas.tool_rule import ToolRule
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
21
22
|
from letta.orm.agents_tags import AgentsTags
|
|
23
|
+
from letta.orm.identity import Identity
|
|
22
24
|
from letta.orm.organization import Organization
|
|
23
25
|
from letta.orm.source import Source
|
|
24
26
|
from letta.orm.tool import Tool
|
|
@@ -119,14 +121,18 @@ class Agent(SqlalchemyBase, OrganizationMixin):
|
|
|
119
121
|
viewonly=True, # Ensures SQLAlchemy doesn't attempt to manage this relationship
|
|
120
122
|
doc="All passages derived created by this agent.",
|
|
121
123
|
)
|
|
124
|
+
identities: Mapped[List["Identity"]] = relationship(
|
|
125
|
+
"Identity",
|
|
126
|
+
secondary="identities_agents",
|
|
127
|
+
lazy="selectin",
|
|
128
|
+
back_populates="agents",
|
|
129
|
+
passive_deletes=True,
|
|
130
|
+
)
|
|
122
131
|
|
|
123
132
|
def to_pydantic(self) -> PydanticAgentState:
|
|
124
133
|
"""converts to the basic pydantic model counterpart"""
|
|
125
134
|
# add default rule for having send_message be a terminal tool
|
|
126
135
|
tool_rules = self.tool_rules
|
|
127
|
-
if not tool_rules:
|
|
128
|
-
tool_rules = [TerminalToolRule(tool_name="send_message"), TerminalToolRule(tool_name="send_message_to_agent_async")]
|
|
129
|
-
|
|
130
136
|
state = {
|
|
131
137
|
"id": self.id,
|
|
132
138
|
"organization_id": self.organization_id,
|
|
@@ -151,6 +157,7 @@ class Agent(SqlalchemyBase, OrganizationMixin):
|
|
|
151
157
|
"project_id": self.project_id,
|
|
152
158
|
"template_id": self.template_id,
|
|
153
159
|
"base_template_id": self.base_template_id,
|
|
160
|
+
"identity_ids": [identity.id for identity in self.identities],
|
|
154
161
|
"message_buffer_autoclear": self.message_buffer_autoclear,
|
|
155
162
|
}
|
|
156
163
|
|
letta/orm/custom_columns.py
CHANGED
|
@@ -1,159 +1,80 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
from typing import List, Union
|
|
3
|
-
|
|
4
|
-
import numpy as np
|
|
5
|
-
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
|
|
6
|
-
from openai.types.chat.chat_completion_message_tool_call import Function as OpenAIFunction
|
|
7
1
|
from sqlalchemy import JSON
|
|
8
2
|
from sqlalchemy.types import BINARY, TypeDecorator
|
|
9
3
|
|
|
10
|
-
from letta.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
from letta.helpers.converters import (
|
|
5
|
+
deserialize_embedding_config,
|
|
6
|
+
deserialize_llm_config,
|
|
7
|
+
deserialize_tool_calls,
|
|
8
|
+
deserialize_tool_rules,
|
|
9
|
+
deserialize_vector,
|
|
10
|
+
serialize_embedding_config,
|
|
11
|
+
serialize_llm_config,
|
|
12
|
+
serialize_tool_calls,
|
|
13
|
+
serialize_tool_rules,
|
|
14
|
+
serialize_vector,
|
|
15
|
+
)
|
|
14
16
|
|
|
15
17
|
|
|
16
|
-
class
|
|
17
|
-
"""Custom type for storing
|
|
18
|
+
class LLMConfigColumn(TypeDecorator):
|
|
19
|
+
"""Custom SQLAlchemy column type for storing LLMConfig as JSON."""
|
|
18
20
|
|
|
19
21
|
impl = JSON
|
|
20
22
|
cache_ok = True
|
|
21
23
|
|
|
22
|
-
def load_dialect_impl(self, dialect):
|
|
23
|
-
return dialect.type_descriptor(JSON())
|
|
24
|
-
|
|
25
24
|
def process_bind_param(self, value, dialect):
|
|
26
|
-
|
|
27
|
-
return value.model_dump()
|
|
28
|
-
return value
|
|
25
|
+
return serialize_llm_config(value)
|
|
29
26
|
|
|
30
27
|
def process_result_value(self, value, dialect):
|
|
31
|
-
|
|
32
|
-
return EmbeddingConfig(**value)
|
|
33
|
-
return value
|
|
28
|
+
return deserialize_llm_config(value)
|
|
34
29
|
|
|
35
30
|
|
|
36
|
-
class
|
|
37
|
-
"""Custom type for storing
|
|
31
|
+
class EmbeddingConfigColumn(TypeDecorator):
|
|
32
|
+
"""Custom SQLAlchemy column type for storing EmbeddingConfig as JSON."""
|
|
38
33
|
|
|
39
34
|
impl = JSON
|
|
40
35
|
cache_ok = True
|
|
41
36
|
|
|
42
|
-
def load_dialect_impl(self, dialect):
|
|
43
|
-
return dialect.type_descriptor(JSON())
|
|
44
|
-
|
|
45
37
|
def process_bind_param(self, value, dialect):
|
|
46
|
-
|
|
47
|
-
return value.model_dump()
|
|
48
|
-
return value
|
|
38
|
+
return serialize_embedding_config(value)
|
|
49
39
|
|
|
50
40
|
def process_result_value(self, value, dialect):
|
|
51
|
-
|
|
52
|
-
return LLMConfig(**value)
|
|
53
|
-
return value
|
|
41
|
+
return deserialize_embedding_config(value)
|
|
54
42
|
|
|
55
43
|
|
|
56
44
|
class ToolRulesColumn(TypeDecorator):
|
|
57
|
-
"""Custom type for storing a list of ToolRules as JSON"""
|
|
45
|
+
"""Custom SQLAlchemy column type for storing a list of ToolRules as JSON."""
|
|
58
46
|
|
|
59
47
|
impl = JSON
|
|
60
48
|
cache_ok = True
|
|
61
49
|
|
|
62
|
-
def load_dialect_impl(self, dialect):
|
|
63
|
-
return dialect.type_descriptor(JSON())
|
|
64
|
-
|
|
65
50
|
def process_bind_param(self, value, dialect):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
d["type"] = d["type"].value
|
|
71
|
-
|
|
72
|
-
for d in data:
|
|
73
|
-
assert not (d["type"] == "ToolRule" and "children" not in d), "ToolRule does not have children field"
|
|
74
|
-
return data
|
|
75
|
-
return value
|
|
76
|
-
|
|
77
|
-
def process_result_value(self, value, dialect) -> List[Union[ChildToolRule, InitToolRule, TerminalToolRule]]:
|
|
78
|
-
"""Convert JSON back to a list of ToolRules."""
|
|
79
|
-
if value:
|
|
80
|
-
return [self.deserialize_tool_rule(rule_data) for rule_data in value]
|
|
81
|
-
return value
|
|
82
|
-
|
|
83
|
-
@staticmethod
|
|
84
|
-
def deserialize_tool_rule(data: dict) -> Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule]:
|
|
85
|
-
"""Deserialize a dictionary to the appropriate ToolRule subclass based on the 'type'."""
|
|
86
|
-
rule_type = ToolRuleType(data.get("type")) # Remove 'type' field if it exists since it is a class var
|
|
87
|
-
if rule_type == ToolRuleType.run_first or rule_type == "InitToolRule":
|
|
88
|
-
data["type"] = ToolRuleType.run_first
|
|
89
|
-
return InitToolRule(**data)
|
|
90
|
-
elif rule_type == ToolRuleType.exit_loop or rule_type == "TerminalToolRule":
|
|
91
|
-
data["type"] = ToolRuleType.exit_loop
|
|
92
|
-
return TerminalToolRule(**data)
|
|
93
|
-
elif rule_type == ToolRuleType.constrain_child_tools or rule_type == "ToolRule":
|
|
94
|
-
data["type"] = ToolRuleType.constrain_child_tools
|
|
95
|
-
rule = ChildToolRule(**data)
|
|
96
|
-
return rule
|
|
97
|
-
elif rule_type == ToolRuleType.conditional:
|
|
98
|
-
rule = ConditionalToolRule(**data)
|
|
99
|
-
return rule
|
|
100
|
-
else:
|
|
101
|
-
raise ValueError(f"Unknown tool rule type: {rule_type}")
|
|
51
|
+
return serialize_tool_rules(value)
|
|
52
|
+
|
|
53
|
+
def process_result_value(self, value, dialect):
|
|
54
|
+
return deserialize_tool_rules(value)
|
|
102
55
|
|
|
103
56
|
|
|
104
57
|
class ToolCallColumn(TypeDecorator):
|
|
58
|
+
"""Custom SQLAlchemy column type for storing OpenAI ToolCall objects as JSON."""
|
|
105
59
|
|
|
106
60
|
impl = JSON
|
|
107
61
|
cache_ok = True
|
|
108
62
|
|
|
109
|
-
def load_dialect_impl(self, dialect):
|
|
110
|
-
return dialect.type_descriptor(JSON())
|
|
111
|
-
|
|
112
63
|
def process_bind_param(self, value, dialect):
|
|
113
|
-
|
|
114
|
-
values = []
|
|
115
|
-
for v in value:
|
|
116
|
-
if isinstance(v, OpenAIToolCall):
|
|
117
|
-
values.append(v.model_dump())
|
|
118
|
-
else:
|
|
119
|
-
values.append(v)
|
|
120
|
-
return values
|
|
121
|
-
|
|
122
|
-
return value
|
|
64
|
+
return serialize_tool_calls(value)
|
|
123
65
|
|
|
124
66
|
def process_result_value(self, value, dialect):
|
|
125
|
-
|
|
126
|
-
tools = []
|
|
127
|
-
for tool_value in value:
|
|
128
|
-
if "function" in tool_value:
|
|
129
|
-
tool_call_function = OpenAIFunction(**tool_value["function"])
|
|
130
|
-
del tool_value["function"]
|
|
131
|
-
else:
|
|
132
|
-
tool_call_function = None
|
|
133
|
-
tools.append(OpenAIToolCall(function=tool_call_function, **tool_value))
|
|
134
|
-
return tools
|
|
135
|
-
return value
|
|
67
|
+
return deserialize_tool_calls(value)
|
|
136
68
|
|
|
137
69
|
|
|
138
70
|
class CommonVector(TypeDecorator):
|
|
139
|
-
"""
|
|
71
|
+
"""Custom SQLAlchemy column type for storing vectors in SQLite."""
|
|
140
72
|
|
|
141
73
|
impl = BINARY
|
|
142
74
|
cache_ok = True
|
|
143
75
|
|
|
144
|
-
def load_dialect_impl(self, dialect):
|
|
145
|
-
return dialect.type_descriptor(BINARY())
|
|
146
|
-
|
|
147
76
|
def process_bind_param(self, value, dialect):
|
|
148
|
-
|
|
149
|
-
return value
|
|
150
|
-
if isinstance(value, list):
|
|
151
|
-
value = np.array(value, dtype=np.float32)
|
|
152
|
-
return base64.b64encode(value.tobytes())
|
|
77
|
+
return serialize_vector(value)
|
|
153
78
|
|
|
154
79
|
def process_result_value(self, value, dialect):
|
|
155
|
-
|
|
156
|
-
return value
|
|
157
|
-
if dialect.name == "sqlite":
|
|
158
|
-
value = base64.b64decode(value)
|
|
159
|
-
return np.frombuffer(value, dtype=np.float32)
|
|
80
|
+
return deserialize_vector(value, dialect)
|
|
@@ -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
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import String, UniqueConstraint
|
|
5
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
7
|
+
|
|
8
|
+
from letta.orm.mixins import OrganizationMixin
|
|
9
|
+
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
10
|
+
from letta.schemas.identity import Identity as PydanticIdentity
|
|
11
|
+
from letta.schemas.identity import IdentityProperty
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Identity(SqlalchemyBase, OrganizationMixin):
|
|
15
|
+
"""Identity ORM class"""
|
|
16
|
+
|
|
17
|
+
__tablename__ = "identities"
|
|
18
|
+
__pydantic_model__ = PydanticIdentity
|
|
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
|
+
)
|
|
28
|
+
|
|
29
|
+
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"identity-{uuid.uuid4()}")
|
|
30
|
+
identifier_key: Mapped[str] = mapped_column(nullable=False, doc="External, user-generated identifier key of the identity.")
|
|
31
|
+
name: Mapped[str] = mapped_column(nullable=False, doc="The name of the identity.")
|
|
32
|
+
identity_type: Mapped[str] = mapped_column(nullable=False, doc="The type of the identity.")
|
|
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
|
+
)
|
|
37
|
+
|
|
38
|
+
# relationships
|
|
39
|
+
organization: Mapped["Organization"] = relationship("Organization", back_populates="identities")
|
|
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]
|
|
48
|
+
|
|
49
|
+
def to_pydantic(self) -> PydanticIdentity:
|
|
50
|
+
state = {
|
|
51
|
+
"id": self.id,
|
|
52
|
+
"identifier_key": self.identifier_key,
|
|
53
|
+
"name": self.name,
|
|
54
|
+
"identity_type": self.identity_type,
|
|
55
|
+
"project_id": self.project_id,
|
|
56
|
+
"agent_ids": self.agent_ids,
|
|
57
|
+
"organization_id": self.organization_id,
|
|
58
|
+
"properties": self.properties,
|
|
59
|
+
}
|
|
60
|
+
return PydanticIdentity(**state)
|
letta/orm/organization.py
CHANGED
|
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
|
|
|
9
9
|
|
|
10
10
|
from letta.orm.agent import Agent
|
|
11
11
|
from letta.orm.file import FileMetadata
|
|
12
|
+
from letta.orm.identity import Identity
|
|
12
13
|
from letta.orm.provider import Provider
|
|
13
14
|
from letta.orm.sandbox_config import AgentEnvironmentVariable
|
|
14
15
|
from letta.orm.tool import Tool
|
|
@@ -47,6 +48,7 @@ class Organization(SqlalchemyBase):
|
|
|
47
48
|
)
|
|
48
49
|
agent_passages: Mapped[List["AgentPassage"]] = relationship("AgentPassage", back_populates="organization", cascade="all, delete-orphan")
|
|
49
50
|
providers: Mapped[List["Provider"]] = relationship("Provider", back_populates="organization", cascade="all, delete-orphan")
|
|
51
|
+
identities: Mapped[List["Identity"]] = relationship("Identity", back_populates="organization", cascade="all, delete-orphan")
|
|
50
52
|
|
|
51
53
|
@property
|
|
52
54
|
def passages(self) -> List[Union["SourcePassage", "AgentPassage"]]:
|
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,6 +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
|
+
identity_ids: List[str] = Field([], description="The ids of the identities associated with this agent.")
|
|
86
87
|
|
|
87
88
|
# An advanced configuration that makes it so this agent does not remember any previous messages
|
|
88
89
|
message_buffer_autoclear: bool = Field(
|
|
@@ -129,6 +130,9 @@ class CreateAgent(BaseModel, validate_assignment=True): #
|
|
|
129
130
|
include_multi_agent_tools: bool = Field(
|
|
130
131
|
False, description="If true, attaches the Letta multi-agent tools (e.g. sending a message to another agent)."
|
|
131
132
|
)
|
|
133
|
+
include_base_tool_rules: bool = Field(
|
|
134
|
+
True, description="If true, attaches the Letta base tool rules (e.g. deny all tools not explicitly allowed)."
|
|
135
|
+
)
|
|
132
136
|
description: Optional[str] = Field(None, description="The description of the agent.")
|
|
133
137
|
metadata: Optional[Dict] = Field(None, description="The metadata of the agent.")
|
|
134
138
|
model: Optional[str] = Field(
|
|
@@ -143,7 +147,11 @@ class CreateAgent(BaseModel, validate_assignment=True): #
|
|
|
143
147
|
embedding_chunk_size: Optional[int] = Field(DEFAULT_EMBEDDING_CHUNK_SIZE, description="The embedding chunk size used by the agent.")
|
|
144
148
|
from_template: Optional[str] = Field(None, description="The template id used to configure the agent")
|
|
145
149
|
template: bool = Field(False, description="Whether the agent is a template")
|
|
146
|
-
project: Optional[str] = Field(
|
|
150
|
+
project: Optional[str] = Field(
|
|
151
|
+
None,
|
|
152
|
+
deprecated=True,
|
|
153
|
+
description="Deprecated: Project should now be passed via the X-Project header instead of in the request body. If using the sdk, this can be done via the new x_project field below.",
|
|
154
|
+
)
|
|
147
155
|
tool_exec_environment_variables: Optional[Dict[str, str]] = Field(
|
|
148
156
|
None, description="The environment variables for tool execution specific to this agent."
|
|
149
157
|
)
|
|
@@ -151,6 +159,7 @@ class CreateAgent(BaseModel, validate_assignment=True): #
|
|
|
151
159
|
project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
|
|
152
160
|
template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
|
|
153
161
|
base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
|
|
162
|
+
identity_ids: Optional[List[str]] = Field(None, description="The ids of the identities associated with this agent.")
|
|
154
163
|
message_buffer_autoclear: bool = Field(
|
|
155
164
|
False,
|
|
156
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.",
|
|
@@ -225,6 +234,7 @@ class UpdateAgent(BaseModel):
|
|
|
225
234
|
project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
|
|
226
235
|
template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
|
|
227
236
|
base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
|
|
237
|
+
identity_ids: Optional[List[str]] = Field(None, description="The ids of the identities associated with this agent.")
|
|
228
238
|
message_buffer_autoclear: Optional[bool] = Field(
|
|
229
239
|
None,
|
|
230
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.",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import List, Optional, Union
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
from letta.schemas.letta_base import LettaBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IdentityType(str, Enum):
|
|
10
|
+
"""
|
|
11
|
+
Enum to represent the type of the identity.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
org = "org"
|
|
15
|
+
user = "user"
|
|
16
|
+
other = "other"
|
|
17
|
+
|
|
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
|
+
|
|
30
|
+
class IdentityBase(LettaBase):
|
|
31
|
+
__id_prefix__ = "identity"
|
|
32
|
+
|
|
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
|
+
|
|
42
|
+
class Identity(IdentityBase):
|
|
43
|
+
id: str = IdentityBase.generate_id_field()
|
|
44
|
+
identifier_key: str = Field(..., description="External, user-generated identifier key of the identity.")
|
|
45
|
+
name: str = Field(..., description="The name of the identity.")
|
|
46
|
+
identity_type: IdentityType = Field(..., description="The type of the identity.")
|
|
47
|
+
project_id: Optional[str] = Field(None, description="The project id of the identity, if applicable.")
|
|
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")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class IdentityCreate(LettaBase):
|
|
54
|
+
identifier_key: str = Field(..., description="External, user-generated identifier key of the identity.")
|
|
55
|
+
name: str = Field(..., description="The name of the identity.")
|
|
56
|
+
identity_type: IdentityType = Field(..., description="The type of the identity.")
|
|
57
|
+
project_id: Optional[str] = Field(None, description="The project id of the identity, if applicable.")
|
|
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.")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class IdentityUpdate(LettaBase):
|
|
63
|
+
identifier_key: Optional[str] = Field(None, description="External, user-generated identifier key of the identity.")
|
|
64
|
+
name: Optional[str] = Field(None, description="The name of the identity.")
|
|
65
|
+
identity_type: Optional[IdentityType] = Field(None, description="The type of the identity.")
|
|
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.")
|
letta/schemas/llm_config.py
CHANGED
|
@@ -33,6 +33,7 @@ class LLMConfig(BaseModel):
|
|
|
33
33
|
"webui-legacy",
|
|
34
34
|
"lmstudio",
|
|
35
35
|
"lmstudio-legacy",
|
|
36
|
+
"lmstudio-chatcompletions",
|
|
36
37
|
"llamacpp",
|
|
37
38
|
"koboldcpp",
|
|
38
39
|
"vllm",
|
|
@@ -40,6 +41,7 @@ class LLMConfig(BaseModel):
|
|
|
40
41
|
"mistral",
|
|
41
42
|
"together", # completions endpoint
|
|
42
43
|
"bedrock",
|
|
44
|
+
"deepseek",
|
|
43
45
|
] = Field(..., description="The endpoint type for the model.")
|
|
44
46
|
model_endpoint: Optional[str] = Field(None, description="The endpoint for the model.")
|
|
45
47
|
model_wrapper: Optional[str] = Field(None, description="The wrapper for the model.")
|
letta/schemas/message.py
CHANGED
|
@@ -647,7 +647,7 @@ class Message(BaseMessage):
|
|
|
647
647
|
# role: str ('user' or 'model')
|
|
648
648
|
|
|
649
649
|
if self.role != "tool" and self.name is not None:
|
|
650
|
-
|
|
650
|
+
warnings.warn(f"Using Google AI with non-null 'name' field ({self.name}) not yet supported.")
|
|
651
651
|
|
|
652
652
|
if self.role == "system":
|
|
653
653
|
# NOTE: Gemini API doesn't have a 'system' role, use 'user' instead
|
|
@@ -39,6 +39,7 @@ class Message(BaseModel):
|
|
|
39
39
|
tool_calls: Optional[List[ToolCall]] = None
|
|
40
40
|
role: str
|
|
41
41
|
function_call: Optional[FunctionCall] = None # Deprecated
|
|
42
|
+
reasoning_content: Optional[str] = None # Used in newer reasoning APIs
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
class Choice(BaseModel):
|
|
@@ -115,6 +116,7 @@ class MessageDelta(BaseModel):
|
|
|
115
116
|
"""
|
|
116
117
|
|
|
117
118
|
content: Optional[str] = None
|
|
119
|
+
reasoning_content: Optional[str] = None
|
|
118
120
|
tool_calls: Optional[List[ToolCallDelta]] = None
|
|
119
121
|
role: Optional[str] = None
|
|
120
122
|
function_call: Optional[FunctionCallDelta] = None # Deprecated
|