agently 4.0.7.1__py3-none-any.whl → 4.0.7.2__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.
- agently/_default_init.py +4 -0
- agently/_default_settings.yaml +1 -0
- agently/base.py +2 -0
- agently/builtins/agent_extensions/ChatSessionExtension.py +2 -2
- agently/builtins/agent_extensions/SessionExtension.py +294 -0
- agently/builtins/agent_extensions/__init__.py +1 -0
- agently/builtins/plugins/PromptGenerator/AgentlyPromptGenerator.py +36 -12
- agently/builtins/plugins/Session/AgentlyMemoSession.py +652 -0
- agently/builtins/tools/Browse.py +11 -3
- agently/builtins/tools/Cmd.py +112 -0
- agently/builtins/tools/Search.py +27 -1
- agently/builtins/tools/__init__.py +1 -0
- agently/core/Agent.py +7 -7
- agently/core/ModelRequest.py +0 -4
- agently/core/Prompt.py +1 -1
- agently/core/Session.py +85 -0
- agently/integrations/chromadb.py +4 -4
- agently/types/data/__init__.py +2 -0
- agently/types/data/prompt.py +6 -1
- agently/types/data/tool.py +9 -0
- agently/types/plugins/BuiltInTool.py +22 -0
- agently/types/plugins/Session.py +159 -0
- agently/types/plugins/__init__.py +21 -0
- agently/types/plugins/base.py +1 -1
- agently/utils/AGENT_UTILS_GUIDE.md +175 -0
- agently/utils/DataFormatter.py +6 -2
- agently/utils/FunctionShifter.py +3 -2
- agently/utils/TimeInfo.py +22 -0
- agently/utils/__init__.py +1 -0
- agently-4.0.7.2.dist-info/METADATA +433 -0
- {agently-4.0.7.1.dist-info → agently-4.0.7.2.dist-info}/RECORD +33 -25
- {agently-4.0.7.1.dist-info → agently-4.0.7.2.dist-info}/WHEEL +1 -1
- agently-4.0.7.1.dist-info/METADATA +0 -194
- {agently-4.0.7.1.dist-info → agently-4.0.7.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
# Copyright 2023-2025 AgentEra(Agently.Tech)
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Literal, TYPE_CHECKING, cast
|
|
19
|
+
import inspect
|
|
20
|
+
import json
|
|
21
|
+
import uuid
|
|
22
|
+
import yaml
|
|
23
|
+
|
|
24
|
+
from agently.utils import Settings, DataFormatter, DataLocator, FunctionShifter
|
|
25
|
+
from agently.core import ModelRequest
|
|
26
|
+
from agently.types.data import ChatMessage
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from agently.base import Agent
|
|
30
|
+
from agently.types.data import SerializableData, ChatMessageDict
|
|
31
|
+
from agently.types.plugins import (
|
|
32
|
+
MemoResizePolicyHandler,
|
|
33
|
+
MemoResizePolicyAsyncHandler,
|
|
34
|
+
MemoResizeHandler,
|
|
35
|
+
MemoResizeAsyncHandler,
|
|
36
|
+
MemoResizeDecision,
|
|
37
|
+
MemoResizePolicyResult,
|
|
38
|
+
AttachmentSummaryHandler,
|
|
39
|
+
AttachmentSummaryAsyncHandler,
|
|
40
|
+
SessionMode,
|
|
41
|
+
SessionLimit,
|
|
42
|
+
MemoUpdateHandler,
|
|
43
|
+
MemoUpdateAsyncHandler,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AgentlyMemoSession:
|
|
48
|
+
name = "AgentlyMemoSession"
|
|
49
|
+
DEFAULT_SETTINGS: dict[str, Any] = {}
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _on_register():
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _on_unregister():
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
policy_handler: "MemoResizePolicyHandler | None" = None,
|
|
63
|
+
resize_handlers: "dict[Literal['lite', 'deep'] | str, MemoResizeHandler] | None" = None,
|
|
64
|
+
attachment_summary_handler: "AttachmentSummaryHandler | None" = None,
|
|
65
|
+
memo_update_handler: "MemoUpdateHandler | None" = None,
|
|
66
|
+
parent_settings: Settings | None = None,
|
|
67
|
+
agent: "Agent | None" = None,
|
|
68
|
+
):
|
|
69
|
+
self.id = uuid.uuid4().hex
|
|
70
|
+
self.full_chat_history = []
|
|
71
|
+
self.current_chat_history = []
|
|
72
|
+
self.memo = {}
|
|
73
|
+
self._turns = 0
|
|
74
|
+
self._last_resize_turn = 0
|
|
75
|
+
self._memo_cursor = 0
|
|
76
|
+
|
|
77
|
+
self._policy_handler: MemoResizePolicyAsyncHandler = cast(
|
|
78
|
+
"MemoResizePolicyAsyncHandler",
|
|
79
|
+
FunctionShifter.asyncify(policy_handler or self._default_policy_handler),
|
|
80
|
+
)
|
|
81
|
+
if resize_handlers is None:
|
|
82
|
+
resize_handlers = {
|
|
83
|
+
"lite": self._default_lite_resize_handler,
|
|
84
|
+
"deep": self._default_deep_resize_handler,
|
|
85
|
+
}
|
|
86
|
+
self._resize_handlers: dict[Literal["lite", "deep"] | str, MemoResizeAsyncHandler] = {
|
|
87
|
+
key: cast("MemoResizeAsyncHandler", FunctionShifter.asyncify(handler))
|
|
88
|
+
for key, handler in resize_handlers.items()
|
|
89
|
+
}
|
|
90
|
+
self._attachment_summary_handler: AttachmentSummaryAsyncHandler = cast(
|
|
91
|
+
"AttachmentSummaryAsyncHandler",
|
|
92
|
+
FunctionShifter.asyncify(attachment_summary_handler or self._default_attachment_summary_handler),
|
|
93
|
+
)
|
|
94
|
+
self._memo_update_handler: MemoUpdateAsyncHandler = cast(
|
|
95
|
+
"MemoUpdateAsyncHandler",
|
|
96
|
+
FunctionShifter.asyncify(memo_update_handler or self._default_memo_update_handler),
|
|
97
|
+
)
|
|
98
|
+
self.settings = Settings(parent=parent_settings)
|
|
99
|
+
self._agent = agent
|
|
100
|
+
|
|
101
|
+
self.settings.setdefault("session.resize.every_n_turns", 8)
|
|
102
|
+
self.settings.setdefault("session.resize.max_messages_text_length", 12_000)
|
|
103
|
+
self.settings.setdefault("session.resize.max_keep_messages_count", None)
|
|
104
|
+
self.settings.setdefault("session.mode", "lite")
|
|
105
|
+
self.settings.setdefault(
|
|
106
|
+
"session.memo.instruct",
|
|
107
|
+
[
|
|
108
|
+
"Update the memo dictionary based on the provided messages.",
|
|
109
|
+
"Keep stable preferences, constraints, and facts that help future turns.",
|
|
110
|
+
"Add new keys or update existing ones as needed.",
|
|
111
|
+
"Return the updated memo dictionary.",
|
|
112
|
+
],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self.set_settings = self.settings.set_settings
|
|
116
|
+
self.judge_resize = FunctionShifter.syncify(self.async_judge_resize)
|
|
117
|
+
self.resize = FunctionShifter.syncify(self.async_resize)
|
|
118
|
+
|
|
119
|
+
def _is_memo_enabled(self) -> bool:
|
|
120
|
+
enabled = self.settings.get("session.memo.enabled", None)
|
|
121
|
+
if enabled is not None:
|
|
122
|
+
return bool(enabled)
|
|
123
|
+
return str(self.settings.get("session.mode", "lite")) == "memo"
|
|
124
|
+
|
|
125
|
+
def configure(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
mode: "SessionMode | None" = None,
|
|
129
|
+
limit: "SessionLimit | None" = None,
|
|
130
|
+
every_n_turns: int | None = None,
|
|
131
|
+
):
|
|
132
|
+
if mode is not None:
|
|
133
|
+
self.settings.set("session.mode", str(mode))
|
|
134
|
+
if str(mode) == "memo":
|
|
135
|
+
self.settings.set("session.memo.enabled", True)
|
|
136
|
+
elif str(mode) == "lite":
|
|
137
|
+
self.settings.set("session.memo.enabled", False)
|
|
138
|
+
if limit is not None:
|
|
139
|
+
self.settings.set("session.limit", DataFormatter.sanitize(limit))
|
|
140
|
+
if isinstance(limit, dict):
|
|
141
|
+
if "chars" in limit:
|
|
142
|
+
self.settings.set("session.resize.max_messages_text_length", limit["chars"])
|
|
143
|
+
if "messages" in limit:
|
|
144
|
+
self.settings.set("session.resize.max_keep_messages_count", limit["messages"])
|
|
145
|
+
if every_n_turns is not None:
|
|
146
|
+
self.settings.set("session.resize.every_n_turns", every_n_turns)
|
|
147
|
+
return self
|
|
148
|
+
|
|
149
|
+
def set_limit(
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
chars: int | None = None,
|
|
153
|
+
messages: int | None = None,
|
|
154
|
+
):
|
|
155
|
+
limit: "SessionLimit" = {}
|
|
156
|
+
if chars is not None:
|
|
157
|
+
limit["chars"] = chars
|
|
158
|
+
if messages is not None:
|
|
159
|
+
limit["messages"] = messages
|
|
160
|
+
if limit:
|
|
161
|
+
return self.configure(limit=limit)
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def use_lite(
|
|
165
|
+
self,
|
|
166
|
+
*,
|
|
167
|
+
chars: int | None = None,
|
|
168
|
+
messages: int | None = None,
|
|
169
|
+
every_n_turns: int | None = None,
|
|
170
|
+
):
|
|
171
|
+
limit: "SessionLimit" = {}
|
|
172
|
+
if chars is not None:
|
|
173
|
+
limit["chars"] = chars
|
|
174
|
+
if messages is not None:
|
|
175
|
+
limit["messages"] = messages
|
|
176
|
+
return self.configure(
|
|
177
|
+
mode="lite",
|
|
178
|
+
limit=limit if limit else None,
|
|
179
|
+
every_n_turns=every_n_turns,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def use_memo(
|
|
183
|
+
self,
|
|
184
|
+
*,
|
|
185
|
+
chars: int | None = None,
|
|
186
|
+
messages: int | None = None,
|
|
187
|
+
every_n_turns: int | None = None,
|
|
188
|
+
):
|
|
189
|
+
limit: "SessionLimit" = {}
|
|
190
|
+
if chars is not None:
|
|
191
|
+
limit["chars"] = chars
|
|
192
|
+
if messages is not None:
|
|
193
|
+
limit["messages"] = messages
|
|
194
|
+
return self.configure(
|
|
195
|
+
mode="memo",
|
|
196
|
+
limit=limit if limit else None,
|
|
197
|
+
every_n_turns=every_n_turns,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _approx_message_chars(self, message: "ChatMessage") -> int:
|
|
201
|
+
content = message.content
|
|
202
|
+
if isinstance(content, str):
|
|
203
|
+
return len(content)
|
|
204
|
+
if isinstance(content, list):
|
|
205
|
+
total = 0
|
|
206
|
+
for part in content:
|
|
207
|
+
if isinstance(part, str):
|
|
208
|
+
total += len(part)
|
|
209
|
+
elif isinstance(part, dict):
|
|
210
|
+
if "text" in part and isinstance(part["text"], str):
|
|
211
|
+
total += len(part["text"])
|
|
212
|
+
else:
|
|
213
|
+
total += len(json.dumps(part, ensure_ascii=False, default=str))
|
|
214
|
+
else:
|
|
215
|
+
total += len(str(part))
|
|
216
|
+
return total
|
|
217
|
+
return len(str(content))
|
|
218
|
+
|
|
219
|
+
def _approx_chat_history_chars(self, chat_history: "list[ChatMessage]") -> int:
|
|
220
|
+
return sum(len(m.role) + 1 + self._approx_message_chars(m) for m in chat_history)
|
|
221
|
+
|
|
222
|
+
def _get_limits(self) -> tuple[Any, Any]:
|
|
223
|
+
max_current_chars = self.settings.get("session.resize.max_messages_text_length", 12_000)
|
|
224
|
+
max_keep_messages_count = self.settings.get("session.resize.max_keep_messages_count", None)
|
|
225
|
+
limit = self.settings.get("session.limit", None)
|
|
226
|
+
if isinstance(limit, dict):
|
|
227
|
+
if "chars" in limit:
|
|
228
|
+
max_current_chars = limit["chars"]
|
|
229
|
+
if "messages" in limit:
|
|
230
|
+
max_keep_messages_count = limit["messages"]
|
|
231
|
+
return max_current_chars, max_keep_messages_count
|
|
232
|
+
|
|
233
|
+
def _get_model_requester(self):
|
|
234
|
+
if self._agent:
|
|
235
|
+
return ModelRequest(
|
|
236
|
+
self._agent.plugin_manager,
|
|
237
|
+
agent_name=self._agent.name,
|
|
238
|
+
parent_settings=self.settings,
|
|
239
|
+
)
|
|
240
|
+
from agently.base import plugin_manager
|
|
241
|
+
|
|
242
|
+
return ModelRequest(
|
|
243
|
+
plugin_manager,
|
|
244
|
+
parent_settings=self.settings,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def _serialize_chat_history(self, chat_history: "list[ChatMessage]") -> list[dict[str, Any]]:
|
|
248
|
+
return [DataFormatter.sanitize(message.model_dump()) for message in chat_history]
|
|
249
|
+
|
|
250
|
+
def _dump_data(self) -> dict[str, Any]:
|
|
251
|
+
return {
|
|
252
|
+
"id": self.id,
|
|
253
|
+
"memo": DataFormatter.sanitize(self.memo),
|
|
254
|
+
"full_chat_history": self._serialize_chat_history(self.full_chat_history),
|
|
255
|
+
"current_chat_history": self._serialize_chat_history(self.current_chat_history),
|
|
256
|
+
"settings": DataFormatter.sanitize(self.settings.get()),
|
|
257
|
+
"turns": self._turns,
|
|
258
|
+
"last_resize_turn": self._last_resize_turn,
|
|
259
|
+
"memo_cursor": self._memo_cursor,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
def to_json(self) -> str:
|
|
263
|
+
return json.dumps(self._dump_data(), ensure_ascii=True)
|
|
264
|
+
|
|
265
|
+
def to_yaml(self) -> str:
|
|
266
|
+
return yaml.safe_dump(self._dump_data(), allow_unicode=False)
|
|
267
|
+
|
|
268
|
+
def _load_data(self, data: dict[str, Any]):
|
|
269
|
+
from agently.types.data.prompt import validate_chat_history
|
|
270
|
+
|
|
271
|
+
if not isinstance(data, dict):
|
|
272
|
+
raise TypeError("Session.load expects a dictionary data object.")
|
|
273
|
+
if "id" in data:
|
|
274
|
+
self.id = str(data["id"])
|
|
275
|
+
if "memo" in data:
|
|
276
|
+
self.memo = data["memo"]
|
|
277
|
+
if "full_chat_history" in data:
|
|
278
|
+
self.full_chat_history = validate_chat_history(data["full_chat_history"])
|
|
279
|
+
if "current_chat_history" in data:
|
|
280
|
+
self.current_chat_history = validate_chat_history(data["current_chat_history"])
|
|
281
|
+
if "settings" in data and isinstance(data["settings"], dict):
|
|
282
|
+
self.settings.update(data["settings"])
|
|
283
|
+
if "turns" in data:
|
|
284
|
+
self._turns = int(data["turns"])
|
|
285
|
+
if "last_resize_turn" in data:
|
|
286
|
+
self._last_resize_turn = int(data["last_resize_turn"])
|
|
287
|
+
if "memo_cursor" in data:
|
|
288
|
+
self._memo_cursor = int(data["memo_cursor"])
|
|
289
|
+
return self
|
|
290
|
+
|
|
291
|
+
def _ensure_list(self, value: Any) -> list[Any]:
|
|
292
|
+
if value is None:
|
|
293
|
+
return []
|
|
294
|
+
if isinstance(value, list):
|
|
295
|
+
return value
|
|
296
|
+
if isinstance(value, tuple):
|
|
297
|
+
return list(value)
|
|
298
|
+
return [value]
|
|
299
|
+
|
|
300
|
+
def _get_memo_instruct(self) -> list[str]:
|
|
301
|
+
base = self._ensure_list(self.settings.get("session.memo.instruct", []))
|
|
302
|
+
return [str(item) for item in base if item is not None]
|
|
303
|
+
|
|
304
|
+
async def _default_memo_update_handler(
|
|
305
|
+
self,
|
|
306
|
+
memo: dict[str, Any],
|
|
307
|
+
messages: "list[ChatMessage]",
|
|
308
|
+
attachments: list[dict[str, Any]],
|
|
309
|
+
settings: Settings,
|
|
310
|
+
) -> dict[str, Any]:
|
|
311
|
+
requester = self._get_model_requester()
|
|
312
|
+
prompt_input = {
|
|
313
|
+
"current_memo": memo,
|
|
314
|
+
"messages": self._serialize_chat_history(messages),
|
|
315
|
+
"attachments": attachments,
|
|
316
|
+
}
|
|
317
|
+
output_schema = {"memo": (dict[str, Any], "Updated memo dictionary")}
|
|
318
|
+
result = requester.input(prompt_input).instruct(self._get_memo_instruct()).output(output_schema)
|
|
319
|
+
data = await result.async_get_data()
|
|
320
|
+
if isinstance(data, dict) and isinstance(data.get("memo"), dict):
|
|
321
|
+
return data["memo"]
|
|
322
|
+
if isinstance(data, dict):
|
|
323
|
+
return data
|
|
324
|
+
return memo
|
|
325
|
+
|
|
326
|
+
async def _update_memo(
|
|
327
|
+
self,
|
|
328
|
+
memo: dict[str, Any],
|
|
329
|
+
messages: "list[ChatMessage]",
|
|
330
|
+
) -> dict[str, Any]:
|
|
331
|
+
if not self._is_memo_enabled():
|
|
332
|
+
return memo
|
|
333
|
+
if not messages:
|
|
334
|
+
return memo
|
|
335
|
+
attachments = await self._collect_attachment_refs(messages)
|
|
336
|
+
result = await self._memo_update_handler(memo, messages, attachments, self.settings)
|
|
337
|
+
if inspect.isawaitable(result):
|
|
338
|
+
result = await result
|
|
339
|
+
if isinstance(result, dict):
|
|
340
|
+
return result
|
|
341
|
+
return memo
|
|
342
|
+
|
|
343
|
+
def _chunk_by_max_chars(
|
|
344
|
+
self,
|
|
345
|
+
chat_history: "list[ChatMessage]",
|
|
346
|
+
max_chars: int,
|
|
347
|
+
) -> list["list[ChatMessage]"]:
|
|
348
|
+
if max_chars <= 0:
|
|
349
|
+
return [chat_history] if chat_history else []
|
|
350
|
+
chunks: list[list[ChatMessage]] = []
|
|
351
|
+
current: list[ChatMessage] = []
|
|
352
|
+
current_chars = 0
|
|
353
|
+
for message in chat_history:
|
|
354
|
+
message_chars = len(message.role) + 1 + self._approx_message_chars(message)
|
|
355
|
+
if current and current_chars + message_chars > max_chars:
|
|
356
|
+
chunks.append(current)
|
|
357
|
+
current = [message]
|
|
358
|
+
current_chars = message_chars
|
|
359
|
+
else:
|
|
360
|
+
current.append(message)
|
|
361
|
+
current_chars += message_chars
|
|
362
|
+
if current:
|
|
363
|
+
chunks.append(current)
|
|
364
|
+
return chunks
|
|
365
|
+
|
|
366
|
+
def load_json(self, value: str):
|
|
367
|
+
self._load_data(json.loads(value))
|
|
368
|
+
return self
|
|
369
|
+
|
|
370
|
+
def load_yaml(self, value: str):
|
|
371
|
+
self._load_data(yaml.safe_load(value))
|
|
372
|
+
return self
|
|
373
|
+
|
|
374
|
+
def _split_by_max_chars(
|
|
375
|
+
self,
|
|
376
|
+
chat_history: "list[ChatMessage]",
|
|
377
|
+
max_chars: int,
|
|
378
|
+
) -> tuple["list[ChatMessage]", "list[ChatMessage]"]:
|
|
379
|
+
if max_chars <= 0 or not chat_history:
|
|
380
|
+
return [], chat_history
|
|
381
|
+
|
|
382
|
+
kept: list[ChatMessage] = []
|
|
383
|
+
kept_chars = 0
|
|
384
|
+
for message in reversed(chat_history):
|
|
385
|
+
message_chars = len(message.role) + 1 + self._approx_message_chars(message)
|
|
386
|
+
if kept and kept_chars + message_chars > max_chars:
|
|
387
|
+
break
|
|
388
|
+
kept.append(message)
|
|
389
|
+
kept_chars += message_chars
|
|
390
|
+
|
|
391
|
+
kept.reverse()
|
|
392
|
+
pruned = chat_history[: len(chat_history) - len(kept)]
|
|
393
|
+
return pruned, kept
|
|
394
|
+
|
|
395
|
+
def _default_attachment_summary_handler(self, message: "ChatMessage") -> list[dict[str, Any]]:
|
|
396
|
+
content = message.content
|
|
397
|
+
if not isinstance(content, list):
|
|
398
|
+
return []
|
|
399
|
+
summaries: list[dict[str, Any]] = []
|
|
400
|
+
for part in content:
|
|
401
|
+
if isinstance(part, dict):
|
|
402
|
+
payload = part
|
|
403
|
+
elif hasattr(part, "model_dump"):
|
|
404
|
+
payload = part.model_dump()
|
|
405
|
+
else:
|
|
406
|
+
continue
|
|
407
|
+
payload = DataFormatter.sanitize(payload)
|
|
408
|
+
part_type = payload.get("type")
|
|
409
|
+
if not part_type or part_type == "text":
|
|
410
|
+
continue
|
|
411
|
+
ref = None
|
|
412
|
+
for key in ("file", "url", "path", "id", "name"):
|
|
413
|
+
value = DataLocator.locate_path_in_dict(payload, key, default=None)
|
|
414
|
+
if value:
|
|
415
|
+
ref = value
|
|
416
|
+
break
|
|
417
|
+
summaries.append(
|
|
418
|
+
{
|
|
419
|
+
"type": part_type,
|
|
420
|
+
"ref": ref,
|
|
421
|
+
"meta": DataFormatter.sanitize(
|
|
422
|
+
{
|
|
423
|
+
key: payload.get(key)
|
|
424
|
+
for key in ("name", "mime_type", "size", "width", "height", "duration")
|
|
425
|
+
if key in payload
|
|
426
|
+
}
|
|
427
|
+
),
|
|
428
|
+
}
|
|
429
|
+
)
|
|
430
|
+
return summaries
|
|
431
|
+
|
|
432
|
+
async def _collect_attachment_refs(self, messages: "list[ChatMessage]") -> list[dict[str, Any]]:
|
|
433
|
+
refs: list[dict[str, Any]] = []
|
|
434
|
+
for message in messages:
|
|
435
|
+
result = await self._attachment_summary_handler(message)
|
|
436
|
+
if inspect.isawaitable(result):
|
|
437
|
+
result = await result
|
|
438
|
+
refs.extend(cast(list[dict[str, Any]], result))
|
|
439
|
+
return refs
|
|
440
|
+
|
|
441
|
+
def _ensure_decision(self, result: "MemoResizePolicyResult") -> "MemoResizeDecision | None":
|
|
442
|
+
if result is None:
|
|
443
|
+
return None
|
|
444
|
+
if isinstance(result, str):
|
|
445
|
+
return {"type": result}
|
|
446
|
+
if isinstance(result, dict) and "type" in result:
|
|
447
|
+
return result
|
|
448
|
+
raise TypeError(f"Invalid policy result: {type(result)} {result}")
|
|
449
|
+
|
|
450
|
+
async def _default_policy_handler(
|
|
451
|
+
self,
|
|
452
|
+
full_chat_history: "list[ChatMessage]",
|
|
453
|
+
current_chat_history: "list[ChatMessage]",
|
|
454
|
+
settings: Settings,
|
|
455
|
+
):
|
|
456
|
+
max_current_chars, max_keep_messages_count = self._get_limits()
|
|
457
|
+
every_n_turns = settings.get("session.resize.every_n_turns", 8)
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
max_current_chars_int = int(max_current_chars) # type: ignore
|
|
461
|
+
except Exception:
|
|
462
|
+
max_current_chars_int = 12_000
|
|
463
|
+
try:
|
|
464
|
+
every_n_turns_int = int(every_n_turns) # type: ignore
|
|
465
|
+
except Exception:
|
|
466
|
+
every_n_turns_int = 8
|
|
467
|
+
|
|
468
|
+
if max_current_chars_int > 0 and self._approx_chat_history_chars(current_chat_history) >= max_current_chars_int:
|
|
469
|
+
return {"type": "deep", "reason": "max_messages_text_length", "severity": 100}
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
max_keep_messages_count_int = int(max_keep_messages_count) # type: ignore[arg-type]
|
|
473
|
+
except Exception:
|
|
474
|
+
max_keep_messages_count_int = None
|
|
475
|
+
if max_keep_messages_count_int and len(current_chat_history) > max_keep_messages_count_int:
|
|
476
|
+
return {"type": "lite", "reason": "max_keep_messages_count", "severity": 50}
|
|
477
|
+
|
|
478
|
+
turns_since_last_resize = self._turns - self._last_resize_turn
|
|
479
|
+
if every_n_turns_int > 0 and turns_since_last_resize >= every_n_turns_int:
|
|
480
|
+
return {"type": "lite", "reason": "every_n_turns", "severity": 10}
|
|
481
|
+
|
|
482
|
+
return None
|
|
483
|
+
|
|
484
|
+
async def _default_lite_resize_handler(
|
|
485
|
+
self,
|
|
486
|
+
full_chat_history: "list[ChatMessage]",
|
|
487
|
+
current_chat_history: "list[ChatMessage]",
|
|
488
|
+
memo: "SerializableData",
|
|
489
|
+
settings: Settings,
|
|
490
|
+
):
|
|
491
|
+
memo_dict: dict[str, Any] = memo if isinstance(memo, dict) else {}
|
|
492
|
+
delta_messages = full_chat_history[self._memo_cursor :]
|
|
493
|
+
if delta_messages:
|
|
494
|
+
memo_dict = await self._update_memo(memo_dict, delta_messages)
|
|
495
|
+
self._memo_cursor = len(full_chat_history)
|
|
496
|
+
|
|
497
|
+
max_current_chars, keep_last_messages = self._get_limits()
|
|
498
|
+
try:
|
|
499
|
+
keep_last_messages_int = max(0, int(keep_last_messages)) # type: ignore[arg-type]
|
|
500
|
+
except Exception:
|
|
501
|
+
keep_last_messages_int = None
|
|
502
|
+
try:
|
|
503
|
+
max_current_chars_int = int(max_current_chars) # type: ignore[arg-type]
|
|
504
|
+
except Exception:
|
|
505
|
+
max_current_chars_int = 12_000
|
|
506
|
+
|
|
507
|
+
if keep_last_messages_int is None:
|
|
508
|
+
pruned, remaining = self._split_by_max_chars(current_chat_history, max_current_chars_int)
|
|
509
|
+
else:
|
|
510
|
+
if keep_last_messages_int == 0 or len(current_chat_history) <= keep_last_messages_int:
|
|
511
|
+
memo_dict["last_resize"] = {"type": "lite", "turn": self._turns, "reason": "lite_resize"}
|
|
512
|
+
return full_chat_history, current_chat_history, memo_dict
|
|
513
|
+
pruned = current_chat_history[:-keep_last_messages_int]
|
|
514
|
+
remaining = current_chat_history[-keep_last_messages_int:]
|
|
515
|
+
if max_current_chars_int > 0:
|
|
516
|
+
extra_pruned, remaining = self._split_by_max_chars(remaining, max_current_chars_int)
|
|
517
|
+
if extra_pruned:
|
|
518
|
+
pruned = pruned + extra_pruned
|
|
519
|
+
|
|
520
|
+
memo_dict["last_resize"] = {"type": "lite", "turn": self._turns, "reason": "lite_resize"}
|
|
521
|
+
|
|
522
|
+
return full_chat_history, remaining, memo_dict
|
|
523
|
+
|
|
524
|
+
async def _default_deep_resize_handler(
|
|
525
|
+
self,
|
|
526
|
+
full_chat_history: "list[ChatMessage]",
|
|
527
|
+
current_chat_history: "list[ChatMessage]",
|
|
528
|
+
memo: "SerializableData",
|
|
529
|
+
settings: Settings,
|
|
530
|
+
):
|
|
531
|
+
memo_dict: dict[str, Any] = memo if isinstance(memo, dict) else {}
|
|
532
|
+
max_current_chars, keep_last_messages = self._get_limits()
|
|
533
|
+
try:
|
|
534
|
+
max_current_chars_int = int(max_current_chars) # type: ignore[arg-type]
|
|
535
|
+
except Exception:
|
|
536
|
+
max_current_chars_int = 12_000
|
|
537
|
+
for batch in self._chunk_by_max_chars(full_chat_history, max_current_chars_int):
|
|
538
|
+
memo_dict = await self._update_memo(memo_dict, batch)
|
|
539
|
+
self._memo_cursor = len(full_chat_history)
|
|
540
|
+
|
|
541
|
+
try:
|
|
542
|
+
keep_last_messages_int = max(0, int(keep_last_messages)) # type: ignore[arg-type]
|
|
543
|
+
except Exception:
|
|
544
|
+
keep_last_messages_int = None
|
|
545
|
+
|
|
546
|
+
if keep_last_messages_int is None:
|
|
547
|
+
pruned, remaining = self._split_by_max_chars(current_chat_history, max_current_chars_int)
|
|
548
|
+
else:
|
|
549
|
+
if keep_last_messages_int == 0:
|
|
550
|
+
pruned = current_chat_history
|
|
551
|
+
remaining = []
|
|
552
|
+
elif len(current_chat_history) <= keep_last_messages_int:
|
|
553
|
+
pruned = []
|
|
554
|
+
remaining = current_chat_history
|
|
555
|
+
else:
|
|
556
|
+
pruned = current_chat_history[:-keep_last_messages_int]
|
|
557
|
+
remaining = current_chat_history[-keep_last_messages_int:]
|
|
558
|
+
if max_current_chars_int > 0 and remaining:
|
|
559
|
+
extra_pruned, remaining = self._split_by_max_chars(remaining, max_current_chars_int)
|
|
560
|
+
if extra_pruned:
|
|
561
|
+
pruned = pruned + extra_pruned
|
|
562
|
+
|
|
563
|
+
memo_dict["last_resize"] = {"type": "deep", "turn": self._turns, "reason": "deep_resize"}
|
|
564
|
+
|
|
565
|
+
return full_chat_history, remaining, memo_dict
|
|
566
|
+
|
|
567
|
+
def set_policy_handler(self, policy_handler: "MemoResizePolicyHandler"):
|
|
568
|
+
self._policy_handler = cast("MemoResizePolicyAsyncHandler", FunctionShifter.asyncify(policy_handler))
|
|
569
|
+
return self
|
|
570
|
+
|
|
571
|
+
def set_attachment_summary_handler(self, attachment_summary_handler: "AttachmentSummaryHandler"):
|
|
572
|
+
self._attachment_summary_handler = cast(
|
|
573
|
+
"AttachmentSummaryAsyncHandler",
|
|
574
|
+
FunctionShifter.asyncify(attachment_summary_handler),
|
|
575
|
+
)
|
|
576
|
+
return self
|
|
577
|
+
|
|
578
|
+
def set_memo_update_handler(self, memo_update_handler: "MemoUpdateHandler"):
|
|
579
|
+
self._memo_update_handler = cast(
|
|
580
|
+
"MemoUpdateAsyncHandler",
|
|
581
|
+
FunctionShifter.asyncify(memo_update_handler),
|
|
582
|
+
)
|
|
583
|
+
return self
|
|
584
|
+
|
|
585
|
+
def set_resize_handlers(self, resize_type: Literal["lite", "deep"] | str, resize_handler: "MemoResizeHandler"):
|
|
586
|
+
self._resize_handlers[resize_type] = cast(
|
|
587
|
+
"MemoResizeAsyncHandler",
|
|
588
|
+
FunctionShifter.asyncify(resize_handler),
|
|
589
|
+
)
|
|
590
|
+
return self
|
|
591
|
+
|
|
592
|
+
def append_message(self, message: "ChatMessage | ChatMessageDict"):
|
|
593
|
+
if isinstance(message, dict):
|
|
594
|
+
message = ChatMessage(
|
|
595
|
+
role=message["role"],
|
|
596
|
+
content=message["content"],
|
|
597
|
+
)
|
|
598
|
+
self.full_chat_history.append(message)
|
|
599
|
+
self.current_chat_history.append(message)
|
|
600
|
+
if message.role == "assistant":
|
|
601
|
+
self._turns += 1
|
|
602
|
+
return self
|
|
603
|
+
|
|
604
|
+
async def async_judge_resize(self, force: Literal["lite", "deep", False, None] | str = False):
|
|
605
|
+
if force:
|
|
606
|
+
decision: "MemoResizeDecision | None" = {
|
|
607
|
+
"type": str(force) if isinstance(force, str) else "deep",
|
|
608
|
+
"reason": "force",
|
|
609
|
+
"severity": 1000,
|
|
610
|
+
}
|
|
611
|
+
else:
|
|
612
|
+
policy_result = await self._policy_handler(
|
|
613
|
+
self.full_chat_history,
|
|
614
|
+
self.current_chat_history,
|
|
615
|
+
self.settings,
|
|
616
|
+
)
|
|
617
|
+
if inspect.isawaitable(policy_result):
|
|
618
|
+
policy_result = await policy_result
|
|
619
|
+
decision = self._ensure_decision(cast("MemoResizePolicyResult", policy_result))
|
|
620
|
+
|
|
621
|
+
if decision is None:
|
|
622
|
+
return None
|
|
623
|
+
|
|
624
|
+
return decision
|
|
625
|
+
|
|
626
|
+
async def async_resize(self, force: Literal["lite", "deep", False, None] | str = False):
|
|
627
|
+
decision = await self.async_judge_resize(force=force)
|
|
628
|
+
if decision is None:
|
|
629
|
+
return self.current_chat_history
|
|
630
|
+
|
|
631
|
+
resize_type = decision["type"]
|
|
632
|
+
handler = self._resize_handlers.get(resize_type)
|
|
633
|
+
if handler is None:
|
|
634
|
+
raise KeyError(f"Missing resize handler for type: {resize_type}")
|
|
635
|
+
|
|
636
|
+
handler_result = await handler(
|
|
637
|
+
self.full_chat_history,
|
|
638
|
+
self.current_chat_history,
|
|
639
|
+
self.memo,
|
|
640
|
+
self.settings,
|
|
641
|
+
)
|
|
642
|
+
if inspect.isawaitable(handler_result):
|
|
643
|
+
handler_result = await handler_result
|
|
644
|
+
self.full_chat_history, self.current_chat_history, self.memo = handler_result
|
|
645
|
+
if isinstance(self.memo, dict) and isinstance(decision, dict) and decision.get("reason"):
|
|
646
|
+
last_resize = self.memo.get("last_resize")
|
|
647
|
+
if not isinstance(last_resize, dict):
|
|
648
|
+
last_resize = {}
|
|
649
|
+
self.memo["last_resize"] = last_resize
|
|
650
|
+
last_resize["reason"] = decision["reason"] if "reason" in decision else ""
|
|
651
|
+
self._last_resize_turn = self._turns
|
|
652
|
+
return self.current_chat_history
|
agently/builtins/tools/Browse.py
CHANGED
|
@@ -12,18 +12,26 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
from agently.types.plugins import BuiltInTool
|
|
16
16
|
from agently.utils import LazyImport
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class Browse:
|
|
20
|
-
|
|
19
|
+
class Browse(BuiltInTool):
|
|
21
20
|
def __init__(
|
|
22
21
|
self,
|
|
23
22
|
proxy: str | None = None,
|
|
24
23
|
timeout: int | None = None,
|
|
25
24
|
headers: dict[str, str] | None = None,
|
|
26
25
|
):
|
|
26
|
+
self.tool_info_list = [
|
|
27
|
+
{
|
|
28
|
+
"name": "browse",
|
|
29
|
+
"desc": "Browse the page at {url}",
|
|
30
|
+
"kwargs": {"url": ("str", "Accessible URL")},
|
|
31
|
+
"func": self.browse,
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
|
|
27
35
|
LazyImport.import_package("httpx")
|
|
28
36
|
LazyImport.import_package("bs4", install_name="beautifulsoup4")
|
|
29
37
|
self.proxy = proxy
|