pyagent-context 0.1.0__tar.gz → 0.2.3__tar.gz
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.
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/PKG-INFO +196 -2
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/README.md +195 -1
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/pyproject.toml +1 -1
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/compression.py +3 -7
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/item.py +10 -5
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/ledger.py +6 -6
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/lifecycle.py +5 -2
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/memory/__init__.py +2 -2
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/memory/semantic.py +106 -19
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/memory/session.py +0 -1
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/memory/working.py +3 -1
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/redaction.py +1 -1
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/retrieval.py +8 -7
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/test_compression.py +37 -27
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/test_ledger.py +4 -2
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/test_lifecycle.py +36 -26
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/test_memory.py +10 -6
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/test_retrieval.py +23 -17
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/.gitignore +0 -0
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/__init__.py +0 -0
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/py.typed +0 -0
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/__init__.py +0 -0
- {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/test_item.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyagent-context
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Three-tier memory with trust-aware context ledger for multi-agent LLM systems
|
|
5
5
|
Project-URL: Homepage, https://pyagent.org
|
|
6
6
|
Project-URL: Repository, https://github.com/pyagent-core/pyagent
|
|
@@ -281,6 +281,200 @@ context_messages = ledger.to_messages(max_tokens=2000)
|
|
|
281
281
|
ledger.add(result.output, "pipeline", TrustLevel.INFERRED)
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
+
## Architecture — Three-Tier Memory Model
|
|
285
|
+
|
|
286
|
+
```mermaid
|
|
287
|
+
flowchart TD
|
|
288
|
+
subgraph Agent Interaction
|
|
289
|
+
A1[Agent 1] -->|append output| CL[ContextLedger]
|
|
290
|
+
CL -->|to_messages budget| A2[Agent 2 input]
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
subgraph Three-Tier Memory
|
|
294
|
+
CL --> WM[WorkingMemory — bounded deque, current turn]
|
|
295
|
+
CL --> SM[SessionMemory — JSON/SQLite, cross-turn persistence]
|
|
296
|
+
CL --> SEM[SemanticMemory — TF-IDF similarity, long-term recall]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
subgraph Processing
|
|
300
|
+
CL --> CC[ContextCompressor — policy-based trimming]
|
|
301
|
+
CL --> TR[TrustAwareRetriever — ranked retrieval]
|
|
302
|
+
CL --> CR[ContextRedactor — sensitivity filtering]
|
|
303
|
+
CL --> LC[ContextLifecycle — expiry, decay, consolidation]
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Memory Tier Details
|
|
308
|
+
|
|
309
|
+
| Tier | Class | Storage | Capacity | Eviction | Use Case |
|
|
310
|
+
|------|-------|---------|----------|----------|----------|
|
|
311
|
+
| **Working** | `WorkingMemory` | In-memory deque | `max_items` or `max_tokens` | Oldest-first when full | Current conversation turn |
|
|
312
|
+
| **Session** | `SessionMemory` | JSON file or SQLite | Unlimited | Manual clear | Cross-turn persistence (multi-step workflows) |
|
|
313
|
+
| **Semantic** | `InMemorySemanticStore` | In-memory TF-IDF index | Unlimited | Manual | Long-term recall via similarity search |
|
|
314
|
+
|
|
315
|
+
### WorkingMemory Eviction
|
|
316
|
+
|
|
317
|
+
When `WorkingMemory` reaches its `max_items` or `max_tokens` limit, the oldest items are evicted first. Utilization metrics are available:
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
from pyagent_context import WorkingMemory
|
|
321
|
+
|
|
322
|
+
wm = WorkingMemory(max_items=20, max_tokens=8000)
|
|
323
|
+
wm.add(item)
|
|
324
|
+
|
|
325
|
+
print(wm.utilization) # 0.05 → 5% of max_items used
|
|
326
|
+
print(wm.token_usage) # current token estimate total
|
|
327
|
+
print(len(wm)) # number of items in memory
|
|
328
|
+
wm.clear() # flush all items
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### SessionMemory Backends
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from pyagent_context import SessionMemory
|
|
335
|
+
|
|
336
|
+
# JSON backend — simple file-based persistence
|
|
337
|
+
sm_json = SessionMemory(backend="json", path="session.json")
|
|
338
|
+
|
|
339
|
+
# SQLite backend — more robust, concurrent-safe
|
|
340
|
+
sm_sqlite = SessionMemory(backend="sqlite", path="session.db")
|
|
341
|
+
|
|
342
|
+
sm_json.add(item)
|
|
343
|
+
sm_json.save() # persist to disk
|
|
344
|
+
sm_json.load() # reload from disk
|
|
345
|
+
items = sm_json.retrieve(query="billing", top_k=5)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### SemanticMemory — TF-IDF Similarity Search
|
|
349
|
+
|
|
350
|
+
The `InMemorySemanticStore` uses TF-IDF vectorization for similarity-based retrieval across the full context history:
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
from pyagent_context import InMemorySemanticStore
|
|
354
|
+
|
|
355
|
+
store = InMemorySemanticStore()
|
|
356
|
+
store.add(item1)
|
|
357
|
+
store.add(item2)
|
|
358
|
+
store.add(item3)
|
|
359
|
+
|
|
360
|
+
# Retrieve items most similar to the query
|
|
361
|
+
results = store.search("billing question", top_k=3)
|
|
362
|
+
for item, score in results:
|
|
363
|
+
print(f"[{score:.2f}] {item.content[:80]}...")
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## ContextLedger — Token-Budgeted Message Conversion
|
|
367
|
+
|
|
368
|
+
The `ContextLedger` is an append-only log that converts stored `ContextItem` objects into LLM-compatible messages with automatic token budgeting:
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
from pyagent_context import ContextLedger, ContextItem, TrustLevel
|
|
372
|
+
|
|
373
|
+
ledger = ContextLedger()
|
|
374
|
+
ledger.append(ContextItem(content="Revenue was $25.2B", source="database", trust=TrustLevel.VERIFIED))
|
|
375
|
+
ledger.append(ContextItem(content="Margin expanded to 17%", source="agent_1", trust=TrustLevel.INFERRED))
|
|
376
|
+
|
|
377
|
+
# Convert to messages with a token budget
|
|
378
|
+
# Higher-trust items are prioritized when budget is tight
|
|
379
|
+
messages = ledger.to_messages(budget=4000)
|
|
380
|
+
print(ledger.total_tokens()) # total token estimate across all items
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## TrustAwareRetriever — Composite Scoring
|
|
384
|
+
|
|
385
|
+
The retriever ranks items using a composite score of three signals:
|
|
386
|
+
|
|
387
|
+
| Signal | Weight | Description |
|
|
388
|
+
|--------|--------|-------------|
|
|
389
|
+
| **Trust** | Configurable | `VERIFIED` > `INFERRED` > `USER_PROVIDED` > `UNVERIFIED` |
|
|
390
|
+
| **Recency** | Half-life decay | Newer items score higher; decay rate is configurable |
|
|
391
|
+
| **Relevance** | Keyword overlap | TF-IDF similarity between query and item content |
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
from pyagent_context import TrustAwareRetriever
|
|
395
|
+
|
|
396
|
+
retriever = TrustAwareRetriever(
|
|
397
|
+
trust_weight=0.4,
|
|
398
|
+
recency_weight=0.3,
|
|
399
|
+
relevance_weight=0.3,
|
|
400
|
+
half_life_hours=24.0,
|
|
401
|
+
)
|
|
402
|
+
results = retriever.retrieve(items, query="billing issue", top_k=5)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Integration with pyagent-patterns
|
|
406
|
+
|
|
407
|
+
Context flows between agents via the `ContextLedger`:
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
from pyagent_patterns.base import Agent, MockLLM
|
|
411
|
+
from pyagent_patterns.orchestration import Pipeline
|
|
412
|
+
from pyagent_context import ContextLedger, ContextItem, TrustLevel
|
|
413
|
+
|
|
414
|
+
ledger = ContextLedger()
|
|
415
|
+
|
|
416
|
+
# Before agent execution: read context to prepend as system/user messages
|
|
417
|
+
context_messages = ledger.to_messages(budget=4000)
|
|
418
|
+
|
|
419
|
+
# After agent execution: write output as a new context item
|
|
420
|
+
ledger.append(ContextItem(
|
|
421
|
+
content=result.output,
|
|
422
|
+
source="analyst",
|
|
423
|
+
trust=TrustLevel.INFERRED,
|
|
424
|
+
))
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
In the hook-based integration model, agents automatically read from and write to the ledger when one is attached via `set_context()`.
|
|
428
|
+
|
|
429
|
+
## Integration with pyagent-compress
|
|
430
|
+
|
|
431
|
+
Two levels of compression work together:
|
|
432
|
+
|
|
433
|
+
| Layer | Package | What It Compresses | How |
|
|
434
|
+
|-------|---------|-------------------|-----|
|
|
435
|
+
| **Message-level** | `pyagent-compress` | Individual agent outputs | Extractive: remove filler, rank sentences, keep top-N |
|
|
436
|
+
| **Context-level** | `pyagent-context` | Accumulated context items | Policy-based: FIFO, semantic lossless, sawtooth |
|
|
437
|
+
|
|
438
|
+
```python
|
|
439
|
+
from pyagent_context import ContextCompressor, ContextLedger
|
|
440
|
+
from pyagent_compress import MessageCompressor
|
|
441
|
+
|
|
442
|
+
# Context compression: decide which items to keep
|
|
443
|
+
compressor = ContextCompressor(policy="semantic_lossless")
|
|
444
|
+
trimmed = compressor.compress(ledger.items(), target_tokens=4000)
|
|
445
|
+
|
|
446
|
+
# Message compression: reduce verbosity of individual outputs
|
|
447
|
+
msg_compressor = MessageCompressor(target_ratio=0.5)
|
|
448
|
+
compressed = msg_compressor.compress(agent_output)
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Integration with pyagent-trace
|
|
452
|
+
|
|
453
|
+
Context operations can be tracked via the `TraceEventBus`:
|
|
454
|
+
|
|
455
|
+
- **Ledger writes** — When agents append items, trace events capture the source, trust level, and token count
|
|
456
|
+
- **Memory tier transitions** — Working → session → semantic migrations emit trace events
|
|
457
|
+
- **Retrieval** — Trust-aware retrieval results (scores, items selected) are logged for debugging
|
|
458
|
+
- **Compression** — Context compression events show which items were kept/dropped and the token savings
|
|
459
|
+
|
|
460
|
+
## Integration with pyagent-blueprint
|
|
461
|
+
|
|
462
|
+
The `context` section of a blueprint YAML maps directly to context package configuration:
|
|
463
|
+
|
|
464
|
+
```yaml
|
|
465
|
+
context:
|
|
466
|
+
memory:
|
|
467
|
+
backend: sqlite
|
|
468
|
+
working_max_tokens: 128000
|
|
469
|
+
compression:
|
|
470
|
+
policy: semantic_lossless
|
|
471
|
+
target_ratio: 0.6
|
|
472
|
+
redaction:
|
|
473
|
+
max_sensitivity: internal
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
After `BlueprintCompiler.compile()`, these settings are available on the `RuntimeGraph` for the consumer to wire into agents.
|
|
477
|
+
|
|
284
478
|
## Full Documentation
|
|
285
479
|
|
|
286
|
-
See [pyagent.
|
|
480
|
+
See [pyagent.org](https://pyagent.org) for full API reference and integration guides.
|
|
@@ -250,6 +250,200 @@ context_messages = ledger.to_messages(max_tokens=2000)
|
|
|
250
250
|
ledger.add(result.output, "pipeline", TrustLevel.INFERRED)
|
|
251
251
|
```
|
|
252
252
|
|
|
253
|
+
## Architecture — Three-Tier Memory Model
|
|
254
|
+
|
|
255
|
+
```mermaid
|
|
256
|
+
flowchart TD
|
|
257
|
+
subgraph Agent Interaction
|
|
258
|
+
A1[Agent 1] -->|append output| CL[ContextLedger]
|
|
259
|
+
CL -->|to_messages budget| A2[Agent 2 input]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
subgraph Three-Tier Memory
|
|
263
|
+
CL --> WM[WorkingMemory — bounded deque, current turn]
|
|
264
|
+
CL --> SM[SessionMemory — JSON/SQLite, cross-turn persistence]
|
|
265
|
+
CL --> SEM[SemanticMemory — TF-IDF similarity, long-term recall]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
subgraph Processing
|
|
269
|
+
CL --> CC[ContextCompressor — policy-based trimming]
|
|
270
|
+
CL --> TR[TrustAwareRetriever — ranked retrieval]
|
|
271
|
+
CL --> CR[ContextRedactor — sensitivity filtering]
|
|
272
|
+
CL --> LC[ContextLifecycle — expiry, decay, consolidation]
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Memory Tier Details
|
|
277
|
+
|
|
278
|
+
| Tier | Class | Storage | Capacity | Eviction | Use Case |
|
|
279
|
+
|------|-------|---------|----------|----------|----------|
|
|
280
|
+
| **Working** | `WorkingMemory` | In-memory deque | `max_items` or `max_tokens` | Oldest-first when full | Current conversation turn |
|
|
281
|
+
| **Session** | `SessionMemory` | JSON file or SQLite | Unlimited | Manual clear | Cross-turn persistence (multi-step workflows) |
|
|
282
|
+
| **Semantic** | `InMemorySemanticStore` | In-memory TF-IDF index | Unlimited | Manual | Long-term recall via similarity search |
|
|
283
|
+
|
|
284
|
+
### WorkingMemory Eviction
|
|
285
|
+
|
|
286
|
+
When `WorkingMemory` reaches its `max_items` or `max_tokens` limit, the oldest items are evicted first. Utilization metrics are available:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from pyagent_context import WorkingMemory
|
|
290
|
+
|
|
291
|
+
wm = WorkingMemory(max_items=20, max_tokens=8000)
|
|
292
|
+
wm.add(item)
|
|
293
|
+
|
|
294
|
+
print(wm.utilization) # 0.05 → 5% of max_items used
|
|
295
|
+
print(wm.token_usage) # current token estimate total
|
|
296
|
+
print(len(wm)) # number of items in memory
|
|
297
|
+
wm.clear() # flush all items
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### SessionMemory Backends
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
from pyagent_context import SessionMemory
|
|
304
|
+
|
|
305
|
+
# JSON backend — simple file-based persistence
|
|
306
|
+
sm_json = SessionMemory(backend="json", path="session.json")
|
|
307
|
+
|
|
308
|
+
# SQLite backend — more robust, concurrent-safe
|
|
309
|
+
sm_sqlite = SessionMemory(backend="sqlite", path="session.db")
|
|
310
|
+
|
|
311
|
+
sm_json.add(item)
|
|
312
|
+
sm_json.save() # persist to disk
|
|
313
|
+
sm_json.load() # reload from disk
|
|
314
|
+
items = sm_json.retrieve(query="billing", top_k=5)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### SemanticMemory — TF-IDF Similarity Search
|
|
318
|
+
|
|
319
|
+
The `InMemorySemanticStore` uses TF-IDF vectorization for similarity-based retrieval across the full context history:
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
from pyagent_context import InMemorySemanticStore
|
|
323
|
+
|
|
324
|
+
store = InMemorySemanticStore()
|
|
325
|
+
store.add(item1)
|
|
326
|
+
store.add(item2)
|
|
327
|
+
store.add(item3)
|
|
328
|
+
|
|
329
|
+
# Retrieve items most similar to the query
|
|
330
|
+
results = store.search("billing question", top_k=3)
|
|
331
|
+
for item, score in results:
|
|
332
|
+
print(f"[{score:.2f}] {item.content[:80]}...")
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## ContextLedger — Token-Budgeted Message Conversion
|
|
336
|
+
|
|
337
|
+
The `ContextLedger` is an append-only log that converts stored `ContextItem` objects into LLM-compatible messages with automatic token budgeting:
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
from pyagent_context import ContextLedger, ContextItem, TrustLevel
|
|
341
|
+
|
|
342
|
+
ledger = ContextLedger()
|
|
343
|
+
ledger.append(ContextItem(content="Revenue was $25.2B", source="database", trust=TrustLevel.VERIFIED))
|
|
344
|
+
ledger.append(ContextItem(content="Margin expanded to 17%", source="agent_1", trust=TrustLevel.INFERRED))
|
|
345
|
+
|
|
346
|
+
# Convert to messages with a token budget
|
|
347
|
+
# Higher-trust items are prioritized when budget is tight
|
|
348
|
+
messages = ledger.to_messages(budget=4000)
|
|
349
|
+
print(ledger.total_tokens()) # total token estimate across all items
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## TrustAwareRetriever — Composite Scoring
|
|
353
|
+
|
|
354
|
+
The retriever ranks items using a composite score of three signals:
|
|
355
|
+
|
|
356
|
+
| Signal | Weight | Description |
|
|
357
|
+
|--------|--------|-------------|
|
|
358
|
+
| **Trust** | Configurable | `VERIFIED` > `INFERRED` > `USER_PROVIDED` > `UNVERIFIED` |
|
|
359
|
+
| **Recency** | Half-life decay | Newer items score higher; decay rate is configurable |
|
|
360
|
+
| **Relevance** | Keyword overlap | TF-IDF similarity between query and item content |
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
from pyagent_context import TrustAwareRetriever
|
|
364
|
+
|
|
365
|
+
retriever = TrustAwareRetriever(
|
|
366
|
+
trust_weight=0.4,
|
|
367
|
+
recency_weight=0.3,
|
|
368
|
+
relevance_weight=0.3,
|
|
369
|
+
half_life_hours=24.0,
|
|
370
|
+
)
|
|
371
|
+
results = retriever.retrieve(items, query="billing issue", top_k=5)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Integration with pyagent-patterns
|
|
375
|
+
|
|
376
|
+
Context flows between agents via the `ContextLedger`:
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
from pyagent_patterns.base import Agent, MockLLM
|
|
380
|
+
from pyagent_patterns.orchestration import Pipeline
|
|
381
|
+
from pyagent_context import ContextLedger, ContextItem, TrustLevel
|
|
382
|
+
|
|
383
|
+
ledger = ContextLedger()
|
|
384
|
+
|
|
385
|
+
# Before agent execution: read context to prepend as system/user messages
|
|
386
|
+
context_messages = ledger.to_messages(budget=4000)
|
|
387
|
+
|
|
388
|
+
# After agent execution: write output as a new context item
|
|
389
|
+
ledger.append(ContextItem(
|
|
390
|
+
content=result.output,
|
|
391
|
+
source="analyst",
|
|
392
|
+
trust=TrustLevel.INFERRED,
|
|
393
|
+
))
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
In the hook-based integration model, agents automatically read from and write to the ledger when one is attached via `set_context()`.
|
|
397
|
+
|
|
398
|
+
## Integration with pyagent-compress
|
|
399
|
+
|
|
400
|
+
Two levels of compression work together:
|
|
401
|
+
|
|
402
|
+
| Layer | Package | What It Compresses | How |
|
|
403
|
+
|-------|---------|-------------------|-----|
|
|
404
|
+
| **Message-level** | `pyagent-compress` | Individual agent outputs | Extractive: remove filler, rank sentences, keep top-N |
|
|
405
|
+
| **Context-level** | `pyagent-context` | Accumulated context items | Policy-based: FIFO, semantic lossless, sawtooth |
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
from pyagent_context import ContextCompressor, ContextLedger
|
|
409
|
+
from pyagent_compress import MessageCompressor
|
|
410
|
+
|
|
411
|
+
# Context compression: decide which items to keep
|
|
412
|
+
compressor = ContextCompressor(policy="semantic_lossless")
|
|
413
|
+
trimmed = compressor.compress(ledger.items(), target_tokens=4000)
|
|
414
|
+
|
|
415
|
+
# Message compression: reduce verbosity of individual outputs
|
|
416
|
+
msg_compressor = MessageCompressor(target_ratio=0.5)
|
|
417
|
+
compressed = msg_compressor.compress(agent_output)
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Integration with pyagent-trace
|
|
421
|
+
|
|
422
|
+
Context operations can be tracked via the `TraceEventBus`:
|
|
423
|
+
|
|
424
|
+
- **Ledger writes** — When agents append items, trace events capture the source, trust level, and token count
|
|
425
|
+
- **Memory tier transitions** — Working → session → semantic migrations emit trace events
|
|
426
|
+
- **Retrieval** — Trust-aware retrieval results (scores, items selected) are logged for debugging
|
|
427
|
+
- **Compression** — Context compression events show which items were kept/dropped and the token savings
|
|
428
|
+
|
|
429
|
+
## Integration with pyagent-blueprint
|
|
430
|
+
|
|
431
|
+
The `context` section of a blueprint YAML maps directly to context package configuration:
|
|
432
|
+
|
|
433
|
+
```yaml
|
|
434
|
+
context:
|
|
435
|
+
memory:
|
|
436
|
+
backend: sqlite
|
|
437
|
+
working_max_tokens: 128000
|
|
438
|
+
compression:
|
|
439
|
+
policy: semantic_lossless
|
|
440
|
+
target_ratio: 0.6
|
|
441
|
+
redaction:
|
|
442
|
+
max_sensitivity: internal
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
After `BlueprintCompiler.compile()`, these settings are available on the `RuntimeGraph` for the consumer to wire into agents.
|
|
446
|
+
|
|
253
447
|
## Full Documentation
|
|
254
448
|
|
|
255
|
-
See [pyagent.
|
|
449
|
+
See [pyagent.org](https://pyagent.org) for full API reference and integration guides.
|
|
@@ -3,22 +3,18 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from enum import StrEnum
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
6
|
|
|
8
7
|
from pyagent_context.item import ContextItem, TrustLevel
|
|
9
8
|
from pyagent_context.ledger import ContextLedger
|
|
10
9
|
|
|
11
|
-
if TYPE_CHECKING:
|
|
12
|
-
pass
|
|
13
|
-
|
|
14
10
|
|
|
15
11
|
class CompressionPolicy(StrEnum):
|
|
16
12
|
"""Available compression strategies."""
|
|
17
13
|
|
|
18
14
|
NONE = "none"
|
|
19
|
-
FIFO = "fifo"
|
|
20
|
-
SEMANTIC_LOSSLESS = "semantic_lossless"
|
|
21
|
-
SAWTOOTH = "sawtooth"
|
|
15
|
+
FIFO = "fifo" # drop oldest items
|
|
16
|
+
SEMANTIC_LOSSLESS = "semantic_lossless" # compress text, keep Knowledge block
|
|
17
|
+
SAWTOOTH = "sawtooth" # compress to floor, grow again
|
|
22
18
|
|
|
23
19
|
|
|
24
20
|
class ContextCompressor:
|
|
@@ -11,13 +11,18 @@ from enum import StrEnum
|
|
|
11
11
|
class TrustLevel(StrEnum):
|
|
12
12
|
"""Trust classification for context items."""
|
|
13
13
|
|
|
14
|
-
VERIFIED = "verified"
|
|
15
|
-
INFERRED = "inferred"
|
|
16
|
-
USER_PROVIDED = "user"
|
|
17
|
-
EXTERNAL = "external"
|
|
14
|
+
VERIFIED = "verified" # from trusted source, validated
|
|
15
|
+
INFERRED = "inferred" # LLM-generated, not validated
|
|
16
|
+
USER_PROVIDED = "user" # direct user input
|
|
17
|
+
EXTERNAL = "external" # from tool/API call
|
|
18
18
|
|
|
19
19
|
def __ge__(self, other: TrustLevel) -> bool:
|
|
20
|
-
order = {
|
|
20
|
+
order = {
|
|
21
|
+
TrustLevel.INFERRED: 0,
|
|
22
|
+
TrustLevel.EXTERNAL: 1,
|
|
23
|
+
TrustLevel.USER_PROVIDED: 2,
|
|
24
|
+
TrustLevel.VERIFIED: 3,
|
|
25
|
+
}
|
|
21
26
|
return order.get(self, 0) >= order.get(other, 0)
|
|
22
27
|
|
|
23
28
|
def __gt__(self, other: TrustLevel) -> bool:
|
|
@@ -6,7 +6,8 @@ import time
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from pyagent_patterns.base import Message
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
from pyagent_context.item import TRUST_ORDER, ContextItem, TrustLevel
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class ContextLedger:
|
|
@@ -62,7 +63,9 @@ class ContextLedger:
|
|
|
62
63
|
now = time.time()
|
|
63
64
|
results: list[ContextItem] = []
|
|
64
65
|
for item in self._items:
|
|
65
|
-
if min_trust is not None and TRUST_ORDER.get(item.trust_level, 0) < TRUST_ORDER.get(
|
|
66
|
+
if min_trust is not None and TRUST_ORDER.get(item.trust_level, 0) < TRUST_ORDER.get(
|
|
67
|
+
min_trust, 0
|
|
68
|
+
):
|
|
66
69
|
continue
|
|
67
70
|
if max_age_seconds is not None and (now - item.timestamp) > max_age_seconds:
|
|
68
71
|
continue
|
|
@@ -89,10 +92,7 @@ class ContextLedger:
|
|
|
89
92
|
List of ``Message`` objects.
|
|
90
93
|
"""
|
|
91
94
|
if max_tokens is None:
|
|
92
|
-
return [
|
|
93
|
-
Message.assistant(item.content, name=item.source)
|
|
94
|
-
for item in self._items
|
|
95
|
-
]
|
|
95
|
+
return [Message.assistant(item.content, name=item.source) for item in self._items]
|
|
96
96
|
|
|
97
97
|
# Walk backward, accumulate until budget exhausted
|
|
98
98
|
selected: list[ContextItem] = []
|
|
@@ -18,7 +18,7 @@ class ContextLifecycle:
|
|
|
18
18
|
- **Consolidation**: merge items from the same source with similar content.
|
|
19
19
|
|
|
20
20
|
Args:
|
|
21
|
-
consolidation_threshold: Minimum keyword overlap ratio (0.0
|
|
21
|
+
consolidation_threshold: Minimum keyword overlap ratio (0.0-1.0)
|
|
22
22
|
to consider two items similar enough to merge.
|
|
23
23
|
"""
|
|
24
24
|
|
|
@@ -123,7 +123,10 @@ class ContextLifecycle:
|
|
|
123
123
|
if j in merged_indices:
|
|
124
124
|
continue
|
|
125
125
|
item_b = items[j]
|
|
126
|
-
if
|
|
126
|
+
if (
|
|
127
|
+
self._similarity(item_a.content, item_b.content)
|
|
128
|
+
>= self._consolidation_threshold
|
|
129
|
+
):
|
|
127
130
|
merged_content = f"{merged_content}\n{item_b.content}"
|
|
128
131
|
if item_b.trust_level > best_trust:
|
|
129
132
|
best_trust = item_b.trust_level
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Three-tier memory: working, session, and semantic."""
|
|
2
2
|
|
|
3
|
-
from pyagent_context.memory.
|
|
3
|
+
from pyagent_context.memory.semantic import InMemorySemanticStore, SemanticMemoryProtocol
|
|
4
4
|
from pyagent_context.memory.session import SessionMemory
|
|
5
|
-
from pyagent_context.memory.
|
|
5
|
+
from pyagent_context.memory.working import WorkingMemory
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"InMemorySemanticStore",
|
|
@@ -4,10 +4,11 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
6
|
from collections import Counter
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from typing import Protocol, runtime_checkable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pyagent_context.item import ContextItem
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@dataclass(frozen=True)
|
|
@@ -16,7 +17,7 @@ class SearchResult:
|
|
|
16
17
|
|
|
17
18
|
Attributes:
|
|
18
19
|
item: The matching context item.
|
|
19
|
-
score: Relevance score (0.0
|
|
20
|
+
score: Relevance score (0.0-1.0).
|
|
20
21
|
"""
|
|
21
22
|
|
|
22
23
|
item: ContextItem
|
|
@@ -60,18 +61,100 @@ class InMemorySemanticStore:
|
|
|
60
61
|
self._items: dict[str, ContextItem] = {}
|
|
61
62
|
self._tf_cache: dict[str, dict[str, float]] = {}
|
|
62
63
|
self._stop_words = stop_words or {
|
|
63
|
-
"a",
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
64
|
+
"a",
|
|
65
|
+
"an",
|
|
66
|
+
"the",
|
|
67
|
+
"is",
|
|
68
|
+
"are",
|
|
69
|
+
"was",
|
|
70
|
+
"were",
|
|
71
|
+
"be",
|
|
72
|
+
"been",
|
|
73
|
+
"being",
|
|
74
|
+
"have",
|
|
75
|
+
"has",
|
|
76
|
+
"had",
|
|
77
|
+
"do",
|
|
78
|
+
"does",
|
|
79
|
+
"did",
|
|
80
|
+
"will",
|
|
81
|
+
"would",
|
|
82
|
+
"could",
|
|
83
|
+
"should",
|
|
84
|
+
"may",
|
|
85
|
+
"might",
|
|
86
|
+
"can",
|
|
87
|
+
"shall",
|
|
88
|
+
"to",
|
|
89
|
+
"of",
|
|
90
|
+
"in",
|
|
91
|
+
"for",
|
|
92
|
+
"on",
|
|
93
|
+
"with",
|
|
94
|
+
"at",
|
|
95
|
+
"by",
|
|
96
|
+
"from",
|
|
97
|
+
"as",
|
|
98
|
+
"into",
|
|
99
|
+
"through",
|
|
100
|
+
"during",
|
|
101
|
+
"before",
|
|
102
|
+
"after",
|
|
103
|
+
"and",
|
|
104
|
+
"but",
|
|
105
|
+
"or",
|
|
106
|
+
"nor",
|
|
107
|
+
"not",
|
|
108
|
+
"so",
|
|
109
|
+
"yet",
|
|
110
|
+
"both",
|
|
111
|
+
"either",
|
|
112
|
+
"neither",
|
|
113
|
+
"each",
|
|
114
|
+
"every",
|
|
115
|
+
"all",
|
|
116
|
+
"any",
|
|
117
|
+
"few",
|
|
118
|
+
"more",
|
|
119
|
+
"most",
|
|
120
|
+
"other",
|
|
121
|
+
"some",
|
|
122
|
+
"such",
|
|
123
|
+
"no",
|
|
124
|
+
"only",
|
|
125
|
+
"own",
|
|
126
|
+
"same",
|
|
127
|
+
"than",
|
|
128
|
+
"too",
|
|
129
|
+
"very",
|
|
130
|
+
"just",
|
|
131
|
+
"because",
|
|
132
|
+
"about",
|
|
133
|
+
"between",
|
|
134
|
+
"it",
|
|
135
|
+
"its",
|
|
136
|
+
"this",
|
|
137
|
+
"that",
|
|
138
|
+
"these",
|
|
139
|
+
"those",
|
|
140
|
+
"i",
|
|
141
|
+
"me",
|
|
142
|
+
"my",
|
|
143
|
+
"we",
|
|
144
|
+
"our",
|
|
145
|
+
"you",
|
|
146
|
+
"your",
|
|
147
|
+
"he",
|
|
148
|
+
"him",
|
|
149
|
+
"his",
|
|
150
|
+
"she",
|
|
151
|
+
"her",
|
|
152
|
+
"they",
|
|
153
|
+
"them",
|
|
154
|
+
"their",
|
|
155
|
+
"what",
|
|
156
|
+
"which",
|
|
157
|
+
"who",
|
|
75
158
|
}
|
|
76
159
|
|
|
77
160
|
def add(self, item: ContextItem) -> None:
|
|
@@ -118,7 +201,11 @@ class InMemorySemanticStore:
|
|
|
118
201
|
|
|
119
202
|
def _tokenize(self, text: str) -> list[str]:
|
|
120
203
|
words = text.lower().split()
|
|
121
|
-
return [
|
|
204
|
+
return [
|
|
205
|
+
w.strip(".,!?;:\"'()[]{}")
|
|
206
|
+
for w in words
|
|
207
|
+
if w.strip(".,!?;:\"'()[]{}") not in self._stop_words
|
|
208
|
+
]
|
|
122
209
|
|
|
123
210
|
def _compute_tf(self, text: str) -> dict[str, float]:
|
|
124
211
|
tokens = self._tokenize(text)
|
|
@@ -146,8 +233,8 @@ class InMemorySemanticStore:
|
|
|
146
233
|
if not common_keys:
|
|
147
234
|
return 0.0
|
|
148
235
|
dot = sum(a[k] * b[k] for k in common_keys)
|
|
149
|
-
mag_a = math.sqrt(sum(v
|
|
150
|
-
mag_b = math.sqrt(sum(v
|
|
236
|
+
mag_a = math.sqrt(sum(v**2 for v in a.values()))
|
|
237
|
+
mag_b = math.sqrt(sum(v**2 for v in b.values()))
|
|
151
238
|
if mag_a == 0 or mag_b == 0:
|
|
152
239
|
return 0.0
|
|
153
240
|
return dot / (mag_a * mag_b)
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
"""TrustAwareRetriever: score candidates by trust
|
|
1
|
+
"""TrustAwareRetriever: score candidates by trust x recency x relevance."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
6
|
import time
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
8
9
|
|
|
9
|
-
from pyagent_context.item import
|
|
10
|
-
|
|
10
|
+
from pyagent_context.item import TRUST_ORDER, ContextItem
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pyagent_context.ledger import ContextLedger
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
@dataclass(frozen=True)
|
|
@@ -16,7 +19,7 @@ class ScoredItem:
|
|
|
16
19
|
|
|
17
20
|
Attributes:
|
|
18
21
|
item: The context item.
|
|
19
|
-
score: Composite score (0.0
|
|
22
|
+
score: Composite score (0.0-1.0).
|
|
20
23
|
trust_score: Trust component of the score.
|
|
21
24
|
recency_score: Recency component of the score.
|
|
22
25
|
relevance_score: Relevance component of the score.
|
|
@@ -92,9 +95,7 @@ class TrustAwareRetriever:
|
|
|
92
95
|
relevance = self._keyword_relevance(item.content, query_words)
|
|
93
96
|
|
|
94
97
|
score = (
|
|
95
|
-
self._w_trust * trust
|
|
96
|
-
+ self._w_recency * recency
|
|
97
|
-
+ self._w_relevance * relevance
|
|
98
|
+
self._w_trust * trust + self._w_recency * recency + self._w_relevance * relevance
|
|
98
99
|
)
|
|
99
100
|
|
|
100
101
|
if score >= min_score:
|
|
@@ -14,12 +14,14 @@ def _make_ledger(n: int, tokens_per_item: int = 100) -> ContextLedger:
|
|
|
14
14
|
ledger = ContextLedger()
|
|
15
15
|
for i in range(n):
|
|
16
16
|
content = f"Item {i}: " + "x" * (tokens_per_item * 4)
|
|
17
|
-
ledger.append(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
ledger.append(
|
|
18
|
+
ContextItem(
|
|
19
|
+
content=content,
|
|
20
|
+
source="test",
|
|
21
|
+
timestamp=time.time() - (n - i),
|
|
22
|
+
token_estimate=tokens_per_item,
|
|
23
|
+
)
|
|
24
|
+
)
|
|
23
25
|
return ledger
|
|
24
26
|
|
|
25
27
|
|
|
@@ -54,18 +56,22 @@ def test_fifo_compress() -> None:
|
|
|
54
56
|
|
|
55
57
|
def test_fifo_preserves_verified() -> None:
|
|
56
58
|
ledger = ContextLedger()
|
|
57
|
-
ledger.append(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
trust_level=TrustLevel.VERIFIED,
|
|
61
|
-
token_estimate=100,
|
|
62
|
-
))
|
|
63
|
-
for i in range(9):
|
|
64
|
-
ledger.append(ContextItem(
|
|
65
|
-
content=f"Inferred {i}",
|
|
59
|
+
ledger.append(
|
|
60
|
+
ContextItem(
|
|
61
|
+
content="Verified fact",
|
|
66
62
|
source="test",
|
|
63
|
+
trust_level=TrustLevel.VERIFIED,
|
|
67
64
|
token_estimate=100,
|
|
68
|
-
)
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
for i in range(9):
|
|
68
|
+
ledger.append(
|
|
69
|
+
ContextItem(
|
|
70
|
+
content=f"Inferred {i}",
|
|
71
|
+
source="test",
|
|
72
|
+
token_estimate=100,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
69
75
|
|
|
70
76
|
compressor = ContextCompressor(
|
|
71
77
|
policy=CompressionPolicy.FIFO,
|
|
@@ -79,17 +85,21 @@ def test_fifo_preserves_verified() -> None:
|
|
|
79
85
|
|
|
80
86
|
def test_semantic_lossless() -> None:
|
|
81
87
|
ledger = ContextLedger()
|
|
82
|
-
ledger.append(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
ledger.append(
|
|
89
|
+
ContextItem(
|
|
90
|
+
content="First sentence. Second sentence. Third sentence.",
|
|
91
|
+
source="test",
|
|
92
|
+
token_estimate=50,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
ledger.append(
|
|
96
|
+
ContextItem(
|
|
97
|
+
content="Verified content stays intact",
|
|
98
|
+
source="test",
|
|
99
|
+
trust_level=TrustLevel.VERIFIED,
|
|
100
|
+
token_estimate=50,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
93
103
|
|
|
94
104
|
compressor = ContextCompressor(
|
|
95
105
|
policy=CompressionPolicy.SEMANTIC_LOSSLESS,
|
|
@@ -8,7 +8,9 @@ from pyagent_context.item import ContextItem, TrustLevel
|
|
|
8
8
|
from pyagent_context.ledger import ContextLedger
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def _make_item(
|
|
11
|
+
def _make_item(
|
|
12
|
+
content: str, source: str = "agent", trust: TrustLevel = TrustLevel.INFERRED, age: float = 0.0
|
|
13
|
+
) -> ContextItem:
|
|
12
14
|
return ContextItem(
|
|
13
15
|
content=content,
|
|
14
16
|
source=source,
|
|
@@ -44,7 +46,7 @@ def test_query_by_trust() -> None:
|
|
|
44
46
|
def test_query_by_age() -> None:
|
|
45
47
|
ledger = ContextLedger()
|
|
46
48
|
ledger.append(_make_item("old", age=7200)) # 2 hours ago
|
|
47
|
-
ledger.append(_make_item("new", age=60))
|
|
49
|
+
ledger.append(_make_item("new", age=60)) # 1 minute ago
|
|
48
50
|
|
|
49
51
|
results = ledger.query(max_age_seconds=3600)
|
|
50
52
|
assert len(results) == 1
|
|
@@ -26,19 +26,23 @@ def test_sweep_expired() -> None:
|
|
|
26
26
|
def test_freshness_decay() -> None:
|
|
27
27
|
ledger = ContextLedger()
|
|
28
28
|
# Old item: 2 hours ago
|
|
29
|
-
ledger.append(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
ledger.append(
|
|
30
|
+
ContextItem(
|
|
31
|
+
content="old item",
|
|
32
|
+
source="s",
|
|
33
|
+
timestamp=time.time() - 7200,
|
|
34
|
+
token_estimate=100,
|
|
35
|
+
)
|
|
36
|
+
)
|
|
35
37
|
# New item: just now
|
|
36
|
-
ledger.append(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
ledger.append(
|
|
39
|
+
ContextItem(
|
|
40
|
+
content="new item",
|
|
41
|
+
source="s",
|
|
42
|
+
timestamp=time.time(),
|
|
43
|
+
token_estimate=100,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
42
46
|
|
|
43
47
|
lifecycle = ContextLifecycle()
|
|
44
48
|
decayed = lifecycle.apply_freshness_decay(ledger, half_life_seconds=3600)
|
|
@@ -53,21 +57,27 @@ def test_freshness_decay() -> None:
|
|
|
53
57
|
def test_consolidation() -> None:
|
|
54
58
|
ledger = ContextLedger()
|
|
55
59
|
# Two similar items from same source
|
|
56
|
-
ledger.append(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
ledger.append(
|
|
61
|
+
ContextItem(
|
|
62
|
+
content="Python asyncio patterns for concurrency",
|
|
63
|
+
source="researcher",
|
|
64
|
+
trust_level=TrustLevel.INFERRED,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
ledger.append(
|
|
68
|
+
ContextItem(
|
|
69
|
+
content="Python asyncio patterns for parallel tasks",
|
|
70
|
+
source="researcher",
|
|
71
|
+
trust_level=TrustLevel.VERIFIED,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
66
74
|
# Different source
|
|
67
|
-
ledger.append(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
ledger.append(
|
|
76
|
+
ContextItem(
|
|
77
|
+
content="JavaScript React hooks",
|
|
78
|
+
source="frontend_agent",
|
|
79
|
+
)
|
|
80
|
+
)
|
|
71
81
|
|
|
72
82
|
lifecycle = ContextLifecycle(consolidation_threshold=0.4)
|
|
73
83
|
consolidated = lifecycle.consolidate(ledger)
|
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import tempfile
|
|
6
|
-
from pathlib import Path
|
|
7
6
|
|
|
8
|
-
from pyagent_context.item import ContextItem
|
|
9
|
-
from pyagent_context.memory.working import WorkingMemory
|
|
10
|
-
from pyagent_context.memory.session import SessionMemory
|
|
7
|
+
from pyagent_context.item import ContextItem
|
|
11
8
|
from pyagent_context.memory.semantic import InMemorySemanticStore
|
|
12
|
-
|
|
9
|
+
from pyagent_context.memory.session import SessionMemory
|
|
10
|
+
from pyagent_context.memory.working import WorkingMemory
|
|
13
11
|
|
|
14
12
|
# ── WorkingMemory ─────────────────────────────────────────────────────
|
|
15
13
|
|
|
14
|
+
|
|
16
15
|
def test_working_memory_add() -> None:
|
|
17
16
|
wm = WorkingMemory(max_items=10, max_tokens=5000)
|
|
18
17
|
item = ContextItem(content="hello", source="test")
|
|
@@ -48,6 +47,7 @@ def test_working_memory_utilization() -> None:
|
|
|
48
47
|
|
|
49
48
|
# ── SessionMemory (JSON) ─────────────────────────────────────────────
|
|
50
49
|
|
|
50
|
+
|
|
51
51
|
def test_session_memory_json_persist() -> None:
|
|
52
52
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
53
53
|
sm = SessionMemory("test-session", backend="json", storage_path=tmpdir)
|
|
@@ -73,6 +73,7 @@ def test_session_memory_json_clear() -> None:
|
|
|
73
73
|
|
|
74
74
|
# ── SessionMemory (SQLite) ───────────────────────────────────────────
|
|
75
75
|
|
|
76
|
+
|
|
76
77
|
def test_session_memory_sqlite_persist() -> None:
|
|
77
78
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
78
79
|
sm = SessionMemory("test-session", backend="sqlite", storage_path=tmpdir)
|
|
@@ -88,6 +89,7 @@ def test_session_memory_sqlite_persist() -> None:
|
|
|
88
89
|
|
|
89
90
|
# ── InMemorySemanticStore ────────────────────────────────────────────
|
|
90
91
|
|
|
92
|
+
|
|
91
93
|
def test_semantic_add_and_search() -> None:
|
|
92
94
|
store = InMemorySemanticStore()
|
|
93
95
|
store.add(ContextItem(content="Python asyncio event loop concurrency", source="docs"))
|
|
@@ -97,7 +99,9 @@ def test_semantic_add_and_search() -> None:
|
|
|
97
99
|
results = store.search("Python async web")
|
|
98
100
|
assert len(results) > 0
|
|
99
101
|
# Python items should score higher
|
|
100
|
-
assert
|
|
102
|
+
assert (
|
|
103
|
+
"python" in results[0].item.content.lower() or "python" in results[0].item.content.lower()
|
|
104
|
+
)
|
|
101
105
|
|
|
102
106
|
|
|
103
107
|
def test_semantic_remove() -> None:
|
|
@@ -27,18 +27,22 @@ def test_retrieval_ranks_by_relevance() -> None:
|
|
|
27
27
|
def test_retrieval_ranks_by_trust() -> None:
|
|
28
28
|
now = time.time()
|
|
29
29
|
ledger = ContextLedger()
|
|
30
|
-
ledger.append(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
ledger.append(
|
|
31
|
+
ContextItem(
|
|
32
|
+
content="Database connection pooling",
|
|
33
|
+
source="s",
|
|
34
|
+
trust_level=TrustLevel.INFERRED,
|
|
35
|
+
timestamp=now,
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
ledger.append(
|
|
39
|
+
ContextItem(
|
|
40
|
+
content="Database connection pooling",
|
|
41
|
+
source="s",
|
|
42
|
+
trust_level=TrustLevel.VERIFIED,
|
|
43
|
+
timestamp=now,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
42
46
|
|
|
43
47
|
retriever = TrustAwareRetriever(weight_trust=0.9, weight_relevance=0.05, weight_recency=0.05)
|
|
44
48
|
results = retriever.retrieve(ledger, "database connection")
|
|
@@ -50,11 +54,13 @@ def test_retrieval_ranks_by_trust() -> None:
|
|
|
50
54
|
|
|
51
55
|
def test_retrieval_excludes_expired() -> None:
|
|
52
56
|
ledger = ContextLedger()
|
|
53
|
-
ledger.append(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
ledger.append(
|
|
58
|
+
ContextItem(
|
|
59
|
+
content="expired item about Python",
|
|
60
|
+
source="s",
|
|
61
|
+
expires_at=time.time() - 10,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
58
64
|
ledger.append(ContextItem(content="active item about Python", source="s"))
|
|
59
65
|
|
|
60
66
|
retriever = TrustAwareRetriever()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|