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
@@ -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
|
|
@@ -82,28 +111,26 @@ class FileProcessor:
|
|
82
111
|
)
|
83
112
|
all_passages.extend(passages)
|
84
113
|
|
85
|
-
all_passages = await self.passage_manager.
|
114
|
+
all_passages = await self.passage_manager.create_many_source_passages_async(
|
115
|
+
passages=all_passages, file_metadata=file_metadata, actor=self.actor
|
116
|
+
)
|
86
117
|
|
87
118
|
logger.info(f"Successfully processed {filename}: {len(all_passages)} passages")
|
88
119
|
|
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
120
|
# update job status
|
98
121
|
if job:
|
99
122
|
job.status = JobStatus.completed
|
100
123
|
job.metadata["num_passages"] = len(all_passages)
|
101
124
|
await self.job_manager.update_job_by_id_async(job_id=job.id, job_update=JobUpdate(**job.model_dump()), actor=self.actor)
|
102
125
|
|
126
|
+
await self.source_manager.update_file_status(
|
127
|
+
file_id=file_metadata.id, actor=self.actor, processing_status=FileProcessingStatus.COMPLETED
|
128
|
+
)
|
129
|
+
|
103
130
|
return all_passages
|
104
131
|
|
105
132
|
except Exception as e:
|
106
|
-
logger.error(f"
|
133
|
+
logger.error(f"File processing failed for {filename}: {str(e)}")
|
107
134
|
|
108
135
|
# update job status
|
109
136
|
if job:
|
@@ -111,6 +138,10 @@ class FileProcessor:
|
|
111
138
|
job.metadata["error"] = str(e)
|
112
139
|
await self.job_manager.update_job_by_id_async(job_id=job.id, job_update=JobUpdate(**job.model_dump()), actor=self.actor)
|
113
140
|
|
141
|
+
await self.source_manager.update_file_status(
|
142
|
+
file_id=file_metadata.id, actor=self.actor, processing_status=FileProcessingStatus.ERROR, error_message=str(e)
|
143
|
+
)
|
144
|
+
|
114
145
|
return []
|
115
146
|
|
116
147
|
def _extract_upload_file_metadata(self, file: UploadFile, source_id: str) -> FileMetadata:
|
@@ -9,6 +9,16 @@ from letta.settings import settings
|
|
9
9
|
logger = get_logger(__name__)
|
10
10
|
|
11
11
|
|
12
|
+
SIMPLE_TEXT_MIME_TYPES = {
|
13
|
+
"text/plain",
|
14
|
+
"text/markdown",
|
15
|
+
"text/x-markdown",
|
16
|
+
"application/json",
|
17
|
+
"application/jsonl",
|
18
|
+
"application/x-jsonlines",
|
19
|
+
}
|
20
|
+
|
21
|
+
|
12
22
|
class MistralFileParser(FileParser):
|
13
23
|
"""Mistral-based OCR extraction"""
|
14
24
|
|
@@ -23,7 +33,7 @@ class MistralFileParser(FileParser):
|
|
23
33
|
|
24
34
|
# TODO: Kind of hacky...we try to exit early here?
|
25
35
|
# TODO: Create our internal file parser representation we return instead of OCRResponse
|
26
|
-
if mime_type
|
36
|
+
if mime_type in SIMPLE_TEXT_MIME_TYPES or mime_type.startswith("text/"):
|
27
37
|
text = content.decode("utf-8", errors="replace")
|
28
38
|
return OCRResponse(
|
29
39
|
model=self.model,
|
@@ -5,10 +5,11 @@ from sqlalchemy import and_, func, select, update
|
|
5
5
|
|
6
6
|
from letta.orm.errors import NoResultFound
|
7
7
|
from letta.orm.files_agents import FileAgent as FileAgentModel
|
8
|
+
from letta.otel.tracing import trace_method
|
9
|
+
from letta.schemas.block import Block as PydanticBlock
|
8
10
|
from letta.schemas.file import FileAgent as PydanticFileAgent
|
9
11
|
from letta.schemas.user import User as PydanticUser
|
10
12
|
from letta.server.db import db_registry
|
11
|
-
from letta.tracing import trace_method
|
12
13
|
from letta.utils import enforce_types
|
13
14
|
|
14
15
|
|
@@ -22,6 +23,7 @@ class FileAgentManager:
|
|
22
23
|
*,
|
23
24
|
agent_id: str,
|
24
25
|
file_id: str,
|
26
|
+
file_name: str,
|
25
27
|
actor: PydanticUser,
|
26
28
|
is_open: bool = True,
|
27
29
|
visible_content: Optional[str] = None,
|
@@ -38,6 +40,7 @@ class FileAgentManager:
|
|
38
40
|
and_(
|
39
41
|
FileAgentModel.agent_id == agent_id,
|
40
42
|
FileAgentModel.file_id == file_id,
|
43
|
+
FileAgentModel.file_name == file_name,
|
41
44
|
FileAgentModel.organization_id == actor.organization_id,
|
42
45
|
)
|
43
46
|
)
|
@@ -61,6 +64,7 @@ class FileAgentManager:
|
|
61
64
|
assoc = FileAgentModel(
|
62
65
|
agent_id=agent_id,
|
63
66
|
file_id=file_id,
|
67
|
+
file_name=file_name,
|
64
68
|
organization_id=actor.organization_id,
|
65
69
|
is_open=is_open,
|
66
70
|
visible_content=visible_content,
|
@@ -71,7 +75,7 @@ class FileAgentManager:
|
|
71
75
|
|
72
76
|
@enforce_types
|
73
77
|
@trace_method
|
74
|
-
async def
|
78
|
+
async def update_file_agent_by_id(
|
75
79
|
self,
|
76
80
|
*,
|
77
81
|
agent_id: str,
|
@@ -82,7 +86,33 @@ class FileAgentManager:
|
|
82
86
|
) -> PydanticFileAgent:
|
83
87
|
"""Patch an existing association row."""
|
84
88
|
async with db_registry.async_session() as session:
|
85
|
-
assoc = await self.
|
89
|
+
assoc = await self._get_association_by_file_id(session, agent_id, file_id, actor)
|
90
|
+
|
91
|
+
if is_open is not None:
|
92
|
+
assoc.is_open = is_open
|
93
|
+
if visible_content is not None:
|
94
|
+
assoc.visible_content = visible_content
|
95
|
+
|
96
|
+
# touch timestamp
|
97
|
+
assoc.last_accessed_at = datetime.now(timezone.utc)
|
98
|
+
|
99
|
+
await assoc.update_async(session, actor=actor)
|
100
|
+
return assoc.to_pydantic()
|
101
|
+
|
102
|
+
@enforce_types
|
103
|
+
@trace_method
|
104
|
+
async def update_file_agent_by_name(
|
105
|
+
self,
|
106
|
+
*,
|
107
|
+
agent_id: str,
|
108
|
+
file_name: str,
|
109
|
+
actor: PydanticUser,
|
110
|
+
is_open: Optional[bool] = None,
|
111
|
+
visible_content: Optional[str] = None,
|
112
|
+
) -> PydanticFileAgent:
|
113
|
+
"""Patch an existing association row."""
|
114
|
+
async with db_registry.async_session() as session:
|
115
|
+
assoc = await self._get_association_by_file_name(session, agent_id, file_name, actor)
|
86
116
|
|
87
117
|
if is_open is not None:
|
88
118
|
assoc.is_open = is_open
|
@@ -100,15 +130,61 @@ class FileAgentManager:
|
|
100
130
|
async def detach_file(self, *, agent_id: str, file_id: str, actor: PydanticUser) -> None:
|
101
131
|
"""Hard-delete the association."""
|
102
132
|
async with db_registry.async_session() as session:
|
103
|
-
assoc = await self.
|
133
|
+
assoc = await self._get_association_by_file_id(session, agent_id, file_id, actor)
|
104
134
|
await assoc.hard_delete_async(session, actor=actor)
|
105
135
|
|
106
136
|
@enforce_types
|
107
137
|
@trace_method
|
108
|
-
async def
|
138
|
+
async def get_file_agent_by_id(self, *, agent_id: str, file_id: str, actor: PydanticUser) -> Optional[PydanticFileAgent]:
|
139
|
+
async with db_registry.async_session() as session:
|
140
|
+
try:
|
141
|
+
assoc = await self._get_association_by_file_id(session, agent_id, file_id, actor)
|
142
|
+
return assoc.to_pydantic()
|
143
|
+
except NoResultFound:
|
144
|
+
return None
|
145
|
+
|
146
|
+
@enforce_types
|
147
|
+
@trace_method
|
148
|
+
async def get_all_file_blocks_by_name(
|
149
|
+
self,
|
150
|
+
*,
|
151
|
+
file_names: List[str],
|
152
|
+
actor: PydanticUser,
|
153
|
+
) -> List[PydanticBlock]:
|
154
|
+
"""
|
155
|
+
Retrieve multiple FileAgent associations by their IDs in a single query.
|
156
|
+
|
157
|
+
Args:
|
158
|
+
file_names: List of file names to retrieve
|
159
|
+
actor: The user making the request
|
160
|
+
|
161
|
+
Returns:
|
162
|
+
List of PydanticFileAgent objects found (may be fewer than requested if some IDs don't exist)
|
163
|
+
"""
|
164
|
+
if not file_names:
|
165
|
+
return []
|
166
|
+
|
167
|
+
async with db_registry.async_session() as session:
|
168
|
+
# Use IN clause for efficient bulk retrieval
|
169
|
+
query = select(FileAgentModel).where(
|
170
|
+
and_(
|
171
|
+
FileAgentModel.file_name.in_(file_names),
|
172
|
+
FileAgentModel.organization_id == actor.organization_id,
|
173
|
+
)
|
174
|
+
)
|
175
|
+
|
176
|
+
# Execute query and get all results
|
177
|
+
rows = (await session.execute(query)).scalars().all()
|
178
|
+
|
179
|
+
# Convert to Pydantic models
|
180
|
+
return [row.to_pydantic_block() for row in rows]
|
181
|
+
|
182
|
+
@enforce_types
|
183
|
+
@trace_method
|
184
|
+
async def get_file_agent_by_file_name(self, *, agent_id: str, file_name: str, actor: PydanticUser) -> Optional[PydanticFileAgent]:
|
109
185
|
async with db_registry.async_session() as session:
|
110
186
|
try:
|
111
|
-
assoc = await self.
|
187
|
+
assoc = await self._get_association_by_file_name(session, agent_id, file_name, actor)
|
112
188
|
return assoc.to_pydantic()
|
113
189
|
except NoResultFound:
|
114
190
|
return None
|
@@ -170,7 +246,7 @@ class FileAgentManager:
|
|
170
246
|
await session.execute(stmt)
|
171
247
|
await session.commit()
|
172
248
|
|
173
|
-
async def
|
249
|
+
async def _get_association_by_file_id(self, session, agent_id: str, file_id: str, actor: PydanticUser) -> FileAgentModel:
|
174
250
|
q = select(FileAgentModel).where(
|
175
251
|
and_(
|
176
252
|
FileAgentModel.agent_id == agent_id,
|
@@ -182,3 +258,16 @@ class FileAgentManager:
|
|
182
258
|
if not assoc:
|
183
259
|
raise NoResultFound(f"FileAgent(agent_id={agent_id}, file_id={file_id}) not found in org {actor.organization_id}")
|
184
260
|
return assoc
|
261
|
+
|
262
|
+
async def _get_association_by_file_name(self, session, agent_id: str, file_name: str, actor: PydanticUser) -> FileAgentModel:
|
263
|
+
q = select(FileAgentModel).where(
|
264
|
+
and_(
|
265
|
+
FileAgentModel.agent_id == agent_id,
|
266
|
+
FileAgentModel.file_name == file_name,
|
267
|
+
FileAgentModel.organization_id == actor.organization_id,
|
268
|
+
)
|
269
|
+
)
|
270
|
+
assoc = await session.scalar(q)
|
271
|
+
if not assoc:
|
272
|
+
raise NoResultFound(f"FileAgent(agent_id={agent_id}, file_name={file_name}) not found in org {actor.organization_id}")
|
273
|
+
return assoc
|
letta/services/group_manager.py
CHANGED
@@ -7,13 +7,13 @@ from letta.orm.agent import Agent as AgentModel
|
|
7
7
|
from letta.orm.errors import NoResultFound
|
8
8
|
from letta.orm.group import Group as GroupModel
|
9
9
|
from letta.orm.message import Message as MessageModel
|
10
|
+
from letta.otel.tracing import trace_method
|
10
11
|
from letta.schemas.group import Group as PydanticGroup
|
11
12
|
from letta.schemas.group import GroupCreate, GroupUpdate, ManagerType
|
12
13
|
from letta.schemas.letta_message import LettaMessage
|
13
14
|
from letta.schemas.message import Message as PydanticMessage
|
14
15
|
from letta.schemas.user import User as PydanticUser
|
15
16
|
from letta.server.db import db_registry
|
16
|
-
from letta.tracing import trace_method
|
17
17
|
from letta.utils import enforce_types
|
18
18
|
|
19
19
|
|
@@ -152,9 +152,9 @@ class GroupManager:
|
|
152
152
|
|
153
153
|
@trace_method
|
154
154
|
@enforce_types
|
155
|
-
def
|
156
|
-
with db_registry.
|
157
|
-
group = GroupModel.
|
155
|
+
async def modify_group_async(self, group_id: str, group_update: GroupUpdate, actor: PydanticUser) -> PydanticGroup:
|
156
|
+
async with db_registry.async_session() as session:
|
157
|
+
group = await GroupModel.read_async(db_session=session, identifier=group_id, actor=actor)
|
158
158
|
|
159
159
|
sleeptime_agent_frequency = None
|
160
160
|
max_message_buffer_length = None
|
@@ -206,11 +206,11 @@ class GroupManager:
|
|
206
206
|
if group_update.description:
|
207
207
|
group.description = group_update.description
|
208
208
|
if group_update.agent_ids:
|
209
|
-
self.
|
209
|
+
await self._process_agent_relationship_async(
|
210
210
|
session=session, group=group, agent_ids=group_update.agent_ids, allow_partial=False, replace=True
|
211
211
|
)
|
212
212
|
|
213
|
-
group.
|
213
|
+
await group.update_async(session, actor=actor)
|
214
214
|
return group.to_pydantic()
|
215
215
|
|
216
216
|
@trace_method
|