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
@@ -1,12 +1,12 @@
|
|
1
1
|
from typing import List, Optional, Union
|
2
2
|
|
3
3
|
from letta.orm.provider import Provider as ProviderModel
|
4
|
+
from letta.otel.tracing import trace_method
|
4
5
|
from letta.schemas.enums import ProviderCategory, ProviderType
|
5
6
|
from letta.schemas.providers import Provider as PydanticProvider
|
6
7
|
from letta.schemas.providers import ProviderCheck, ProviderCreate, ProviderUpdate
|
7
8
|
from letta.schemas.user import User as PydanticUser
|
8
9
|
from letta.server.db import db_registry
|
9
|
-
from letta.tracing import trace_method
|
10
10
|
from letta.utils import enforce_types
|
11
11
|
|
12
12
|
|
@@ -5,6 +5,7 @@ from letta.log import get_logger
|
|
5
5
|
from letta.orm.errors import NoResultFound
|
6
6
|
from letta.orm.sandbox_config import SandboxConfig as SandboxConfigModel
|
7
7
|
from letta.orm.sandbox_config import SandboxEnvironmentVariable as SandboxEnvVarModel
|
8
|
+
from letta.otel.tracing import trace_method
|
8
9
|
from letta.schemas.environment_variables import SandboxEnvironmentVariable as PydanticEnvVar
|
9
10
|
from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate, SandboxEnvironmentVariableUpdate
|
10
11
|
from letta.schemas.sandbox_config import LocalSandboxConfig
|
@@ -12,7 +13,6 @@ from letta.schemas.sandbox_config import SandboxConfig as PydanticSandboxConfig
|
|
12
13
|
from letta.schemas.sandbox_config import SandboxConfigCreate, SandboxConfigUpdate, SandboxType
|
13
14
|
from letta.schemas.user import User as PydanticUser
|
14
15
|
from letta.server.db import db_registry
|
15
|
-
from letta.tracing import trace_method
|
16
16
|
from letta.utils import enforce_types, printd
|
17
17
|
|
18
18
|
logger = get_logger(__name__)
|
letta/services/source_manager.py
CHANGED
@@ -1,16 +1,25 @@
|
|
1
1
|
import asyncio
|
2
|
+
from datetime import datetime
|
2
3
|
from typing import List, Optional
|
3
4
|
|
5
|
+
from sqlalchemy import select, update
|
6
|
+
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
7
|
+
from sqlalchemy.exc import IntegrityError
|
8
|
+
from sqlalchemy.orm import selectinload
|
9
|
+
|
4
10
|
from letta.orm.errors import NoResultFound
|
11
|
+
from letta.orm.file import FileContent as FileContentModel
|
5
12
|
from letta.orm.file import FileMetadata as FileMetadataModel
|
6
13
|
from letta.orm.source import Source as SourceModel
|
14
|
+
from letta.orm.sqlalchemy_base import AccessType
|
15
|
+
from letta.otel.tracing import trace_method
|
7
16
|
from letta.schemas.agent import AgentState as PydanticAgentState
|
17
|
+
from letta.schemas.enums import FileProcessingStatus
|
8
18
|
from letta.schemas.file import FileMetadata as PydanticFileMetadata
|
9
19
|
from letta.schemas.source import Source as PydanticSource
|
10
20
|
from letta.schemas.source import SourceUpdate
|
11
21
|
from letta.schemas.user import User as PydanticUser
|
12
22
|
from letta.server.db import db_registry
|
13
|
-
from letta.tracing import trace_method
|
14
23
|
from letta.utils import enforce_types, printd
|
15
24
|
|
16
25
|
|
@@ -142,41 +151,191 @@ class SourceManager:
|
|
142
151
|
|
143
152
|
@enforce_types
|
144
153
|
@trace_method
|
145
|
-
async def create_file(
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
154
|
+
async def create_file(
|
155
|
+
self,
|
156
|
+
file_metadata: PydanticFileMetadata,
|
157
|
+
actor: PydanticUser,
|
158
|
+
*,
|
159
|
+
text: Optional[str] = None,
|
160
|
+
) -> PydanticFileMetadata:
|
161
|
+
|
162
|
+
# short-circuit if it already exists
|
163
|
+
existing = await self.get_file_by_id(file_metadata.id, actor=actor)
|
164
|
+
if existing:
|
165
|
+
return existing
|
166
|
+
|
167
|
+
async with db_registry.async_session() as session:
|
168
|
+
try:
|
152
169
|
file_metadata.organization_id = actor.organization_id
|
153
|
-
|
154
|
-
await
|
155
|
-
|
170
|
+
file_orm = FileMetadataModel(**file_metadata.model_dump(to_orm=True, exclude_none=True))
|
171
|
+
await file_orm.create_async(session, actor=actor, no_commit=True)
|
172
|
+
|
173
|
+
if text is not None:
|
174
|
+
content_orm = FileContentModel(file_id=file_orm.id, text=text)
|
175
|
+
await content_orm.create_async(session, actor=actor, no_commit=True)
|
176
|
+
|
177
|
+
await session.commit()
|
178
|
+
await session.refresh(file_orm)
|
179
|
+
return await file_orm.to_pydantic_async()
|
180
|
+
|
181
|
+
except IntegrityError:
|
182
|
+
await session.rollback()
|
183
|
+
return await self.get_file_by_id(file_metadata.id, actor=actor)
|
156
184
|
|
157
185
|
# TODO: We make actor optional for now, but should most likely be enforced due to security reasons
|
158
186
|
@enforce_types
|
159
187
|
@trace_method
|
160
|
-
async def get_file_by_id(
|
161
|
-
|
188
|
+
async def get_file_by_id(
|
189
|
+
self,
|
190
|
+
file_id: str,
|
191
|
+
actor: Optional[PydanticUser] = None,
|
192
|
+
*,
|
193
|
+
include_content: bool = False,
|
194
|
+
) -> Optional[PydanticFileMetadata]:
|
195
|
+
"""Retrieve a file by its ID.
|
196
|
+
|
197
|
+
If `include_content=True`, the FileContent relationship is eagerly
|
198
|
+
loaded so `to_pydantic(include_content=True)` never triggers a
|
199
|
+
lazy SELECT (avoids MissingGreenlet).
|
200
|
+
"""
|
162
201
|
async with db_registry.async_session() as session:
|
163
202
|
try:
|
164
|
-
|
165
|
-
|
203
|
+
if include_content:
|
204
|
+
# explicit eager load
|
205
|
+
query = (
|
206
|
+
select(FileMetadataModel).where(FileMetadataModel.id == file_id).options(selectinload(FileMetadataModel.content))
|
207
|
+
)
|
208
|
+
# apply org-scoping if actor provided
|
209
|
+
if actor:
|
210
|
+
query = FileMetadataModel.apply_access_predicate(
|
211
|
+
query,
|
212
|
+
actor,
|
213
|
+
access=["read"],
|
214
|
+
access_type=AccessType.ORGANIZATION,
|
215
|
+
)
|
216
|
+
|
217
|
+
result = await session.execute(query)
|
218
|
+
file_orm = result.scalar_one()
|
219
|
+
else:
|
220
|
+
# fast path (metadata only)
|
221
|
+
file_orm = await FileMetadataModel.read_async(
|
222
|
+
db_session=session,
|
223
|
+
identifier=file_id,
|
224
|
+
actor=actor,
|
225
|
+
)
|
226
|
+
|
227
|
+
return await file_orm.to_pydantic_async(include_content=include_content)
|
228
|
+
|
166
229
|
except NoResultFound:
|
167
230
|
return None
|
168
231
|
|
232
|
+
@enforce_types
|
233
|
+
@trace_method
|
234
|
+
async def update_file_status(
|
235
|
+
self,
|
236
|
+
*,
|
237
|
+
file_id: str,
|
238
|
+
actor: PydanticUser,
|
239
|
+
processing_status: Optional[FileProcessingStatus] = None,
|
240
|
+
error_message: Optional[str] = None,
|
241
|
+
) -> PydanticFileMetadata:
|
242
|
+
"""
|
243
|
+
Update processing_status and/or error_message on a FileMetadata row.
|
244
|
+
|
245
|
+
* 1st round-trip → UPDATE
|
246
|
+
* 2nd round-trip → SELECT fresh row (same as read_async)
|
247
|
+
"""
|
248
|
+
|
249
|
+
if processing_status is None and error_message is None:
|
250
|
+
raise ValueError("Nothing to update")
|
251
|
+
|
252
|
+
values: dict[str, object] = {"updated_at": datetime.utcnow()}
|
253
|
+
if processing_status is not None:
|
254
|
+
values["processing_status"] = processing_status
|
255
|
+
if error_message is not None:
|
256
|
+
values["error_message"] = error_message
|
257
|
+
|
258
|
+
async with db_registry.async_session() as session:
|
259
|
+
# Fast in-place update – no ORM hydration
|
260
|
+
stmt = (
|
261
|
+
update(FileMetadataModel)
|
262
|
+
.where(
|
263
|
+
FileMetadataModel.id == file_id,
|
264
|
+
FileMetadataModel.organization_id == actor.organization_id,
|
265
|
+
)
|
266
|
+
.values(**values)
|
267
|
+
)
|
268
|
+
await session.execute(stmt)
|
269
|
+
await session.commit()
|
270
|
+
|
271
|
+
# Reload via normal accessor so we return a fully-attached object
|
272
|
+
file_orm = await FileMetadataModel.read_async(
|
273
|
+
db_session=session,
|
274
|
+
identifier=file_id,
|
275
|
+
actor=actor,
|
276
|
+
)
|
277
|
+
return await file_orm.to_pydantic_async()
|
278
|
+
|
279
|
+
@enforce_types
|
280
|
+
@trace_method
|
281
|
+
async def upsert_file_content(
|
282
|
+
self,
|
283
|
+
*,
|
284
|
+
file_id: str,
|
285
|
+
text: str,
|
286
|
+
actor: PydanticUser,
|
287
|
+
) -> PydanticFileMetadata:
|
288
|
+
async with db_registry.async_session() as session:
|
289
|
+
await FileMetadataModel.read_async(session, file_id, actor)
|
290
|
+
|
291
|
+
dialect_name = session.bind.dialect.name
|
292
|
+
|
293
|
+
if dialect_name == "postgresql":
|
294
|
+
stmt = (
|
295
|
+
pg_insert(FileContentModel)
|
296
|
+
.values(file_id=file_id, text=text)
|
297
|
+
.on_conflict_do_update(
|
298
|
+
index_elements=[FileContentModel.file_id],
|
299
|
+
set_={"text": text},
|
300
|
+
)
|
301
|
+
)
|
302
|
+
await session.execute(stmt)
|
303
|
+
else:
|
304
|
+
# Emulate upsert for SQLite and others
|
305
|
+
stmt = select(FileContentModel).where(FileContentModel.file_id == file_id)
|
306
|
+
result = await session.execute(stmt)
|
307
|
+
existing = result.scalar_one_or_none()
|
308
|
+
|
309
|
+
if existing:
|
310
|
+
await session.execute(update(FileContentModel).where(FileContentModel.file_id == file_id).values(text=text))
|
311
|
+
else:
|
312
|
+
session.add(FileContentModel(file_id=file_id, text=text))
|
313
|
+
|
314
|
+
await session.commit()
|
315
|
+
|
316
|
+
# Reload with content
|
317
|
+
query = select(FileMetadataModel).options(selectinload(FileMetadataModel.content)).where(FileMetadataModel.id == file_id)
|
318
|
+
result = await session.execute(query)
|
319
|
+
return await result.scalar_one().to_pydantic_async(include_content=True)
|
320
|
+
|
169
321
|
@enforce_types
|
170
322
|
@trace_method
|
171
323
|
async def list_files(
|
172
|
-
self, source_id: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50
|
324
|
+
self, source_id: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50, include_content: bool = False
|
173
325
|
) -> List[PydanticFileMetadata]:
|
174
326
|
"""List all files with optional pagination."""
|
175
327
|
async with db_registry.async_session() as session:
|
328
|
+
options = [selectinload(FileMetadataModel.content)] if include_content else None
|
329
|
+
|
176
330
|
files = await FileMetadataModel.list_async(
|
177
|
-
db_session=session,
|
331
|
+
db_session=session,
|
332
|
+
after=after,
|
333
|
+
limit=limit,
|
334
|
+
organization_id=actor.organization_id,
|
335
|
+
source_id=source_id,
|
336
|
+
query_options=options,
|
178
337
|
)
|
179
|
-
return [file.
|
338
|
+
return [await file.to_pydantic_async(include_content=include_content) for file in files]
|
180
339
|
|
181
340
|
@enforce_types
|
182
341
|
@trace_method
|
@@ -185,4 +344,4 @@ class SourceManager:
|
|
185
344
|
async with db_registry.async_session() as session:
|
186
345
|
file = await FileMetadataModel.read_async(db_session=session, identifier=file_id)
|
187
346
|
await file.hard_delete_async(db_session=session, actor=actor)
|
188
|
-
return file.
|
347
|
+
return await file.to_pydantic_async()
|
letta/services/step_manager.py
CHANGED
@@ -5,16 +5,16 @@ from sqlalchemy import select
|
|
5
5
|
from sqlalchemy.ext.asyncio import AsyncSession
|
6
6
|
from sqlalchemy.orm import Session
|
7
7
|
|
8
|
+
from letta.helpers.singleton import singleton
|
8
9
|
from letta.orm.errors import NoResultFound
|
9
10
|
from letta.orm.job import Job as JobModel
|
10
11
|
from letta.orm.sqlalchemy_base import AccessType
|
11
12
|
from letta.orm.step import Step as StepModel
|
13
|
+
from letta.otel.tracing import get_trace_id, trace_method
|
12
14
|
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
13
15
|
from letta.schemas.step import Step as PydanticStep
|
14
16
|
from letta.schemas.user import User as PydanticUser
|
15
17
|
from letta.server.db import db_registry
|
16
|
-
from letta.services.helpers.noop_helper import singleton
|
17
|
-
from letta.tracing import get_trace_id, trace_method
|
18
18
|
from letta.utils import enforce_types
|
19
19
|
|
20
20
|
|
@@ -6,11 +6,11 @@ from typing import List, Optional, Tuple, Union
|
|
6
6
|
from letta.agents.ephemeral_summary_agent import EphemeralSummaryAgent
|
7
7
|
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
|
8
8
|
from letta.log import get_logger
|
9
|
+
from letta.otel.tracing import trace_method
|
9
10
|
from letta.schemas.enums import MessageRole
|
10
11
|
from letta.schemas.letta_message_content import TextContent
|
11
12
|
from letta.schemas.message import Message, MessageCreate
|
12
13
|
from letta.services.summarizer.enums import SummarizationMode
|
13
|
-
from letta.tracing import trace_method
|
14
14
|
|
15
15
|
logger = get_logger(__name__)
|
16
16
|
|
@@ -1,11 +1,11 @@
|
|
1
1
|
from letta.helpers.json_helpers import json_dumps, json_loads
|
2
|
+
from letta.helpers.singleton import singleton
|
2
3
|
from letta.orm.provider_trace import ProviderTrace as ProviderTraceModel
|
3
4
|
from letta.schemas.provider_trace import ProviderTrace as PydanticProviderTrace
|
4
5
|
from letta.schemas.provider_trace import ProviderTraceCreate
|
5
6
|
from letta.schemas.step import Step as PydanticStep
|
6
7
|
from letta.schemas.user import User as PydanticUser
|
7
8
|
from letta.server.db import db_registry
|
8
|
-
from letta.services.helpers.noop_helper import singleton
|
9
9
|
from letta.utils import enforce_types
|
10
10
|
|
11
11
|
|
@@ -0,0 +1,117 @@
|
|
1
|
+
import json
|
2
|
+
from textwrap import shorten
|
3
|
+
from typing import Any, Dict, Literal, Optional
|
4
|
+
|
5
|
+
from letta.constants import WEB_SEARCH_CLIP_CONTENT, WEB_SEARCH_INCLUDE_SCORE, WEB_SEARCH_SEPARATOR
|
6
|
+
from letta.otel.tracing import trace_method
|
7
|
+
from letta.schemas.agent import AgentState
|
8
|
+
from letta.schemas.sandbox_config import SandboxConfig
|
9
|
+
from letta.schemas.tool import Tool
|
10
|
+
from letta.schemas.tool_execution_result import ToolExecutionResult
|
11
|
+
from letta.schemas.user import User
|
12
|
+
from letta.services.tool_executor.tool_executor_base import ToolExecutor
|
13
|
+
from letta.settings import tool_settings
|
14
|
+
|
15
|
+
|
16
|
+
class LettaBuiltinToolExecutor(ToolExecutor):
|
17
|
+
"""Executor for built in Letta tools."""
|
18
|
+
|
19
|
+
@trace_method
|
20
|
+
async def execute(
|
21
|
+
self,
|
22
|
+
function_name: str,
|
23
|
+
function_args: dict,
|
24
|
+
tool: Tool,
|
25
|
+
actor: User,
|
26
|
+
agent_state: Optional[AgentState] = None,
|
27
|
+
sandbox_config: Optional[SandboxConfig] = None,
|
28
|
+
sandbox_env_vars: Optional[Dict[str, Any]] = None,
|
29
|
+
) -> ToolExecutionResult:
|
30
|
+
function_map = {"run_code": self.run_code, "web_search": self.web_search}
|
31
|
+
|
32
|
+
if function_name not in function_map:
|
33
|
+
raise ValueError(f"Unknown function: {function_name}")
|
34
|
+
|
35
|
+
# Execute the appropriate function
|
36
|
+
function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
|
37
|
+
function_response = await function_map[function_name](**function_args_copy)
|
38
|
+
|
39
|
+
return ToolExecutionResult(
|
40
|
+
status="success",
|
41
|
+
func_return=function_response,
|
42
|
+
agent_state=agent_state,
|
43
|
+
)
|
44
|
+
|
45
|
+
async def run_code(self, code: str, language: Literal["python", "js", "ts", "r", "java"]) -> str:
|
46
|
+
from e2b_code_interpreter import AsyncSandbox
|
47
|
+
|
48
|
+
if tool_settings.e2b_api_key is None:
|
49
|
+
raise ValueError("E2B_API_KEY is not set")
|
50
|
+
|
51
|
+
sbx = await AsyncSandbox.create(api_key=tool_settings.e2b_api_key)
|
52
|
+
params = {"code": code}
|
53
|
+
if language != "python":
|
54
|
+
# Leave empty for python
|
55
|
+
params["language"] = language
|
56
|
+
|
57
|
+
res = self._llm_friendly_result(await sbx.run_code(**params))
|
58
|
+
return json.dumps(res, ensure_ascii=False)
|
59
|
+
|
60
|
+
def _llm_friendly_result(self, res):
|
61
|
+
out = {
|
62
|
+
"results": [r.text if hasattr(r, "text") else str(r) for r in res.results],
|
63
|
+
"logs": {
|
64
|
+
"stdout": getattr(res.logs, "stdout", []),
|
65
|
+
"stderr": getattr(res.logs, "stderr", []),
|
66
|
+
},
|
67
|
+
}
|
68
|
+
err = getattr(res, "error", None)
|
69
|
+
if err is not None:
|
70
|
+
out["error"] = err
|
71
|
+
return out
|
72
|
+
|
73
|
+
async def web_search(agent_state: "AgentState", query: str) -> str:
|
74
|
+
"""
|
75
|
+
Search the web for information.
|
76
|
+
Args:
|
77
|
+
query (str): The query to search the web for.
|
78
|
+
Returns:
|
79
|
+
str: The search results.
|
80
|
+
"""
|
81
|
+
|
82
|
+
try:
|
83
|
+
from tavily import AsyncTavilyClient
|
84
|
+
except ImportError:
|
85
|
+
raise ImportError("tavily is not installed in the tool execution environment")
|
86
|
+
|
87
|
+
# Check if the API key exists
|
88
|
+
if tool_settings.tavily_api_key is None:
|
89
|
+
raise ValueError("TAVILY_API_KEY is not set")
|
90
|
+
|
91
|
+
# Instantiate client and search
|
92
|
+
tavily_client = AsyncTavilyClient(api_key=tool_settings.tavily_api_key)
|
93
|
+
search_results = await tavily_client.search(query=query, auto_parameters=True)
|
94
|
+
|
95
|
+
results = search_results.get("results", [])
|
96
|
+
if not results:
|
97
|
+
return "No search results found."
|
98
|
+
|
99
|
+
# ---- format for the LLM -------------------------------------------------
|
100
|
+
formatted_blocks = []
|
101
|
+
for idx, item in enumerate(results, start=1):
|
102
|
+
title = item.get("title") or "Untitled"
|
103
|
+
url = item.get("url") or "Unknown URL"
|
104
|
+
# keep each content snippet reasonably short so you don’t blow up context
|
105
|
+
content = (
|
106
|
+
shorten(item.get("content", "").strip(), width=600, placeholder=" …")
|
107
|
+
if WEB_SEARCH_CLIP_CONTENT
|
108
|
+
else item.get("content", "").strip()
|
109
|
+
)
|
110
|
+
score = item.get("score")
|
111
|
+
if WEB_SEARCH_INCLUDE_SCORE:
|
112
|
+
block = f"\nRESULT {idx}:\n" f"Title: {title}\n" f"URL: {url}\n" f"Relevance score: {score:.4f}\n" f"Content: {content}\n"
|
113
|
+
else:
|
114
|
+
block = f"\nRESULT {idx}:\n" f"Title: {title}\n" f"URL: {url}\n" f"Content: {content}\n"
|
115
|
+
formatted_blocks.append(block)
|
116
|
+
|
117
|
+
return WEB_SEARCH_SEPARATOR.join(formatted_blocks)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
from typing import Any, Dict, Optional
|
2
|
+
|
3
|
+
from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY
|
4
|
+
from letta.functions.composio_helpers import execute_composio_action_async, generate_composio_action_from_func_name
|
5
|
+
from letta.helpers.composio_helpers import get_composio_api_key_async
|
6
|
+
from letta.otel.tracing import trace_method
|
7
|
+
from letta.schemas.agent import AgentState
|
8
|
+
from letta.schemas.sandbox_config import SandboxConfig
|
9
|
+
from letta.schemas.tool import Tool
|
10
|
+
from letta.schemas.tool_execution_result import ToolExecutionResult
|
11
|
+
from letta.schemas.user import User
|
12
|
+
from letta.services.tool_executor.tool_executor_base import ToolExecutor
|
13
|
+
|
14
|
+
|
15
|
+
class ExternalComposioToolExecutor(ToolExecutor):
|
16
|
+
"""Executor for external Composio tools."""
|
17
|
+
|
18
|
+
@trace_method
|
19
|
+
async def execute(
|
20
|
+
self,
|
21
|
+
function_name: str,
|
22
|
+
function_args: dict,
|
23
|
+
tool: Tool,
|
24
|
+
actor: User,
|
25
|
+
agent_state: Optional[AgentState] = None,
|
26
|
+
sandbox_config: Optional[SandboxConfig] = None,
|
27
|
+
sandbox_env_vars: Optional[Dict[str, Any]] = None,
|
28
|
+
) -> ToolExecutionResult:
|
29
|
+
assert agent_state is not None, "Agent state is required for external Composio tools"
|
30
|
+
action_name = generate_composio_action_from_func_name(tool.name)
|
31
|
+
|
32
|
+
# Get entity ID from the agent_state
|
33
|
+
entity_id = self._get_entity_id(agent_state)
|
34
|
+
|
35
|
+
# Get composio_api_key
|
36
|
+
composio_api_key = await get_composio_api_key_async(actor=actor)
|
37
|
+
|
38
|
+
# TODO (matt): Roll in execute_composio_action into this class
|
39
|
+
function_response = await execute_composio_action_async(
|
40
|
+
action_name=action_name, args=function_args, api_key=composio_api_key, entity_id=entity_id
|
41
|
+
)
|
42
|
+
|
43
|
+
return ToolExecutionResult(
|
44
|
+
status="success",
|
45
|
+
func_return=function_response,
|
46
|
+
)
|
47
|
+
|
48
|
+
def _get_entity_id(self, agent_state: AgentState) -> Optional[str]:
|
49
|
+
"""Extract the entity ID from environment variables."""
|
50
|
+
for env_var in agent_state.tool_exec_environment_variables:
|
51
|
+
if env_var.key == COMPOSIO_ENTITY_ENV_VAR_KEY:
|
52
|
+
return env_var.value
|
53
|
+
return None
|