letta-nightly 0.11.7.dev20251008104128__py3-none-any.whl → 0.12.0.dev20251009104148__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letta/__init__.py +1 -1
- letta/agents/letta_agent_v3.py +33 -5
- letta/database_utils.py +161 -0
- letta/llm_api/anthropic_client.py +1 -0
- letta/llm_api/google_vertex_client.py +1 -0
- letta/orm/__init__.py +1 -0
- letta/orm/run_metrics.py +82 -0
- letta/schemas/message.py +90 -30
- letta/schemas/run_metrics.py +21 -0
- letta/server/db.py +3 -10
- letta/server/rest_api/routers/v1/runs.py +27 -18
- letta/services/context_window_calculator/token_counter.py +1 -1
- letta/services/helpers/run_manager_helper.py +5 -21
- letta/services/run_manager.py +63 -0
- letta/system.py +5 -1
- {letta_nightly-0.11.7.dev20251008104128.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +1 -1
- {letta_nightly-0.11.7.dev20251008104128.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +20 -17
- {letta_nightly-0.11.7.dev20251008104128.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251008104128.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251008104128.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/licenses/LICENSE +0 -0
letta/__init__.py
CHANGED
letta/agents/letta_agent_v3.py
CHANGED
@@ -595,9 +595,30 @@ class LettaAgentV3(LettaAgentV2):
|
|
595
595
|
# -1. no tool call, no content
|
596
596
|
if tool_call is None and (content is None or len(content) == 0):
|
597
597
|
# Edge case is when there's also no content - basically, the LLM "no-op'd"
|
598
|
-
#
|
599
|
-
|
600
|
-
|
598
|
+
# If RequiredBeforeExitToolRule exists and not all required tools have been called,
|
599
|
+
# inject a rule-violation heartbeat to keep looping and inform the model.
|
600
|
+
uncalled = tool_rules_solver.get_uncalled_required_tools(available_tools=set([t.name for t in agent_state.tools]))
|
601
|
+
if uncalled:
|
602
|
+
# TODO: we may need to change this to not have a "heartbeat" prefix for v3?
|
603
|
+
heartbeat_reason = (
|
604
|
+
f"{NON_USER_MSG_PREFIX}ToolRuleViolated: You must call {', '.join(uncalled)} at least once to exit the loop."
|
605
|
+
)
|
606
|
+
from letta.server.rest_api.utils import create_heartbeat_system_message
|
607
|
+
|
608
|
+
heartbeat_msg = create_heartbeat_system_message(
|
609
|
+
agent_id=agent_state.id,
|
610
|
+
model=agent_state.llm_config.model,
|
611
|
+
function_call_success=True,
|
612
|
+
timezone=agent_state.timezone,
|
613
|
+
heartbeat_reason=heartbeat_reason,
|
614
|
+
run_id=run_id,
|
615
|
+
)
|
616
|
+
messages_to_persist = (initial_messages or []) + [heartbeat_msg]
|
617
|
+
continue_stepping, stop_reason = True, None
|
618
|
+
else:
|
619
|
+
# In this case, we actually do not want to persist the no-op message
|
620
|
+
continue_stepping, heartbeat_reason, stop_reason = False, None, LettaStopReason(stop_reason=StopReasonType.end_turn.value)
|
621
|
+
messages_to_persist = initial_messages or []
|
601
622
|
|
602
623
|
# 0. If there's no tool call, we can early exit
|
603
624
|
elif tool_call is None:
|
@@ -627,7 +648,8 @@ class LettaAgentV3(LettaAgentV2):
|
|
627
648
|
run_id=run_id,
|
628
649
|
is_approval_response=is_approval or is_denial,
|
629
650
|
force_set_request_heartbeat=False,
|
630
|
-
|
651
|
+
# If we're continuing due to a required-before-exit rule, include a heartbeat to guide the model
|
652
|
+
add_heartbeat_on_continue=bool(heartbeat_reason),
|
631
653
|
)
|
632
654
|
messages_to_persist = (initial_messages or []) + assistant_message
|
633
655
|
|
@@ -843,7 +865,13 @@ class LettaAgentV3(LettaAgentV2):
|
|
843
865
|
stop_reason: LettaStopReason | None = None
|
844
866
|
|
845
867
|
if tool_call_name is None:
|
846
|
-
# No tool call
|
868
|
+
# No tool call – if there are required-before-exit tools uncalled, keep stepping
|
869
|
+
# and provide explicit feedback to the model; otherwise end the loop.
|
870
|
+
uncalled = tool_rules_solver.get_uncalled_required_tools(available_tools=set([t.name for t in agent_state.tools]))
|
871
|
+
if uncalled and not is_final_step:
|
872
|
+
reason = f"{NON_USER_MSG_PREFIX}ToolRuleViolated: You must call {', '.join(uncalled)} at least once to exit the loop."
|
873
|
+
return True, reason, None
|
874
|
+
# No required tools remaining → end turn
|
847
875
|
return False, None, LettaStopReason(stop_reason=StopReasonType.end_turn.value)
|
848
876
|
else:
|
849
877
|
if tool_rule_violated:
|
letta/database_utils.py
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
"""
|
2
|
+
Database URI utilities for consistent database connection handling across the application.
|
3
|
+
|
4
|
+
This module provides utilities for parsing and converting database URIs to ensure
|
5
|
+
consistent behavior between the main application, alembic migrations, and other
|
6
|
+
database-related components.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Optional
|
10
|
+
from urllib.parse import urlparse, urlunparse
|
11
|
+
|
12
|
+
|
13
|
+
def parse_database_uri(uri: str) -> dict[str, Optional[str]]:
|
14
|
+
"""
|
15
|
+
Parse a database URI into its components.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
uri: Database URI (e.g., postgresql://user:pass@host:port/db)
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
Dictionary with parsed components: scheme, driver, user, password, host, port, database
|
22
|
+
"""
|
23
|
+
parsed = urlparse(uri)
|
24
|
+
|
25
|
+
# Extract driver from scheme (e.g., postgresql+asyncpg -> asyncpg)
|
26
|
+
scheme_parts = parsed.scheme.split("+")
|
27
|
+
base_scheme = scheme_parts[0] if scheme_parts else ""
|
28
|
+
driver = scheme_parts[1] if len(scheme_parts) > 1 else None
|
29
|
+
|
30
|
+
return {
|
31
|
+
"scheme": base_scheme,
|
32
|
+
"driver": driver,
|
33
|
+
"user": parsed.username,
|
34
|
+
"password": parsed.password,
|
35
|
+
"host": parsed.hostname,
|
36
|
+
"port": str(parsed.port) if parsed.port else None,
|
37
|
+
"database": parsed.path.lstrip("/") if parsed.path else None,
|
38
|
+
"query": parsed.query,
|
39
|
+
"fragment": parsed.fragment,
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
def build_database_uri(
|
44
|
+
scheme: str = "postgresql",
|
45
|
+
driver: Optional[str] = None,
|
46
|
+
user: Optional[str] = None,
|
47
|
+
password: Optional[str] = None,
|
48
|
+
host: Optional[str] = None,
|
49
|
+
port: Optional[str] = None,
|
50
|
+
database: Optional[str] = None,
|
51
|
+
query: Optional[str] = None,
|
52
|
+
fragment: Optional[str] = None,
|
53
|
+
) -> str:
|
54
|
+
"""
|
55
|
+
Build a database URI from components.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
scheme: Base scheme (e.g., "postgresql")
|
59
|
+
driver: Driver name (e.g., "asyncpg", "pg8000")
|
60
|
+
user: Username
|
61
|
+
password: Password
|
62
|
+
host: Hostname
|
63
|
+
port: Port number
|
64
|
+
database: Database name
|
65
|
+
query: Query string
|
66
|
+
fragment: Fragment
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
Complete database URI
|
70
|
+
"""
|
71
|
+
# Combine scheme and driver
|
72
|
+
full_scheme = f"{scheme}+{driver}" if driver else scheme
|
73
|
+
|
74
|
+
# Build netloc (user:password@host:port)
|
75
|
+
netloc_parts = []
|
76
|
+
if user:
|
77
|
+
if password:
|
78
|
+
netloc_parts.append(f"{user}:{password}")
|
79
|
+
else:
|
80
|
+
netloc_parts.append(user)
|
81
|
+
|
82
|
+
if host:
|
83
|
+
if port:
|
84
|
+
netloc_parts.append(f"{host}:{port}")
|
85
|
+
else:
|
86
|
+
netloc_parts.append(host)
|
87
|
+
|
88
|
+
netloc = "@".join(netloc_parts) if netloc_parts else ""
|
89
|
+
|
90
|
+
# Build path
|
91
|
+
path = f"/{database}" if database else ""
|
92
|
+
|
93
|
+
# Build the URI
|
94
|
+
return urlunparse((full_scheme, netloc, path, "", query or "", fragment or ""))
|
95
|
+
|
96
|
+
|
97
|
+
def convert_to_async_uri(uri: str) -> str:
|
98
|
+
"""
|
99
|
+
Convert a database URI to use the asyncpg driver for async operations.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
uri: Original database URI
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
URI with asyncpg driver and ssl parameter adjustments
|
106
|
+
"""
|
107
|
+
components = parse_database_uri(uri)
|
108
|
+
|
109
|
+
# Convert to asyncpg driver
|
110
|
+
components["driver"] = "asyncpg"
|
111
|
+
|
112
|
+
# Build the new URI
|
113
|
+
new_uri = build_database_uri(**components)
|
114
|
+
|
115
|
+
# Replace sslmode= with ssl= for asyncpg compatibility
|
116
|
+
new_uri = new_uri.replace("sslmode=", "ssl=")
|
117
|
+
|
118
|
+
return new_uri
|
119
|
+
|
120
|
+
|
121
|
+
def convert_to_sync_uri(uri: str) -> str:
|
122
|
+
"""
|
123
|
+
Convert a database URI to use the pg8000 driver for sync operations (alembic).
|
124
|
+
|
125
|
+
Args:
|
126
|
+
uri: Original database URI
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
URI with pg8000 driver and sslmode parameter adjustments
|
130
|
+
"""
|
131
|
+
components = parse_database_uri(uri)
|
132
|
+
|
133
|
+
# Convert to pg8000 driver
|
134
|
+
components["driver"] = "pg8000"
|
135
|
+
|
136
|
+
# Build the new URI
|
137
|
+
new_uri = build_database_uri(**components)
|
138
|
+
|
139
|
+
# Replace ssl= with sslmode= for pg8000 compatibility
|
140
|
+
new_uri = new_uri.replace("ssl=", "sslmode=")
|
141
|
+
|
142
|
+
return new_uri
|
143
|
+
|
144
|
+
|
145
|
+
def get_database_uri_for_context(uri: str, context: str = "async") -> str:
|
146
|
+
"""
|
147
|
+
Get the appropriate database URI for a specific context.
|
148
|
+
|
149
|
+
Args:
|
150
|
+
uri: Original database URI
|
151
|
+
context: Context type ("async" for asyncpg, "sync" for pg8000, "alembic" for pg8000)
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
URI formatted for the specified context
|
155
|
+
"""
|
156
|
+
if context in ["async"]:
|
157
|
+
return convert_to_async_uri(uri)
|
158
|
+
elif context in ["sync", "alembic"]:
|
159
|
+
return convert_to_sync_uri(uri)
|
160
|
+
else:
|
161
|
+
raise ValueError(f"Unknown context: {context}. Must be 'async', 'sync', or 'alembic'")
|
@@ -325,6 +325,7 @@ class AnthropicClient(LLMClientBase):
|
|
325
325
|
data["system"] = self._add_cache_control_to_system_message(system_content)
|
326
326
|
data["messages"] = PydanticMessage.to_anthropic_dicts_from_list(
|
327
327
|
messages=messages[1:],
|
328
|
+
current_model=llm_config.model,
|
328
329
|
inner_thoughts_xml_tag=inner_thoughts_xml_tag,
|
329
330
|
put_inner_thoughts_in_kwargs=put_kwargs,
|
330
331
|
# if react, use native content + strip heartbeats
|
@@ -311,6 +311,7 @@ class GoogleVertexClient(LLMClientBase):
|
|
311
311
|
contents = self.add_dummy_model_messages(
|
312
312
|
PydanticMessage.to_google_dicts_from_list(
|
313
313
|
messages,
|
314
|
+
current_model=llm_config.model,
|
314
315
|
put_inner_thoughts_in_kwargs=False if agent_type == AgentType.letta_v1_agent else True,
|
315
316
|
native_content=True if agent_type == AgentType.letta_v1_agent else False,
|
316
317
|
),
|
letta/orm/__init__.py
CHANGED
@@ -27,6 +27,7 @@ from letta.orm.prompt import Prompt
|
|
27
27
|
from letta.orm.provider import Provider
|
28
28
|
from letta.orm.provider_trace import ProviderTrace
|
29
29
|
from letta.orm.run import Run
|
30
|
+
from letta.orm.run_metrics import RunMetrics
|
30
31
|
from letta.orm.sandbox_config import AgentEnvironmentVariable, SandboxConfig, SandboxEnvironmentVariable
|
31
32
|
from letta.orm.source import Source
|
32
33
|
from letta.orm.sources_agents import SourcesAgents
|
letta/orm/run_metrics.py
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
from datetime import datetime, timezone
|
2
|
+
from typing import TYPE_CHECKING, Optional
|
3
|
+
|
4
|
+
from sqlalchemy import BigInteger, ForeignKey, Integer, String
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
6
|
+
from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
|
7
|
+
|
8
|
+
from letta.orm.mixins import AgentMixin, OrganizationMixin, ProjectMixin, TemplateMixin
|
9
|
+
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
10
|
+
from letta.schemas.run_metrics import RunMetrics as PydanticRunMetrics
|
11
|
+
from letta.schemas.user import User
|
12
|
+
from letta.settings import DatabaseChoice, settings
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from letta.orm.agent import Agent
|
16
|
+
from letta.orm.run import Run
|
17
|
+
from letta.orm.step import Step
|
18
|
+
|
19
|
+
|
20
|
+
class RunMetrics(SqlalchemyBase, ProjectMixin, AgentMixin, OrganizationMixin, TemplateMixin):
|
21
|
+
"""Tracks performance metrics for agent steps."""
|
22
|
+
|
23
|
+
__tablename__ = "run_metrics"
|
24
|
+
__pydantic_model__ = PydanticRunMetrics
|
25
|
+
|
26
|
+
id: Mapped[str] = mapped_column(
|
27
|
+
ForeignKey("runs.id", ondelete="CASCADE"),
|
28
|
+
primary_key=True,
|
29
|
+
doc="The unique identifier of the run this metric belongs to (also serves as PK)",
|
30
|
+
)
|
31
|
+
run_start_ns: Mapped[Optional[int]] = mapped_column(
|
32
|
+
BigInteger,
|
33
|
+
nullable=True,
|
34
|
+
doc="The timestamp of the start of the run in nanoseconds",
|
35
|
+
)
|
36
|
+
run_ns: Mapped[Optional[int]] = mapped_column(
|
37
|
+
BigInteger,
|
38
|
+
nullable=True,
|
39
|
+
doc="Total time for the run in nanoseconds",
|
40
|
+
)
|
41
|
+
num_steps: Mapped[Optional[int]] = mapped_column(
|
42
|
+
Integer,
|
43
|
+
nullable=True,
|
44
|
+
doc="The number of steps in the run",
|
45
|
+
)
|
46
|
+
run: Mapped[Optional["Run"]] = relationship("Run", foreign_keys=[id])
|
47
|
+
agent: Mapped[Optional["Agent"]] = relationship("Agent")
|
48
|
+
|
49
|
+
def create(
|
50
|
+
self,
|
51
|
+
db_session: Session,
|
52
|
+
actor: Optional[User] = None,
|
53
|
+
no_commit: bool = False,
|
54
|
+
) -> "RunMetrics":
|
55
|
+
"""Override create to handle SQLite timestamp issues"""
|
56
|
+
# For SQLite, explicitly set timestamps as server_default may not work
|
57
|
+
if settings.database_engine == DatabaseChoice.SQLITE:
|
58
|
+
now = datetime.now(timezone.utc)
|
59
|
+
if not self.created_at:
|
60
|
+
self.created_at = now
|
61
|
+
if not self.updated_at:
|
62
|
+
self.updated_at = now
|
63
|
+
|
64
|
+
return super().create(db_session, actor=actor, no_commit=no_commit)
|
65
|
+
|
66
|
+
async def create_async(
|
67
|
+
self,
|
68
|
+
db_session: AsyncSession,
|
69
|
+
actor: Optional[User] = None,
|
70
|
+
no_commit: bool = False,
|
71
|
+
no_refresh: bool = False,
|
72
|
+
) -> "RunMetrics":
|
73
|
+
"""Override create_async to handle SQLite timestamp issues"""
|
74
|
+
# For SQLite, explicitly set timestamps as server_default may not work
|
75
|
+
if settings.database_engine == DatabaseChoice.SQLITE:
|
76
|
+
now = datetime.now(timezone.utc)
|
77
|
+
if not self.created_at:
|
78
|
+
self.created_at = now
|
79
|
+
if not self.updated_at:
|
80
|
+
self.updated_at = now
|
81
|
+
|
82
|
+
return await super().create_async(db_session, actor=actor, no_commit=no_commit, no_refresh=no_refresh)
|
letta/schemas/message.py
CHANGED
@@ -965,7 +965,13 @@ class Message(BaseMessage):
|
|
965
965
|
}
|
966
966
|
|
967
967
|
elif self.role == "assistant" or self.role == "approval":
|
968
|
-
|
968
|
+
try:
|
969
|
+
assert self.tool_calls is not None or text_content is not None, vars(self)
|
970
|
+
except AssertionError as e:
|
971
|
+
# relax check if this message only contains reasoning content
|
972
|
+
if self.content is not None and len(self.content) > 0 and isinstance(self.content[0], ReasoningContent):
|
973
|
+
return None
|
974
|
+
raise e
|
969
975
|
|
970
976
|
# if native content, then put it directly inside the content
|
971
977
|
if native_content:
|
@@ -1040,6 +1046,7 @@ class Message(BaseMessage):
|
|
1040
1046
|
put_inner_thoughts_in_kwargs: bool = False,
|
1041
1047
|
use_developer_message: bool = False,
|
1042
1048
|
) -> List[dict]:
|
1049
|
+
messages = Message.filter_messages_for_llm_api(messages)
|
1043
1050
|
result = [
|
1044
1051
|
m.to_openai_dict(
|
1045
1052
|
max_tool_id_length=max_tool_id_length,
|
@@ -1149,6 +1156,7 @@ class Message(BaseMessage):
|
|
1149
1156
|
messages: List[Message],
|
1150
1157
|
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
1151
1158
|
) -> List[dict]:
|
1159
|
+
messages = Message.filter_messages_for_llm_api(messages)
|
1152
1160
|
result = []
|
1153
1161
|
for message in messages:
|
1154
1162
|
result.extend(message.to_openai_responses_dicts(max_tool_id_length=max_tool_id_length))
|
@@ -1156,6 +1164,7 @@ class Message(BaseMessage):
|
|
1156
1164
|
|
1157
1165
|
def to_anthropic_dict(
|
1158
1166
|
self,
|
1167
|
+
current_model: str,
|
1159
1168
|
inner_thoughts_xml_tag="thinking",
|
1160
1169
|
put_inner_thoughts_in_kwargs: bool = False,
|
1161
1170
|
# if true, then treat the content field as AssistantMessage
|
@@ -1242,20 +1251,22 @@ class Message(BaseMessage):
|
|
1242
1251
|
for content_part in self.content:
|
1243
1252
|
# TextContent, ImageContent, ToolCallContent, ToolReturnContent, ReasoningContent, RedactedReasoningContent, OmittedReasoningContent
|
1244
1253
|
if isinstance(content_part, ReasoningContent):
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1254
|
+
if current_model == self.model:
|
1255
|
+
content.append(
|
1256
|
+
{
|
1257
|
+
"type": "thinking",
|
1258
|
+
"thinking": content_part.reasoning,
|
1259
|
+
"signature": content_part.signature,
|
1260
|
+
}
|
1261
|
+
)
|
1252
1262
|
elif isinstance(content_part, RedactedReasoningContent):
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1263
|
+
if current_model == self.model:
|
1264
|
+
content.append(
|
1265
|
+
{
|
1266
|
+
"type": "redacted_thinking",
|
1267
|
+
"data": content_part.data,
|
1268
|
+
}
|
1269
|
+
)
|
1259
1270
|
elif isinstance(content_part, TextContent):
|
1260
1271
|
content.append(
|
1261
1272
|
{
|
@@ -1272,20 +1283,22 @@ class Message(BaseMessage):
|
|
1272
1283
|
if self.content is not None and len(self.content) >= 1:
|
1273
1284
|
for content_part in self.content:
|
1274
1285
|
if isinstance(content_part, ReasoningContent):
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1286
|
+
if current_model == self.model:
|
1287
|
+
content.append(
|
1288
|
+
{
|
1289
|
+
"type": "thinking",
|
1290
|
+
"thinking": content_part.reasoning,
|
1291
|
+
"signature": content_part.signature,
|
1292
|
+
}
|
1293
|
+
)
|
1282
1294
|
if isinstance(content_part, RedactedReasoningContent):
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1295
|
+
if current_model == self.model:
|
1296
|
+
content.append(
|
1297
|
+
{
|
1298
|
+
"type": "redacted_thinking",
|
1299
|
+
"data": content_part.data,
|
1300
|
+
}
|
1301
|
+
)
|
1289
1302
|
if isinstance(content_part, TextContent):
|
1290
1303
|
content.append(
|
1291
1304
|
{
|
@@ -1349,14 +1362,17 @@ class Message(BaseMessage):
|
|
1349
1362
|
@staticmethod
|
1350
1363
|
def to_anthropic_dicts_from_list(
|
1351
1364
|
messages: List[Message],
|
1365
|
+
current_model: str,
|
1352
1366
|
inner_thoughts_xml_tag: str = "thinking",
|
1353
1367
|
put_inner_thoughts_in_kwargs: bool = False,
|
1354
1368
|
# if true, then treat the content field as AssistantMessage
|
1355
1369
|
native_content: bool = False,
|
1356
1370
|
strip_request_heartbeat: bool = False,
|
1357
1371
|
) -> List[dict]:
|
1372
|
+
messages = Message.filter_messages_for_llm_api(messages)
|
1358
1373
|
result = [
|
1359
1374
|
m.to_anthropic_dict(
|
1375
|
+
current_model=current_model,
|
1360
1376
|
inner_thoughts_xml_tag=inner_thoughts_xml_tag,
|
1361
1377
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
1362
1378
|
native_content=native_content,
|
@@ -1369,6 +1385,7 @@ class Message(BaseMessage):
|
|
1369
1385
|
|
1370
1386
|
def to_google_dict(
|
1371
1387
|
self,
|
1388
|
+
current_model: str,
|
1372
1389
|
put_inner_thoughts_in_kwargs: bool = True,
|
1373
1390
|
# if true, then treat the content field as AssistantMessage
|
1374
1391
|
native_content: bool = False,
|
@@ -1484,11 +1501,12 @@ class Message(BaseMessage):
|
|
1484
1501
|
for content in self.content:
|
1485
1502
|
if isinstance(content, TextContent):
|
1486
1503
|
native_part = {"text": content.text}
|
1487
|
-
if content.signature:
|
1504
|
+
if content.signature and current_model == self.model:
|
1488
1505
|
native_part["thought_signature"] = content.signature
|
1489
1506
|
native_google_content_parts.append(native_part)
|
1490
1507
|
elif isinstance(content, ReasoningContent):
|
1491
|
-
|
1508
|
+
if current_model == self.model:
|
1509
|
+
native_google_content_parts.append({"text": content.reasoning, "thought": True})
|
1492
1510
|
elif isinstance(content, ToolCallContent):
|
1493
1511
|
native_part = {
|
1494
1512
|
"function_call": {
|
@@ -1496,7 +1514,7 @@ class Message(BaseMessage):
|
|
1496
1514
|
"args": content.input,
|
1497
1515
|
},
|
1498
1516
|
}
|
1499
|
-
if content.signature:
|
1517
|
+
if content.signature and current_model == self.model:
|
1500
1518
|
native_part["thought_signature"] = content.signature
|
1501
1519
|
native_google_content_parts.append(native_part)
|
1502
1520
|
else:
|
@@ -1554,11 +1572,14 @@ class Message(BaseMessage):
|
|
1554
1572
|
@staticmethod
|
1555
1573
|
def to_google_dicts_from_list(
|
1556
1574
|
messages: List[Message],
|
1575
|
+
current_model: str,
|
1557
1576
|
put_inner_thoughts_in_kwargs: bool = True,
|
1558
1577
|
native_content: bool = False,
|
1559
1578
|
):
|
1579
|
+
messages = Message.filter_messages_for_llm_api(messages)
|
1560
1580
|
result = [
|
1561
1581
|
m.to_google_dict(
|
1582
|
+
current_model=current_model,
|
1562
1583
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
1563
1584
|
native_content=native_content,
|
1564
1585
|
)
|
@@ -1567,6 +1588,45 @@ class Message(BaseMessage):
|
|
1567
1588
|
result = [m for m in result if m is not None]
|
1568
1589
|
return result
|
1569
1590
|
|
1591
|
+
def is_approval_request(self) -> bool:
|
1592
|
+
return self.role == "approval" and self.tool_calls is not None and len(self.tool_calls) > 0
|
1593
|
+
|
1594
|
+
def is_approval_response(self) -> bool:
|
1595
|
+
return self.role == "approval" and self.tool_calls is None and self.approve is not None
|
1596
|
+
|
1597
|
+
def is_summarization_message(self) -> bool:
|
1598
|
+
return (
|
1599
|
+
self.role == "user"
|
1600
|
+
and self.content is not None
|
1601
|
+
and len(self.content) == 1
|
1602
|
+
and isinstance(self.content[0], TextContent)
|
1603
|
+
and "system_alert" in self.content[0].text
|
1604
|
+
)
|
1605
|
+
|
1606
|
+
@staticmethod
|
1607
|
+
def filter_messages_for_llm_api(
|
1608
|
+
messages: List[Message],
|
1609
|
+
) -> List[Message]:
|
1610
|
+
messages = [m for m in messages if m is not None]
|
1611
|
+
if len(messages) == 0:
|
1612
|
+
return []
|
1613
|
+
# Add special handling for legacy bug where summarization triggers in the middle of hitl
|
1614
|
+
messages_to_filter = []
|
1615
|
+
for i in range(len(messages) - 1):
|
1616
|
+
first_message_is_approval = messages[i].is_approval_request()
|
1617
|
+
second_message_is_summary = messages[i + 1].is_summarization_message()
|
1618
|
+
third_message_is_optional_approval = i + 2 >= len(messages) or messages[i + 2].is_approval_response()
|
1619
|
+
if first_message_is_approval and second_message_is_summary and third_message_is_optional_approval:
|
1620
|
+
messages_to_filter.append(messages[i])
|
1621
|
+
for idx in reversed(messages_to_filter): # reverse to avoid index shift
|
1622
|
+
messages.remove(idx)
|
1623
|
+
|
1624
|
+
# Filter last message if it is a lone approval request without a response - this only occurs for token counting
|
1625
|
+
if messages[-1].role == "approval" and messages[-1].tool_calls is not None and len(messages[-1].tool_calls) > 0:
|
1626
|
+
messages.remove(messages[-1])
|
1627
|
+
|
1628
|
+
return messages
|
1629
|
+
|
1570
1630
|
@staticmethod
|
1571
1631
|
def generate_otid_from_id(message_id: str, index: int) -> str:
|
1572
1632
|
"""
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from pydantic import Field
|
4
|
+
|
5
|
+
from letta.schemas.letta_base import LettaBase
|
6
|
+
|
7
|
+
|
8
|
+
class RunMetricsBase(LettaBase):
|
9
|
+
__id_prefix__ = "run"
|
10
|
+
|
11
|
+
|
12
|
+
class RunMetrics(RunMetricsBase):
|
13
|
+
id: str = Field(..., description="The id of the run this metric belongs to (matches runs.id).")
|
14
|
+
organization_id: Optional[str] = Field(None, description="The unique identifier of the organization.")
|
15
|
+
agent_id: Optional[str] = Field(None, description="The unique identifier of the agent.")
|
16
|
+
project_id: Optional[str] = Field(None, description="The project that the run belongs to (cloud only).")
|
17
|
+
run_start_ns: Optional[int] = Field(None, description="The timestamp of the start of the run in nanoseconds.")
|
18
|
+
run_ns: Optional[int] = Field(None, description="Total time for the run in nanoseconds.")
|
19
|
+
num_steps: Optional[int] = Field(None, description="The number of steps in the run.")
|
20
|
+
template_id: Optional[str] = Field(None, description="The template ID that the run belongs to (cloud only).")
|
21
|
+
base_template_id: Optional[str] = Field(None, description="The base template ID that the run belongs to (cloud only).")
|
letta/server/db.py
CHANGED
@@ -10,18 +10,11 @@ from sqlalchemy.ext.asyncio import (
|
|
10
10
|
create_async_engine,
|
11
11
|
)
|
12
12
|
|
13
|
+
from letta.database_utils import get_database_uri_for_context
|
13
14
|
from letta.settings import settings
|
14
15
|
|
15
|
-
# Convert PostgreSQL URI to async format
|
16
|
-
|
17
|
-
if pg_uri.startswith("postgresql://"):
|
18
|
-
async_pg_uri = pg_uri.replace("postgresql://", "postgresql+asyncpg://")
|
19
|
-
else:
|
20
|
-
# Handle other URI formats (e.g., postgresql+pg8000://)
|
21
|
-
async_pg_uri = f"postgresql+asyncpg://{pg_uri.split('://', 1)[1]}" if "://" in pg_uri else pg_uri
|
22
|
-
|
23
|
-
# Replace sslmode with ssl for asyncpg
|
24
|
-
async_pg_uri = async_pg_uri.replace("sslmode=", "ssl=")
|
16
|
+
# Convert PostgreSQL URI to async format using common utility
|
17
|
+
async_pg_uri = get_database_uri_for_context(settings.letta_pg_uri, "async")
|
25
18
|
|
26
19
|
# Build engine configuration based on settings
|
27
20
|
engine_args = {
|
@@ -13,6 +13,7 @@ from letta.schemas.letta_request import RetrieveStreamRequest
|
|
13
13
|
from letta.schemas.letta_stop_reason import StopReasonType
|
14
14
|
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
15
15
|
from letta.schemas.run import Run
|
16
|
+
from letta.schemas.run_metrics import RunMetrics
|
16
17
|
from letta.schemas.step import Step
|
17
18
|
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
18
19
|
from letta.server.rest_api.redis_stream_manager import redis_sse_stream_generator
|
@@ -224,6 +225,23 @@ async def retrieve_run_usage(
|
|
224
225
|
raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
|
225
226
|
|
226
227
|
|
228
|
+
@router.get("/{run_id}/metrics", response_model=RunMetrics, operation_id="retrieve_metrics_for_run")
|
229
|
+
async def retrieve_metrics_for_run(
|
230
|
+
run_id: str,
|
231
|
+
headers: HeaderParams = Depends(get_headers),
|
232
|
+
server: "SyncServer" = Depends(get_letta_server),
|
233
|
+
):
|
234
|
+
"""
|
235
|
+
Get run metrics by run ID.
|
236
|
+
"""
|
237
|
+
try:
|
238
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
239
|
+
runs_manager = RunManager()
|
240
|
+
return await runs_manager.get_run_metrics_async(run_id=run_id, actor=actor)
|
241
|
+
except NoResultFound:
|
242
|
+
raise HTTPException(status_code=404, detail="Run metrics not found")
|
243
|
+
|
244
|
+
|
227
245
|
@router.get(
|
228
246
|
"/{run_id}/steps",
|
229
247
|
response_model=List[Step],
|
@@ -247,18 +265,14 @@ async def list_run_steps(
|
|
247
265
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
248
266
|
runs_manager = RunManager()
|
249
267
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
)
|
259
|
-
return steps
|
260
|
-
except NoResultFound as e:
|
261
|
-
raise HTTPException(status_code=404, detail=str(e))
|
268
|
+
return await runs_manager.get_run_steps(
|
269
|
+
run_id=run_id,
|
270
|
+
actor=actor,
|
271
|
+
limit=limit,
|
272
|
+
before=before,
|
273
|
+
after=after,
|
274
|
+
ascending=(order == "asc"),
|
275
|
+
)
|
262
276
|
|
263
277
|
|
264
278
|
@router.delete("/{run_id}", response_model=Run, operation_id="delete_run")
|
@@ -272,12 +286,7 @@ async def delete_run(
|
|
272
286
|
"""
|
273
287
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
274
288
|
runs_manager = RunManager()
|
275
|
-
|
276
|
-
try:
|
277
|
-
run = await runs_manager.delete_run_by_id(run_id=run_id, actor=actor)
|
278
|
-
return run
|
279
|
-
except NoResultFound:
|
280
|
-
raise HTTPException(status_code=404, detail="Run not found")
|
289
|
+
return await runs_manager.delete_run_by_id(run_id=run_id, actor=actor)
|
281
290
|
|
282
291
|
|
283
292
|
@router.post(
|
@@ -74,7 +74,7 @@ class AnthropicTokenCounter(TokenCounter):
|
|
74
74
|
return await self.client.count_tokens(model=self.model, tools=tools)
|
75
75
|
|
76
76
|
def convert_messages(self, messages: List[Any]) -> List[Dict[str, Any]]:
|
77
|
-
return Message.to_anthropic_dicts_from_list(messages)
|
77
|
+
return Message.to_anthropic_dicts_from_list(messages, current_model=self.model)
|
78
78
|
|
79
79
|
|
80
80
|
class TiktokenCounter(TokenCounter):
|
@@ -2,14 +2,10 @@ from datetime import datetime
|
|
2
2
|
from typing import Optional
|
3
3
|
|
4
4
|
from sqlalchemy import asc, desc, nulls_last, select
|
5
|
-
from letta.settings import DatabaseChoice, settings
|
6
5
|
|
7
6
|
from letta.orm.run import Run as RunModel
|
8
|
-
from letta.settings import DatabaseChoice, settings
|
9
|
-
from sqlalchemy import asc, desc
|
10
|
-
from typing import Optional
|
11
|
-
|
12
7
|
from letta.services.helpers.agent_manager_helper import _cursor_filter
|
8
|
+
from letta.settings import DatabaseChoice, settings
|
13
9
|
|
14
10
|
|
15
11
|
async def _apply_pagination_async(
|
@@ -29,17 +25,11 @@ async def _apply_pagination_async(
|
|
29
25
|
sort_nulls_last = False
|
30
26
|
|
31
27
|
if after:
|
32
|
-
result = (
|
33
|
-
await session.execute(
|
34
|
-
select(sort_column, RunModel.id).where(RunModel.id == after)
|
35
|
-
)
|
36
|
-
).first()
|
28
|
+
result = (await session.execute(select(sort_column, RunModel.id).where(RunModel.id == after))).first()
|
37
29
|
if result:
|
38
30
|
after_sort_value, after_id = result
|
39
31
|
# SQLite does not support as granular timestamping, so we need to round the timestamp
|
40
|
-
if settings.database_engine is DatabaseChoice.SQLITE and isinstance(
|
41
|
-
after_sort_value, datetime
|
42
|
-
):
|
32
|
+
if settings.database_engine is DatabaseChoice.SQLITE and isinstance(after_sort_value, datetime):
|
43
33
|
after_sort_value = after_sort_value.strftime("%Y-%m-%d %H:%M:%S")
|
44
34
|
query = query.where(
|
45
35
|
_cursor_filter(
|
@@ -53,17 +43,11 @@ async def _apply_pagination_async(
|
|
53
43
|
)
|
54
44
|
|
55
45
|
if before:
|
56
|
-
result = (
|
57
|
-
await session.execute(
|
58
|
-
select(sort_column, RunModel.id).where(RunModel.id == before)
|
59
|
-
)
|
60
|
-
).first()
|
46
|
+
result = (await session.execute(select(sort_column, RunModel.id).where(RunModel.id == before))).first()
|
61
47
|
if result:
|
62
48
|
before_sort_value, before_id = result
|
63
49
|
# SQLite does not support as granular timestamping, so we need to round the timestamp
|
64
|
-
if settings.database_engine is DatabaseChoice.SQLITE and isinstance(
|
65
|
-
before_sort_value, datetime
|
66
|
-
):
|
50
|
+
if settings.database_engine is DatabaseChoice.SQLITE and isinstance(before_sort_value, datetime):
|
67
51
|
before_sort_value = before_sort_value.strftime("%Y-%m-%d %H:%M:%S")
|
68
52
|
query = query.where(
|
69
53
|
_cursor_filter(
|
letta/services/run_manager.py
CHANGED
@@ -8,9 +8,11 @@ from sqlalchemy.orm import Session
|
|
8
8
|
|
9
9
|
from letta.helpers.datetime_helpers import get_utc_time
|
10
10
|
from letta.log import get_logger
|
11
|
+
from letta.orm.agent import Agent as AgentModel
|
11
12
|
from letta.orm.errors import NoResultFound
|
12
13
|
from letta.orm.message import Message as MessageModel
|
13
14
|
from letta.orm.run import Run as RunModel
|
15
|
+
from letta.orm.run_metrics import RunMetrics as RunMetricsModel
|
14
16
|
from letta.orm.sqlalchemy_base import AccessType
|
15
17
|
from letta.orm.step import Step as StepModel
|
16
18
|
from letta.otel.tracing import log_event, trace_method
|
@@ -21,6 +23,7 @@ from letta.schemas.letta_response import LettaResponse
|
|
21
23
|
from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
|
22
24
|
from letta.schemas.message import Message as PydanticMessage
|
23
25
|
from letta.schemas.run import Run as PydanticRun, RunUpdate
|
26
|
+
from letta.schemas.run_metrics import RunMetrics as PydanticRunMetrics
|
24
27
|
from letta.schemas.step import Step as PydanticStep
|
25
28
|
from letta.schemas.usage import LettaUsageStatistics
|
26
29
|
from letta.schemas.user import User as PydanticUser
|
@@ -62,6 +65,23 @@ class RunManager:
|
|
62
65
|
run = RunModel(**run_data)
|
63
66
|
run.organization_id = organization_id
|
64
67
|
run = await run.create_async(session, actor=actor, no_commit=True, no_refresh=True)
|
68
|
+
|
69
|
+
# Create run metrics with start timestamp
|
70
|
+
import time
|
71
|
+
|
72
|
+
# Get the project_id from the agent
|
73
|
+
agent = await session.get(AgentModel, agent_id)
|
74
|
+
project_id = agent.project_id if agent else None
|
75
|
+
|
76
|
+
metrics = RunMetricsModel(
|
77
|
+
id=run.id,
|
78
|
+
organization_id=organization_id,
|
79
|
+
agent_id=agent_id,
|
80
|
+
project_id=project_id,
|
81
|
+
run_start_ns=int(time.time() * 1e9), # Current time in nanoseconds
|
82
|
+
num_steps=0, # Initialize to 0
|
83
|
+
)
|
84
|
+
await metrics.create_async(session)
|
65
85
|
await session.commit()
|
66
86
|
|
67
87
|
return run.to_pydantic()
|
@@ -178,6 +198,21 @@ class RunManager:
|
|
178
198
|
await run.update_async(db_session=session, actor=actor, no_commit=True, no_refresh=True)
|
179
199
|
final_metadata = run.metadata_
|
180
200
|
pydantic_run = run.to_pydantic()
|
201
|
+
|
202
|
+
await session.commit()
|
203
|
+
|
204
|
+
# update run metrics table
|
205
|
+
num_steps = len(await self.step_manager.list_steps_async(run_id=run_id, actor=actor))
|
206
|
+
async with db_registry.async_session() as session:
|
207
|
+
metrics = await RunMetricsModel.read_async(db_session=session, identifier=run_id, actor=actor)
|
208
|
+
# Calculate runtime if run is completing
|
209
|
+
if is_terminal_update and metrics.run_start_ns:
|
210
|
+
import time
|
211
|
+
|
212
|
+
current_ns = int(time.time() * 1e9)
|
213
|
+
metrics.run_ns = current_ns - metrics.run_start_ns
|
214
|
+
metrics.num_steps = num_steps
|
215
|
+
await metrics.update_async(db_session=session, actor=actor, no_commit=True, no_refresh=True)
|
181
216
|
await session.commit()
|
182
217
|
|
183
218
|
# Dispatch callback outside of database session if needed
|
@@ -299,3 +334,31 @@ class RunManager:
|
|
299
334
|
raise NoResultFound(f"Run with id {run_id} not found")
|
300
335
|
pydantic_run = run.to_pydantic()
|
301
336
|
return pydantic_run.request_config
|
337
|
+
|
338
|
+
@enforce_types
|
339
|
+
async def get_run_metrics_async(self, run_id: str, actor: PydanticUser) -> PydanticRunMetrics:
|
340
|
+
"""Get metrics for a run."""
|
341
|
+
async with db_registry.async_session() as session:
|
342
|
+
metrics = await RunMetricsModel.read_async(db_session=session, identifier=run_id, actor=actor)
|
343
|
+
return metrics.to_pydantic()
|
344
|
+
|
345
|
+
@enforce_types
|
346
|
+
async def get_run_steps(
|
347
|
+
self,
|
348
|
+
run_id: str,
|
349
|
+
actor: PydanticUser,
|
350
|
+
limit: Optional[int] = 100,
|
351
|
+
before: Optional[str] = None,
|
352
|
+
after: Optional[str] = None,
|
353
|
+
ascending: bool = False,
|
354
|
+
) -> List[PydanticStep]:
|
355
|
+
"""Get steps for a run."""
|
356
|
+
async with db_registry.async_session() as session:
|
357
|
+
run = await RunModel.read_async(db_session=session, identifier=run_id, actor=actor, access_type=AccessType.ORGANIZATION)
|
358
|
+
if not run:
|
359
|
+
raise NoResultFound(f"Run with id {run_id} not found")
|
360
|
+
|
361
|
+
steps = await self.step_manager.list_steps_async(
|
362
|
+
actor=actor, run_id=run_id, limit=limit, before=before, after=after, order="asc" if ascending else "desc"
|
363
|
+
)
|
364
|
+
return steps
|
letta/system.py
CHANGED
@@ -248,7 +248,11 @@ def unpack_message(packed_message: str) -> str:
|
|
248
248
|
warnings.warn(f"Was unable to find 'message' field in packed message object: '{packed_message}'")
|
249
249
|
return packed_message
|
250
250
|
else:
|
251
|
-
|
251
|
+
try:
|
252
|
+
message_type = message_json["type"]
|
253
|
+
except:
|
254
|
+
return packed_message
|
255
|
+
|
252
256
|
if message_type != "user_message":
|
253
257
|
warnings.warn(f"Expected type to be 'user_message', but was '{message_type}', so not unpacking: '{packed_message}'")
|
254
258
|
return packed_message
|
@@ -1,7 +1,8 @@
|
|
1
|
-
letta/__init__.py,sha256=
|
1
|
+
letta/__init__.py,sha256=YF1Hcr7H49Jrjsp2dkyB-FWzee4pnuGLJnLBERmmsi0,1620
|
2
2
|
letta/agent.py,sha256=xP13DDysFq-h3Ny7DiTN5ZC2txRripMNvPFt8dfFDbE,89498
|
3
3
|
letta/config.py,sha256=JFGY4TWW0Wm5fTbZamOwWqk5G8Nn-TXyhgByGoAqy2c,12375
|
4
4
|
letta/constants.py,sha256=Yxp2DGlVnesnevIQbXqfYadxvVuYYk_6-lsF7YmHixs,16255
|
5
|
+
letta/database_utils.py,sha256=D0lIbOkUQDBd2cJL1bmepyrsD5Rl29ZZLMkwnB7z8o0,4492
|
5
6
|
letta/embeddings.py,sha256=d2o1nOVTaofBk6j-WwsE0_ugvxa1nIOcceqGuJ4w_pc,2045
|
6
7
|
letta/errors.py,sha256=cpZQlm1VaGypu1SWj1-5bqfbW2kwKwdfcSculec5J5M,11534
|
7
8
|
letta/interface.py,sha256=kYOTv1xhNWcoL9qP7cs8hGp9gKBimcBmqIhCUeoil18,13026
|
@@ -12,7 +13,7 @@ letta/pytest.ini,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
13
|
letta/settings.py,sha256=CbSbYJ21q8MSrWJBie0YxXC2ijmPLNr7mojU4DPZuH4,15810
|
13
14
|
letta/streaming_interface.py,sha256=rPMfwUcjqITWk2tVqFQm1hmP99tU2IOHg9gU2dgPSo8,16400
|
14
15
|
letta/streaming_utils.py,sha256=ZRFGFpQqn9ujCEbgZdLM7yTjiuNNvqQ47sNhV8ix-yQ,16553
|
15
|
-
letta/system.py,sha256=
|
16
|
+
letta/system.py,sha256=NClLhwB9Dw20L6IDPpfcnmxXqYQlA5E92xzJ0hGPLo0,8993
|
16
17
|
letta/utils.py,sha256=c108IxvNVf0NeuQRrr2NK-asqjpzOI79YKSHYyhEqrQ,43541
|
17
18
|
letta/adapters/letta_llm_adapter.py,sha256=Uo9l2lUI-n9mymWlK8lTResdWx0mc-F_ttmmKRJKzxo,3527
|
18
19
|
letta/adapters/letta_llm_request_adapter.py,sha256=ApEaMOkvtH5l09NvVgm0r2qaqTnGSqwVqAFA7n5N1HA,4757
|
@@ -30,7 +31,7 @@ letta/agents/helpers.py,sha256=Hz8dKczPxMmDh-ILiUAEHgT55hMgIHO1yV8Vo0PORcw,16837
|
|
30
31
|
letta/agents/letta_agent.py,sha256=4SSx6aORD28B1IogEoWeCTQr4jBaniX9jJolD4pssaQ,98156
|
31
32
|
letta/agents/letta_agent_batch.py,sha256=NizNeIHvFtG4XpZCIplOSF8GohwHvNEkAUvsFBSdtSs,27950
|
32
33
|
letta/agents/letta_agent_v2.py,sha256=MBoXVMIPixozb3xts2Bl8BCbExMIv3vtGbKZK1RitT4,60101
|
33
|
-
letta/agents/letta_agent_v3.py,sha256=
|
34
|
+
letta/agents/letta_agent_v3.py,sha256=i8idcEKVcZxJk67T_g0zLFBcrlHfygcuN9V_xk4Sj0c,48081
|
34
35
|
letta/agents/voice_agent.py,sha256=DP7g7TPFhaIAxmpur9yyJVI8Sb_MnoWfpHm7Q-t7mJI,23177
|
35
36
|
letta/agents/voice_sleeptime_agent.py,sha256=_JzCbWBOKrmo1cTaqZFTrQudpJEapwAyrXYtAHUILGo,8675
|
36
37
|
letta/cli/cli.py,sha256=tKtghlX36Rp0_HbkMosvlAapL07JXhA0vKLGTNKnxSQ,1615
|
@@ -98,13 +99,13 @@ letta/jobs/llm_batch_job_polling.py,sha256=HUCTa1lTOiLAB_8m95RUfeNJa4lxlF8paGdCV
|
|
98
99
|
letta/jobs/scheduler.py,sha256=Ub5VTCA8P5C9Y-0mPK2YIPJSEzKbSd2l5Sp0sOWctD8,8697
|
99
100
|
letta/jobs/types.py,sha256=K8GKEnqEgAT6Kq4F2hUrBC4ZAFM9OkfOjVMStzxKuXQ,742
|
100
101
|
letta/llm_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
101
|
-
letta/llm_api/anthropic_client.py,sha256=
|
102
|
+
letta/llm_api/anthropic_client.py,sha256=xZc2tvqz0_P3oNc0Wd2iWpK8qopQJkOSGnOFyo7v7hU,42204
|
102
103
|
letta/llm_api/azure_client.py,sha256=BeChGsH4brrSgZBbCf8UE5RkW-3ZughpKnsBY2VYxwI,3841
|
103
104
|
letta/llm_api/bedrock_client.py,sha256=xB01zdk1bzzf0ExkPcWcuxuLb1GXaJglvgRMYP2zai4,3572
|
104
105
|
letta/llm_api/deepseek_client.py,sha256=YYja4-LWODE8fAJPSIVGxA7q0hb6-QmWRxWhlboTp64,17144
|
105
106
|
letta/llm_api/google_ai_client.py,sha256=JweTUHZXvK6kcZBGXA7XEU53KP4vM7_zdD7AorCtsdI,8166
|
106
107
|
letta/llm_api/google_constants.py,sha256=eOjOv-FImyJ4b4QGIaod-mEROMtrBFz0yhuYHqOEkwY,797
|
107
|
-
letta/llm_api/google_vertex_client.py,sha256=
|
108
|
+
letta/llm_api/google_vertex_client.py,sha256=__j0OQDjzW1auyzvXKJoWLg3oPDLQgLL79A26K6GT84,39509
|
108
109
|
letta/llm_api/groq_client.py,sha256=C34aWj2fv5A9AG_BKLdz-pXcvPQ6zf38f28EXkTtCjY,3361
|
109
110
|
letta/llm_api/helpers.py,sha256=GXV_SuaU7uSCDj6bxDcCCF7CUjuZQCVWd5qZ3OsHVNk,17587
|
110
111
|
letta/llm_api/llm_api_tools.py,sha256=EZLya93AbRblpnzpz5c1jEUI8n-zIbH7wbBH5LnLd6Q,12674
|
@@ -157,7 +158,7 @@ letta/local_llm/webui/legacy_settings.py,sha256=BLmd3TSx5StnY3ibjwaxYATPt_Lvq-o1
|
|
157
158
|
letta/local_llm/webui/settings.py,sha256=gmLHfiOl1u4JmlAZU2d2O8YKF9lafdakyjwR_ftVPh8,552
|
158
159
|
letta/openai_backcompat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
159
160
|
letta/openai_backcompat/openai_object.py,sha256=GSzeCTwLpLD2fH4X8wVqzwdmoTjKK2I4PnriBY453lc,13505
|
160
|
-
letta/orm/__init__.py,sha256
|
161
|
+
letta/orm/__init__.py,sha256=ZlnnsQu3PSzpHyGmXsBnHZCfRiiszUkND3G69Ma6fr0,1707
|
161
162
|
letta/orm/agent.py,sha256=HS8hbS3uJoH8uezxKQ1vNr0YoXyTKBKhPsQdz4SrVK4,17747
|
162
163
|
letta/orm/agents_tags.py,sha256=-rWR8DoEiHM4yc9vAHgHuvjIwgMXMWzKnTKFlBBu3TQ,1076
|
163
164
|
letta/orm/archive.py,sha256=82engq9ZfNrp1yu8QS5df4vsIwQC33CADZA-05doNko,3920
|
@@ -191,6 +192,7 @@ letta/orm/prompt.py,sha256=NpFPTm3jD8Aewxhlnq8s4eIzANJ3bAEtbq6UmFqyc3U,489
|
|
191
192
|
letta/orm/provider.py,sha256=dBvqXcQ7vMEkbsCQ82NezUB4RTCT3KiyZlkC3Z9ZTPg,1955
|
192
193
|
letta/orm/provider_trace.py,sha256=CJMGz-rLqagJ-yXh9SJRbiGr5nAYdxY524hmiTgDFx4,1153
|
193
194
|
letta/orm/run.py,sha256=eQgqfq-F9W9hHRbnfSVOhrW936J0xiTb5bvaVviTxvM,3820
|
195
|
+
letta/orm/run_metrics.py,sha256=WiWiJgx-eaQxn6ldUkv7aloUUKdB2l8lzTommoDOhCg,3094
|
194
196
|
letta/orm/sandbox_config.py,sha256=FxFhKlbfGwE3d0quNbKXLkVcKSWh0HPmQVShdvktCoo,4497
|
195
197
|
letta/orm/source.py,sha256=tRxqjWM8n1LBhlDTIiKhJ82-tpWErTeiDXMFz1rwY4g,1715
|
196
198
|
letta/orm/sources_agents.py,sha256=Fvo8ujDNeeqleiiqXk-0VUPiQDawLAN-d28RxdvWnbs,554
|
@@ -270,7 +272,7 @@ letta/schemas/llm_config.py,sha256=dzEiIvm1l5xlYF0Q1It-18l9HKdwMd4Yr09A3gWhhNI,1
|
|
270
272
|
letta/schemas/llm_config_overrides.py,sha256=E6qJuVA8TwAAy3VjGitJ5jSQo5PbN-6VPcZOF5qhP9A,1815
|
271
273
|
letta/schemas/mcp.py,sha256=Wiu3FL5qupaHFaMqKFp-w1Ev6ShQ5dPfAtKIMGmRiF8,15527
|
272
274
|
letta/schemas/memory.py,sha256=g2cPd0CF_3atzVkQA8ioIm52oZbsr6Ng-w31qGgNJ_g,20206
|
273
|
-
letta/schemas/message.py,sha256=
|
275
|
+
letta/schemas/message.py,sha256=kXTfYnbFm7uzj2TIy-R3ixrSJGnR6xasdt1wgYJJOvw,74961
|
274
276
|
letta/schemas/npm_requirement.py,sha256=HkvBF7KjHUH-MG-RAEYJHO2MLRS2rxFUcmbpbZVznLk,457
|
275
277
|
letta/schemas/organization.py,sha256=TXrHN4IBQnX-mWvRuCOH57XZSLYCVOY0wWm2_UzDQIA,1279
|
276
278
|
letta/schemas/passage.py,sha256=_bO19zOIQtQ3F3VqDSgIJqh15V0IIrJ_KdlbCt6-4D0,3940
|
@@ -280,6 +282,7 @@ letta/schemas/provider_trace.py,sha256=L-L5gEt9X1wMiI5w1fQ79qvJ1g1Kvo2TobeQC1p9a
|
|
280
282
|
letta/schemas/providers.py,sha256=oHDHP3wUVPe3S2RfefUcsgE2DFCnkMFXx75FE7h0Pzw,69393
|
281
283
|
letta/schemas/response_format.py,sha256=b2onyfSDCxnNkSHd4NsfJg_4ni5qBIK_F6zeJoMvjq0,2131
|
282
284
|
letta/schemas/run.py,sha256=wjpDNsgme51WVr5EJ12Px6qhtOYbw-PMpJP32lche14,3630
|
285
|
+
letta/schemas/run_metrics.py,sha256=xbeObgpP-0guO3RRvEfEK5YNV6G7ol4whx3_ds88mIk,1147
|
283
286
|
letta/schemas/sandbox_config.py,sha256=iw3-QS7PNy649tdynTJUxBbaruprykYAuGO6q28w-gU,5974
|
284
287
|
letta/schemas/secret.py,sha256=1vq33Z-Oe2zSeVnGuj6j04YoO0Y9LxX1bmkprg6C1NA,15659
|
285
288
|
letta/schemas/source.py,sha256=Uxsm8-XA3vuIt5Ihu_l2Aau7quuqmyIDg7qIryklUqY,3565
|
@@ -328,7 +331,7 @@ letta/serialize_schemas/marshmallow_tool.py,sha256=Cq4JQeLBrHdeoi8YhT84rXMXBCtiA
|
|
328
331
|
letta/serialize_schemas/pydantic_agent_schema.py,sha256=CqGqSFzArYE2CzFsIU8LXVmH1A1jYFQpFy7Sj62n_4A,3171
|
329
332
|
letta/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
330
333
|
letta/server/constants.py,sha256=yAdGbLkzlOU_dLTx0lKDmAnj0ZgRXCEaIcPJWO69eaE,92
|
331
|
-
letta/server/db.py,sha256=
|
334
|
+
letta/server/db.py,sha256=bh294tLdOQxOrO9jICDrXa_UXTAYFByu_47tPXlKrjs,3006
|
332
335
|
letta/server/generate_openapi_schema.sh,sha256=14Q6r0fbNNVVdf4X3Z-H0ZtyrVw5zYLzL5Doom3kM9k,351
|
333
336
|
letta/server/server.py,sha256=nEvHxECQJ6I6EC2GQRNzHZpxg5HgFkhCXzqFwB-U9E4,78499
|
334
337
|
letta/server/startup.sh,sha256=z-Fea-7LiuS_aG1tJqS8JAsDQaamwC_kuDhv9D3PPPY,2698
|
@@ -367,7 +370,7 @@ letta/server/rest_api/routers/v1/llms.py,sha256=sv5VWqB0-iSRi6LyzqsM1fLmOFm9UhM9
|
|
367
370
|
letta/server/rest_api/routers/v1/messages.py,sha256=MgfHaTuPGyO1RjtjDuu4fC5dkmXEannUc9XGEK_aFLg,8559
|
368
371
|
letta/server/rest_api/routers/v1/organizations.py,sha256=Un7qRo-69m9bC_TYyMnIRNLXf3fHHbNh1aVghnzzips,2932
|
369
372
|
letta/server/rest_api/routers/v1/providers.py,sha256=_gKcCbEN2tW7c0BO7_cMFEYdhhsxDlKaopqXlZXMU1s,5996
|
370
|
-
letta/server/rest_api/routers/v1/runs.py,sha256=
|
373
|
+
letta/server/rest_api/routers/v1/runs.py,sha256=qdCnp90Mdycu_itmPk78U_gYVbboBcadsCCAAdl4Oe8,14957
|
371
374
|
letta/server/rest_api/routers/v1/sandbox_configs.py,sha256=1x1QOOv-7jB9qf_AiYXH7ITyYMYicAgA4a5Qc8OHwB0,8702
|
372
375
|
letta/server/rest_api/routers/v1/sources.py,sha256=8fkCbprC4PZlcf5HnBYj2-8PjWFIkL0TWZBlp95N7nE,22319
|
373
376
|
letta/server/rest_api/routers/v1/steps.py,sha256=OIExfKSwilCmtrVHhF80h8g3yqhf5ww533FIw7N8noI,8251
|
@@ -404,7 +407,7 @@ letta/services/organization_manager.py,sha256=fHDTtZVCLOeH3_NPCRlDH-a3i2iPYAjSs-
|
|
404
407
|
letta/services/passage_manager.py,sha256=TsdO9VN7oh4xV_oPvi9eyFOIrVA1fT7qGB8taYnf0b8,38419
|
405
408
|
letta/services/per_agent_lock_manager.py,sha256=cMaW8r-qhucQbiK27jVqz8wzhlr2yuRNXbdkaMO4lnk,627
|
406
409
|
letta/services/provider_manager.py,sha256=qSCoN2Qy5SQcsZZOThAE2rAfDjT7zr32omenGSHBYAs,7877
|
407
|
-
letta/services/run_manager.py,sha256=
|
410
|
+
letta/services/run_manager.py,sha256=fWTTEKG0tVEDcKUrR0sEj9ozgZ7ziPFV7LvZO2UdXgk,16292
|
408
411
|
letta/services/sandbox_config_manager.py,sha256=dBDb6fuL7K-Ds6eL3TmcyCDQ7UKlF_nbJ2bixpmxm98,14956
|
409
412
|
letta/services/source_manager.py,sha256=IteOcn9ydoO7KARoPh-JuuYwO4jWcsBoTsrkGWvDk9c,18864
|
410
413
|
letta/services/step_manager.py,sha256=V9LlX5dOMuTVFkoncrFnzcgtn0sokhsOcUNSahiCZS0,22666
|
@@ -414,7 +417,7 @@ letta/services/tool_schema_generator.py,sha256=PA-hOhLi_w9VNbPpC3kt1YWybc2uQfkX5
|
|
414
417
|
letta/services/user_manager.py,sha256=0LgTED-EPUCmQkXm0LPMBV6ONbOFtsXJnHxjoG5YjRU,6159
|
415
418
|
letta/services/context_window_calculator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
416
419
|
letta/services/context_window_calculator/context_window_calculator.py,sha256=Ux1UU_l0Nk08HGUMUUrjbtnm2thVRC9bHE7poo8gb8o,8241
|
417
|
-
letta/services/context_window_calculator/token_counter.py,sha256=
|
420
|
+
letta/services/context_window_calculator/token_counter.py,sha256=cdSoF8muJmBezE9gf8kL-LG6QOeP5mlaPAp2xr2Upbg,4841
|
418
421
|
letta/services/file_processor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
419
422
|
letta/services/file_processor/file_processor.py,sha256=VkQDtb79Xlj4bWLbGGG53ZPwaDf55w7t50WhlwNYvfI,15923
|
420
423
|
letta/services/file_processor/file_types.py,sha256=kHe_EWK2k8y_5glVkCkkdZy1BLALyBvtlpZ9V-kiqHE,13070
|
@@ -432,7 +435,7 @@ letta/services/file_processor/parser/base_parser.py,sha256=WfnXP6fL-xQz4eIHEWa6-
|
|
432
435
|
letta/services/file_processor/parser/markitdown_parser.py,sha256=BpCM82ocDKbNTKhb2Zu3ffYUXR5fqudiUiwxmmUePg4,3715
|
433
436
|
letta/services/file_processor/parser/mistral_parser.py,sha256=NmCdVdpAB5f-VjILJp85pz2rSjlghKEg7qKTFzZLhP8,2384
|
434
437
|
letta/services/helpers/agent_manager_helper.py,sha256=FGOcaFkuOnTadaHFQ2H3wi3w5MVPNMgT32E99mSrKkg,51126
|
435
|
-
letta/services/helpers/run_manager_helper.py,sha256=
|
438
|
+
letta/services/helpers/run_manager_helper.py,sha256=3hzxJ2gA1QEVSP-IhNf6NyTxoXogI8bqPGgqTTNpyRk,2599
|
436
439
|
letta/services/helpers/tool_execution_helper.py,sha256=45L7woJ98jK5MQAnhE_4NZdCeyOOzC4328FTQPM7iTA,9159
|
437
440
|
letta/services/helpers/tool_parser_helper.py,sha256=gJ-XwvqIgVPlnPVbseHL0YPfTUtk6svqC43-U4VcM5k,4467
|
438
441
|
letta/services/lettuce/__init__.py,sha256=UU0Jb58TV72EW204f8U2rru_Q9HlpaxqhECDF3t5N98,151
|
@@ -473,8 +476,8 @@ letta/templates/sandbox_code_file.py.j2,sha256=eXga5J_04Z8-pGdwfOCDjcRnMceIqcF5i
|
|
473
476
|
letta/templates/sandbox_code_file_async.py.j2,sha256=lb7nh_P2W9VZHzU_9TxSCEMUod7SDziPXgvT75xVds0,2748
|
474
477
|
letta/templates/summary_request_text.j2,sha256=ZttQwXonW2lk4pJLYzLK0pmo4EO4EtUUIXjgXKiizuc,842
|
475
478
|
letta/types/__init__.py,sha256=hokKjCVFGEfR7SLMrtZsRsBfsC7yTIbgKPLdGg4K1eY,147
|
476
|
-
letta_nightly-0.
|
477
|
-
letta_nightly-0.
|
478
|
-
letta_nightly-0.
|
479
|
-
letta_nightly-0.
|
480
|
-
letta_nightly-0.
|
479
|
+
letta_nightly-0.12.0.dev20251009104148.dist-info/METADATA,sha256=-xts5F15qFLK5pjWrucujMu7g3rWG_qDh3uG9DQuzjE,24468
|
480
|
+
letta_nightly-0.12.0.dev20251009104148.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
481
|
+
letta_nightly-0.12.0.dev20251009104148.dist-info/entry_points.txt,sha256=m-94Paj-kxiR6Ktu0us0_2qfhn29DzF2oVzqBE6cu8w,41
|
482
|
+
letta_nightly-0.12.0.dev20251009104148.dist-info/licenses/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
|
483
|
+
letta_nightly-0.12.0.dev20251009104148.dist-info/RECORD,,
|
File without changes
|
File without changes
|