everalgo-agent-memory 0.1.0__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.
- everalgo/agent_memory/__init__.py +16 -0
- everalgo/agent_memory/_text.py +52 -0
- everalgo/agent_memory/boundary.py +129 -0
- everalgo/agent_memory/case.py +595 -0
- everalgo/agent_memory/prompts/__init__.py +7 -0
- everalgo/agent_memory/prompts/case_compress.py +76 -0
- everalgo/agent_memory/prompts/case_filter.py +37 -0
- everalgo/agent_memory/prompts/skill_failure.py +87 -0
- everalgo/agent_memory/prompts/skill_maturity.py +52 -0
- everalgo/agent_memory/prompts/skill_success.py +106 -0
- everalgo/agent_memory/prompts/tool_pre_compress.py +42 -0
- everalgo/agent_memory/py.typed +0 -0
- everalgo/agent_memory/skill.py +246 -0
- everalgo/agent_memory/skill_ops.py +482 -0
- everalgo_agent_memory-0.1.0.dist-info/METADATA +153 -0
- everalgo_agent_memory-0.1.0.dist-info/RECORD +18 -0
- everalgo_agent_memory-0.1.0.dist-info/WHEEL +4 -0
- everalgo_agent_memory-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Agent-side memory extractors — 2 Extractors + boundary facade."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from everalgo.agent_memory.boundary import AgentBoundaryDetector
|
|
6
|
+
from everalgo.agent_memory.case import AgentCaseExtractor
|
|
7
|
+
from everalgo.agent_memory.skill import AgentSkillExtractor
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AgentBoundaryDetector",
|
|
11
|
+
"AgentCaseExtractor",
|
|
12
|
+
"AgentSkillExtractor",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# Library logging setup (ADR-013): NullHandler on each subpackage logger.
|
|
16
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Token-aware text utilities for agent_memory extractors (head/tail truncation, json_default)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from everalgo._tokenize import _get_tokenizer, count_tokens
|
|
9
|
+
|
|
10
|
+
__all__ = ["count_tokens", "json_default", "truncate_text"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def truncate_text(
|
|
14
|
+
text: str,
|
|
15
|
+
max_tokens: int,
|
|
16
|
+
*,
|
|
17
|
+
head_ratio: float = 0.7,
|
|
18
|
+
suffix: str | None = None,
|
|
19
|
+
) -> str:
|
|
20
|
+
r"""Truncate ``text`` to ``max_tokens`` tokens, preserving head + tail with a marker.
|
|
21
|
+
|
|
22
|
+
When ``suffix`` is ``None``: keep ``head_ratio`` of tokens from the start and the remainder from
|
|
23
|
+
the end, joined by ``"\\n[... trimmed N tokens ...]\\n"`` (or ``"..."`` when ``head_ratio == 1.0``).
|
|
24
|
+
When ``suffix`` is set: keep the head only and append ``suffix``.
|
|
25
|
+
Short / empty inputs are returned unchanged.
|
|
26
|
+
"""
|
|
27
|
+
if not text:
|
|
28
|
+
return text
|
|
29
|
+
tokenizer = _get_tokenizer()
|
|
30
|
+
tokens = tokenizer.encode(text)
|
|
31
|
+
if len(tokens) <= max_tokens:
|
|
32
|
+
return text
|
|
33
|
+
|
|
34
|
+
if suffix is not None:
|
|
35
|
+
head = tokenizer.decode(tokens[:max_tokens])
|
|
36
|
+
return head.rstrip() + suffix
|
|
37
|
+
|
|
38
|
+
head_count = int(max_tokens * head_ratio)
|
|
39
|
+
tail_count = max_tokens - head_count
|
|
40
|
+
head_text = tokenizer.decode(tokens[:head_count])
|
|
41
|
+
if tail_count <= 0:
|
|
42
|
+
return head_text.rstrip() + "..."
|
|
43
|
+
tail_text = tokenizer.decode(tokens[-tail_count:])
|
|
44
|
+
trimmed = len(tokens) - max_tokens
|
|
45
|
+
return f"{head_text}\n[... trimmed {trimmed} tokens ...]\n{tail_text}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def json_default(obj: Any) -> Any:
|
|
49
|
+
"""``json.dumps(default=...)`` fallback: datetime → ISO string, anything else → ``str``."""
|
|
50
|
+
if isinstance(obj, datetime):
|
|
51
|
+
return obj.isoformat()
|
|
52
|
+
return str(obj)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Agent trajectory boundary detection facade.
|
|
2
|
+
|
|
3
|
+
Wraps the low-level ``everalgo.boundary.detect_boundaries`` primitive with a filter → detect → remap
|
|
4
|
+
pipeline to handle mixed trajectories (``ChatMessage`` + ``ToolCallRequest`` + ``ToolCallResult``).
|
|
5
|
+
|
|
6
|
+
Tool items are filtered out before the LLM detection step so the LLM sees a clean conversation
|
|
7
|
+
view; boundary indices from the chat-only result are then remapped back to the original
|
|
8
|
+
``ConversationItem`` index space so that the output ``MemCell.items`` preserves the full
|
|
9
|
+
trajectory (including tool calls).
|
|
10
|
+
|
|
11
|
+
Uses explicit composition rather than inheritance: filter → detect → remap layers are separate
|
|
12
|
+
functions rather than an extended class hierarchy.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from asgiref.sync import async_to_sync
|
|
21
|
+
|
|
22
|
+
from everalgo.boundary import DetectionResult, detect_boundaries
|
|
23
|
+
from everalgo.types import ChatMessage, MemCell
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from everalgo.llm.protocols import LLMClient
|
|
27
|
+
from everalgo.types import ConversationItem
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
__all__ = ["AgentBoundaryDetector"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AgentBoundaryDetector:
|
|
35
|
+
"""Boundary detection on an agent trajectory (mixed ``ConversationItem`` list).
|
|
36
|
+
|
|
37
|
+
The filter → detect → remap pipeline:
|
|
38
|
+
|
|
39
|
+
1. **Filter** — extract only ``ChatMessage`` items and remember their original indices.
|
|
40
|
+
2. **Detect** — run ``everalgo.boundary.detect_boundaries`` on the chat-only view.
|
|
41
|
+
3. **Remap** — translate chat-space cell boundaries back to the original ``ConversationItem``
|
|
42
|
+
index space; ``ToolCallRequest`` / ``ToolCallResult`` items between adjacent chat
|
|
43
|
+
messages are folded into the cell that contains the preceding chat message.
|
|
44
|
+
|
|
45
|
+
Invariant: ``sum(len(c.items) for c in result.cells) + len(result.tail) == len(items)``.
|
|
46
|
+
|
|
47
|
+
The LLM client is bound to the instance at construction time.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, *, llm: LLMClient) -> None:
|
|
51
|
+
self._llm = llm
|
|
52
|
+
|
|
53
|
+
async def adetect(
|
|
54
|
+
self,
|
|
55
|
+
items: list[ConversationItem],
|
|
56
|
+
*,
|
|
57
|
+
is_final: bool = False,
|
|
58
|
+
prompt: str | None = None,
|
|
59
|
+
) -> DetectionResult:
|
|
60
|
+
"""Detect boundaries on a mixed agent trajectory.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
items: Ordered ``ConversationItem`` list for one trajectory slice — may contain
|
|
64
|
+
``ChatMessage``, ``ToolCallRequest``, and ``ToolCallResult`` in any mix.
|
|
65
|
+
is_final: When ``True``, the caller guarantees no more items will arrive; trailing tool items
|
|
66
|
+
after the last cell's final chat message are folded into that cell rather than the tail.
|
|
67
|
+
When ``False`` (default), trailing tool items go to the tail.
|
|
68
|
+
prompt: Per-call boundary detection prompt override; ``None`` uses the built-in default.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
``cells`` — remapped ``MemCell`` list preserving tool calls inside each cell.
|
|
72
|
+
``tail`` — remainder items (not yet committed to a cell) for the non-final case.
|
|
73
|
+
"""
|
|
74
|
+
chat_items: list[ChatMessage] = []
|
|
75
|
+
chat_to_orig: list[int] = []
|
|
76
|
+
for orig_idx, item in enumerate(items):
|
|
77
|
+
if isinstance(item, ChatMessage):
|
|
78
|
+
chat_items.append(item)
|
|
79
|
+
chat_to_orig.append(orig_idx)
|
|
80
|
+
|
|
81
|
+
if not chat_items:
|
|
82
|
+
# No chat messages → no LLM signal; everything goes to tail per should_wait semantics.
|
|
83
|
+
logger.debug("adetect: no ChatMessage items in trajectory (n_items=%d)", len(items))
|
|
84
|
+
return DetectionResult(cells=[], tail=list(items)) # type: ignore[arg-type]
|
|
85
|
+
|
|
86
|
+
chat_result = await detect_boundaries(chat_items, llm=self._llm, is_final=is_final, prompt=prompt)
|
|
87
|
+
|
|
88
|
+
remapped_cells: list[MemCell] = []
|
|
89
|
+
chat_cursor = 0
|
|
90
|
+
for cell in chat_result.cells:
|
|
91
|
+
cell_chat_count = len(cell.items)
|
|
92
|
+
start_chat_idx = chat_cursor
|
|
93
|
+
end_chat_idx = chat_cursor + cell_chat_count - 1
|
|
94
|
+
chat_cursor += cell_chat_count
|
|
95
|
+
|
|
96
|
+
start_orig = chat_to_orig[start_chat_idx]
|
|
97
|
+
|
|
98
|
+
if chat_cursor < len(chat_to_orig):
|
|
99
|
+
# More cells follow: include everything up to (exclusive) the next cell's first chat.
|
|
100
|
+
end_orig_exclusive = chat_to_orig[chat_cursor]
|
|
101
|
+
elif is_final:
|
|
102
|
+
# Last cell, caller says stream is done: absorb all remaining items.
|
|
103
|
+
end_orig_exclusive = len(items)
|
|
104
|
+
else:
|
|
105
|
+
# Last cell, stream is ongoing: include only up to (inclusive) the last chat in
|
|
106
|
+
# this cell; trailing tool items go to the tail so they can be re-processed.
|
|
107
|
+
end_orig_exclusive = chat_to_orig[end_chat_idx] + 1
|
|
108
|
+
|
|
109
|
+
remapped_cells.append(MemCell(items=list(items[start_orig:end_orig_exclusive]), timestamp=cell.timestamp))
|
|
110
|
+
|
|
111
|
+
# Tail: items after the last consumed original index.
|
|
112
|
+
if remapped_cells:
|
|
113
|
+
if is_final:
|
|
114
|
+
last_consumed_exclusive = len(items)
|
|
115
|
+
elif chat_cursor < len(chat_to_orig):
|
|
116
|
+
last_consumed_exclusive = chat_to_orig[chat_cursor]
|
|
117
|
+
else:
|
|
118
|
+
# chat_cursor == len(chat_to_orig): all chats consumed; last cell boundary already
|
|
119
|
+
# set to chat_to_orig[end_chat_idx] + 1, so tail starts right after.
|
|
120
|
+
last_consumed_exclusive = chat_to_orig[-1] + 1
|
|
121
|
+
tail = list(items[last_consumed_exclusive:])
|
|
122
|
+
else:
|
|
123
|
+
# detect_boundaries returned no cells — everything is tail.
|
|
124
|
+
tail = list(items)
|
|
125
|
+
|
|
126
|
+
return DetectionResult(cells=remapped_cells, tail=tail) # type: ignore[arg-type]
|
|
127
|
+
|
|
128
|
+
detect = async_to_sync(adetect)
|
|
129
|
+
"""Sync bridge — only callable from non-event-loop contexts."""
|