letta-nightly 0.7.29.dev20250602104315__py3-none-any.whl → 0.8.0.dev20250604104349__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 +7 -1
- letta/agent.py +16 -9
- letta/agents/base_agent.py +1 -0
- letta/agents/ephemeral_summary_agent.py +104 -0
- letta/agents/helpers.py +35 -3
- letta/agents/letta_agent.py +492 -176
- letta/agents/letta_agent_batch.py +22 -16
- letta/agents/prompts/summary_system_prompt.txt +62 -0
- letta/agents/voice_agent.py +22 -7
- letta/agents/voice_sleeptime_agent.py +13 -8
- letta/constants.py +33 -1
- letta/data_sources/connectors.py +52 -36
- letta/errors.py +4 -0
- letta/functions/ast_parsers.py +13 -30
- letta/functions/function_sets/base.py +3 -1
- letta/functions/functions.py +2 -0
- letta/functions/mcp_client/base_client.py +151 -97
- letta/functions/mcp_client/sse_client.py +49 -31
- letta/functions/mcp_client/stdio_client.py +107 -106
- letta/functions/schema_generator.py +22 -22
- letta/groups/helpers.py +3 -4
- letta/groups/sleeptime_multi_agent.py +4 -4
- letta/groups/sleeptime_multi_agent_v2.py +22 -0
- letta/helpers/composio_helpers.py +16 -0
- letta/helpers/converters.py +20 -0
- letta/helpers/datetime_helpers.py +1 -6
- letta/helpers/tool_rule_solver.py +2 -1
- letta/interfaces/anthropic_streaming_interface.py +17 -2
- letta/interfaces/openai_chat_completions_streaming_interface.py +1 -0
- letta/interfaces/openai_streaming_interface.py +18 -2
- letta/jobs/llm_batch_job_polling.py +1 -1
- letta/jobs/scheduler.py +1 -1
- letta/llm_api/anthropic_client.py +24 -3
- letta/llm_api/google_ai_client.py +0 -15
- letta/llm_api/google_vertex_client.py +6 -5
- letta/llm_api/llm_client_base.py +15 -0
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +60 -8
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +45 -43
- letta/orm/base.py +0 -2
- letta/orm/block.py +1 -0
- letta/orm/custom_columns.py +13 -0
- letta/orm/enums.py +5 -0
- letta/orm/file.py +3 -1
- letta/orm/files_agents.py +68 -0
- letta/orm/mcp_server.py +48 -0
- letta/orm/message.py +1 -0
- letta/orm/organization.py +11 -2
- letta/orm/passage.py +25 -10
- letta/orm/sandbox_config.py +5 -2
- letta/orm/sqlalchemy_base.py +171 -110
- letta/prompts/system/memgpt_base.txt +6 -1
- letta/prompts/system/memgpt_v2_chat.txt +57 -0
- letta/prompts/system/sleeptime.txt +2 -0
- letta/prompts/system/sleeptime_v2.txt +28 -0
- letta/schemas/agent.py +87 -20
- letta/schemas/block.py +7 -1
- letta/schemas/file.py +57 -0
- letta/schemas/mcp.py +74 -0
- letta/schemas/memory.py +5 -2
- letta/schemas/message.py +9 -0
- letta/schemas/openai/openai.py +0 -6
- letta/schemas/providers.py +33 -4
- letta/schemas/tool.py +26 -21
- letta/schemas/tool_execution_result.py +5 -0
- letta/server/db.py +23 -8
- letta/server/rest_api/app.py +73 -56
- letta/server/rest_api/interface.py +4 -4
- letta/server/rest_api/routers/v1/agents.py +132 -47
- letta/server/rest_api/routers/v1/blocks.py +3 -2
- letta/server/rest_api/routers/v1/embeddings.py +3 -3
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/jobs.py +14 -17
- letta/server/rest_api/routers/v1/organizations.py +10 -10
- letta/server/rest_api/routers/v1/providers.py +12 -10
- letta/server/rest_api/routers/v1/runs.py +3 -3
- letta/server/rest_api/routers/v1/sandbox_configs.py +12 -12
- letta/server/rest_api/routers/v1/sources.py +108 -43
- letta/server/rest_api/routers/v1/steps.py +8 -6
- letta/server/rest_api/routers/v1/tools.py +134 -95
- letta/server/rest_api/utils.py +12 -1
- letta/server/server.py +272 -73
- letta/services/agent_manager.py +246 -313
- letta/services/block_manager.py +30 -9
- letta/services/context_window_calculator/__init__.py +0 -0
- letta/services/context_window_calculator/context_window_calculator.py +150 -0
- letta/services/context_window_calculator/token_counter.py +82 -0
- letta/services/file_processor/__init__.py +0 -0
- letta/services/file_processor/chunker/__init__.py +0 -0
- letta/services/file_processor/chunker/llama_index_chunker.py +29 -0
- letta/services/file_processor/embedder/__init__.py +0 -0
- letta/services/file_processor/embedder/openai_embedder.py +84 -0
- letta/services/file_processor/file_processor.py +123 -0
- letta/services/file_processor/parser/__init__.py +0 -0
- letta/services/file_processor/parser/base_parser.py +9 -0
- letta/services/file_processor/parser/mistral_parser.py +54 -0
- letta/services/file_processor/types.py +0 -0
- letta/services/files_agents_manager.py +184 -0
- letta/services/group_manager.py +118 -0
- letta/services/helpers/agent_manager_helper.py +76 -21
- letta/services/helpers/tool_execution_helper.py +3 -0
- letta/services/helpers/tool_parser_helper.py +100 -0
- letta/services/identity_manager.py +44 -42
- letta/services/job_manager.py +21 -10
- letta/services/mcp/base_client.py +5 -2
- letta/services/mcp/sse_client.py +3 -5
- letta/services/mcp/stdio_client.py +3 -5
- letta/services/mcp_manager.py +281 -0
- letta/services/message_manager.py +40 -26
- letta/services/organization_manager.py +55 -19
- letta/services/passage_manager.py +211 -13
- letta/services/provider_manager.py +48 -2
- letta/services/sandbox_config_manager.py +105 -0
- letta/services/source_manager.py +4 -5
- letta/services/step_manager.py +9 -6
- letta/services/summarizer/summarizer.py +50 -23
- letta/services/telemetry_manager.py +7 -0
- letta/services/tool_executor/tool_execution_manager.py +11 -52
- letta/services/tool_executor/tool_execution_sandbox.py +4 -34
- letta/services/tool_executor/tool_executor.py +107 -105
- letta/services/tool_manager.py +56 -17
- letta/services/tool_sandbox/base.py +39 -92
- letta/services/tool_sandbox/e2b_sandbox.py +16 -11
- letta/services/tool_sandbox/local_sandbox.py +51 -23
- letta/services/user_manager.py +36 -3
- letta/settings.py +10 -3
- letta/templates/__init__.py +0 -0
- letta/templates/sandbox_code_file.py.j2 +47 -0
- letta/templates/template_helper.py +16 -0
- letta/tracing.py +30 -1
- letta/types/__init__.py +7 -0
- letta/utils.py +25 -1
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/METADATA +7 -2
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/RECORD +138 -112
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/entry_points.txt +0 -0
letta/services/agent_manager.py
CHANGED
@@ -5,12 +5,12 @@ from typing import Dict, List, Optional, Set, Tuple
|
|
5
5
|
|
6
6
|
import numpy as np
|
7
7
|
import sqlalchemy as sa
|
8
|
-
from openai.types.beta.function_tool import FunctionTool as OpenAITool
|
9
8
|
from sqlalchemy import Select, and_, delete, func, insert, literal, or_, select, union_all
|
10
9
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
11
10
|
|
12
11
|
from letta.constants import (
|
13
12
|
BASE_MEMORY_TOOLS,
|
13
|
+
BASE_MEMORY_TOOLS_V2,
|
14
14
|
BASE_SLEEPTIME_CHAT_TOOLS,
|
15
15
|
BASE_SLEEPTIME_TOOLS,
|
16
16
|
BASE_TOOLS,
|
@@ -42,13 +42,13 @@ from letta.orm.sqlalchemy_base import AccessType
|
|
42
42
|
from letta.orm.sqlite_functions import adapt_array
|
43
43
|
from letta.schemas.agent import AgentState as PydanticAgentState
|
44
44
|
from letta.schemas.agent import AgentType, CreateAgent, UpdateAgent, get_prompt_template_for_agent_type
|
45
|
+
from letta.schemas.block import DEFAULT_BLOCKS
|
45
46
|
from letta.schemas.block import Block as PydanticBlock
|
46
47
|
from letta.schemas.block import BlockUpdate
|
47
48
|
from letta.schemas.embedding_config import EmbeddingConfig
|
48
|
-
from letta.schemas.enums import
|
49
|
+
from letta.schemas.enums import ProviderType
|
49
50
|
from letta.schemas.group import Group as PydanticGroup
|
50
51
|
from letta.schemas.group import ManagerType
|
51
|
-
from letta.schemas.letta_message_content import TextContent
|
52
52
|
from letta.schemas.memory import ContextWindowOverview, Memory
|
53
53
|
from letta.schemas.message import Message
|
54
54
|
from letta.schemas.message import Message as PydanticMessage
|
@@ -64,6 +64,8 @@ from letta.serialize_schemas.marshmallow_tool import SerializedToolSchema
|
|
64
64
|
from letta.serialize_schemas.pydantic_agent_schema import AgentSchema
|
65
65
|
from letta.server.db import db_registry
|
66
66
|
from letta.services.block_manager import BlockManager
|
67
|
+
from letta.services.context_window_calculator.context_window_calculator import ContextWindowCalculator
|
68
|
+
from letta.services.context_window_calculator.token_counter import AnthropicTokenCounter, TiktokenCounter
|
67
69
|
from letta.services.helpers.agent_manager_helper import (
|
68
70
|
_apply_filters,
|
69
71
|
_apply_identity_filters,
|
@@ -71,6 +73,7 @@ from letta.services.helpers.agent_manager_helper import (
|
|
71
73
|
_apply_pagination_async,
|
72
74
|
_apply_tag_filter,
|
73
75
|
_process_relationship,
|
76
|
+
_process_relationship_async,
|
74
77
|
check_supports_structured_output,
|
75
78
|
compile_system_message,
|
76
79
|
derive_system_message,
|
@@ -84,7 +87,7 @@ from letta.services.source_manager import SourceManager
|
|
84
87
|
from letta.services.tool_manager import ToolManager
|
85
88
|
from letta.settings import settings
|
86
89
|
from letta.tracing import trace_method
|
87
|
-
from letta.utils import
|
90
|
+
from letta.utils import enforce_types, united_diff
|
88
91
|
|
89
92
|
logger = get_logger(__name__)
|
90
93
|
|
@@ -255,6 +258,8 @@ class AgentManager:
|
|
255
258
|
tool_names |= set(BASE_SLEEPTIME_TOOLS)
|
256
259
|
elif agent_create.enable_sleeptime:
|
257
260
|
tool_names |= set(BASE_SLEEPTIME_CHAT_TOOLS)
|
261
|
+
elif agent_create.agent_type == AgentType.memgpt_v2_agent:
|
262
|
+
tool_names |= set(BASE_TOOLS + BASE_MEMORY_TOOLS_V2)
|
258
263
|
else:
|
259
264
|
tool_names |= set(BASE_TOOLS + BASE_MEMORY_TOOLS)
|
260
265
|
if agent_create.include_multi_agent_tools:
|
@@ -386,8 +391,19 @@ class AgentManager:
|
|
386
391
|
# blocks
|
387
392
|
block_ids = list(agent_create.block_ids or [])
|
388
393
|
if agent_create.memory_blocks:
|
394
|
+
|
389
395
|
pydantic_blocks = [PydanticBlock(**b.model_dump(to_orm=True)) for b in agent_create.memory_blocks]
|
390
|
-
|
396
|
+
|
397
|
+
# Inject a description for the default blocks if the user didn't specify them
|
398
|
+
# Used for `persona`, `human`, etc
|
399
|
+
default_blocks = {block.label: block for block in DEFAULT_BLOCKS}
|
400
|
+
for block in pydantic_blocks:
|
401
|
+
if block.label in default_blocks:
|
402
|
+
if block.description is None:
|
403
|
+
block.description = default_blocks[block.label].description
|
404
|
+
|
405
|
+
# Actually create the blocks
|
406
|
+
created_blocks = await self.block_manager.batch_create_blocks_async(
|
391
407
|
pydantic_blocks,
|
392
408
|
actor=actor,
|
393
409
|
)
|
@@ -404,6 +420,8 @@ class AgentManager:
|
|
404
420
|
tool_names |= set(BASE_SLEEPTIME_TOOLS)
|
405
421
|
elif agent_create.enable_sleeptime:
|
406
422
|
tool_names |= set(BASE_SLEEPTIME_CHAT_TOOLS)
|
423
|
+
elif agent_create.agent_type == AgentType.memgpt_v2_agent:
|
424
|
+
tool_names |= set(BASE_TOOLS + BASE_MEMORY_TOOLS_V2)
|
407
425
|
else:
|
408
426
|
tool_names |= set(BASE_TOOLS + BASE_MEMORY_TOOLS)
|
409
427
|
if agent_create.include_multi_agent_tools:
|
@@ -433,7 +451,7 @@ class AgentManager:
|
|
433
451
|
for tn in tool_names:
|
434
452
|
if tn in {"send_message", "send_message_to_agent_async", "memory_finish_edits"}:
|
435
453
|
tool_rules.append(TerminalToolRule(tool_name=tn))
|
436
|
-
elif tn in (BASE_TOOLS + BASE_MEMORY_TOOLS + BASE_SLEEPTIME_TOOLS):
|
454
|
+
elif tn in (BASE_TOOLS + BASE_MEMORY_TOOLS + BASE_MEMORY_TOOLS_V2 + BASE_SLEEPTIME_TOOLS):
|
437
455
|
tool_rules.append(ContinueToolRule(tool_name=tn))
|
438
456
|
|
439
457
|
if tool_rules:
|
@@ -520,11 +538,10 @@ class AgentManager:
|
|
520
538
|
new_agent.message_ids = [msg.id for msg in init_messages]
|
521
539
|
|
522
540
|
await session.refresh(new_agent)
|
541
|
+
result = await new_agent.to_pydantic_async()
|
523
542
|
|
524
|
-
# Using the synchronous version since we don't have an async version yet
|
525
|
-
# If you implement an async version of create_many_messages, you can switch to that
|
526
543
|
await self.message_manager.create_many_messages_async(pydantic_msgs=init_messages, actor=actor)
|
527
|
-
return
|
544
|
+
return result
|
528
545
|
|
529
546
|
@enforce_types
|
530
547
|
def _generate_initial_message_sequence(
|
@@ -561,6 +578,14 @@ class AgentManager:
|
|
561
578
|
init_messages = self._generate_initial_message_sequence(actor, agent_state, initial_message_sequence)
|
562
579
|
return self.append_to_in_context_messages(init_messages, agent_id=agent_state.id, actor=actor)
|
563
580
|
|
581
|
+
@trace_method
|
582
|
+
@enforce_types
|
583
|
+
async def append_initial_message_sequence_to_in_context_messages_async(
|
584
|
+
self, actor: PydanticUser, agent_state: PydanticAgentState, initial_message_sequence: Optional[List[MessageCreate]] = None
|
585
|
+
) -> PydanticAgentState:
|
586
|
+
init_messages = self._generate_initial_message_sequence(actor, agent_state, initial_message_sequence)
|
587
|
+
return await self.append_to_in_context_messages_async(init_messages, agent_id=agent_state.id, actor=actor)
|
588
|
+
|
564
589
|
@trace_method
|
565
590
|
@enforce_types
|
566
591
|
def update_agent(
|
@@ -962,6 +987,50 @@ class AgentManager:
|
|
962
987
|
|
963
988
|
return list(session.execute(query).scalars())
|
964
989
|
|
990
|
+
@enforce_types
|
991
|
+
@trace_method
|
992
|
+
async def list_agents_matching_tags_async(
|
993
|
+
self,
|
994
|
+
actor: PydanticUser,
|
995
|
+
match_all: List[str],
|
996
|
+
match_some: List[str],
|
997
|
+
limit: Optional[int] = 50,
|
998
|
+
) -> List[PydanticAgentState]:
|
999
|
+
"""
|
1000
|
+
Retrieves agents in the same organization that match all specified `match_all` tags
|
1001
|
+
and at least one tag from `match_some`. The query is optimized for efficiency by
|
1002
|
+
leveraging indexed filtering and aggregation.
|
1003
|
+
|
1004
|
+
Args:
|
1005
|
+
actor (PydanticUser): The user requesting the agent list.
|
1006
|
+
match_all (List[str]): Agents must have all these tags.
|
1007
|
+
match_some (List[str]): Agents must have at least one of these tags.
|
1008
|
+
limit (Optional[int]): Maximum number of agents to return.
|
1009
|
+
|
1010
|
+
Returns:
|
1011
|
+
List[PydanticAgentState: The filtered list of matching agents.
|
1012
|
+
"""
|
1013
|
+
async with db_registry.async_session() as session:
|
1014
|
+
query = select(AgentModel).where(AgentModel.organization_id == actor.organization_id)
|
1015
|
+
|
1016
|
+
if match_all:
|
1017
|
+
# Subquery to find agent IDs that contain all match_all tags
|
1018
|
+
subquery = (
|
1019
|
+
select(AgentsTags.agent_id)
|
1020
|
+
.where(AgentsTags.tag.in_(match_all))
|
1021
|
+
.group_by(AgentsTags.agent_id)
|
1022
|
+
.having(func.count(AgentsTags.tag) == literal(len(match_all)))
|
1023
|
+
)
|
1024
|
+
query = query.where(AgentModel.id.in_(subquery))
|
1025
|
+
|
1026
|
+
if match_some:
|
1027
|
+
# Ensures agents match at least one tag in match_some
|
1028
|
+
query = query.join(AgentsTags).where(AgentsTags.tag.in_(match_some))
|
1029
|
+
|
1030
|
+
query = query.distinct(AgentModel.id).order_by(AgentModel.id).limit(limit)
|
1031
|
+
result = await session.execute(query)
|
1032
|
+
return await asyncio.gather(*[agent.to_pydantic_async() for agent in result.scalars()])
|
1033
|
+
|
965
1034
|
@trace_method
|
966
1035
|
def size(
|
967
1036
|
self,
|
@@ -1286,12 +1355,6 @@ class AgentManager:
|
|
1286
1355
|
message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
|
1287
1356
|
return self.message_manager.get_messages_by_ids(message_ids=message_ids, actor=actor)
|
1288
1357
|
|
1289
|
-
@trace_method
|
1290
|
-
@enforce_types
|
1291
|
-
async def get_in_context_messages_async(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
|
1292
|
-
agent = await self.get_agent_by_id_async(agent_id=agent_id, include_relationships=[], actor=actor)
|
1293
|
-
return await self.message_manager.get_messages_by_ids_async(message_ids=agent.message_ids, actor=actor)
|
1294
|
-
|
1295
1358
|
@trace_method
|
1296
1359
|
@enforce_types
|
1297
1360
|
def get_system_message(self, agent_id: str, actor: PydanticUser) -> PydanticMessage:
|
@@ -1349,7 +1412,6 @@ class AgentManager:
|
|
1349
1412
|
system_prompt=agent_state.system,
|
1350
1413
|
in_context_memory=agent_state.memory,
|
1351
1414
|
in_context_memory_last_edit=memory_edit_timestamp,
|
1352
|
-
recent_passages=self.list_passages(actor=actor, agent_id=agent_id, ascending=False, limit=10),
|
1353
1415
|
previous_message_count=num_messages,
|
1354
1416
|
archival_memory_size=num_archival_memories,
|
1355
1417
|
)
|
@@ -1417,7 +1479,6 @@ class AgentManager:
|
|
1417
1479
|
system_prompt=agent_state.system,
|
1418
1480
|
in_context_memory=agent_state.memory,
|
1419
1481
|
in_context_memory_last_edit=memory_edit_timestamp,
|
1420
|
-
recent_passages=self.list_passages(actor=actor, agent_id=agent_id, ascending=False, limit=10),
|
1421
1482
|
previous_message_count=num_messages,
|
1422
1483
|
archival_memory_size=num_archival_memories,
|
1423
1484
|
)
|
@@ -1484,7 +1545,20 @@ class AgentManager:
|
|
1484
1545
|
|
1485
1546
|
@trace_method
|
1486
1547
|
@enforce_types
|
1487
|
-
def
|
1548
|
+
async def append_to_in_context_messages_async(
|
1549
|
+
self, messages: List[PydanticMessage], agent_id: str, actor: PydanticUser
|
1550
|
+
) -> PydanticAgentState:
|
1551
|
+
messages = await self.message_manager.create_many_messages_async(messages, actor=actor)
|
1552
|
+
agent = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor)
|
1553
|
+
message_ids = agent.message_ids or []
|
1554
|
+
message_ids += [m.id for m in messages]
|
1555
|
+
return await self.set_in_context_messages_async(agent_id=agent_id, message_ids=message_ids, actor=actor)
|
1556
|
+
|
1557
|
+
@trace_method
|
1558
|
+
@enforce_types
|
1559
|
+
async def reset_messages_async(
|
1560
|
+
self, agent_id: str, actor: PydanticUser, add_default_initial_messages: bool = False
|
1561
|
+
) -> PydanticAgentState:
|
1488
1562
|
"""
|
1489
1563
|
Removes all in-context messages for the specified agent by:
|
1490
1564
|
1) Clearing the agent.messages relationship (which cascades delete-orphans).
|
@@ -1501,22 +1575,22 @@ class AgentManager:
|
|
1501
1575
|
Returns:
|
1502
1576
|
PydanticAgentState: The updated agent state with no linked messages.
|
1503
1577
|
"""
|
1504
|
-
with db_registry.
|
1578
|
+
async with db_registry.async_session() as session:
|
1505
1579
|
# Retrieve the existing agent (will raise NoResultFound if invalid)
|
1506
|
-
agent = AgentModel.
|
1580
|
+
agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
1507
1581
|
|
1508
1582
|
# Also clear out the message_ids field to keep in-context memory consistent
|
1509
1583
|
agent.message_ids = []
|
1510
1584
|
|
1511
1585
|
# Commit the update
|
1512
|
-
agent.
|
1586
|
+
await agent.update_async(db_session=session, actor=actor)
|
1513
1587
|
|
1514
|
-
agent_state = agent.
|
1588
|
+
agent_state = await agent.to_pydantic_async()
|
1515
1589
|
|
1516
|
-
self.message_manager.
|
1590
|
+
await self.message_manager.delete_all_messages_for_agent_async(agent_id=agent_id, actor=actor)
|
1517
1591
|
|
1518
1592
|
if add_default_initial_messages:
|
1519
|
-
return self.
|
1593
|
+
return await self.append_initial_message_sequence_to_in_context_messages_async(actor, agent_state)
|
1520
1594
|
else:
|
1521
1595
|
# We still want to always have a system message
|
1522
1596
|
init_messages = initialize_message_sequence(
|
@@ -1527,12 +1601,11 @@ class AgentManager:
|
|
1527
1601
|
model=agent_state.llm_config.model,
|
1528
1602
|
openai_message_dict=init_messages[0],
|
1529
1603
|
)
|
1530
|
-
return self.
|
1604
|
+
return await self.append_to_in_context_messages_async([system_message], agent_id=agent_state.id, actor=actor)
|
1531
1605
|
|
1532
|
-
# TODO: I moved this from agent.py - replace all mentions of this with the agent_manager version
|
1533
1606
|
@trace_method
|
1534
1607
|
@enforce_types
|
1535
|
-
def
|
1608
|
+
async def update_memory_if_changed_async(self, agent_id: str, new_memory: Memory, actor: PydanticUser) -> PydanticAgentState:
|
1536
1609
|
"""
|
1537
1610
|
Update internal memory object and system prompt if there have been modifications.
|
1538
1611
|
|
@@ -1544,8 +1617,8 @@ class AgentManager:
|
|
1544
1617
|
Returns:
|
1545
1618
|
modified (bool): whether the memory was updated
|
1546
1619
|
"""
|
1547
|
-
agent_state = self.
|
1548
|
-
system_message = self.message_manager.
|
1620
|
+
agent_state = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor)
|
1621
|
+
system_message = await self.message_manager.get_message_by_id_async(message_id=agent_state.message_ids[0], actor=actor)
|
1549
1622
|
if new_memory.compile() not in system_message.content[0].text:
|
1550
1623
|
# update the blocks (LRW) in the DB
|
1551
1624
|
for label in agent_state.memory.list_block_labels():
|
@@ -1553,18 +1626,24 @@ class AgentManager:
|
|
1553
1626
|
if updated_value != agent_state.memory.get_block(label).value:
|
1554
1627
|
# update the block if it's changed
|
1555
1628
|
block_id = agent_state.memory.get_block(label).id
|
1556
|
-
self.block_manager.
|
1629
|
+
await self.block_manager.update_block_async(
|
1630
|
+
block_id=block_id, block_update=BlockUpdate(value=updated_value), actor=actor
|
1631
|
+
)
|
1557
1632
|
|
1558
1633
|
# refresh memory from DB (using block ids)
|
1634
|
+
blocks = await asyncio.gather(
|
1635
|
+
*[self.block_manager.get_block_by_id_async(block.id, actor=actor) for block in agent_state.memory.get_blocks()]
|
1636
|
+
)
|
1559
1637
|
agent_state.memory = Memory(
|
1560
|
-
blocks=
|
1638
|
+
blocks=blocks,
|
1639
|
+
file_blocks=agent_state.memory.file_blocks,
|
1561
1640
|
prompt_template=get_prompt_template_for_agent_type(agent_state.agent_type),
|
1562
1641
|
)
|
1563
1642
|
|
1564
1643
|
# NOTE: don't do this since re-buildin the memory is handled at the start of the step
|
1565
1644
|
# rebuild memory - this records the last edited timestamp of the memory
|
1566
1645
|
# TODO: pass in update timestamp from block edit time
|
1567
|
-
agent_state = self.
|
1646
|
+
agent_state = await self.rebuild_system_prompt_async(agent_id=agent_id, actor=actor)
|
1568
1647
|
|
1569
1648
|
return agent_state
|
1570
1649
|
|
@@ -1575,9 +1654,8 @@ class AgentManager:
|
|
1575
1654
|
if not block_ids:
|
1576
1655
|
return agent_state
|
1577
1656
|
|
1578
|
-
|
1579
|
-
|
1580
|
-
)
|
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]
|
1581
1659
|
return agent_state
|
1582
1660
|
|
1583
1661
|
# ======================================================================================================================
|
@@ -1585,7 +1663,7 @@ class AgentManager:
|
|
1585
1663
|
# ======================================================================================================================
|
1586
1664
|
@trace_method
|
1587
1665
|
@enforce_types
|
1588
|
-
def
|
1666
|
+
async def attach_source_async(self, agent_id: str, source_id: str, actor: PydanticUser) -> PydanticAgentState:
|
1589
1667
|
"""
|
1590
1668
|
Attaches a source to an agent.
|
1591
1669
|
|
@@ -1599,12 +1677,12 @@ class AgentManager:
|
|
1599
1677
|
IntegrityError: If the source is already attached to the agent
|
1600
1678
|
"""
|
1601
1679
|
|
1602
|
-
with db_registry.
|
1680
|
+
async with db_registry.async_session() as session:
|
1603
1681
|
# Verify both agent and source exist and user has permission to access them
|
1604
|
-
agent = AgentModel.
|
1682
|
+
agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
1605
1683
|
|
1606
1684
|
# The _process_relationship helper already handles duplicate checking via unique constraint
|
1607
|
-
|
1685
|
+
await _process_relationship_async(
|
1608
1686
|
session=session,
|
1609
1687
|
agent=agent,
|
1610
1688
|
relationship_name="sources",
|
@@ -1615,18 +1693,18 @@ class AgentManager:
|
|
1615
1693
|
)
|
1616
1694
|
|
1617
1695
|
# Commit the changes
|
1618
|
-
agent.
|
1696
|
+
await agent.update_async(session, actor=actor)
|
1619
1697
|
|
1620
1698
|
# Force rebuild of system prompt so that the agent is updated with passage count
|
1621
1699
|
# and recent passages and add system message alert to agent
|
1622
|
-
self.
|
1623
|
-
self.
|
1700
|
+
pydantic_agent = await self.rebuild_system_prompt_async(agent_id=agent_id, actor=actor, force=True)
|
1701
|
+
await self.append_system_message_async(
|
1624
1702
|
agent_id=agent_id,
|
1625
1703
|
content=DATA_SOURCE_ATTACH_ALERT,
|
1626
1704
|
actor=actor,
|
1627
1705
|
)
|
1628
1706
|
|
1629
|
-
return
|
1707
|
+
return pydantic_agent
|
1630
1708
|
|
1631
1709
|
@trace_method
|
1632
1710
|
@enforce_types
|
@@ -1641,6 +1719,19 @@ class AgentManager:
|
|
1641
1719
|
# update agent in-context message IDs
|
1642
1720
|
self.append_to_in_context_messages(messages=[message], agent_id=agent_id, actor=actor)
|
1643
1721
|
|
1722
|
+
@trace_method
|
1723
|
+
@enforce_types
|
1724
|
+
async def append_system_message_async(self, agent_id: str, content: str, actor: PydanticUser):
|
1725
|
+
|
1726
|
+
# get the agent
|
1727
|
+
agent = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor)
|
1728
|
+
message = PydanticMessage.dict_to_message(
|
1729
|
+
agent_id=agent.id, model=agent.llm_config.model, openai_message_dict={"role": "system", "content": content}
|
1730
|
+
)
|
1731
|
+
|
1732
|
+
# update agent in-context message IDs
|
1733
|
+
await self.append_to_in_context_messages_async(messages=[message], agent_id=agent_id, actor=actor)
|
1734
|
+
|
1644
1735
|
@trace_method
|
1645
1736
|
@enforce_types
|
1646
1737
|
def list_attached_sources(self, agent_id: str, actor: PydanticUser) -> List[PydanticSource]:
|
@@ -1709,6 +1800,34 @@ class AgentManager:
|
|
1709
1800
|
agent.update(session, actor=actor)
|
1710
1801
|
return agent.to_pydantic()
|
1711
1802
|
|
1803
|
+
@trace_method
|
1804
|
+
@enforce_types
|
1805
|
+
async def detach_source_async(self, agent_id: str, source_id: str, actor: PydanticUser) -> PydanticAgentState:
|
1806
|
+
"""
|
1807
|
+
Detaches a source from an agent.
|
1808
|
+
|
1809
|
+
Args:
|
1810
|
+
agent_id: ID of the agent to detach the source from
|
1811
|
+
source_id: ID of the source to detach
|
1812
|
+
actor: User performing the action
|
1813
|
+
"""
|
1814
|
+
async with db_registry.async_session() as session:
|
1815
|
+
# Verify agent exists and user has permission to access it
|
1816
|
+
agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
1817
|
+
|
1818
|
+
# Remove the source from the relationship
|
1819
|
+
remaining_sources = [s for s in agent.sources if s.id != source_id]
|
1820
|
+
|
1821
|
+
if len(remaining_sources) == len(agent.sources): # Source ID was not in the relationship
|
1822
|
+
logger.warning(f"Attempted to remove unattached source id={source_id} from agent id={agent_id} by actor={actor}")
|
1823
|
+
|
1824
|
+
# Update the sources relationship
|
1825
|
+
agent.sources = remaining_sources
|
1826
|
+
|
1827
|
+
# Commit the changes
|
1828
|
+
await agent.update_async(session, actor=actor)
|
1829
|
+
return await agent.to_pydantic_async()
|
1830
|
+
|
1712
1831
|
# ======================================================================================================================
|
1713
1832
|
# Block management
|
1714
1833
|
# ======================================================================================================================
|
@@ -2303,6 +2422,42 @@ class AgentManager:
|
|
2303
2422
|
agent.update(session, actor=actor)
|
2304
2423
|
return agent.to_pydantic()
|
2305
2424
|
|
2425
|
+
@trace_method
|
2426
|
+
@enforce_types
|
2427
|
+
async def attach_tool_async(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
|
2428
|
+
"""
|
2429
|
+
Attaches a tool to an agent.
|
2430
|
+
|
2431
|
+
Args:
|
2432
|
+
agent_id: ID of the agent to attach the tool to.
|
2433
|
+
tool_id: ID of the tool to attach.
|
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
|
+
async with db_registry.async_session() as session:
|
2443
|
+
# Verify the agent exists and user has permission to access it
|
2444
|
+
agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
2445
|
+
|
2446
|
+
# Use the _process_relationship helper to attach the tool
|
2447
|
+
await _process_relationship_async(
|
2448
|
+
session=session,
|
2449
|
+
agent=agent,
|
2450
|
+
relationship_name="tools",
|
2451
|
+
model_class=ToolModel,
|
2452
|
+
item_ids=[tool_id],
|
2453
|
+
allow_partial=False, # Ensure the tool exists
|
2454
|
+
replace=False, # Extend the existing tools
|
2455
|
+
)
|
2456
|
+
|
2457
|
+
# Commit and refresh the agent
|
2458
|
+
await agent.update_async(session, actor=actor)
|
2459
|
+
return await agent.to_pydantic_async()
|
2460
|
+
|
2306
2461
|
@trace_method
|
2307
2462
|
@enforce_types
|
2308
2463
|
def detach_tool(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
|
@@ -2337,6 +2492,40 @@ class AgentManager:
|
|
2337
2492
|
agent.update(session, actor=actor)
|
2338
2493
|
return agent.to_pydantic()
|
2339
2494
|
|
2495
|
+
@trace_method
|
2496
|
+
@enforce_types
|
2497
|
+
async def detach_tool_async(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
|
2498
|
+
"""
|
2499
|
+
Detaches a tool from an agent.
|
2500
|
+
|
2501
|
+
Args:
|
2502
|
+
agent_id: ID of the agent to detach the tool from.
|
2503
|
+
tool_id: ID of the tool to detach.
|
2504
|
+
actor: User performing the action.
|
2505
|
+
|
2506
|
+
Raises:
|
2507
|
+
NoResultFound: If the agent or tool is not found.
|
2508
|
+
|
2509
|
+
Returns:
|
2510
|
+
PydanticAgentState: The updated agent state.
|
2511
|
+
"""
|
2512
|
+
async with db_registry.async_session() as session:
|
2513
|
+
# Verify the agent exists and user has permission to access it
|
2514
|
+
agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
2515
|
+
|
2516
|
+
# Filter out the tool to be detached
|
2517
|
+
remaining_tools = [tool for tool in agent.tools if tool.id != tool_id]
|
2518
|
+
|
2519
|
+
if len(remaining_tools) == len(agent.tools): # Tool ID was not in the relationship
|
2520
|
+
logger.warning(f"Attempted to remove unattached tool id={tool_id} from agent id={agent_id} by actor={actor}")
|
2521
|
+
|
2522
|
+
# Update the tools relationship
|
2523
|
+
agent.tools = remaining_tools
|
2524
|
+
|
2525
|
+
# Commit and refresh the agent
|
2526
|
+
await agent.update_async(session, actor=actor)
|
2527
|
+
return await agent.to_pydantic_async()
|
2528
|
+
|
2340
2529
|
@trace_method
|
2341
2530
|
@enforce_types
|
2342
2531
|
def list_attached_tools(self, agent_id: str, actor: PydanticUser) -> List[PydanticTool]:
|
@@ -2430,280 +2619,24 @@ class AgentManager:
|
|
2430
2619
|
result = await session.execute(query)
|
2431
2620
|
# Extract the tag values from the result
|
2432
2621
|
results = [row[0] for row in result.all()]
|
2433
|
-
|
2622
|
+
return results
|
2434
2623
|
|
2435
2624
|
async def get_context_window(self, agent_id: str, actor: PydanticUser) -> ContextWindowOverview:
|
2436
|
-
|
2437
|
-
|
2438
|
-
return await self.get_context_window_from_tiktoken_async(agent_id=agent_id, actor=actor)
|
2439
|
-
|
2440
|
-
async def get_context_window_from_anthropic_async(self, agent_id: str, actor: PydanticUser) -> ContextWindowOverview:
|
2441
|
-
"""Get the context window of the agent"""
|
2442
|
-
agent_state = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor)
|
2443
|
-
anthropic_client = LLMClient.create(provider_type=ProviderType.anthropic, actor=actor)
|
2444
|
-
model = agent_state.llm_config.model if agent_state.llm_config.model_endpoint_type == "anthropic" else None
|
2445
|
-
|
2446
|
-
# Grab the in-context messages
|
2447
|
-
# conversion of messages to anthropic dict format, which is passed to the token counter
|
2448
|
-
(in_context_messages, passage_manager_size, message_manager_size) = await asyncio.gather(
|
2449
|
-
self.get_in_context_messages_async(agent_id=agent_id, actor=actor),
|
2450
|
-
self.passage_manager.size_async(actor=actor, agent_id=agent_id),
|
2451
|
-
self.message_manager.size_async(actor=actor, agent_id=agent_id),
|
2452
|
-
)
|
2453
|
-
in_context_messages_anthropic = [m.to_anthropic_dict() for m in in_context_messages]
|
2454
|
-
|
2455
|
-
# Extract system, memory and external summary
|
2456
|
-
if (
|
2457
|
-
len(in_context_messages) > 0
|
2458
|
-
and in_context_messages[0].role == MessageRole.system
|
2459
|
-
and in_context_messages[0].content
|
2460
|
-
and len(in_context_messages[0].content) == 1
|
2461
|
-
and isinstance(in_context_messages[0].content[0], TextContent)
|
2462
|
-
):
|
2463
|
-
system_message = in_context_messages[0].content[0].text
|
2464
|
-
|
2465
|
-
external_memory_marker_pos = system_message.find("###")
|
2466
|
-
core_memory_marker_pos = system_message.find("<", external_memory_marker_pos)
|
2467
|
-
if external_memory_marker_pos != -1 and core_memory_marker_pos != -1:
|
2468
|
-
system_prompt = system_message[:external_memory_marker_pos].strip()
|
2469
|
-
external_memory_summary = system_message[external_memory_marker_pos:core_memory_marker_pos].strip()
|
2470
|
-
core_memory = system_message[core_memory_marker_pos:].strip()
|
2471
|
-
else:
|
2472
|
-
# if no markers found, put everything in system message
|
2473
|
-
system_prompt = system_message
|
2474
|
-
external_memory_summary = None
|
2475
|
-
core_memory = None
|
2476
|
-
else:
|
2477
|
-
# if no system message, fall back on agent's system prompt
|
2478
|
-
system_prompt = agent_state.system
|
2479
|
-
external_memory_summary = None
|
2480
|
-
core_memory = None
|
2481
|
-
|
2482
|
-
num_tokens_system_coroutine = anthropic_client.count_tokens(model=model, messages=[{"role": "user", "content": system_prompt}])
|
2483
|
-
num_tokens_core_memory_coroutine = (
|
2484
|
-
anthropic_client.count_tokens(model=model, messages=[{"role": "user", "content": core_memory}])
|
2485
|
-
if core_memory
|
2486
|
-
else asyncio.sleep(0, result=0)
|
2487
|
-
)
|
2488
|
-
num_tokens_external_memory_summary_coroutine = (
|
2489
|
-
anthropic_client.count_tokens(model=model, messages=[{"role": "user", "content": external_memory_summary}])
|
2490
|
-
if external_memory_summary
|
2491
|
-
else asyncio.sleep(0, result=0)
|
2492
|
-
)
|
2625
|
+
agent_state = await self.rebuild_system_prompt_async(agent_id=agent_id, actor=actor, force=True)
|
2626
|
+
calculator = ContextWindowCalculator()
|
2493
2627
|
|
2494
|
-
|
2495
|
-
|
2496
|
-
|
2497
|
-
and in_context_messages[1].role == MessageRole.user
|
2498
|
-
and in_context_messages[1].content
|
2499
|
-
and len(in_context_messages[1].content) == 1
|
2500
|
-
and isinstance(in_context_messages[1].content[0], TextContent)
|
2501
|
-
# TODO remove hardcoding
|
2502
|
-
and "The following is a summary of the previous " in in_context_messages[1].content[0].text
|
2503
|
-
):
|
2504
|
-
# Summary message exists
|
2505
|
-
text_content = in_context_messages[1].content[0].text
|
2506
|
-
assert text_content is not None
|
2507
|
-
summary_memory = text_content
|
2508
|
-
num_tokens_summary_memory_coroutine = anthropic_client.count_tokens(
|
2509
|
-
model=model, messages=[{"role": "user", "content": summary_memory}]
|
2510
|
-
)
|
2511
|
-
# with a summary message, the real messages start at index 2
|
2512
|
-
num_tokens_messages_coroutine = (
|
2513
|
-
anthropic_client.count_tokens(model=model, messages=in_context_messages_anthropic[2:])
|
2514
|
-
if len(in_context_messages_anthropic) > 2
|
2515
|
-
else asyncio.sleep(0, result=0)
|
2516
|
-
)
|
2517
|
-
|
2518
|
-
else:
|
2519
|
-
summary_memory = None
|
2520
|
-
num_tokens_summary_memory_coroutine = asyncio.sleep(0, result=0)
|
2521
|
-
# with no summary message, the real messages start at index 1
|
2522
|
-
num_tokens_messages_coroutine = (
|
2523
|
-
anthropic_client.count_tokens(model=model, messages=in_context_messages_anthropic[1:])
|
2524
|
-
if len(in_context_messages_anthropic) > 1
|
2525
|
-
else asyncio.sleep(0, result=0)
|
2526
|
-
)
|
2628
|
+
if os.getenv("LETTA_ENVIRONMENT") == "PRODUCTION" or agent_state.llm_config.model_endpoint_type == "anthropic":
|
2629
|
+
anthropic_client = LLMClient.create(provider_type=ProviderType.anthropic, actor=actor)
|
2630
|
+
model = agent_state.llm_config.model if agent_state.llm_config.model_endpoint_type == "anthropic" else None
|
2527
2631
|
|
2528
|
-
|
2529
|
-
if agent_state.tools and len(agent_state.tools) > 0:
|
2530
|
-
available_functions_definitions = [OpenAITool(type="function", function=f.json_schema) for f in agent_state.tools]
|
2531
|
-
num_tokens_available_functions_definitions_coroutine = anthropic_client.count_tokens(
|
2532
|
-
model=model,
|
2533
|
-
tools=available_functions_definitions,
|
2534
|
-
)
|
2632
|
+
token_counter = AnthropicTokenCounter(anthropic_client, model) # noqa
|
2535
2633
|
else:
|
2536
|
-
|
2537
|
-
num_tokens_available_functions_definitions_coroutine = asyncio.sleep(0, result=0)
|
2538
|
-
|
2539
|
-
(
|
2540
|
-
num_tokens_system,
|
2541
|
-
num_tokens_core_memory,
|
2542
|
-
num_tokens_external_memory_summary,
|
2543
|
-
num_tokens_summary_memory,
|
2544
|
-
num_tokens_messages,
|
2545
|
-
num_tokens_available_functions_definitions,
|
2546
|
-
) = await asyncio.gather(
|
2547
|
-
num_tokens_system_coroutine,
|
2548
|
-
num_tokens_core_memory_coroutine,
|
2549
|
-
num_tokens_external_memory_summary_coroutine,
|
2550
|
-
num_tokens_summary_memory_coroutine,
|
2551
|
-
num_tokens_messages_coroutine,
|
2552
|
-
num_tokens_available_functions_definitions_coroutine,
|
2553
|
-
)
|
2554
|
-
|
2555
|
-
num_tokens_used_total = (
|
2556
|
-
num_tokens_system # system prompt
|
2557
|
-
+ num_tokens_available_functions_definitions # function definitions
|
2558
|
-
+ num_tokens_core_memory # core memory
|
2559
|
-
+ num_tokens_external_memory_summary # metadata (statistics) about recall/archival
|
2560
|
-
+ num_tokens_summary_memory # summary of ongoing conversation
|
2561
|
-
+ num_tokens_messages # tokens taken by messages
|
2562
|
-
)
|
2563
|
-
assert isinstance(num_tokens_used_total, int)
|
2564
|
-
|
2565
|
-
return ContextWindowOverview(
|
2566
|
-
# context window breakdown (in messages)
|
2567
|
-
num_messages=len(in_context_messages),
|
2568
|
-
num_archival_memory=passage_manager_size,
|
2569
|
-
num_recall_memory=message_manager_size,
|
2570
|
-
num_tokens_external_memory_summary=num_tokens_external_memory_summary,
|
2571
|
-
external_memory_summary=external_memory_summary,
|
2572
|
-
# top-level information
|
2573
|
-
context_window_size_max=agent_state.llm_config.context_window,
|
2574
|
-
context_window_size_current=num_tokens_used_total,
|
2575
|
-
# context window breakdown (in tokens)
|
2576
|
-
num_tokens_system=num_tokens_system,
|
2577
|
-
system_prompt=system_prompt,
|
2578
|
-
num_tokens_core_memory=num_tokens_core_memory,
|
2579
|
-
core_memory=core_memory,
|
2580
|
-
num_tokens_summary_memory=num_tokens_summary_memory,
|
2581
|
-
summary_memory=summary_memory,
|
2582
|
-
num_tokens_messages=num_tokens_messages,
|
2583
|
-
messages=in_context_messages,
|
2584
|
-
# related to functions
|
2585
|
-
num_tokens_functions_definitions=num_tokens_available_functions_definitions,
|
2586
|
-
functions_definitions=available_functions_definitions,
|
2587
|
-
)
|
2588
|
-
|
2589
|
-
async def get_context_window_from_tiktoken_async(self, agent_id: str, actor: PydanticUser) -> ContextWindowOverview:
|
2590
|
-
"""Get the context window of the agent"""
|
2591
|
-
from letta.local_llm.utils import num_tokens_from_functions, num_tokens_from_messages
|
2634
|
+
token_counter = TiktokenCounter(agent_state.llm_config.model)
|
2592
2635
|
|
2593
|
-
|
2594
|
-
|
2595
|
-
|
2596
|
-
|
2597
|
-
self.
|
2598
|
-
self.passage_manager
|
2599
|
-
self.message_manager.size_async(actor=actor, agent_id=agent_id),
|
2600
|
-
)
|
2601
|
-
in_context_messages_openai = [m.to_openai_dict() for m in in_context_messages]
|
2602
|
-
|
2603
|
-
# Extract system, memory and external summary
|
2604
|
-
if (
|
2605
|
-
len(in_context_messages) > 0
|
2606
|
-
and in_context_messages[0].role == MessageRole.system
|
2607
|
-
and in_context_messages[0].content
|
2608
|
-
and len(in_context_messages[0].content) == 1
|
2609
|
-
and isinstance(in_context_messages[0].content[0], TextContent)
|
2610
|
-
):
|
2611
|
-
system_message = in_context_messages[0].content[0].text
|
2612
|
-
|
2613
|
-
external_memory_marker_pos = system_message.find("###")
|
2614
|
-
core_memory_marker_pos = system_message.find("<", external_memory_marker_pos)
|
2615
|
-
if external_memory_marker_pos != -1 and core_memory_marker_pos != -1:
|
2616
|
-
system_prompt = system_message[:external_memory_marker_pos].strip()
|
2617
|
-
external_memory_summary = system_message[external_memory_marker_pos:core_memory_marker_pos].strip()
|
2618
|
-
core_memory = system_message[core_memory_marker_pos:].strip()
|
2619
|
-
else:
|
2620
|
-
# if no markers found, put everything in system message
|
2621
|
-
system_prompt = system_message
|
2622
|
-
external_memory_summary = ""
|
2623
|
-
core_memory = ""
|
2624
|
-
else:
|
2625
|
-
# if no system message, fall back on agent's system prompt
|
2626
|
-
system_prompt = agent_state.system
|
2627
|
-
external_memory_summary = ""
|
2628
|
-
core_memory = ""
|
2629
|
-
|
2630
|
-
num_tokens_system = count_tokens(system_prompt)
|
2631
|
-
num_tokens_core_memory = count_tokens(core_memory)
|
2632
|
-
num_tokens_external_memory_summary = count_tokens(external_memory_summary)
|
2633
|
-
|
2634
|
-
# Check if there's a summary message in the message queue
|
2635
|
-
if (
|
2636
|
-
len(in_context_messages) > 1
|
2637
|
-
and in_context_messages[1].role == MessageRole.user
|
2638
|
-
and in_context_messages[1].content
|
2639
|
-
and len(in_context_messages[1].content) == 1
|
2640
|
-
and isinstance(in_context_messages[1].content[0], TextContent)
|
2641
|
-
# TODO remove hardcoding
|
2642
|
-
and "The following is a summary of the previous " in in_context_messages[1].content[0].text
|
2643
|
-
):
|
2644
|
-
# Summary message exists
|
2645
|
-
text_content = in_context_messages[1].content[0].text
|
2646
|
-
assert text_content is not None
|
2647
|
-
summary_memory = text_content
|
2648
|
-
num_tokens_summary_memory = count_tokens(text_content)
|
2649
|
-
# with a summary message, the real messages start at index 2
|
2650
|
-
num_tokens_messages = (
|
2651
|
-
num_tokens_from_messages(messages=in_context_messages_openai[2:], model=agent_state.llm_config.model)
|
2652
|
-
if len(in_context_messages_openai) > 2
|
2653
|
-
else 0
|
2654
|
-
)
|
2655
|
-
|
2656
|
-
else:
|
2657
|
-
summary_memory = None
|
2658
|
-
num_tokens_summary_memory = 0
|
2659
|
-
# with no summary message, the real messages start at index 1
|
2660
|
-
num_tokens_messages = (
|
2661
|
-
num_tokens_from_messages(messages=in_context_messages_openai[1:], model=agent_state.llm_config.model)
|
2662
|
-
if len(in_context_messages_openai) > 1
|
2663
|
-
else 0
|
2664
|
-
)
|
2665
|
-
|
2666
|
-
# tokens taken up by function definitions
|
2667
|
-
agent_state_tool_jsons = [t.json_schema for t in agent_state.tools]
|
2668
|
-
if agent_state_tool_jsons:
|
2669
|
-
available_functions_definitions = [OpenAITool(type="function", function=f) for f in agent_state_tool_jsons]
|
2670
|
-
num_tokens_available_functions_definitions = num_tokens_from_functions(
|
2671
|
-
functions=agent_state_tool_jsons, model=agent_state.llm_config.model
|
2672
|
-
)
|
2673
|
-
else:
|
2674
|
-
available_functions_definitions = []
|
2675
|
-
num_tokens_available_functions_definitions = 0
|
2676
|
-
|
2677
|
-
num_tokens_used_total = (
|
2678
|
-
num_tokens_system # system prompt
|
2679
|
-
+ num_tokens_available_functions_definitions # function definitions
|
2680
|
-
+ num_tokens_core_memory # core memory
|
2681
|
-
+ num_tokens_external_memory_summary # metadata (statistics) about recall/archival
|
2682
|
-
+ num_tokens_summary_memory # summary of ongoing conversation
|
2683
|
-
+ num_tokens_messages # tokens taken by messages
|
2684
|
-
)
|
2685
|
-
assert isinstance(num_tokens_used_total, int)
|
2686
|
-
|
2687
|
-
return ContextWindowOverview(
|
2688
|
-
# context window breakdown (in messages)
|
2689
|
-
num_messages=len(in_context_messages),
|
2690
|
-
num_archival_memory=passage_manager_size,
|
2691
|
-
num_recall_memory=message_manager_size,
|
2692
|
-
num_tokens_external_memory_summary=num_tokens_external_memory_summary,
|
2693
|
-
external_memory_summary=external_memory_summary,
|
2694
|
-
# top-level information
|
2695
|
-
context_window_size_max=agent_state.llm_config.context_window,
|
2696
|
-
context_window_size_current=num_tokens_used_total,
|
2697
|
-
# context window breakdown (in tokens)
|
2698
|
-
num_tokens_system=num_tokens_system,
|
2699
|
-
system_prompt=system_prompt,
|
2700
|
-
num_tokens_core_memory=num_tokens_core_memory,
|
2701
|
-
core_memory=core_memory,
|
2702
|
-
num_tokens_summary_memory=num_tokens_summary_memory,
|
2703
|
-
summary_memory=summary_memory,
|
2704
|
-
num_tokens_messages=num_tokens_messages,
|
2705
|
-
messages=in_context_messages,
|
2706
|
-
# related to functions
|
2707
|
-
num_tokens_functions_definitions=num_tokens_available_functions_definitions,
|
2708
|
-
functions_definitions=available_functions_definitions,
|
2636
|
+
return await calculator.calculate_context_window(
|
2637
|
+
agent_state=agent_state,
|
2638
|
+
actor=actor,
|
2639
|
+
token_counter=token_counter,
|
2640
|
+
message_manager=self.message_manager,
|
2641
|
+
passage_manager=self.passage_manager,
|
2709
2642
|
)
|