agentstate-reducer 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.
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agentstate-reducer: Framework-agnostic message reducer for AI agent state management.
|
|
3
|
+
|
|
4
|
+
Works with LangGraph, CrewAI, and plain dicts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .models import ReducerConfig, ReducerResult
|
|
8
|
+
from .reducer import MessageReducer
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"MessageReducer",
|
|
12
|
+
"ReducerConfig",
|
|
13
|
+
"ReducerResult",
|
|
14
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adapters for extracting fields from messages in a framework-agnostic way.
|
|
3
|
+
|
|
4
|
+
Works with:
|
|
5
|
+
- Plain dicts: {"role": "ai", "content": "...", "id": "...", "tool_calls": [...]}
|
|
6
|
+
- LangChain BaseMessage subclasses: AIMessage, HumanMessage, ToolMessage, etc.
|
|
7
|
+
- Any object with the expected attributes (duck typing)
|
|
8
|
+
|
|
9
|
+
This module does NOT import langchain_core. It uses duck typing and class-name
|
|
10
|
+
inspection so the package remains zero-dependency.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ── Role mapping from LangChain class names ──
|
|
17
|
+
|
|
18
|
+
_CLASS_NAME_TO_ROLE = {
|
|
19
|
+
"human": "human",
|
|
20
|
+
"humanmessage": "human",
|
|
21
|
+
"ai": "ai",
|
|
22
|
+
"aimessage": "ai",
|
|
23
|
+
"tool": "tool",
|
|
24
|
+
"toolmessage": "tool",
|
|
25
|
+
"system": "system",
|
|
26
|
+
"systemmessage": "system",
|
|
27
|
+
"function": "function",
|
|
28
|
+
"functionmessage": "function",
|
|
29
|
+
"chatmessage": "chat",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_role(msg: Any) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Extract the role/type from a message.
|
|
36
|
+
|
|
37
|
+
Handles:
|
|
38
|
+
- dict with "role" key: {"role": "ai", ...}
|
|
39
|
+
- dict with "type" key: {"type": "ai", ...} (LangChain serialized format)
|
|
40
|
+
- LangChain BaseMessage: uses class name → role mapping
|
|
41
|
+
- Any object with a .type attribute
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Lowercase role string: "human", "ai", "tool", "system", etc.
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(msg, dict):
|
|
47
|
+
return (msg.get("role") or msg.get("type") or "").lower()
|
|
48
|
+
|
|
49
|
+
# Try class name mapping (covers all LangChain message types)
|
|
50
|
+
class_name = type(msg).__name__.lower()
|
|
51
|
+
role = _CLASS_NAME_TO_ROLE.get(class_name)
|
|
52
|
+
if role:
|
|
53
|
+
return role
|
|
54
|
+
|
|
55
|
+
# Fallback: .type attribute (LangChain BaseMessage has this)
|
|
56
|
+
return getattr(msg, "type", "unknown").lower()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_id(msg: Any) -> Optional[str]:
|
|
60
|
+
"""
|
|
61
|
+
Extract the unique message ID.
|
|
62
|
+
|
|
63
|
+
Handles:
|
|
64
|
+
- dict: msg["id"]
|
|
65
|
+
- Object: msg.id
|
|
66
|
+
"""
|
|
67
|
+
if isinstance(msg, dict):
|
|
68
|
+
return msg.get("id")
|
|
69
|
+
return getattr(msg, "id", None)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_content(msg: Any) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Extract message content.
|
|
75
|
+
|
|
76
|
+
Handles:
|
|
77
|
+
- dict: msg["content"]
|
|
78
|
+
- Object: msg.content
|
|
79
|
+
"""
|
|
80
|
+
if isinstance(msg, dict):
|
|
81
|
+
return msg.get("content", "")
|
|
82
|
+
return getattr(msg, "content", "")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_tool_calls(msg: Any) -> List[Dict]:
|
|
86
|
+
"""
|
|
87
|
+
Extract tool_calls from an AI message.
|
|
88
|
+
|
|
89
|
+
Handles:
|
|
90
|
+
- dict: msg["tool_calls"] → list of dicts
|
|
91
|
+
- LangChain AIMessage: msg.tool_calls → list of ToolCall objects or dicts
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of dicts with at least {"id": ...} for each tool call.
|
|
95
|
+
Returns empty list if no tool_calls found.
|
|
96
|
+
"""
|
|
97
|
+
if isinstance(msg, dict):
|
|
98
|
+
return msg.get("tool_calls", [])
|
|
99
|
+
|
|
100
|
+
raw = getattr(msg, "tool_calls", [])
|
|
101
|
+
if not raw:
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
result = []
|
|
105
|
+
for tc in raw:
|
|
106
|
+
if isinstance(tc, dict):
|
|
107
|
+
result.append(tc)
|
|
108
|
+
else:
|
|
109
|
+
# LangChain ToolCall object → convert to dict
|
|
110
|
+
result.append({
|
|
111
|
+
"id": getattr(tc, "id", None),
|
|
112
|
+
"name": getattr(tc, "name", None),
|
|
113
|
+
"args": getattr(tc, "args", {}),
|
|
114
|
+
})
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_tool_call_id(msg: Any) -> Optional[str]:
|
|
119
|
+
"""
|
|
120
|
+
Extract tool_call_id from a ToolMessage.
|
|
121
|
+
|
|
122
|
+
Handles:
|
|
123
|
+
- dict: msg["tool_call_id"]
|
|
124
|
+
- LangChain ToolMessage: msg.tool_call_id
|
|
125
|
+
"""
|
|
126
|
+
if isinstance(msg, dict):
|
|
127
|
+
return msg.get("tool_call_id")
|
|
128
|
+
return getattr(msg, "tool_call_id", None)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared data models for the agentstate-reducer package.
|
|
3
|
+
|
|
4
|
+
These are framework-agnostic — no imports from langchain, crewai, etc.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Callable, List, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ReducerConfig:
|
|
13
|
+
"""
|
|
14
|
+
Configuration for the MessageReducer.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
min_messages: Number of messages to retain after pruning.
|
|
18
|
+
max_messages: Pruning triggers when message count exceeds this.
|
|
19
|
+
Set to None to disable pruning.
|
|
20
|
+
preserve_first: If True, index 0 is never pruned (system message).
|
|
21
|
+
cascade_tool_messages: If True, when an AIMessage is pruned, also prune
|
|
22
|
+
any ToolMessages linked to it via tool_call_id.
|
|
23
|
+
summarize_fn: Optional callable(pruned_messages) -> str.
|
|
24
|
+
Called with the list of pruned messages so you can
|
|
25
|
+
generate a summary (e.g., via LLM) of what was removed.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
min_messages: int = 10
|
|
29
|
+
max_messages: Optional[int] = 20
|
|
30
|
+
preserve_first: bool = True
|
|
31
|
+
cascade_tool_messages: bool = True
|
|
32
|
+
summarize_fn: Optional[Callable[[List[Any]], str]] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ReducerResult:
|
|
37
|
+
"""
|
|
38
|
+
Output of a reduce operation.
|
|
39
|
+
|
|
40
|
+
Both downstream consumers need to know what survived AND what was pruned:
|
|
41
|
+
- langgraph-checkpoint-cosmosdb: stores `surviving`, may log `pruned`
|
|
42
|
+
- crewai-persistence-cosmosdb: stores `surviving`, may call summarize on `pruned`
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
surviving: Messages that remain after pruning.
|
|
46
|
+
pruned: Messages that were removed.
|
|
47
|
+
summary: If a summarize_fn was configured and pruning occurred,
|
|
48
|
+
this contains the summary string.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
surviving: List[Any] = field(default_factory=list)
|
|
52
|
+
pruned: List[Any] = field(default_factory=list)
|
|
53
|
+
summary: Optional[str] = None
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core message reducer — framework-agnostic.
|
|
3
|
+
|
|
4
|
+
Works with:
|
|
5
|
+
- Plain dicts: {"role": "ai", "content": "...", "id": "..."}
|
|
6
|
+
- LangChain types: AIMessage, HumanMessage, ToolMessage, SystemMessage
|
|
7
|
+
- Any object with role/id/tool_calls attributes (duck typing)
|
|
8
|
+
|
|
9
|
+
Ported from langgraph-reducer's Reducer class with these improvements:
|
|
10
|
+
- Works with both LangChain types and plain dicts
|
|
11
|
+
- Returns ReducerResult(surviving, pruned, summary) so callers decide
|
|
12
|
+
what to do with pruned messages
|
|
13
|
+
- Configurable via ReducerConfig dataclass
|
|
14
|
+
- Provides as_langgraph_reducer() bridge for Annotated[list, fn] usage
|
|
15
|
+
|
|
16
|
+
Preserves original behaviors:
|
|
17
|
+
1. Skip index 0 (system message) during pruning
|
|
18
|
+
2. When pruning an AIMessage, also prune linked ToolMessages (cascade)
|
|
19
|
+
3. Windowed pruning: prune when len > max_messages, keep min_messages
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from typing import Any, List, Optional, Set
|
|
24
|
+
|
|
25
|
+
from .adapters import get_role, get_tool_call_id, get_tool_calls
|
|
26
|
+
from .models import ReducerConfig, ReducerResult
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MessageReducer:
|
|
32
|
+
"""
|
|
33
|
+
Framework-agnostic message reducer.
|
|
34
|
+
|
|
35
|
+
Concatenates existing + new messages, then prunes oldest messages
|
|
36
|
+
when the total exceeds ``max_messages``, retaining ``min_messages``.
|
|
37
|
+
|
|
38
|
+
Pruning rules:
|
|
39
|
+
- Index 0 is preserved (configurable via ``preserve_first``).
|
|
40
|
+
- Only ``ai`` and ``human`` messages are pruned.
|
|
41
|
+
- When an ``ai`` message is pruned, any ``tool`` messages linked to
|
|
42
|
+
it (by ``tool_call_id``) are also pruned (cascade).
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
min_messages: Number of messages to keep after pruning.
|
|
46
|
+
max_messages: Threshold to trigger pruning. ``None`` disables pruning.
|
|
47
|
+
config: Optional ``ReducerConfig`` (overrides min/max params).
|
|
48
|
+
|
|
49
|
+
Example with plain dicts::
|
|
50
|
+
|
|
51
|
+
reducer = MessageReducer(min_messages=5, max_messages=10)
|
|
52
|
+
result = reducer.reduce(
|
|
53
|
+
existing=[{"role": "system", "content": "You are helpful"}],
|
|
54
|
+
new=[{"role": "human", "content": "Hello"}],
|
|
55
|
+
)
|
|
56
|
+
# result.surviving = [...]
|
|
57
|
+
# result.pruned = [...]
|
|
58
|
+
|
|
59
|
+
Example with LangChain messages::
|
|
60
|
+
|
|
61
|
+
from langchain_core.messages import HumanMessage, AIMessage
|
|
62
|
+
reducer = MessageReducer(min_messages=5, max_messages=10)
|
|
63
|
+
result = reducer.reduce(
|
|
64
|
+
existing=[SystemMessage(content="...")],
|
|
65
|
+
new=[HumanMessage(content="Hello")],
|
|
66
|
+
)
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
min_messages: int = 0,
|
|
72
|
+
max_messages: Optional[int] = None,
|
|
73
|
+
*,
|
|
74
|
+
config: Optional[ReducerConfig] = None,
|
|
75
|
+
):
|
|
76
|
+
if config is not None:
|
|
77
|
+
self.config = config
|
|
78
|
+
else:
|
|
79
|
+
self.config = ReducerConfig(
|
|
80
|
+
min_messages=min_messages,
|
|
81
|
+
max_messages=max_messages,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def min_messages(self) -> int:
|
|
86
|
+
return self.config.min_messages
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def max_messages(self) -> Optional[int]:
|
|
90
|
+
return self.config.max_messages
|
|
91
|
+
|
|
92
|
+
def reduce(
|
|
93
|
+
self,
|
|
94
|
+
existing: Optional[List[Any]] = None,
|
|
95
|
+
new: Optional[List[Any]] = None,
|
|
96
|
+
) -> ReducerResult:
|
|
97
|
+
"""
|
|
98
|
+
Concatenate existing + new messages, then prune if over threshold.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
existing: Current message list. Defaults to empty list.
|
|
102
|
+
new: New messages to append. Defaults to empty list.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
``ReducerResult`` with surviving messages, pruned messages,
|
|
106
|
+
and optional summary.
|
|
107
|
+
"""
|
|
108
|
+
if existing is None:
|
|
109
|
+
existing = []
|
|
110
|
+
if new is None:
|
|
111
|
+
new = []
|
|
112
|
+
|
|
113
|
+
messages = list(existing) + list(new)
|
|
114
|
+
max_msgs = self.config.max_messages
|
|
115
|
+
|
|
116
|
+
# No pruning needed
|
|
117
|
+
if max_msgs is None or len(messages) <= max_msgs:
|
|
118
|
+
return ReducerResult(surviving=messages, pruned=[])
|
|
119
|
+
|
|
120
|
+
# ── Identify indices to prune ──
|
|
121
|
+
to_delete: Set[int] = set()
|
|
122
|
+
excess_count = len(messages) - self.config.min_messages
|
|
123
|
+
start_idx = 1 if self.config.preserve_first else 0
|
|
124
|
+
|
|
125
|
+
# Iterate over the pruning window: from start_idx to excess_count
|
|
126
|
+
for i, msg in enumerate(messages[start_idx:excess_count], start=start_idx):
|
|
127
|
+
role = get_role(msg)
|
|
128
|
+
|
|
129
|
+
if role in ("ai", "human"):
|
|
130
|
+
to_delete.add(i)
|
|
131
|
+
|
|
132
|
+
# ToolMessage cascade: when pruning an AIMessage, also prune
|
|
133
|
+
# any ToolMessages that reference its tool_call IDs
|
|
134
|
+
if role == "ai" and self.config.cascade_tool_messages:
|
|
135
|
+
for tc in get_tool_calls(messages[i]):
|
|
136
|
+
tc_id = (
|
|
137
|
+
tc.get("id")
|
|
138
|
+
if isinstance(tc, dict)
|
|
139
|
+
else getattr(tc, "id", None)
|
|
140
|
+
)
|
|
141
|
+
if tc_id is None:
|
|
142
|
+
continue
|
|
143
|
+
for j in range(i + 1, len(messages)):
|
|
144
|
+
if (
|
|
145
|
+
get_role(messages[j]) == "tool"
|
|
146
|
+
and get_tool_call_id(messages[j]) == tc_id
|
|
147
|
+
):
|
|
148
|
+
to_delete.add(j)
|
|
149
|
+
|
|
150
|
+
# ── Split into surviving and pruned ──
|
|
151
|
+
pruned = [messages[i] for i in sorted(to_delete)]
|
|
152
|
+
surviving = [msg for i, msg in enumerate(messages) if i not in to_delete]
|
|
153
|
+
|
|
154
|
+
logger.debug(
|
|
155
|
+
"Reduced messages from %d to %d (pruned %d)",
|
|
156
|
+
len(messages),
|
|
157
|
+
len(surviving),
|
|
158
|
+
len(pruned),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# ── Optional summarization ──
|
|
162
|
+
summary = None
|
|
163
|
+
if pruned and self.config.summarize_fn is not None:
|
|
164
|
+
try:
|
|
165
|
+
summary = self.config.summarize_fn(pruned)
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
logger.warning("Summarization failed: %s", exc)
|
|
168
|
+
|
|
169
|
+
return ReducerResult(
|
|
170
|
+
surviving=surviving,
|
|
171
|
+
pruned=pruned,
|
|
172
|
+
summary=summary,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def as_langgraph_reducer(self):
|
|
176
|
+
"""
|
|
177
|
+
Return a function compatible with LangGraph's ``Annotated[list, fn]`` pattern.
|
|
178
|
+
|
|
179
|
+
The returned function has the signature ``(existing, new) -> merged_list``
|
|
180
|
+
which is what LangGraph expects for a custom reducer annotated on a
|
|
181
|
+
state field.
|
|
182
|
+
|
|
183
|
+
Usage::
|
|
184
|
+
|
|
185
|
+
reducer = MessageReducer(min_messages=10, max_messages=20)
|
|
186
|
+
|
|
187
|
+
class MyState(TypedDict):
|
|
188
|
+
messages: Annotated[list, reducer.as_langgraph_reducer()]
|
|
189
|
+
|
|
190
|
+
This is the bridge that lets ``PrunableStateFactory`` keep working
|
|
191
|
+
unchanged when migrating from ``langgraph-reducer``.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def _reduce(existing=None, new=None):
|
|
195
|
+
if existing is None:
|
|
196
|
+
existing = []
|
|
197
|
+
result = self.reduce(existing, new)
|
|
198
|
+
return result.surviving
|
|
199
|
+
|
|
200
|
+
return _reduce
|
|
201
|
+
|
|
202
|
+
def __repr__(self) -> str:
|
|
203
|
+
return (
|
|
204
|
+
f"MessageReducer(min_messages={self.config.min_messages}, "
|
|
205
|
+
f"max_messages={self.config.max_messages}, "
|
|
206
|
+
f"preserve_first={self.config.preserve_first}, "
|
|
207
|
+
f"cascade_tool_messages={self.config.cascade_tool_messages})"
|
|
208
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentstate-reducer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Framework-agnostic message reducer for AI agent state management. Works with LangGraph, CrewAI, and plain dicts.
|
|
5
|
+
Project-URL: Homepage, https://github.com/skamalj/agentstate-reducer
|
|
6
|
+
Project-URL: Repository, https://github.com/skamalj/agentstate-reducer
|
|
7
|
+
Author-email: Kamal <skamalj@github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: langchain-core>=0.2; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# agentstate-reducer
|
|
19
|
+
|
|
20
|
+
Framework-agnostic message reducer for AI agent state management. Works with **LangGraph**, **CrewAI**, and **plain dicts**.
|
|
21
|
+
|
|
22
|
+
## What It Does
|
|
23
|
+
|
|
24
|
+
Automatically prunes message history when it exceeds a threshold, keeping conversations manageable:
|
|
25
|
+
|
|
26
|
+
- **Windowed pruning**: Trigger at `max_messages`, retain `min_messages`
|
|
27
|
+
- **System message preservation**: Index 0 (system prompt) is never pruned
|
|
28
|
+
- **ToolMessage cascade**: When an AI message is pruned, linked ToolMessages are pruned too
|
|
29
|
+
- **Optional summarization**: Get a callback with pruned messages to generate summaries
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install agentstate-reducer
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Zero dependencies.** Works with Python 3.10+.
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from agentstate_reducer import MessageReducer
|
|
43
|
+
|
|
44
|
+
reducer = MessageReducer(min_messages=10, max_messages=20)
|
|
45
|
+
|
|
46
|
+
# Works with plain dicts
|
|
47
|
+
result = reducer.reduce(
|
|
48
|
+
existing=[
|
|
49
|
+
{"role": "system", "content": "You are helpful"},
|
|
50
|
+
{"role": "human", "content": "Hello"},
|
|
51
|
+
{"role": "ai", "content": "Hi there!"},
|
|
52
|
+
],
|
|
53
|
+
new=[{"role": "human", "content": "New message"}],
|
|
54
|
+
)
|
|
55
|
+
print(result.surviving) # Messages that remain
|
|
56
|
+
print(result.pruned) # Messages that were removed
|
|
57
|
+
|
|
58
|
+
# Works with LangChain BaseMessage objects too
|
|
59
|
+
from langchain_core.messages import HumanMessage, AIMessage
|
|
60
|
+
result = reducer.reduce(
|
|
61
|
+
existing=[HumanMessage(content="Hello")],
|
|
62
|
+
new=[AIMessage(content="Hi!")],
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## LangGraph Integration
|
|
67
|
+
|
|
68
|
+
Use with `PrunableStateFactory` or directly as a state annotation:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from agentstate_reducer import MessageReducer
|
|
72
|
+
from typing_extensions import Annotated, TypedDict
|
|
73
|
+
|
|
74
|
+
reducer = MessageReducer(min_messages=10, max_messages=20)
|
|
75
|
+
|
|
76
|
+
class MyState(TypedDict):
|
|
77
|
+
messages: Annotated[list, reducer.as_langgraph_reducer()]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## CrewAI Integration
|
|
81
|
+
|
|
82
|
+
Use with a CosmosDB persistence backend:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from agentstate_reducer import MessageReducer
|
|
86
|
+
|
|
87
|
+
reducer = MessageReducer(min_messages=10, max_messages=20)
|
|
88
|
+
# Pass to your persistence class which calls reducer.reduce()
|
|
89
|
+
# on save_state() to prune before storing
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Summarization
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from agentstate_reducer import MessageReducer, ReducerConfig
|
|
96
|
+
|
|
97
|
+
def summarize(pruned_messages):
|
|
98
|
+
# Call your LLM to summarize what was removed
|
|
99
|
+
return f"Summary of {len(pruned_messages)} messages"
|
|
100
|
+
|
|
101
|
+
config = ReducerConfig(
|
|
102
|
+
min_messages=10,
|
|
103
|
+
max_messages=20,
|
|
104
|
+
summarize_fn=summarize,
|
|
105
|
+
)
|
|
106
|
+
reducer = MessageReducer(config=config)
|
|
107
|
+
result = reducer.reduce(existing=messages)
|
|
108
|
+
print(result.summary) # "Summary of 5 messages"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## API
|
|
112
|
+
|
|
113
|
+
### `MessageReducer(min_messages, max_messages, *, config)`
|
|
114
|
+
|
|
115
|
+
| Param | Default | Description |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `min_messages` | `0` | Messages to retain after pruning |
|
|
118
|
+
| `max_messages` | `None` | Threshold to trigger pruning (`None` = never prune) |
|
|
119
|
+
| `config` | `None` | `ReducerConfig` object (overrides other params) |
|
|
120
|
+
|
|
121
|
+
### `reducer.reduce(existing, new) -> ReducerResult`
|
|
122
|
+
|
|
123
|
+
| Field | Type | Description |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `surviving` | `list` | Messages that remain |
|
|
126
|
+
| `pruned` | `list` | Messages that were removed |
|
|
127
|
+
| `summary` | `str \| None` | Summary from `summarize_fn` if configured |
|
|
128
|
+
|
|
129
|
+
### `reducer.as_langgraph_reducer() -> Callable`
|
|
130
|
+
|
|
131
|
+
Returns a function with signature `(existing, new) -> list` for use with LangGraph's `Annotated[list, fn]` pattern.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
agentstate_reducer/__init__.py,sha256=ZG4H-nc_p9ZLYaQ8Hw8kHMROsTZJDIkNNyGztR9z1kI,307
|
|
2
|
+
agentstate_reducer/adapters.py,sha256=oFkfCKV5PVgJamMbWDcfRb9h2p6QFNYWnj2ydVecgys,3406
|
|
3
|
+
agentstate_reducer/models.py,sha256=CmIJbmvzSh25p2OoZxFF_TP2fgHAIOdJQ-vCTcoUW-M,1951
|
|
4
|
+
agentstate_reducer/reducer.py,sha256=Ro9h24WYvpyAma1Ui45pFvKmvLPE72LBYmOPcQkltAs,7166
|
|
5
|
+
agentstate_reducer-0.1.0.dist-info/METADATA,sha256=gR1DO4C9Dfo7jwAgBknbDpyhf4vvmu8sTjAX330llps,4056
|
|
6
|
+
agentstate_reducer-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
agentstate_reducer-0.1.0.dist-info/RECORD,,
|