proxilion 0.0.1__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.
- proxilion/__init__.py +136 -0
- proxilion/audit/__init__.py +133 -0
- proxilion/audit/base_exporters.py +527 -0
- proxilion/audit/compliance/__init__.py +130 -0
- proxilion/audit/compliance/base.py +457 -0
- proxilion/audit/compliance/eu_ai_act.py +603 -0
- proxilion/audit/compliance/iso27001.py +544 -0
- proxilion/audit/compliance/soc2.py +491 -0
- proxilion/audit/events.py +493 -0
- proxilion/audit/explainability.py +1173 -0
- proxilion/audit/exporters/__init__.py +58 -0
- proxilion/audit/exporters/aws_s3.py +636 -0
- proxilion/audit/exporters/azure_storage.py +608 -0
- proxilion/audit/exporters/cloud_base.py +468 -0
- proxilion/audit/exporters/gcp_storage.py +570 -0
- proxilion/audit/exporters/multi_exporter.py +498 -0
- proxilion/audit/hash_chain.py +652 -0
- proxilion/audit/logger.py +543 -0
- proxilion/caching/__init__.py +49 -0
- proxilion/caching/tool_cache.py +633 -0
- proxilion/context/__init__.py +73 -0
- proxilion/context/context_window.py +556 -0
- proxilion/context/message_history.py +505 -0
- proxilion/context/session.py +735 -0
- proxilion/contrib/__init__.py +51 -0
- proxilion/contrib/anthropic.py +609 -0
- proxilion/contrib/google.py +1012 -0
- proxilion/contrib/langchain.py +641 -0
- proxilion/contrib/mcp.py +893 -0
- proxilion/contrib/openai.py +646 -0
- proxilion/core.py +3058 -0
- proxilion/decorators.py +966 -0
- proxilion/engines/__init__.py +287 -0
- proxilion/engines/base.py +266 -0
- proxilion/engines/casbin_engine.py +412 -0
- proxilion/engines/opa_engine.py +493 -0
- proxilion/engines/simple.py +437 -0
- proxilion/exceptions.py +887 -0
- proxilion/guards/__init__.py +54 -0
- proxilion/guards/input_guard.py +522 -0
- proxilion/guards/output_guard.py +634 -0
- proxilion/observability/__init__.py +198 -0
- proxilion/observability/cost_tracker.py +866 -0
- proxilion/observability/hooks.py +683 -0
- proxilion/observability/metrics.py +798 -0
- proxilion/observability/session_cost_tracker.py +1063 -0
- proxilion/policies/__init__.py +67 -0
- proxilion/policies/base.py +304 -0
- proxilion/policies/builtin.py +486 -0
- proxilion/policies/registry.py +376 -0
- proxilion/providers/__init__.py +201 -0
- proxilion/providers/adapter.py +468 -0
- proxilion/providers/anthropic_adapter.py +330 -0
- proxilion/providers/gemini_adapter.py +391 -0
- proxilion/providers/openai_adapter.py +294 -0
- proxilion/py.typed +0 -0
- proxilion/resilience/__init__.py +81 -0
- proxilion/resilience/degradation.py +615 -0
- proxilion/resilience/fallback.py +555 -0
- proxilion/resilience/retry.py +554 -0
- proxilion/scheduling/__init__.py +57 -0
- proxilion/scheduling/priority_queue.py +419 -0
- proxilion/scheduling/scheduler.py +459 -0
- proxilion/security/__init__.py +244 -0
- proxilion/security/agent_trust.py +968 -0
- proxilion/security/behavioral_drift.py +794 -0
- proxilion/security/cascade_protection.py +869 -0
- proxilion/security/circuit_breaker.py +428 -0
- proxilion/security/cost_limiter.py +690 -0
- proxilion/security/idor_protection.py +460 -0
- proxilion/security/intent_capsule.py +849 -0
- proxilion/security/intent_validator.py +495 -0
- proxilion/security/memory_integrity.py +767 -0
- proxilion/security/rate_limiter.py +509 -0
- proxilion/security/scope_enforcer.py +680 -0
- proxilion/security/sequence_validator.py +636 -0
- proxilion/security/trust_boundaries.py +784 -0
- proxilion/streaming/__init__.py +70 -0
- proxilion/streaming/detector.py +761 -0
- proxilion/streaming/transformer.py +674 -0
- proxilion/timeouts/__init__.py +55 -0
- proxilion/timeouts/decorators.py +477 -0
- proxilion/timeouts/manager.py +545 -0
- proxilion/tools/__init__.py +69 -0
- proxilion/tools/decorators.py +493 -0
- proxilion/tools/registry.py +732 -0
- proxilion/types.py +339 -0
- proxilion/validation/__init__.py +93 -0
- proxilion/validation/pydantic_schema.py +351 -0
- proxilion/validation/schema.py +651 -0
- proxilion-0.0.1.dist-info/METADATA +872 -0
- proxilion-0.0.1.dist-info/RECORD +94 -0
- proxilion-0.0.1.dist-info/WHEEL +4 -0
- proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message history management for AI agent sessions.
|
|
3
|
+
|
|
4
|
+
Provides message tracking with token counting and truncation strategies
|
|
5
|
+
for managing conversation context within LLM token limits.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MessageRole(Enum):
|
|
18
|
+
"""Role of a message in conversation history."""
|
|
19
|
+
|
|
20
|
+
USER = "user"
|
|
21
|
+
ASSISTANT = "assistant"
|
|
22
|
+
SYSTEM = "system"
|
|
23
|
+
TOOL_CALL = "tool_call"
|
|
24
|
+
TOOL_RESULT = "tool_result"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def estimate_tokens(text: str) -> int:
|
|
28
|
+
"""
|
|
29
|
+
Estimate token count without tiktoken dependency.
|
|
30
|
+
|
|
31
|
+
Uses a blend of word-based and character-based heuristics that
|
|
32
|
+
approximates tokenizer behavior for English text. This provides
|
|
33
|
+
a reasonable estimate (~10-15% accurate) without external dependencies.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
text: The text to estimate tokens for.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Estimated token count.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
>>> estimate_tokens("Hello, world!")
|
|
43
|
+
4
|
|
44
|
+
>>> estimate_tokens("This is a longer sentence with more words.")
|
|
45
|
+
11
|
|
46
|
+
"""
|
|
47
|
+
if not text:
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
words = len(text.split())
|
|
51
|
+
chars = len(text)
|
|
52
|
+
|
|
53
|
+
# Blend word and character estimates
|
|
54
|
+
# ~1.3 tokens per word, ~4 chars per token
|
|
55
|
+
# Average the two approaches for better accuracy
|
|
56
|
+
word_estimate = int(words * 1.3)
|
|
57
|
+
char_estimate = chars // 4
|
|
58
|
+
|
|
59
|
+
return max(1, (word_estimate + char_estimate) // 2)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Message:
|
|
64
|
+
"""
|
|
65
|
+
A single message in conversation history.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
role: The role of the message sender.
|
|
69
|
+
content: The message content.
|
|
70
|
+
timestamp: When the message was created.
|
|
71
|
+
metadata: Additional metadata (tool name, function args, etc.).
|
|
72
|
+
token_count: Estimated token count for this message.
|
|
73
|
+
message_id: Unique identifier for this message.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
role: MessageRole
|
|
77
|
+
content: str
|
|
78
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
79
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
80
|
+
token_count: int | None = None
|
|
81
|
+
message_id: str | None = None
|
|
82
|
+
|
|
83
|
+
def __post_init__(self) -> None:
|
|
84
|
+
"""Compute token count if not provided."""
|
|
85
|
+
if self.token_count is None:
|
|
86
|
+
self.token_count = estimate_tokens(self.content)
|
|
87
|
+
if self.message_id is None:
|
|
88
|
+
import uuid
|
|
89
|
+
|
|
90
|
+
self.message_id = str(uuid.uuid4())
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> dict[str, Any]:
|
|
93
|
+
"""
|
|
94
|
+
Convert message to dictionary format.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Dictionary representation of the message.
|
|
98
|
+
"""
|
|
99
|
+
return {
|
|
100
|
+
"role": self.role.value,
|
|
101
|
+
"content": self.content,
|
|
102
|
+
"timestamp": self.timestamp.isoformat(),
|
|
103
|
+
"metadata": self.metadata,
|
|
104
|
+
"token_count": self.token_count,
|
|
105
|
+
"message_id": self.message_id,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_dict(cls, data: dict[str, Any]) -> Message:
|
|
110
|
+
"""
|
|
111
|
+
Create message from dictionary.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
data: Dictionary with message data.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Message instance.
|
|
118
|
+
"""
|
|
119
|
+
return cls(
|
|
120
|
+
role=MessageRole(data["role"]),
|
|
121
|
+
content=data["content"],
|
|
122
|
+
timestamp=datetime.fromisoformat(data["timestamp"]),
|
|
123
|
+
metadata=data.get("metadata", {}),
|
|
124
|
+
token_count=data.get("token_count"),
|
|
125
|
+
message_id=data.get("message_id"),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class MessageHistory:
|
|
130
|
+
"""
|
|
131
|
+
Manages message list with token tracking and truncation.
|
|
132
|
+
|
|
133
|
+
Thread-safe collection of messages with support for various
|
|
134
|
+
retrieval patterns and LLM API formatting.
|
|
135
|
+
|
|
136
|
+
Attributes:
|
|
137
|
+
max_messages: Maximum number of messages to retain.
|
|
138
|
+
max_tokens: Maximum total tokens to retain.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> history = MessageHistory(max_messages=100, max_tokens=8000)
|
|
142
|
+
>>> history.append(Message(role=MessageRole.USER, content="Hello!"))
|
|
143
|
+
>>> history.append(Message(role=MessageRole.ASSISTANT, content="Hi there!"))
|
|
144
|
+
>>> len(history)
|
|
145
|
+
2
|
|
146
|
+
>>> history.get_total_tokens()
|
|
147
|
+
6
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
max_messages: int | None = None,
|
|
153
|
+
max_tokens: int | None = None,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Initialize message history.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
max_messages: Maximum number of messages to retain. None for unlimited.
|
|
160
|
+
max_tokens: Maximum total tokens to retain. None for unlimited.
|
|
161
|
+
"""
|
|
162
|
+
self.max_messages = max_messages
|
|
163
|
+
self.max_tokens = max_tokens
|
|
164
|
+
self._messages: list[Message] = []
|
|
165
|
+
self._lock = threading.RLock()
|
|
166
|
+
|
|
167
|
+
def __len__(self) -> int:
|
|
168
|
+
"""Return number of messages."""
|
|
169
|
+
with self._lock:
|
|
170
|
+
return len(self._messages)
|
|
171
|
+
|
|
172
|
+
def __iter__(self):
|
|
173
|
+
"""Iterate over messages."""
|
|
174
|
+
with self._lock:
|
|
175
|
+
return iter(list(self._messages))
|
|
176
|
+
|
|
177
|
+
def __getitem__(self, index: int | slice) -> Message | list[Message]:
|
|
178
|
+
"""Get message by index or slice."""
|
|
179
|
+
with self._lock:
|
|
180
|
+
return self._messages[index]
|
|
181
|
+
|
|
182
|
+
def append(self, message: Message) -> list[Message]:
|
|
183
|
+
"""
|
|
184
|
+
Append a message to history.
|
|
185
|
+
|
|
186
|
+
If max_messages or max_tokens is exceeded, older messages
|
|
187
|
+
are removed (except system messages which are preserved).
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
message: The message to append.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
List of messages that were removed due to limits.
|
|
194
|
+
"""
|
|
195
|
+
with self._lock:
|
|
196
|
+
self._messages.append(message)
|
|
197
|
+
return self._enforce_limits()
|
|
198
|
+
|
|
199
|
+
def _enforce_limits(self) -> list[Message]:
|
|
200
|
+
"""
|
|
201
|
+
Enforce max_messages and max_tokens limits.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
List of removed messages.
|
|
205
|
+
"""
|
|
206
|
+
removed: list[Message] = []
|
|
207
|
+
|
|
208
|
+
# Enforce message limit
|
|
209
|
+
if self.max_messages is not None:
|
|
210
|
+
while len(self._messages) > self.max_messages:
|
|
211
|
+
# Find first non-system message to remove
|
|
212
|
+
for i, msg in enumerate(self._messages):
|
|
213
|
+
if msg.role != MessageRole.SYSTEM:
|
|
214
|
+
removed.append(self._messages.pop(i))
|
|
215
|
+
break
|
|
216
|
+
else:
|
|
217
|
+
# All messages are system messages, remove oldest
|
|
218
|
+
if self._messages:
|
|
219
|
+
removed.append(self._messages.pop(0))
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
# Enforce token limit
|
|
223
|
+
if self.max_tokens is not None:
|
|
224
|
+
while self.get_total_tokens() > self.max_tokens and len(self._messages) > 1:
|
|
225
|
+
# Find first non-system message to remove
|
|
226
|
+
for i, msg in enumerate(self._messages):
|
|
227
|
+
if msg.role != MessageRole.SYSTEM:
|
|
228
|
+
removed.append(self._messages.pop(i))
|
|
229
|
+
break
|
|
230
|
+
else:
|
|
231
|
+
# All messages are system messages, keep at least one
|
|
232
|
+
if len(self._messages) > 1:
|
|
233
|
+
removed.append(self._messages.pop(0))
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
return removed
|
|
237
|
+
|
|
238
|
+
def get_recent(self, n: int) -> list[Message]:
|
|
239
|
+
"""
|
|
240
|
+
Get the n most recent messages.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
n: Number of messages to retrieve.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of most recent messages.
|
|
247
|
+
"""
|
|
248
|
+
with self._lock:
|
|
249
|
+
return list(self._messages[-n:])
|
|
250
|
+
|
|
251
|
+
def get_by_role(self, role: MessageRole) -> list[Message]:
|
|
252
|
+
"""
|
|
253
|
+
Get all messages with a specific role.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
role: The role to filter by.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
List of messages with the specified role.
|
|
260
|
+
"""
|
|
261
|
+
with self._lock:
|
|
262
|
+
return [msg for msg in self._messages if msg.role == role]
|
|
263
|
+
|
|
264
|
+
def truncate_to_token_limit(self, max_tokens: int) -> list[Message]:
|
|
265
|
+
"""
|
|
266
|
+
Truncate history to fit within token limit.
|
|
267
|
+
|
|
268
|
+
Removes oldest non-system messages first to fit within the limit.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
max_tokens: Maximum tokens to retain.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
List of messages that were removed.
|
|
275
|
+
"""
|
|
276
|
+
with self._lock:
|
|
277
|
+
removed: list[Message] = []
|
|
278
|
+
while self.get_total_tokens() > max_tokens and len(self._messages) > 1:
|
|
279
|
+
# Find first non-system message to remove
|
|
280
|
+
for i, msg in enumerate(self._messages):
|
|
281
|
+
if msg.role != MessageRole.SYSTEM:
|
|
282
|
+
removed.append(self._messages.pop(i))
|
|
283
|
+
break
|
|
284
|
+
else:
|
|
285
|
+
# All remaining are system messages
|
|
286
|
+
if len(self._messages) > 1:
|
|
287
|
+
removed.append(self._messages.pop(0))
|
|
288
|
+
break
|
|
289
|
+
return removed
|
|
290
|
+
|
|
291
|
+
def to_llm_format(self, provider: str = "openai") -> list[dict[str, Any]]:
|
|
292
|
+
"""
|
|
293
|
+
Format messages for LLM API calls.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
provider: The LLM provider format to use.
|
|
297
|
+
Supported: "openai", "anthropic", "google"
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
List of message dictionaries formatted for the provider.
|
|
301
|
+
|
|
302
|
+
Example:
|
|
303
|
+
>>> history.to_llm_format("openai")
|
|
304
|
+
[{"role": "user", "content": "Hello!"}, ...]
|
|
305
|
+
"""
|
|
306
|
+
with self._lock:
|
|
307
|
+
result: list[dict[str, Any]] = []
|
|
308
|
+
|
|
309
|
+
for msg in self._messages:
|
|
310
|
+
if provider == "openai":
|
|
311
|
+
result.append(self._to_openai_format(msg))
|
|
312
|
+
elif provider == "anthropic":
|
|
313
|
+
result.append(self._to_anthropic_format(msg))
|
|
314
|
+
elif provider == "google":
|
|
315
|
+
result.append(self._to_google_format(msg))
|
|
316
|
+
else:
|
|
317
|
+
# Default to OpenAI-style format
|
|
318
|
+
result.append(self._to_openai_format(msg))
|
|
319
|
+
|
|
320
|
+
return result
|
|
321
|
+
|
|
322
|
+
def _to_openai_format(self, msg: Message) -> dict[str, Any]:
|
|
323
|
+
"""Convert message to OpenAI format."""
|
|
324
|
+
role_map = {
|
|
325
|
+
MessageRole.USER: "user",
|
|
326
|
+
MessageRole.ASSISTANT: "assistant",
|
|
327
|
+
MessageRole.SYSTEM: "system",
|
|
328
|
+
MessageRole.TOOL_CALL: "assistant",
|
|
329
|
+
MessageRole.TOOL_RESULT: "tool",
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
formatted: dict[str, Any] = {
|
|
333
|
+
"role": role_map.get(msg.role, "user"),
|
|
334
|
+
"content": msg.content,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# Add tool-specific fields
|
|
338
|
+
if msg.role == MessageRole.TOOL_CALL and "tool_calls" in msg.metadata:
|
|
339
|
+
formatted["tool_calls"] = msg.metadata["tool_calls"]
|
|
340
|
+
formatted["content"] = None
|
|
341
|
+
|
|
342
|
+
if msg.role == MessageRole.TOOL_RESULT and "tool_call_id" in msg.metadata:
|
|
343
|
+
formatted["tool_call_id"] = msg.metadata["tool_call_id"]
|
|
344
|
+
|
|
345
|
+
return formatted
|
|
346
|
+
|
|
347
|
+
def _to_anthropic_format(self, msg: Message) -> dict[str, Any]:
|
|
348
|
+
"""Convert message to Anthropic format."""
|
|
349
|
+
role_map = {
|
|
350
|
+
MessageRole.USER: "user",
|
|
351
|
+
MessageRole.ASSISTANT: "assistant",
|
|
352
|
+
MessageRole.SYSTEM: "user", # Anthropic handles system differently
|
|
353
|
+
MessageRole.TOOL_CALL: "assistant",
|
|
354
|
+
MessageRole.TOOL_RESULT: "user",
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
formatted: dict[str, Any] = {
|
|
358
|
+
"role": role_map.get(msg.role, "user"),
|
|
359
|
+
"content": msg.content,
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
# Handle tool use blocks for Anthropic
|
|
363
|
+
if msg.role == MessageRole.TOOL_CALL and "tool_use" in msg.metadata:
|
|
364
|
+
formatted["content"] = msg.metadata["tool_use"]
|
|
365
|
+
|
|
366
|
+
if msg.role == MessageRole.TOOL_RESULT and "tool_result" in msg.metadata:
|
|
367
|
+
formatted["content"] = [msg.metadata["tool_result"]]
|
|
368
|
+
|
|
369
|
+
return formatted
|
|
370
|
+
|
|
371
|
+
def _to_google_format(self, msg: Message) -> dict[str, Any]:
|
|
372
|
+
"""Convert message to Google/Gemini format."""
|
|
373
|
+
role_map = {
|
|
374
|
+
MessageRole.USER: "user",
|
|
375
|
+
MessageRole.ASSISTANT: "model",
|
|
376
|
+
MessageRole.SYSTEM: "user",
|
|
377
|
+
MessageRole.TOOL_CALL: "model",
|
|
378
|
+
MessageRole.TOOL_RESULT: "function",
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
formatted: dict[str, Any] = {
|
|
382
|
+
"role": role_map.get(msg.role, "user"),
|
|
383
|
+
"parts": [{"text": msg.content}],
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
# Handle function calls for Gemini
|
|
387
|
+
if msg.role == MessageRole.TOOL_CALL and "function_call" in msg.metadata:
|
|
388
|
+
formatted["parts"] = [{"function_call": msg.metadata["function_call"]}]
|
|
389
|
+
|
|
390
|
+
if msg.role == MessageRole.TOOL_RESULT and "function_response" in msg.metadata:
|
|
391
|
+
formatted["parts"] = [
|
|
392
|
+
{"function_response": msg.metadata["function_response"]}
|
|
393
|
+
]
|
|
394
|
+
|
|
395
|
+
return formatted
|
|
396
|
+
|
|
397
|
+
def get_total_tokens(self) -> int:
|
|
398
|
+
"""
|
|
399
|
+
Get total token count across all messages.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Total estimated tokens.
|
|
403
|
+
"""
|
|
404
|
+
with self._lock:
|
|
405
|
+
return sum(msg.token_count or 0 for msg in self._messages)
|
|
406
|
+
|
|
407
|
+
def get_messages(self) -> list[Message]:
|
|
408
|
+
"""
|
|
409
|
+
Get a copy of all messages.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
List of all messages in history.
|
|
413
|
+
"""
|
|
414
|
+
with self._lock:
|
|
415
|
+
return list(self._messages)
|
|
416
|
+
|
|
417
|
+
def clear(self) -> list[Message]:
|
|
418
|
+
"""
|
|
419
|
+
Clear all messages from history.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
List of all cleared messages.
|
|
423
|
+
"""
|
|
424
|
+
with self._lock:
|
|
425
|
+
cleared = list(self._messages)
|
|
426
|
+
self._messages = []
|
|
427
|
+
return cleared
|
|
428
|
+
|
|
429
|
+
def clear_except_system(self) -> list[Message]:
|
|
430
|
+
"""
|
|
431
|
+
Clear all non-system messages from history.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
List of cleared messages.
|
|
435
|
+
"""
|
|
436
|
+
with self._lock:
|
|
437
|
+
system_msgs = [m for m in self._messages if m.role == MessageRole.SYSTEM]
|
|
438
|
+
cleared = [m for m in self._messages if m.role != MessageRole.SYSTEM]
|
|
439
|
+
self._messages = system_msgs
|
|
440
|
+
return cleared
|
|
441
|
+
|
|
442
|
+
def find_by_id(self, message_id: str) -> Message | None:
|
|
443
|
+
"""
|
|
444
|
+
Find a message by its ID.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
message_id: The message ID to find.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
The message if found, None otherwise.
|
|
451
|
+
"""
|
|
452
|
+
with self._lock:
|
|
453
|
+
for msg in self._messages:
|
|
454
|
+
if msg.message_id == message_id:
|
|
455
|
+
return msg
|
|
456
|
+
return None
|
|
457
|
+
|
|
458
|
+
def remove_by_id(self, message_id: str) -> Message | None:
|
|
459
|
+
"""
|
|
460
|
+
Remove a message by its ID.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
message_id: The message ID to remove.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
The removed message if found, None otherwise.
|
|
467
|
+
"""
|
|
468
|
+
with self._lock:
|
|
469
|
+
for i, msg in enumerate(self._messages):
|
|
470
|
+
if msg.message_id == message_id:
|
|
471
|
+
return self._messages.pop(i)
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
def to_dict(self) -> dict[str, Any]:
|
|
475
|
+
"""
|
|
476
|
+
Serialize history to dictionary.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Dictionary representation of the history.
|
|
480
|
+
"""
|
|
481
|
+
with self._lock:
|
|
482
|
+
return {
|
|
483
|
+
"max_messages": self.max_messages,
|
|
484
|
+
"max_tokens": self.max_tokens,
|
|
485
|
+
"messages": [msg.to_dict() for msg in self._messages],
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
@classmethod
|
|
489
|
+
def from_dict(cls, data: dict[str, Any]) -> MessageHistory:
|
|
490
|
+
"""
|
|
491
|
+
Deserialize history from dictionary.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
data: Dictionary with history data.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
MessageHistory instance.
|
|
498
|
+
"""
|
|
499
|
+
history = cls(
|
|
500
|
+
max_messages=data.get("max_messages"),
|
|
501
|
+
max_tokens=data.get("max_tokens"),
|
|
502
|
+
)
|
|
503
|
+
for msg_data in data.get("messages", []):
|
|
504
|
+
history._messages.append(Message.from_dict(msg_data))
|
|
505
|
+
return history
|