letta-nightly 0.8.0.dev20250606104326__py3-none-any.whl → 0.8.2.dev20250606215616__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 +1 -1
- letta/agents/letta_agent.py +49 -29
- letta/agents/letta_agent_batch.py +1 -2
- letta/agents/voice_agent.py +19 -13
- letta/agents/voice_sleeptime_agent.py +11 -3
- 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 +1 -1
- 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 +22 -14
- 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/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/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 +37 -36
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/sources.py +26 -3
- letta/server/rest_api/utils.py +9 -6
- letta/server/server.py +18 -12
- letta/services/agent_manager.py +185 -193
- letta/services/block_manager.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 +40 -11
- 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 +373 -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/message_manager.py +1 -1
- letta/services/organization_manager.py +1 -1
- letta/services/passage_manager.py +1 -1
- 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 +131 -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 +21 -1
- letta/utils.py +7 -2
- {letta_nightly-0.8.0.dev20250606104326.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/METADATA +4 -2
- {letta_nightly-0.8.0.dev20250606104326.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/RECORD +96 -74
- {letta_nightly-0.8.0.dev20250606104326.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.0.dev20250606104326.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.0.dev20250606104326.dist-info → letta_nightly-0.8.2.dev20250606215616.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
|
@@ -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,
|
@@ -2266,7 +2105,7 @@ class AgentManager:
|
|
2266
2105
|
) -> List[PydanticPassage]:
|
2267
2106
|
"""Lists all passages attached to an agent."""
|
2268
2107
|
async with db_registry.async_session() as session:
|
2269
|
-
main_query =
|
2108
|
+
main_query = build_passage_query(
|
2270
2109
|
actor=actor,
|
2271
2110
|
agent_id=agent_id,
|
2272
2111
|
file_id=file_id,
|
@@ -2305,6 +2144,100 @@ class AgentManager:
|
|
2305
2144
|
|
2306
2145
|
return [p.to_pydantic() for p in passages]
|
2307
2146
|
|
2147
|
+
@trace_method
|
2148
|
+
@enforce_types
|
2149
|
+
async def list_source_passages_async(
|
2150
|
+
self,
|
2151
|
+
actor: PydanticUser,
|
2152
|
+
agent_id: Optional[str] = None,
|
2153
|
+
file_id: Optional[str] = None,
|
2154
|
+
limit: Optional[int] = 50,
|
2155
|
+
query_text: Optional[str] = None,
|
2156
|
+
start_date: Optional[datetime] = None,
|
2157
|
+
end_date: Optional[datetime] = None,
|
2158
|
+
before: Optional[str] = None,
|
2159
|
+
after: Optional[str] = None,
|
2160
|
+
source_id: Optional[str] = None,
|
2161
|
+
embed_query: bool = False,
|
2162
|
+
ascending: bool = True,
|
2163
|
+
embedding_config: Optional[EmbeddingConfig] = None,
|
2164
|
+
) -> List[PydanticPassage]:
|
2165
|
+
"""Lists all passages attached to an agent."""
|
2166
|
+
async with db_registry.async_session() as session:
|
2167
|
+
main_query = build_source_passage_query(
|
2168
|
+
actor=actor,
|
2169
|
+
agent_id=agent_id,
|
2170
|
+
file_id=file_id,
|
2171
|
+
query_text=query_text,
|
2172
|
+
start_date=start_date,
|
2173
|
+
end_date=end_date,
|
2174
|
+
before=before,
|
2175
|
+
after=after,
|
2176
|
+
source_id=source_id,
|
2177
|
+
embed_query=embed_query,
|
2178
|
+
ascending=ascending,
|
2179
|
+
embedding_config=embedding_config,
|
2180
|
+
)
|
2181
|
+
|
2182
|
+
# Add limit
|
2183
|
+
if limit:
|
2184
|
+
main_query = main_query.limit(limit)
|
2185
|
+
|
2186
|
+
# Execute query
|
2187
|
+
result = await session.execute(main_query)
|
2188
|
+
|
2189
|
+
# Get ORM objects directly using scalars()
|
2190
|
+
passages = result.scalars().all()
|
2191
|
+
|
2192
|
+
# Convert to Pydantic models
|
2193
|
+
return [p.to_pydantic() for p in passages]
|
2194
|
+
|
2195
|
+
@trace_method
|
2196
|
+
@enforce_types
|
2197
|
+
async def list_agent_passages_async(
|
2198
|
+
self,
|
2199
|
+
actor: PydanticUser,
|
2200
|
+
agent_id: Optional[str] = None,
|
2201
|
+
file_id: Optional[str] = None,
|
2202
|
+
limit: Optional[int] = 50,
|
2203
|
+
query_text: Optional[str] = None,
|
2204
|
+
start_date: Optional[datetime] = None,
|
2205
|
+
end_date: Optional[datetime] = None,
|
2206
|
+
before: Optional[str] = None,
|
2207
|
+
after: Optional[str] = None,
|
2208
|
+
source_id: 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__)
|
@@ -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
|
@@ -5,12 +5,13 @@ from fastapi import UploadFile
|
|
5
5
|
|
6
6
|
from letta.log import get_logger
|
7
7
|
from letta.schemas.agent import AgentState
|
8
|
-
from letta.schemas.enums import JobStatus
|
8
|
+
from letta.schemas.enums import FileProcessingStatus, JobStatus
|
9
9
|
from letta.schemas.file import FileMetadata
|
10
10
|
from letta.schemas.job import Job, JobUpdate
|
11
11
|
from letta.schemas.passage import Passage
|
12
12
|
from letta.schemas.user import User
|
13
13
|
from letta.server.server import SyncServer
|
14
|
+
from letta.services.file_processor.chunker.line_chunker import LineChunker
|
14
15
|
from letta.services.file_processor.chunker.llama_index_chunker import LlamaIndexChunker
|
15
16
|
from letta.services.file_processor.embedder.openai_embedder import OpenAIEmbedder
|
16
17
|
from letta.services.file_processor.parser.mistral_parser import MistralFileParser
|
@@ -34,6 +35,7 @@ class FileProcessor:
|
|
34
35
|
):
|
35
36
|
self.file_parser = file_parser
|
36
37
|
self.text_chunker = text_chunker
|
38
|
+
self.line_chunker = LineChunker()
|
37
39
|
self.embedder = embedder
|
38
40
|
self.max_file_size = max_file_size
|
39
41
|
self.source_manager = SourceManager()
|
@@ -52,9 +54,12 @@ class FileProcessor:
|
|
52
54
|
job: Optional[Job] = None,
|
53
55
|
) -> List[Passage]:
|
54
56
|
file_metadata = self._extract_upload_file_metadata(file, source_id=source_id)
|
55
|
-
file_metadata = await self.source_manager.create_file(file_metadata, self.actor)
|
56
57
|
filename = file_metadata.file_name
|
57
58
|
|
59
|
+
# Create file as early as possible with no content
|
60
|
+
file_metadata.processing_status = FileProcessingStatus.PARSING # Parsing now
|
61
|
+
file_metadata = await self.source_manager.create_file(file_metadata, self.actor)
|
62
|
+
|
58
63
|
try:
|
59
64
|
# Ensure we're working with bytes
|
60
65
|
if isinstance(content, str):
|
@@ -66,11 +71,35 @@ class FileProcessor:
|
|
66
71
|
logger.info(f"Starting OCR extraction for {filename}")
|
67
72
|
ocr_response = await self.file_parser.extract_text(content, mime_type=file_metadata.file_type)
|
68
73
|
|
74
|
+
# update file with raw text
|
75
|
+
raw_markdown_text = "".join([page.markdown for page in ocr_response.pages])
|
76
|
+
file_metadata = await self.source_manager.upsert_file_content(
|
77
|
+
file_id=file_metadata.id, text=raw_markdown_text, actor=self.actor
|
78
|
+
)
|
79
|
+
file_metadata = await self.source_manager.update_file_status(
|
80
|
+
file_id=file_metadata.id, actor=self.actor, processing_status=FileProcessingStatus.EMBEDDING
|
81
|
+
)
|
82
|
+
|
83
|
+
# Insert to agent context window
|
84
|
+
# TODO: Rethink this line chunking mechanism
|
85
|
+
content_lines = self.line_chunker.chunk_text(text=raw_markdown_text)
|
86
|
+
visible_content = "\n".join(content_lines)
|
87
|
+
|
88
|
+
await server.insert_file_into_context_windows(
|
89
|
+
source_id=source_id,
|
90
|
+
text=visible_content,
|
91
|
+
file_id=file_metadata.id,
|
92
|
+
file_name=file_metadata.file_name,
|
93
|
+
actor=self.actor,
|
94
|
+
agent_states=agent_states,
|
95
|
+
)
|
96
|
+
|
69
97
|
if not ocr_response or len(ocr_response.pages) == 0:
|
70
98
|
raise ValueError("No text extracted from PDF")
|
71
99
|
|
72
100
|
logger.info("Chunking extracted text")
|
73
101
|
all_passages = []
|
102
|
+
|
74
103
|
for page in ocr_response.pages:
|
75
104
|
chunks = self.text_chunker.chunk_text(page)
|
76
105
|
|
@@ -86,24 +115,20 @@ class FileProcessor:
|
|
86
115
|
|
87
116
|
logger.info(f"Successfully processed {filename}: {len(all_passages)} passages")
|
88
117
|
|
89
|
-
await server.insert_file_into_context_windows(
|
90
|
-
source_id=source_id,
|
91
|
-
text="".join([ocr_response.pages[i].markdown for i in range(min(3, len(ocr_response.pages)))]),
|
92
|
-
file_id=file_metadata.id,
|
93
|
-
actor=self.actor,
|
94
|
-
agent_states=agent_states,
|
95
|
-
)
|
96
|
-
|
97
118
|
# update job status
|
98
119
|
if job:
|
99
120
|
job.status = JobStatus.completed
|
100
121
|
job.metadata["num_passages"] = len(all_passages)
|
101
122
|
await self.job_manager.update_job_by_id_async(job_id=job.id, job_update=JobUpdate(**job.model_dump()), actor=self.actor)
|
102
123
|
|
124
|
+
await self.source_manager.update_file_status(
|
125
|
+
file_id=file_metadata.id, actor=self.actor, processing_status=FileProcessingStatus.COMPLETED
|
126
|
+
)
|
127
|
+
|
103
128
|
return all_passages
|
104
129
|
|
105
130
|
except Exception as e:
|
106
|
-
logger.error(f"
|
131
|
+
logger.error(f"File processing failed for {filename}: {str(e)}")
|
107
132
|
|
108
133
|
# update job status
|
109
134
|
if job:
|
@@ -111,6 +136,10 @@ class FileProcessor:
|
|
111
136
|
job.metadata["error"] = str(e)
|
112
137
|
await self.job_manager.update_job_by_id_async(job_id=job.id, job_update=JobUpdate(**job.model_dump()), actor=self.actor)
|
113
138
|
|
139
|
+
await self.source_manager.update_file_status(
|
140
|
+
file_id=file_metadata.id, actor=self.actor, processing_status=FileProcessingStatus.ERROR, error_message=str(e)
|
141
|
+
)
|
142
|
+
|
114
143
|
return []
|
115
144
|
|
116
145
|
def _extract_upload_file_metadata(self, file: UploadFile, source_id: str) -> FileMetadata:
|