letta-nightly 0.8.0.dev20250606195656__py3-none-any.whl → 0.8.3.dev20250607000559__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/agent.py +16 -12
- letta/agents/base_agent.py +1 -1
- letta/agents/helpers.py +13 -2
- letta/agents/letta_agent.py +72 -34
- letta/agents/letta_agent_batch.py +1 -2
- letta/agents/voice_agent.py +19 -13
- letta/agents/voice_sleeptime_agent.py +23 -6
- letta/constants.py +18 -0
- letta/data_sources/__init__.py +0 -0
- letta/data_sources/redis_client.py +282 -0
- letta/errors.py +0 -4
- letta/functions/function_sets/files.py +58 -0
- letta/functions/schema_generator.py +18 -1
- letta/groups/sleeptime_multi_agent_v2.py +13 -3
- letta/helpers/datetime_helpers.py +47 -3
- letta/helpers/decorators.py +69 -0
- letta/{services/helpers/noop_helper.py → helpers/singleton.py} +5 -0
- letta/interfaces/anthropic_streaming_interface.py +43 -24
- letta/interfaces/openai_streaming_interface.py +21 -19
- letta/llm_api/anthropic.py +1 -1
- letta/llm_api/anthropic_client.py +30 -16
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +36 -30
- letta/llm_api/llm_api_tools.py +1 -1
- letta/llm_api/llm_client_base.py +29 -1
- letta/llm_api/openai.py +1 -1
- letta/llm_api/openai_client.py +6 -8
- letta/local_llm/chat_completion_proxy.py +1 -1
- letta/memory.py +1 -1
- letta/orm/enums.py +1 -0
- letta/orm/file.py +80 -3
- letta/orm/files_agents.py +13 -0
- letta/orm/passage.py +2 -0
- letta/orm/sqlalchemy_base.py +34 -11
- letta/otel/__init__.py +0 -0
- letta/otel/context.py +25 -0
- letta/otel/events.py +0 -0
- letta/otel/metric_registry.py +122 -0
- letta/otel/metrics.py +66 -0
- letta/otel/resource.py +26 -0
- letta/{tracing.py → otel/tracing.py} +55 -78
- letta/plugins/README.md +22 -0
- letta/plugins/__init__.py +0 -0
- letta/plugins/defaults.py +11 -0
- letta/plugins/plugins.py +72 -0
- letta/schemas/enums.py +8 -0
- letta/schemas/file.py +12 -0
- letta/schemas/letta_request.py +6 -0
- letta/schemas/passage.py +1 -0
- letta/schemas/tool.py +4 -0
- letta/server/db.py +7 -7
- letta/server/rest_api/app.py +8 -6
- letta/server/rest_api/routers/v1/agents.py +46 -37
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/sources.py +26 -3
- letta/server/rest_api/routers/v1/tools.py +7 -2
- letta/server/rest_api/utils.py +9 -6
- letta/server/server.py +25 -13
- letta/services/agent_manager.py +186 -194
- letta/services/block_manager.py +1 -1
- letta/services/context_window_calculator/context_window_calculator.py +1 -1
- letta/services/context_window_calculator/token_counter.py +3 -2
- letta/services/file_processor/chunker/line_chunker.py +34 -0
- letta/services/file_processor/file_processor.py +43 -12
- letta/services/file_processor/parser/mistral_parser.py +11 -1
- letta/services/files_agents_manager.py +96 -7
- letta/services/group_manager.py +6 -6
- letta/services/helpers/agent_manager_helper.py +404 -3
- letta/services/identity_manager.py +1 -1
- letta/services/job_manager.py +1 -1
- letta/services/llm_batch_manager.py +1 -1
- letta/services/mcp/stdio_client.py +5 -1
- letta/services/mcp_manager.py +4 -4
- letta/services/message_manager.py +1 -1
- letta/services/organization_manager.py +1 -1
- letta/services/passage_manager.py +604 -19
- letta/services/per_agent_lock_manager.py +1 -1
- letta/services/provider_manager.py +1 -1
- letta/services/sandbox_config_manager.py +1 -1
- letta/services/source_manager.py +178 -19
- letta/services/step_manager.py +2 -2
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/telemetry_manager.py +1 -1
- letta/services/tool_executor/builtin_tool_executor.py +117 -0
- letta/services/tool_executor/composio_tool_executor.py +53 -0
- letta/services/tool_executor/core_tool_executor.py +474 -0
- letta/services/tool_executor/files_tool_executor.py +138 -0
- letta/services/tool_executor/mcp_tool_executor.py +45 -0
- letta/services/tool_executor/multi_agent_tool_executor.py +123 -0
- letta/services/tool_executor/tool_execution_manager.py +34 -14
- letta/services/tool_executor/tool_execution_sandbox.py +1 -1
- letta/services/tool_executor/tool_executor.py +3 -802
- letta/services/tool_executor/tool_executor_base.py +43 -0
- letta/services/tool_manager.py +55 -59
- letta/services/tool_sandbox/e2b_sandbox.py +1 -1
- letta/services/tool_sandbox/local_sandbox.py +6 -3
- letta/services/user_manager.py +6 -3
- letta/settings.py +23 -2
- letta/utils.py +7 -2
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/METADATA +4 -2
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/RECORD +105 -83
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/entry_points.txt +0 -0
letta/services/agent_manager.py
CHANGED
@@ -3,9 +3,8 @@ import os
|
|
3
3
|
from datetime import datetime, timezone
|
4
4
|
from typing import Dict, List, Optional, Set, Tuple
|
5
5
|
|
6
|
-
import numpy as np
|
7
6
|
import sqlalchemy as sa
|
8
|
-
from sqlalchemy import
|
7
|
+
from sqlalchemy import delete, func, insert, literal, or_, select
|
9
8
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
10
9
|
|
11
10
|
from letta.constants import (
|
@@ -17,10 +16,9 @@ from letta.constants import (
|
|
17
16
|
BASE_VOICE_SLEEPTIME_CHAT_TOOLS,
|
18
17
|
BASE_VOICE_SLEEPTIME_TOOLS,
|
19
18
|
DATA_SOURCE_ATTACH_ALERT,
|
20
|
-
|
19
|
+
FILES_TOOLS,
|
21
20
|
MULTI_AGENT_TOOLS,
|
22
21
|
)
|
23
|
-
from letta.embeddings import embedding_model
|
24
22
|
from letta.helpers.datetime_helpers import get_utc_time
|
25
23
|
from letta.llm_api.llm_client import LLMClient
|
26
24
|
from letta.log import get_logger
|
@@ -39,7 +37,7 @@ from letta.orm.errors import NoResultFound
|
|
39
37
|
from letta.orm.sandbox_config import AgentEnvironmentVariable
|
40
38
|
from letta.orm.sandbox_config import AgentEnvironmentVariable as AgentEnvironmentVariableModel
|
41
39
|
from letta.orm.sqlalchemy_base import AccessType
|
42
|
-
from letta.
|
40
|
+
from letta.otel.tracing import trace_method
|
43
41
|
from letta.schemas.agent import AgentState as PydanticAgentState
|
44
42
|
from letta.schemas.agent import AgentType, CreateAgent, UpdateAgent, get_prompt_template_for_agent_type
|
45
43
|
from letta.schemas.block import DEFAULT_BLOCKS
|
@@ -66,6 +64,7 @@ from letta.server.db import db_registry
|
|
66
64
|
from letta.services.block_manager import BlockManager
|
67
65
|
from letta.services.context_window_calculator.context_window_calculator import ContextWindowCalculator
|
68
66
|
from letta.services.context_window_calculator.token_counter import AnthropicTokenCounter, TiktokenCounter
|
67
|
+
from letta.services.files_agents_manager import FileAgentManager
|
69
68
|
from letta.services.helpers.agent_manager_helper import (
|
70
69
|
_apply_filters,
|
71
70
|
_apply_identity_filters,
|
@@ -74,6 +73,9 @@ from letta.services.helpers.agent_manager_helper import (
|
|
74
73
|
_apply_tag_filter,
|
75
74
|
_process_relationship,
|
76
75
|
_process_relationship_async,
|
76
|
+
build_agent_passage_query,
|
77
|
+
build_passage_query,
|
78
|
+
build_source_passage_query,
|
77
79
|
check_supports_structured_output,
|
78
80
|
compile_system_message,
|
79
81
|
derive_system_message,
|
@@ -85,8 +87,6 @@ from letta.services.message_manager import MessageManager
|
|
85
87
|
from letta.services.passage_manager import PassageManager
|
86
88
|
from letta.services.source_manager import SourceManager
|
87
89
|
from letta.services.tool_manager import ToolManager
|
88
|
-
from letta.settings import settings
|
89
|
-
from letta.tracing import trace_method
|
90
90
|
from letta.utils import enforce_types, united_diff
|
91
91
|
|
92
92
|
logger = get_logger(__name__)
|
@@ -102,6 +102,7 @@ class AgentManager:
|
|
102
102
|
self.message_manager = MessageManager()
|
103
103
|
self.passage_manager = PassageManager()
|
104
104
|
self.identity_manager = IdentityManager()
|
105
|
+
self.file_agent_manager = FileAgentManager()
|
105
106
|
|
106
107
|
@staticmethod
|
107
108
|
def _resolve_tools(session, names: Set[str], ids: Set[str], org_id: str) -> Tuple[Dict[str, str], Dict[str, str]]:
|
@@ -1384,6 +1385,11 @@ class AgentManager:
|
|
1384
1385
|
curr_system_message = self.get_system_message(
|
1385
1386
|
agent_id=agent_id, actor=actor
|
1386
1387
|
) # this is the system + memory bank, not just the system prompt
|
1388
|
+
|
1389
|
+
if curr_system_message is None:
|
1390
|
+
logger.warning(f"No system message found for agent {agent_state.id} and user {actor}")
|
1391
|
+
return agent_state
|
1392
|
+
|
1387
1393
|
curr_system_message_openai = curr_system_message.to_openai_dict()
|
1388
1394
|
|
1389
1395
|
# note: we only update the system prompt if the core memory is changed
|
@@ -1451,6 +1457,11 @@ class AgentManager:
|
|
1451
1457
|
curr_system_message = await self.get_system_message_async(
|
1452
1458
|
agent_id=agent_id, actor=actor
|
1453
1459
|
) # this is the system + memory bank, not just the system prompt
|
1460
|
+
|
1461
|
+
if curr_system_message is None:
|
1462
|
+
logger.warning(f"No system message found for agent {agent_state.id} and user {actor}")
|
1463
|
+
return agent_state
|
1464
|
+
|
1454
1465
|
curr_system_message_openai = curr_system_message.to_openai_dict()
|
1455
1466
|
|
1456
1467
|
# note: we only update the system prompt if the core memory is changed
|
@@ -1472,7 +1483,7 @@ class AgentManager:
|
|
1472
1483
|
memory_edit_timestamp = curr_system_message.created_at
|
1473
1484
|
|
1474
1485
|
num_messages = await self.message_manager.size_async(actor=actor, agent_id=agent_id)
|
1475
|
-
num_archival_memories = await self.passage_manager.
|
1486
|
+
num_archival_memories = await self.passage_manager.agent_passage_size_async(actor=actor, agent_id=agent_id)
|
1476
1487
|
|
1477
1488
|
# update memory (TODO: potentially update recall/archival stats separately)
|
1478
1489
|
new_system_message_str = compile_system_message(
|
@@ -1650,12 +1661,18 @@ class AgentManager:
|
|
1650
1661
|
@trace_method
|
1651
1662
|
@enforce_types
|
1652
1663
|
async def refresh_memory_async(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
|
1664
|
+
# TODO: This will NOT work for new blocks/file blocks added intra-step
|
1653
1665
|
block_ids = [b.id for b in agent_state.memory.blocks]
|
1654
|
-
|
1655
|
-
|
1666
|
+
file_block_names = [b.label for b in agent_state.memory.file_blocks]
|
1667
|
+
|
1668
|
+
if block_ids:
|
1669
|
+
blocks = await self.block_manager.get_all_blocks_by_ids_async(block_ids=[b.id for b in agent_state.memory.blocks], actor=actor)
|
1670
|
+
agent_state.memory.blocks = [b for b in blocks if b is not None]
|
1671
|
+
|
1672
|
+
if file_block_names:
|
1673
|
+
file_blocks = await self.file_agent_manager.get_all_file_blocks_by_name(file_names=file_block_names, actor=actor)
|
1674
|
+
agent_state.memory.file_blocks = [b for b in file_blocks if b is not None]
|
1656
1675
|
|
1657
|
-
blocks = await self.block_manager.get_all_blocks_by_ids_async(block_ids=[b.id for b in agent_state.memory.blocks], actor=actor)
|
1658
|
-
agent_state.memory.blocks = [b for b in blocks if b is not None]
|
1659
1676
|
return agent_state
|
1660
1677
|
|
1661
1678
|
# ======================================================================================================================
|
@@ -2006,184 +2023,6 @@ class AgentManager:
|
|
2006
2023
|
# ======================================================================================================================
|
2007
2024
|
# Passage Management
|
2008
2025
|
# ======================================================================================================================
|
2009
|
-
def _build_passage_query(
|
2010
|
-
self,
|
2011
|
-
actor: PydanticUser,
|
2012
|
-
agent_id: Optional[str] = None,
|
2013
|
-
file_id: Optional[str] = None,
|
2014
|
-
query_text: Optional[str] = None,
|
2015
|
-
start_date: Optional[datetime] = None,
|
2016
|
-
end_date: Optional[datetime] = None,
|
2017
|
-
before: Optional[str] = None,
|
2018
|
-
after: Optional[str] = None,
|
2019
|
-
source_id: Optional[str] = None,
|
2020
|
-
embed_query: bool = False,
|
2021
|
-
ascending: bool = True,
|
2022
|
-
embedding_config: Optional[EmbeddingConfig] = None,
|
2023
|
-
agent_only: bool = False,
|
2024
|
-
) -> Select:
|
2025
|
-
"""Helper function to build the base passage query with all filters applied.
|
2026
|
-
Supports both before and after pagination across merged source and agent passages.
|
2027
|
-
|
2028
|
-
Returns the query before any limit or count operations are applied.
|
2029
|
-
"""
|
2030
|
-
embedded_text = None
|
2031
|
-
if embed_query:
|
2032
|
-
assert embedding_config is not None, "embedding_config must be specified for vector search"
|
2033
|
-
assert query_text is not None, "query_text must be specified for vector search"
|
2034
|
-
embedded_text = embedding_model(embedding_config).get_text_embedding(query_text)
|
2035
|
-
embedded_text = np.array(embedded_text)
|
2036
|
-
embedded_text = np.pad(embedded_text, (0, MAX_EMBEDDING_DIM - embedded_text.shape[0]), mode="constant").tolist()
|
2037
|
-
|
2038
|
-
# Start with base query for source passages
|
2039
|
-
source_passages = None
|
2040
|
-
if not agent_only: # Include source passages
|
2041
|
-
if agent_id is not None:
|
2042
|
-
source_passages = (
|
2043
|
-
select(SourcePassage, literal(None).label("agent_id"))
|
2044
|
-
.join(SourcesAgents, SourcesAgents.source_id == SourcePassage.source_id)
|
2045
|
-
.where(SourcesAgents.agent_id == agent_id)
|
2046
|
-
.where(SourcePassage.organization_id == actor.organization_id)
|
2047
|
-
)
|
2048
|
-
else:
|
2049
|
-
source_passages = select(SourcePassage, literal(None).label("agent_id")).where(
|
2050
|
-
SourcePassage.organization_id == actor.organization_id
|
2051
|
-
)
|
2052
|
-
|
2053
|
-
if source_id:
|
2054
|
-
source_passages = source_passages.where(SourcePassage.source_id == source_id)
|
2055
|
-
if file_id:
|
2056
|
-
source_passages = source_passages.where(SourcePassage.file_id == file_id)
|
2057
|
-
|
2058
|
-
# Add agent passages query
|
2059
|
-
agent_passages = None
|
2060
|
-
if agent_id is not None:
|
2061
|
-
agent_passages = (
|
2062
|
-
select(
|
2063
|
-
AgentPassage.id,
|
2064
|
-
AgentPassage.text,
|
2065
|
-
AgentPassage.embedding_config,
|
2066
|
-
AgentPassage.metadata_,
|
2067
|
-
AgentPassage.embedding,
|
2068
|
-
AgentPassage.created_at,
|
2069
|
-
AgentPassage.updated_at,
|
2070
|
-
AgentPassage.is_deleted,
|
2071
|
-
AgentPassage._created_by_id,
|
2072
|
-
AgentPassage._last_updated_by_id,
|
2073
|
-
AgentPassage.organization_id,
|
2074
|
-
literal(None).label("file_id"),
|
2075
|
-
literal(None).label("source_id"),
|
2076
|
-
AgentPassage.agent_id,
|
2077
|
-
)
|
2078
|
-
.where(AgentPassage.agent_id == agent_id)
|
2079
|
-
.where(AgentPassage.organization_id == actor.organization_id)
|
2080
|
-
)
|
2081
|
-
|
2082
|
-
# Combine queries
|
2083
|
-
if source_passages is not None and agent_passages is not None:
|
2084
|
-
combined_query = union_all(source_passages, agent_passages).cte("combined_passages")
|
2085
|
-
elif agent_passages is not None:
|
2086
|
-
combined_query = agent_passages.cte("combined_passages")
|
2087
|
-
elif source_passages is not None:
|
2088
|
-
combined_query = source_passages.cte("combined_passages")
|
2089
|
-
else:
|
2090
|
-
raise ValueError("No passages found")
|
2091
|
-
|
2092
|
-
# Build main query from combined CTE
|
2093
|
-
main_query = select(combined_query)
|
2094
|
-
|
2095
|
-
# Apply filters
|
2096
|
-
if start_date:
|
2097
|
-
main_query = main_query.where(combined_query.c.created_at >= start_date)
|
2098
|
-
if end_date:
|
2099
|
-
main_query = main_query.where(combined_query.c.created_at <= end_date)
|
2100
|
-
if source_id:
|
2101
|
-
main_query = main_query.where(combined_query.c.source_id == source_id)
|
2102
|
-
if file_id:
|
2103
|
-
main_query = main_query.where(combined_query.c.file_id == file_id)
|
2104
|
-
|
2105
|
-
# Vector search
|
2106
|
-
if embedded_text:
|
2107
|
-
if settings.letta_pg_uri_no_default:
|
2108
|
-
# PostgreSQL with pgvector
|
2109
|
-
main_query = main_query.order_by(combined_query.c.embedding.cosine_distance(embedded_text).asc())
|
2110
|
-
else:
|
2111
|
-
# SQLite with custom vector type
|
2112
|
-
query_embedding_binary = adapt_array(embedded_text)
|
2113
|
-
main_query = main_query.order_by(
|
2114
|
-
func.cosine_distance(combined_query.c.embedding, query_embedding_binary).asc(),
|
2115
|
-
combined_query.c.created_at.asc() if ascending else combined_query.c.created_at.desc(),
|
2116
|
-
combined_query.c.id.asc(),
|
2117
|
-
)
|
2118
|
-
else:
|
2119
|
-
if query_text:
|
2120
|
-
main_query = main_query.where(func.lower(combined_query.c.text).contains(func.lower(query_text)))
|
2121
|
-
|
2122
|
-
# Handle pagination
|
2123
|
-
if before or after:
|
2124
|
-
# Create reference CTEs
|
2125
|
-
if before:
|
2126
|
-
before_ref = select(combined_query.c.created_at, combined_query.c.id).where(combined_query.c.id == before).cte("before_ref")
|
2127
|
-
if after:
|
2128
|
-
after_ref = select(combined_query.c.created_at, combined_query.c.id).where(combined_query.c.id == after).cte("after_ref")
|
2129
|
-
|
2130
|
-
if before and after:
|
2131
|
-
# Window-based query (get records between before and after)
|
2132
|
-
main_query = main_query.where(
|
2133
|
-
or_(
|
2134
|
-
combined_query.c.created_at < select(before_ref.c.created_at).scalar_subquery(),
|
2135
|
-
and_(
|
2136
|
-
combined_query.c.created_at == select(before_ref.c.created_at).scalar_subquery(),
|
2137
|
-
combined_query.c.id < select(before_ref.c.id).scalar_subquery(),
|
2138
|
-
),
|
2139
|
-
)
|
2140
|
-
)
|
2141
|
-
main_query = main_query.where(
|
2142
|
-
or_(
|
2143
|
-
combined_query.c.created_at > select(after_ref.c.created_at).scalar_subquery(),
|
2144
|
-
and_(
|
2145
|
-
combined_query.c.created_at == select(after_ref.c.created_at).scalar_subquery(),
|
2146
|
-
combined_query.c.id > select(after_ref.c.id).scalar_subquery(),
|
2147
|
-
),
|
2148
|
-
)
|
2149
|
-
)
|
2150
|
-
else:
|
2151
|
-
# Pure pagination (only before or only after)
|
2152
|
-
if before:
|
2153
|
-
main_query = main_query.where(
|
2154
|
-
or_(
|
2155
|
-
combined_query.c.created_at < select(before_ref.c.created_at).scalar_subquery(),
|
2156
|
-
and_(
|
2157
|
-
combined_query.c.created_at == select(before_ref.c.created_at).scalar_subquery(),
|
2158
|
-
combined_query.c.id < select(before_ref.c.id).scalar_subquery(),
|
2159
|
-
),
|
2160
|
-
)
|
2161
|
-
)
|
2162
|
-
if after:
|
2163
|
-
main_query = main_query.where(
|
2164
|
-
or_(
|
2165
|
-
combined_query.c.created_at > select(after_ref.c.created_at).scalar_subquery(),
|
2166
|
-
and_(
|
2167
|
-
combined_query.c.created_at == select(after_ref.c.created_at).scalar_subquery(),
|
2168
|
-
combined_query.c.id > select(after_ref.c.id).scalar_subquery(),
|
2169
|
-
),
|
2170
|
-
)
|
2171
|
-
)
|
2172
|
-
|
2173
|
-
# Add ordering if not already ordered by similarity
|
2174
|
-
if not embed_query:
|
2175
|
-
if ascending:
|
2176
|
-
main_query = main_query.order_by(
|
2177
|
-
combined_query.c.created_at.asc(),
|
2178
|
-
combined_query.c.id.asc(),
|
2179
|
-
)
|
2180
|
-
else:
|
2181
|
-
main_query = main_query.order_by(
|
2182
|
-
combined_query.c.created_at.desc(),
|
2183
|
-
combined_query.c.id.asc(),
|
2184
|
-
)
|
2185
|
-
|
2186
|
-
return main_query
|
2187
2026
|
|
2188
2027
|
@trace_method
|
2189
2028
|
@enforce_types
|
@@ -2206,7 +2045,7 @@ class AgentManager:
|
|
2206
2045
|
) -> List[PydanticPassage]:
|
2207
2046
|
"""Lists all passages attached to an agent."""
|
2208
2047
|
with db_registry.session() as session:
|
2209
|
-
main_query =
|
2048
|
+
main_query = build_passage_query(
|
2210
2049
|
actor=actor,
|
2211
2050
|
agent_id=agent_id,
|
2212
2051
|
file_id=file_id,
|
@@ -2236,6 +2075,7 @@ class AgentManager:
|
|
2236
2075
|
# This is an AgentPassage - remove source fields
|
2237
2076
|
data.pop("source_id", None)
|
2238
2077
|
data.pop("file_id", None)
|
2078
|
+
data.pop("file_name", None)
|
2239
2079
|
passage = AgentPassage(**data)
|
2240
2080
|
else:
|
2241
2081
|
# This is a SourcePassage - remove agent field
|
@@ -2266,7 +2106,7 @@ class AgentManager:
|
|
2266
2106
|
) -> List[PydanticPassage]:
|
2267
2107
|
"""Lists all passages attached to an agent."""
|
2268
2108
|
async with db_registry.async_session() as session:
|
2269
|
-
main_query =
|
2109
|
+
main_query = build_passage_query(
|
2270
2110
|
actor=actor,
|
2271
2111
|
agent_id=agent_id,
|
2272
2112
|
file_id=file_id,
|
@@ -2296,6 +2136,7 @@ class AgentManager:
|
|
2296
2136
|
# This is an AgentPassage - remove source fields
|
2297
2137
|
data.pop("source_id", None)
|
2298
2138
|
data.pop("file_id", None)
|
2139
|
+
data.pop("file_name", None)
|
2299
2140
|
passage = AgentPassage(**data)
|
2300
2141
|
else:
|
2301
2142
|
# This is a SourcePassage - remove agent field
|
@@ -2305,6 +2146,98 @@ class AgentManager:
|
|
2305
2146
|
|
2306
2147
|
return [p.to_pydantic() for p in passages]
|
2307
2148
|
|
2149
|
+
@trace_method
|
2150
|
+
@enforce_types
|
2151
|
+
async def list_source_passages_async(
|
2152
|
+
self,
|
2153
|
+
actor: PydanticUser,
|
2154
|
+
agent_id: Optional[str] = None,
|
2155
|
+
file_id: Optional[str] = None,
|
2156
|
+
limit: Optional[int] = 50,
|
2157
|
+
query_text: Optional[str] = None,
|
2158
|
+
start_date: Optional[datetime] = None,
|
2159
|
+
end_date: Optional[datetime] = None,
|
2160
|
+
before: Optional[str] = None,
|
2161
|
+
after: Optional[str] = None,
|
2162
|
+
source_id: Optional[str] = None,
|
2163
|
+
embed_query: bool = False,
|
2164
|
+
ascending: bool = True,
|
2165
|
+
embedding_config: Optional[EmbeddingConfig] = None,
|
2166
|
+
) -> List[PydanticPassage]:
|
2167
|
+
"""Lists all passages attached to an agent."""
|
2168
|
+
async with db_registry.async_session() as session:
|
2169
|
+
main_query = build_source_passage_query(
|
2170
|
+
actor=actor,
|
2171
|
+
agent_id=agent_id,
|
2172
|
+
file_id=file_id,
|
2173
|
+
query_text=query_text,
|
2174
|
+
start_date=start_date,
|
2175
|
+
end_date=end_date,
|
2176
|
+
before=before,
|
2177
|
+
after=after,
|
2178
|
+
source_id=source_id,
|
2179
|
+
embed_query=embed_query,
|
2180
|
+
ascending=ascending,
|
2181
|
+
embedding_config=embedding_config,
|
2182
|
+
)
|
2183
|
+
|
2184
|
+
# Add limit
|
2185
|
+
if limit:
|
2186
|
+
main_query = main_query.limit(limit)
|
2187
|
+
|
2188
|
+
# Execute query
|
2189
|
+
result = await session.execute(main_query)
|
2190
|
+
|
2191
|
+
# Get ORM objects directly using scalars()
|
2192
|
+
passages = result.scalars().all()
|
2193
|
+
|
2194
|
+
# Convert to Pydantic models
|
2195
|
+
return [p.to_pydantic() for p in passages]
|
2196
|
+
|
2197
|
+
@trace_method
|
2198
|
+
@enforce_types
|
2199
|
+
async def list_agent_passages_async(
|
2200
|
+
self,
|
2201
|
+
actor: PydanticUser,
|
2202
|
+
agent_id: Optional[str] = None,
|
2203
|
+
limit: Optional[int] = 50,
|
2204
|
+
query_text: Optional[str] = None,
|
2205
|
+
start_date: Optional[datetime] = None,
|
2206
|
+
end_date: Optional[datetime] = None,
|
2207
|
+
before: Optional[str] = None,
|
2208
|
+
after: Optional[str] = None,
|
2209
|
+
embed_query: bool = False,
|
2210
|
+
ascending: bool = True,
|
2211
|
+
embedding_config: Optional[EmbeddingConfig] = None,
|
2212
|
+
) -> List[PydanticPassage]:
|
2213
|
+
"""Lists all passages attached to an agent."""
|
2214
|
+
async with db_registry.async_session() as session:
|
2215
|
+
main_query = build_agent_passage_query(
|
2216
|
+
actor=actor,
|
2217
|
+
agent_id=agent_id,
|
2218
|
+
query_text=query_text,
|
2219
|
+
start_date=start_date,
|
2220
|
+
end_date=end_date,
|
2221
|
+
before=before,
|
2222
|
+
after=after,
|
2223
|
+
embed_query=embed_query,
|
2224
|
+
ascending=ascending,
|
2225
|
+
embedding_config=embedding_config,
|
2226
|
+
)
|
2227
|
+
|
2228
|
+
# Add limit
|
2229
|
+
if limit:
|
2230
|
+
main_query = main_query.limit(limit)
|
2231
|
+
|
2232
|
+
# Execute query
|
2233
|
+
result = await session.execute(main_query)
|
2234
|
+
|
2235
|
+
# Get ORM objects directly using scalars()
|
2236
|
+
passages = result.scalars().all()
|
2237
|
+
|
2238
|
+
# Convert to Pydantic models
|
2239
|
+
return [p.to_pydantic() for p in passages]
|
2240
|
+
|
2308
2241
|
@trace_method
|
2309
2242
|
@enforce_types
|
2310
2243
|
def passage_size(
|
@@ -2325,7 +2258,7 @@ class AgentManager:
|
|
2325
2258
|
) -> int:
|
2326
2259
|
"""Returns the count of passages matching the given criteria."""
|
2327
2260
|
with db_registry.session() as session:
|
2328
|
-
main_query =
|
2261
|
+
main_query = build_passage_query(
|
2329
2262
|
actor=actor,
|
2330
2263
|
agent_id=agent_id,
|
2331
2264
|
file_id=file_id,
|
@@ -2363,7 +2296,7 @@ class AgentManager:
|
|
2363
2296
|
agent_only: bool = False,
|
2364
2297
|
) -> int:
|
2365
2298
|
async with db_registry.async_session() as session:
|
2366
|
-
main_query =
|
2299
|
+
main_query = build_passage_query(
|
2367
2300
|
actor=actor,
|
2368
2301
|
agent_id=agent_id,
|
2369
2302
|
file_id=file_id,
|
@@ -2458,6 +2391,65 @@ class AgentManager:
|
|
2458
2391
|
await agent.update_async(session, actor=actor)
|
2459
2392
|
return await agent.to_pydantic_async()
|
2460
2393
|
|
2394
|
+
@trace_method
|
2395
|
+
@enforce_types
|
2396
|
+
async def attach_missing_files_tools_async(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
|
2397
|
+
"""
|
2398
|
+
Attaches missing core file tools to an agent.
|
2399
|
+
|
2400
|
+
Args:
|
2401
|
+
agent_id: ID of the agent to attach the tools to.
|
2402
|
+
actor: User performing the action.
|
2403
|
+
|
2404
|
+
Raises:
|
2405
|
+
NoResultFound: If the agent or tool is not found.
|
2406
|
+
|
2407
|
+
Returns:
|
2408
|
+
PydanticAgentState: The updated agent state.
|
2409
|
+
"""
|
2410
|
+
# Check if the agent is missing any files tools
|
2411
|
+
core_tool_names = {tool.name for tool in agent_state.tools if tool.tool_type == ToolType.LETTA_FILES_CORE}
|
2412
|
+
missing_tool_names = set(FILES_TOOLS).difference(core_tool_names)
|
2413
|
+
|
2414
|
+
for tool_name in missing_tool_names:
|
2415
|
+
tool_id = await self.tool_manager.get_tool_id_by_name_async(tool_name=tool_name, actor=actor)
|
2416
|
+
|
2417
|
+
# TODO: This is hacky and deserves a rethink - how do we keep all the base tools available in every org always?
|
2418
|
+
if not tool_id:
|
2419
|
+
await self.tool_manager.upsert_base_tools_async(actor=actor, allowed_types={ToolType.LETTA_FILES_CORE})
|
2420
|
+
|
2421
|
+
# TODO: Inefficient - I think this re-retrieves the agent_state?
|
2422
|
+
agent_state = await self.attach_tool_async(agent_id=agent_state.id, tool_id=tool_id, actor=actor)
|
2423
|
+
|
2424
|
+
return agent_state
|
2425
|
+
|
2426
|
+
@trace_method
|
2427
|
+
@enforce_types
|
2428
|
+
async def detach_all_files_tools_async(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
|
2429
|
+
"""
|
2430
|
+
Detach all core file tools from an agent.
|
2431
|
+
|
2432
|
+
Args:
|
2433
|
+
agent_id: ID of the agent to detach the tools from.
|
2434
|
+
actor: User performing the action.
|
2435
|
+
|
2436
|
+
Raises:
|
2437
|
+
NoResultFound: If the agent or tool is not found.
|
2438
|
+
|
2439
|
+
Returns:
|
2440
|
+
PydanticAgentState: The updated agent state.
|
2441
|
+
"""
|
2442
|
+
# Check if the agent is missing any files tools
|
2443
|
+
core_tool_names = {tool.name for tool in agent_state.tools if tool.tool_type == ToolType.LETTA_FILES_CORE}
|
2444
|
+
|
2445
|
+
for tool_name in core_tool_names:
|
2446
|
+
tool_id = await self.tool_manager.get_tool_id_by_name_async(tool_name=tool_name, actor=actor)
|
2447
|
+
|
2448
|
+
# TODO: Inefficient - I think this re-retrieves the agent_state?
|
2449
|
+
agent_state = await self.detach_tool_async(agent_id=agent_state.id, tool_id=tool_id, actor=actor)
|
2450
|
+
|
2451
|
+
return agent_state
|
2452
|
+
|
2461
2453
|
@trace_method
|
2462
2454
|
@enforce_types
|
2463
2455
|
def detach_tool(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
|
letta/services/block_manager.py
CHANGED
@@ -9,12 +9,12 @@ from letta.orm.block import Block as BlockModel
|
|
9
9
|
from letta.orm.block_history import BlockHistory
|
10
10
|
from letta.orm.enums import ActorType
|
11
11
|
from letta.orm.errors import NoResultFound
|
12
|
+
from letta.otel.tracing import trace_method
|
12
13
|
from letta.schemas.agent import AgentState as PydanticAgentState
|
13
14
|
from letta.schemas.block import Block as PydanticBlock
|
14
15
|
from letta.schemas.block import BlockUpdate
|
15
16
|
from letta.schemas.user import User as PydanticUser
|
16
17
|
from letta.server.db import db_registry
|
17
|
-
from letta.tracing import trace_method
|
18
18
|
from letta.utils import enforce_types
|
19
19
|
|
20
20
|
logger = get_logger(__name__)
|
@@ -63,7 +63,7 @@ class ContextWindowCalculator:
|
|
63
63
|
# Fetch data concurrently
|
64
64
|
(in_context_messages, passage_manager_size, message_manager_size) = await asyncio.gather(
|
65
65
|
message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor),
|
66
|
-
passage_manager.
|
66
|
+
passage_manager.agent_passage_size_async(actor=actor, agent_id=agent_state.id),
|
67
67
|
message_manager.size_async(actor=actor, agent_id=agent_state.id),
|
68
68
|
)
|
69
69
|
|
@@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
|
|
2
2
|
from typing import Any, Dict, List
|
3
3
|
|
4
4
|
from letta.llm_api.anthropic_client import AnthropicClient
|
5
|
+
from letta.schemas.openai.chat_completion_request import Tool as OpenAITool
|
5
6
|
from letta.utils import count_tokens
|
6
7
|
|
7
8
|
|
@@ -42,7 +43,7 @@ class AnthropicTokenCounter(TokenCounter):
|
|
42
43
|
return 0
|
43
44
|
return await self.client.count_tokens(model=self.model, messages=messages)
|
44
45
|
|
45
|
-
async def count_tool_tokens(self, tools: List[
|
46
|
+
async def count_tool_tokens(self, tools: List[OpenAITool]) -> int:
|
46
47
|
if not tools:
|
47
48
|
return 0
|
48
49
|
return await self.client.count_tokens(model=self.model, tools=tools)
|
@@ -69,7 +70,7 @@ class TiktokenCounter(TokenCounter):
|
|
69
70
|
|
70
71
|
return num_tokens_from_messages(messages=messages, model=self.model)
|
71
72
|
|
72
|
-
async def count_tool_tokens(self, tools: List[
|
73
|
+
async def count_tool_tokens(self, tools: List[OpenAITool]) -> int:
|
73
74
|
if not tools:
|
74
75
|
return 0
|
75
76
|
from letta.local_llm.utils import num_tokens_from_functions
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from typing import List, Optional
|
2
|
+
|
3
|
+
from letta.log import get_logger
|
4
|
+
|
5
|
+
logger = get_logger(__name__)
|
6
|
+
|
7
|
+
|
8
|
+
class LineChunker:
|
9
|
+
"""Newline chunker"""
|
10
|
+
|
11
|
+
def __init__(self):
|
12
|
+
pass
|
13
|
+
|
14
|
+
# TODO: Make this more general beyond Mistral
|
15
|
+
def chunk_text(self, text: str, start: Optional[int] = None, end: Optional[int] = None) -> List[str]:
|
16
|
+
"""Split lines"""
|
17
|
+
content_lines = [line.strip() for line in text.split("\n") if line.strip()]
|
18
|
+
total_lines = len(content_lines)
|
19
|
+
|
20
|
+
if start and end:
|
21
|
+
content_lines = content_lines[start:end]
|
22
|
+
line_offset = start
|
23
|
+
else:
|
24
|
+
line_offset = 0
|
25
|
+
|
26
|
+
content_lines = [f"Line {i + line_offset}: {line}" for i, line in enumerate(content_lines)]
|
27
|
+
|
28
|
+
# Add metadata about total lines
|
29
|
+
if start and end:
|
30
|
+
content_lines.insert(0, f"[Viewing lines {start} to {end} (out of {total_lines} lines)]")
|
31
|
+
else:
|
32
|
+
content_lines.insert(0, f"[Viewing file start (out of {total_lines} lines)]")
|
33
|
+
|
34
|
+
return content_lines
|