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.
Files changed (94) hide show
  1. proxilion/__init__.py +136 -0
  2. proxilion/audit/__init__.py +133 -0
  3. proxilion/audit/base_exporters.py +527 -0
  4. proxilion/audit/compliance/__init__.py +130 -0
  5. proxilion/audit/compliance/base.py +457 -0
  6. proxilion/audit/compliance/eu_ai_act.py +603 -0
  7. proxilion/audit/compliance/iso27001.py +544 -0
  8. proxilion/audit/compliance/soc2.py +491 -0
  9. proxilion/audit/events.py +493 -0
  10. proxilion/audit/explainability.py +1173 -0
  11. proxilion/audit/exporters/__init__.py +58 -0
  12. proxilion/audit/exporters/aws_s3.py +636 -0
  13. proxilion/audit/exporters/azure_storage.py +608 -0
  14. proxilion/audit/exporters/cloud_base.py +468 -0
  15. proxilion/audit/exporters/gcp_storage.py +570 -0
  16. proxilion/audit/exporters/multi_exporter.py +498 -0
  17. proxilion/audit/hash_chain.py +652 -0
  18. proxilion/audit/logger.py +543 -0
  19. proxilion/caching/__init__.py +49 -0
  20. proxilion/caching/tool_cache.py +633 -0
  21. proxilion/context/__init__.py +73 -0
  22. proxilion/context/context_window.py +556 -0
  23. proxilion/context/message_history.py +505 -0
  24. proxilion/context/session.py +735 -0
  25. proxilion/contrib/__init__.py +51 -0
  26. proxilion/contrib/anthropic.py +609 -0
  27. proxilion/contrib/google.py +1012 -0
  28. proxilion/contrib/langchain.py +641 -0
  29. proxilion/contrib/mcp.py +893 -0
  30. proxilion/contrib/openai.py +646 -0
  31. proxilion/core.py +3058 -0
  32. proxilion/decorators.py +966 -0
  33. proxilion/engines/__init__.py +287 -0
  34. proxilion/engines/base.py +266 -0
  35. proxilion/engines/casbin_engine.py +412 -0
  36. proxilion/engines/opa_engine.py +493 -0
  37. proxilion/engines/simple.py +437 -0
  38. proxilion/exceptions.py +887 -0
  39. proxilion/guards/__init__.py +54 -0
  40. proxilion/guards/input_guard.py +522 -0
  41. proxilion/guards/output_guard.py +634 -0
  42. proxilion/observability/__init__.py +198 -0
  43. proxilion/observability/cost_tracker.py +866 -0
  44. proxilion/observability/hooks.py +683 -0
  45. proxilion/observability/metrics.py +798 -0
  46. proxilion/observability/session_cost_tracker.py +1063 -0
  47. proxilion/policies/__init__.py +67 -0
  48. proxilion/policies/base.py +304 -0
  49. proxilion/policies/builtin.py +486 -0
  50. proxilion/policies/registry.py +376 -0
  51. proxilion/providers/__init__.py +201 -0
  52. proxilion/providers/adapter.py +468 -0
  53. proxilion/providers/anthropic_adapter.py +330 -0
  54. proxilion/providers/gemini_adapter.py +391 -0
  55. proxilion/providers/openai_adapter.py +294 -0
  56. proxilion/py.typed +0 -0
  57. proxilion/resilience/__init__.py +81 -0
  58. proxilion/resilience/degradation.py +615 -0
  59. proxilion/resilience/fallback.py +555 -0
  60. proxilion/resilience/retry.py +554 -0
  61. proxilion/scheduling/__init__.py +57 -0
  62. proxilion/scheduling/priority_queue.py +419 -0
  63. proxilion/scheduling/scheduler.py +459 -0
  64. proxilion/security/__init__.py +244 -0
  65. proxilion/security/agent_trust.py +968 -0
  66. proxilion/security/behavioral_drift.py +794 -0
  67. proxilion/security/cascade_protection.py +869 -0
  68. proxilion/security/circuit_breaker.py +428 -0
  69. proxilion/security/cost_limiter.py +690 -0
  70. proxilion/security/idor_protection.py +460 -0
  71. proxilion/security/intent_capsule.py +849 -0
  72. proxilion/security/intent_validator.py +495 -0
  73. proxilion/security/memory_integrity.py +767 -0
  74. proxilion/security/rate_limiter.py +509 -0
  75. proxilion/security/scope_enforcer.py +680 -0
  76. proxilion/security/sequence_validator.py +636 -0
  77. proxilion/security/trust_boundaries.py +784 -0
  78. proxilion/streaming/__init__.py +70 -0
  79. proxilion/streaming/detector.py +761 -0
  80. proxilion/streaming/transformer.py +674 -0
  81. proxilion/timeouts/__init__.py +55 -0
  82. proxilion/timeouts/decorators.py +477 -0
  83. proxilion/timeouts/manager.py +545 -0
  84. proxilion/tools/__init__.py +69 -0
  85. proxilion/tools/decorators.py +493 -0
  86. proxilion/tools/registry.py +732 -0
  87. proxilion/types.py +339 -0
  88. proxilion/validation/__init__.py +93 -0
  89. proxilion/validation/pydantic_schema.py +351 -0
  90. proxilion/validation/schema.py +651 -0
  91. proxilion-0.0.1.dist-info/METADATA +872 -0
  92. proxilion-0.0.1.dist-info/RECORD +94 -0
  93. proxilion-0.0.1.dist-info/WHEEL +4 -0
  94. 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