convmemory 0.5.0__tar.gz → 0.6.0__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.
- {convmemory-0.5.0 → convmemory-0.6.0}/PKG-INFO +30 -4
- {convmemory-0.5.0 → convmemory-0.6.0}/README.md +33 -8
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/__init__.py +14 -10
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/api.py +129 -6
- convmemory-0.6.0/convmemory/hub.py +87 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/reranker.py +1 -0
- convmemory-0.6.0/convmemory/validity.py +438 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory.egg-info/PKG-INFO +30 -4
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory.egg-info/SOURCES.txt +3 -1
- {convmemory-0.5.0 → convmemory-0.6.0}/pyproject.toml +2 -2
- convmemory-0.6.0/tests/test_validity_context.py +365 -0
- convmemory-0.5.0/convmemory/hub.py +0 -45
- {convmemory-0.5.0 → convmemory-0.6.0}/LICENSE +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/ccge.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/encoder.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/evidence_reranker.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/memory_mla.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/metrics.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/models.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/routing.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/scoring.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory.egg-info/dependency_links.txt +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory.egg-info/requires.txt +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/convmemory.egg-info/top_level.txt +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/setup.cfg +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/tests/test_api_smoke.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/tests/test_ccge.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/tests/test_evidence_reranker.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/tests/test_hub_loading.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/tests/test_memory_mla.py +0 -0
- {convmemory-0.5.0 → convmemory-0.6.0}/tests/test_reranker.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: convmemory
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Lightweight temporal memory reranking for long-term conversational memory.
|
|
5
5
|
Author: ConvMemory contributors
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/pth2002/ConvMemory
|
|
8
8
|
Project-URL: Issues, https://github.com/pth2002/ConvMemory/issues
|
|
9
9
|
Keywords: memory,retrieval,reranking,rag,agents
|
|
@@ -25,6 +25,7 @@ Requires-Dist: tqdm>=4.60
|
|
|
25
25
|
Requires-Dist: scikit-learn>=1.2
|
|
26
26
|
Provides-Extra: hub
|
|
27
27
|
Requires-Dist: huggingface_hub>=0.20; extra == "hub"
|
|
28
|
+
Dynamic: license-file
|
|
28
29
|
|
|
29
30
|
# ConvMemory
|
|
30
31
|
|
|
@@ -54,7 +55,7 @@ reranker. Its intended use is recall-oriented memory selection for structured
|
|
|
54
55
|
memory streams: conversations, user histories, agent traces, task logs, and
|
|
55
56
|
session-level notes.
|
|
56
57
|
|
|
57
|
-
Current package version: `0.
|
|
58
|
+
Current package version: `0.6.0`
|
|
58
59
|
|
|
59
60
|
## When To Use It
|
|
60
61
|
|
|
@@ -312,6 +313,31 @@ ranked = model.retrieve(
|
|
|
312
313
|
See [Evidence Reranker](docs/EVIDENCE_RERANKER.md) for the v363 headline
|
|
313
314
|
numbers, v364 load-bearing ablations, anti-leak guards, and limitations.
|
|
314
315
|
|
|
316
|
+
### ConvMemory v3: Validity Context Layer
|
|
317
|
+
|
|
318
|
+
ConvMemory v3 adds validity evidence for agent memory without changing v1/v2
|
|
319
|
+
ranking by default. In `validity_mode="context"`, returned memories can carry a
|
|
320
|
+
structured `validity` note with possible update evidence; the rank order and
|
|
321
|
+
candidate set are preserved. Automatic demotion is available only as an explicit
|
|
322
|
+
opt-in mode for dense current-state/update workloads.
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
model.load_validity_module("Purdy0228/ConvMemory-v3-Validity-Context")
|
|
326
|
+
|
|
327
|
+
ranked = model.retrieve(
|
|
328
|
+
query=query,
|
|
329
|
+
memories=candidates,
|
|
330
|
+
evidence_reranker="v2",
|
|
331
|
+
validity_mode="context",
|
|
332
|
+
top_k=10,
|
|
333
|
+
)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
See [Validity Context](docs/VALIDITY_CONTEXT.md) for mode semantics and safety
|
|
337
|
+
contracts. See [V3 Model Card](docs/V3_MODEL_CARD.md) for checkpoint
|
|
338
|
+
provenance, package-level benchmark numbers, latency, and the source-of-truth
|
|
339
|
+
ledger.
|
|
340
|
+
|
|
315
341
|
### Experimental Memory-MLA Recall Expander
|
|
316
342
|
|
|
317
343
|
This is **not** the v0.5.0 evidence reranker. See "ConvMemory v2: Evidence
|
|
@@ -26,7 +26,7 @@ reranker. Its intended use is recall-oriented memory selection for structured
|
|
|
26
26
|
memory streams: conversations, user histories, agent traces, task logs, and
|
|
27
27
|
session-level notes.
|
|
28
28
|
|
|
29
|
-
Current package version: `0.
|
|
29
|
+
Current package version: `0.6.0`
|
|
30
30
|
|
|
31
31
|
## When To Use It
|
|
32
32
|
|
|
@@ -281,13 +281,38 @@ ranked = model.retrieve(
|
|
|
281
281
|
)
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
-
See [Evidence Reranker](docs/EVIDENCE_RERANKER.md) for the v363 headline
|
|
285
|
-
numbers, v364 load-bearing ablations, anti-leak guards, and limitations.
|
|
286
|
-
|
|
287
|
-
###
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
284
|
+
See [Evidence Reranker](docs/EVIDENCE_RERANKER.md) for the v363 headline
|
|
285
|
+
numbers, v364 load-bearing ablations, anti-leak guards, and limitations.
|
|
286
|
+
|
|
287
|
+
### ConvMemory v3: Validity Context Layer
|
|
288
|
+
|
|
289
|
+
ConvMemory v3 adds validity evidence for agent memory without changing v1/v2
|
|
290
|
+
ranking by default. In `validity_mode="context"`, returned memories can carry a
|
|
291
|
+
structured `validity` note with possible update evidence; the rank order and
|
|
292
|
+
candidate set are preserved. Automatic demotion is available only as an explicit
|
|
293
|
+
opt-in mode for dense current-state/update workloads.
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
model.load_validity_module("Purdy0228/ConvMemory-v3-Validity-Context")
|
|
297
|
+
|
|
298
|
+
ranked = model.retrieve(
|
|
299
|
+
query=query,
|
|
300
|
+
memories=candidates,
|
|
301
|
+
evidence_reranker="v2",
|
|
302
|
+
validity_mode="context",
|
|
303
|
+
top_k=10,
|
|
304
|
+
)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
See [Validity Context](docs/VALIDITY_CONTEXT.md) for mode semantics and safety
|
|
308
|
+
contracts. See [V3 Model Card](docs/V3_MODEL_CARD.md) for checkpoint
|
|
309
|
+
provenance, package-level benchmark numbers, latency, and the source-of-truth
|
|
310
|
+
ledger.
|
|
311
|
+
|
|
312
|
+
### Experimental Memory-MLA Recall Expander
|
|
313
|
+
|
|
314
|
+
This is **not** the v0.5.0 evidence reranker. See "ConvMemory v2: Evidence
|
|
315
|
+
Reranker" above for the v2 release.
|
|
291
316
|
|
|
292
317
|
Memory-MLA is an opt-in prefix-protected recall expander. It is not a
|
|
293
318
|
replacement for v1, and it is off by default: `retrieve(query, memories)` remains
|
|
@@ -9,14 +9,15 @@ from .ccge import (
|
|
|
9
9
|
from .api import ConvMemory
|
|
10
10
|
from .evidence_reranker import EvidenceReranker, EvidenceRerankerConfig
|
|
11
11
|
from .memory_mla import MemoryMLAConfig, MemoryMLAExpander
|
|
12
|
-
from .reranker import ConvMemoryReranker, RerankConfig, RerankResult
|
|
13
|
-
from .routing import (
|
|
14
|
-
CompressedNoteConfig,
|
|
15
|
-
CompressionRouteConfig,
|
|
16
|
-
CompressionRouteResult,
|
|
12
|
+
from .reranker import ConvMemoryReranker, RerankConfig, RerankResult
|
|
13
|
+
from .routing import (
|
|
14
|
+
CompressedNoteConfig,
|
|
15
|
+
CompressionRouteConfig,
|
|
16
|
+
CompressionRouteResult,
|
|
17
17
|
CompressionRouter,
|
|
18
|
-
build_compressed_notes,
|
|
19
|
-
)
|
|
18
|
+
build_compressed_notes,
|
|
19
|
+
)
|
|
20
|
+
from .validity import ValidityAnnotation, ValidityEvidenceConfig, ValidityEvidenceModule
|
|
20
21
|
|
|
21
22
|
__all__ = [
|
|
22
23
|
"CompressedNoteConfig",
|
|
@@ -32,9 +33,12 @@ __all__ = [
|
|
|
32
33
|
"CompressionRouteConfig",
|
|
33
34
|
"CompressionRouteResult",
|
|
34
35
|
"CompressionRouter",
|
|
35
|
-
"RerankConfig",
|
|
36
|
-
"RerankResult",
|
|
37
|
-
"
|
|
36
|
+
"RerankConfig",
|
|
37
|
+
"RerankResult",
|
|
38
|
+
"ValidityAnnotation",
|
|
39
|
+
"ValidityEvidenceConfig",
|
|
40
|
+
"ValidityEvidenceModule",
|
|
41
|
+
"build_ccge_features",
|
|
38
42
|
"build_compressed_notes",
|
|
39
43
|
"multi_positive_retrieval_loss",
|
|
40
44
|
"rank_candidates",
|
|
@@ -15,6 +15,8 @@ from .memory_mla import MemoryMLAExpander
|
|
|
15
15
|
from .models import build_default_components
|
|
16
16
|
from .reranker import ConvMemoryReranker, RerankConfig, RerankResult
|
|
17
17
|
from .scoring import cosine_scores, lexical_signature
|
|
18
|
+
from .validity import FORBIDDEN_FIELDS as VALIDITY_FORBIDDEN_FIELDS
|
|
19
|
+
from .validity import ValidityEvidenceModule
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
_POSITION_RE = re.compile(r"-?\d+(?:\.\d+)?")
|
|
@@ -39,6 +41,7 @@ class ConvMemory:
|
|
|
39
41
|
ccge_editor=None,
|
|
40
42
|
expander=None,
|
|
41
43
|
evidence_reranker=None,
|
|
44
|
+
validity_module=None,
|
|
42
45
|
):
|
|
43
46
|
self.device = device
|
|
44
47
|
self.config = config or RerankConfig()
|
|
@@ -48,6 +51,7 @@ class ConvMemory:
|
|
|
48
51
|
self.ccge_editor = None
|
|
49
52
|
self.memory_mla_expander = None
|
|
50
53
|
self._evidence_reranker = None
|
|
54
|
+
self._validity_module = None
|
|
51
55
|
self.reranker = ConvMemoryReranker(
|
|
52
56
|
conv_model=conv_model,
|
|
53
57
|
scorer=scorer,
|
|
@@ -62,6 +66,8 @@ class ConvMemory:
|
|
|
62
66
|
self.attach_expander(expander)
|
|
63
67
|
if evidence_reranker is not None:
|
|
64
68
|
self.attach_evidence_reranker(evidence_reranker)
|
|
69
|
+
if validity_module is not None:
|
|
70
|
+
self.attach_validity_module(validity_module)
|
|
65
71
|
|
|
66
72
|
@classmethod
|
|
67
73
|
def from_config(
|
|
@@ -73,6 +79,7 @@ class ConvMemory:
|
|
|
73
79
|
ccge_editor=None,
|
|
74
80
|
expander=None,
|
|
75
81
|
evidence_reranker=None,
|
|
82
|
+
validity_module=None,
|
|
76
83
|
**model_kwargs,
|
|
77
84
|
):
|
|
78
85
|
"""Create a ConvMemory instance from dimensions and config.
|
|
@@ -114,6 +121,7 @@ class ConvMemory:
|
|
|
114
121
|
ccge_editor=ccge_editor,
|
|
115
122
|
expander=expander,
|
|
116
123
|
evidence_reranker=evidence_reranker,
|
|
124
|
+
validity_module=validity_module,
|
|
117
125
|
)
|
|
118
126
|
|
|
119
127
|
@classmethod
|
|
@@ -310,6 +318,34 @@ class ConvMemory:
|
|
|
310
318
|
device=device or self.device,
|
|
311
319
|
)
|
|
312
320
|
return self.attach_evidence_reranker(reranker)
|
|
321
|
+
|
|
322
|
+
def attach_validity_module(self, validity_module):
|
|
323
|
+
"""Attach a ConvMemory v3 validity context module.
|
|
324
|
+
|
|
325
|
+
The module is disabled unless callers pass `validity_mode="context"`
|
|
326
|
+
or `validity_mode="demote"`. Context mode annotates results without
|
|
327
|
+
changing order; demote mode is explicit opt-in and preserves the
|
|
328
|
+
candidate set while allowing score-based reordering.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
if not isinstance(validity_module, ValidityEvidenceModule):
|
|
332
|
+
raise TypeError("validity_module must be a ValidityEvidenceModule")
|
|
333
|
+
self._validity_module = validity_module
|
|
334
|
+
return self
|
|
335
|
+
|
|
336
|
+
def load_validity_module(self, path_or_hub_id: str, device=None):
|
|
337
|
+
"""Load and attach a ConvMemory v3 validity context module.
|
|
338
|
+
|
|
339
|
+
`path_or_hub_id` may be a local checkpoint directory or a Hugging Face
|
|
340
|
+
Hub repo id. Loading is explicit so the plain v1/v2 retrieval path
|
|
341
|
+
remains backward-compatible.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
validity_module = ValidityEvidenceModule.from_pretrained(
|
|
345
|
+
path_or_hub_id,
|
|
346
|
+
device=device or self.device,
|
|
347
|
+
)
|
|
348
|
+
return self.attach_validity_module(validity_module)
|
|
313
349
|
|
|
314
350
|
def encode(self, texts):
|
|
315
351
|
"""Encode texts with the attached sentence-transformer encoder.
|
|
@@ -353,6 +389,7 @@ class ConvMemory:
|
|
|
353
389
|
protect_top_k: int = 7,
|
|
354
390
|
expand_window: int = 16,
|
|
355
391
|
evidence_reranker=None,
|
|
392
|
+
validity_mode=None,
|
|
356
393
|
):
|
|
357
394
|
"""Rerank text memories and return `list[RerankResult]`.
|
|
358
395
|
|
|
@@ -367,7 +404,9 @@ class ConvMemory:
|
|
|
367
404
|
reranker, or window-mode settings.
|
|
368
405
|
"""
|
|
369
406
|
|
|
370
|
-
|
|
407
|
+
memories = list(memories)
|
|
408
|
+
self._validate_validity_memory_input(memories, validity_mode)
|
|
409
|
+
memory_ids, memory_texts = self._parse_memories(memories)
|
|
371
410
|
embeddings = self.encode([query, *memory_texts])
|
|
372
411
|
query_embedding = embeddings[0]
|
|
373
412
|
memory_embeddings = embeddings[1:]
|
|
@@ -393,6 +432,7 @@ class ConvMemory:
|
|
|
393
432
|
protect_top_k=protect_top_k,
|
|
394
433
|
expand_window=expand_window,
|
|
395
434
|
evidence_reranker=evidence_reranker,
|
|
435
|
+
validity_mode=validity_mode,
|
|
396
436
|
)
|
|
397
437
|
return results[:top_k] if top_k is not None else results
|
|
398
438
|
|
|
@@ -414,6 +454,7 @@ class ConvMemory:
|
|
|
414
454
|
protect_top_k: int = 7,
|
|
415
455
|
expand_window: int = 16,
|
|
416
456
|
evidence_reranker=None,
|
|
457
|
+
validity_mode=None,
|
|
417
458
|
):
|
|
418
459
|
"""Retrieve memories and return `list[RerankResult]`.
|
|
419
460
|
|
|
@@ -441,6 +482,7 @@ class ConvMemory:
|
|
|
441
482
|
protect_top_k=protect_top_k,
|
|
442
483
|
expand_window=expand_window,
|
|
443
484
|
evidence_reranker=evidence_reranker,
|
|
485
|
+
validity_mode=validity_mode,
|
|
444
486
|
)
|
|
445
487
|
if selected_mode not in {"expand", "context", "expand_context"}:
|
|
446
488
|
raise ValueError("mode must be either 'rerank' or 'expand'")
|
|
@@ -466,6 +508,7 @@ class ConvMemory:
|
|
|
466
508
|
protect_top_k=protect_top_k,
|
|
467
509
|
expand_window=expand_window,
|
|
468
510
|
evidence_reranker=evidence_reranker,
|
|
511
|
+
validity_mode=validity_mode,
|
|
469
512
|
)
|
|
470
513
|
|
|
471
514
|
def expand_context(
|
|
@@ -484,6 +527,7 @@ class ConvMemory:
|
|
|
484
527
|
protect_top_k: int = 7,
|
|
485
528
|
expand_window: int = 16,
|
|
486
529
|
evidence_reranker=None,
|
|
530
|
+
validity_mode=None,
|
|
487
531
|
):
|
|
488
532
|
"""Build a wider memory context and return `list[RerankResult]`.
|
|
489
533
|
|
|
@@ -496,7 +540,9 @@ class ConvMemory:
|
|
|
496
540
|
Raises `ValueError` for invalid expansion policies, editor settings,
|
|
497
541
|
expander settings, or evidence-reranker settings.
|
|
498
542
|
"""
|
|
499
|
-
|
|
543
|
+
memories = list(memories)
|
|
544
|
+
self._validate_validity_memory_input(memories, validity_mode)
|
|
545
|
+
memory_ids, memory_texts = self._parse_memories(memories)
|
|
500
546
|
embeddings = self.encode([query, *memory_texts])
|
|
501
547
|
query_embedding = embeddings[0]
|
|
502
548
|
memory_embeddings = embeddings[1:]
|
|
@@ -526,6 +572,7 @@ class ConvMemory:
|
|
|
526
572
|
protect_top_k=protect_top_k,
|
|
527
573
|
expand_window=expand_window,
|
|
528
574
|
evidence_reranker=evidence_reranker,
|
|
575
|
+
validity_mode=validity_mode,
|
|
529
576
|
)
|
|
530
577
|
|
|
531
578
|
def rerank_embeddings(
|
|
@@ -544,6 +591,7 @@ class ConvMemory:
|
|
|
544
591
|
protect_top_k: int = 7,
|
|
545
592
|
expand_window: int = 16,
|
|
546
593
|
evidence_reranker=None,
|
|
594
|
+
validity_mode=None,
|
|
547
595
|
):
|
|
548
596
|
"""Rerank precomputed embeddings and return `list[RerankResult]`.
|
|
549
597
|
|
|
@@ -596,6 +644,13 @@ class ConvMemory:
|
|
|
596
644
|
query=query,
|
|
597
645
|
evidence_reranker=evidence_reranker,
|
|
598
646
|
)
|
|
647
|
+
results = self._maybe_apply_validity_module(
|
|
648
|
+
results=results,
|
|
649
|
+
memory_ids=memory_ids,
|
|
650
|
+
memory_texts=memory_texts,
|
|
651
|
+
query=query,
|
|
652
|
+
validity_mode=validity_mode,
|
|
653
|
+
)
|
|
599
654
|
return results[:top_k] if top_k is not None else results
|
|
600
655
|
|
|
601
656
|
def expand_context_embeddings(
|
|
@@ -617,6 +672,7 @@ class ConvMemory:
|
|
|
617
672
|
protect_top_k: int = 7,
|
|
618
673
|
expand_window: int = 16,
|
|
619
674
|
evidence_reranker=None,
|
|
675
|
+
validity_mode=None,
|
|
620
676
|
):
|
|
621
677
|
"""Expand context over precomputed embeddings.
|
|
622
678
|
|
|
@@ -652,6 +708,7 @@ class ConvMemory:
|
|
|
652
708
|
protect_top_k=protect_top_k,
|
|
653
709
|
expand_window=expand_window,
|
|
654
710
|
evidence_reranker=evidence_reranker,
|
|
711
|
+
validity_mode=validity_mode,
|
|
655
712
|
)
|
|
656
713
|
if context_budget <= protected_k:
|
|
657
714
|
return self._rerank_with_new_positions(base_results[:context_budget])
|
|
@@ -687,6 +744,7 @@ class ConvMemory:
|
|
|
687
744
|
protect_top_k=protect_top_k,
|
|
688
745
|
expand_window=expand_window,
|
|
689
746
|
evidence_reranker=evidence_reranker,
|
|
747
|
+
validity_mode=validity_mode,
|
|
690
748
|
)
|
|
691
749
|
rankings.append([result.memory_id for result in local_results])
|
|
692
750
|
result_by_id.update({result.memory_id: result for result in local_results})
|
|
@@ -800,6 +858,70 @@ class ConvMemory:
|
|
|
800
858
|
tail = [result for result in results if result.memory_id not in prefix_ids]
|
|
801
859
|
return self._rerank_with_new_positions([*reordered, *tail])
|
|
802
860
|
|
|
861
|
+
def _resolve_validity_mode(self, validity_mode):
|
|
862
|
+
if validity_mode is None:
|
|
863
|
+
return None
|
|
864
|
+
if isinstance(validity_mode, str):
|
|
865
|
+
mode = validity_mode.lower().strip()
|
|
866
|
+
if mode == "off":
|
|
867
|
+
return None
|
|
868
|
+
if mode in {"context", "demote"}:
|
|
869
|
+
if self._validity_module is None:
|
|
870
|
+
raise ValueError(
|
|
871
|
+
"No validity module is attached. Call "
|
|
872
|
+
"`load_validity_module(path)` or "
|
|
873
|
+
"`attach_validity_module(module)` before using "
|
|
874
|
+
f"validity_mode='{mode}'."
|
|
875
|
+
)
|
|
876
|
+
return mode
|
|
877
|
+
raise ValueError("validity_mode must be None, 'off', 'context', or 'demote'")
|
|
878
|
+
|
|
879
|
+
def _maybe_apply_validity_module(
|
|
880
|
+
self,
|
|
881
|
+
*,
|
|
882
|
+
results,
|
|
883
|
+
memory_ids,
|
|
884
|
+
memory_texts,
|
|
885
|
+
query,
|
|
886
|
+
validity_mode,
|
|
887
|
+
):
|
|
888
|
+
mode = self._resolve_validity_mode(validity_mode)
|
|
889
|
+
if mode is None or not results:
|
|
890
|
+
return results
|
|
891
|
+
memories = self._validity_memories(memory_ids, memory_texts)
|
|
892
|
+
return self._validity_module.apply(
|
|
893
|
+
query=query,
|
|
894
|
+
results=results,
|
|
895
|
+
memories=memories,
|
|
896
|
+
mode=mode,
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
@staticmethod
|
|
900
|
+
def _validity_memories(memory_ids, memory_texts):
|
|
901
|
+
if memory_texts is None:
|
|
902
|
+
memory_texts = ["" for _ in memory_ids]
|
|
903
|
+
memories = []
|
|
904
|
+
for idx, (memory_id, text) in enumerate(zip(memory_ids, memory_texts)):
|
|
905
|
+
memories.append(
|
|
906
|
+
{
|
|
907
|
+
"id": str(memory_id),
|
|
908
|
+
"text": str(text),
|
|
909
|
+
"position": idx,
|
|
910
|
+
}
|
|
911
|
+
)
|
|
912
|
+
return memories
|
|
913
|
+
|
|
914
|
+
def _validate_validity_memory_input(self, memories, validity_mode):
|
|
915
|
+
if self._resolve_validity_mode(validity_mode) is None:
|
|
916
|
+
return
|
|
917
|
+
for memory in memories:
|
|
918
|
+
if isinstance(memory, str):
|
|
919
|
+
continue
|
|
920
|
+
blocked = VALIDITY_FORBIDDEN_FIELDS.intersection(memory.keys())
|
|
921
|
+
if blocked:
|
|
922
|
+
field = sorted(blocked)[0]
|
|
923
|
+
raise ValueError(f"field '{field}' is not allowed at inference")
|
|
924
|
+
|
|
803
925
|
def _resolve_expander(self, expander):
|
|
804
926
|
message = "expander must be None, 'memory_mla', or a MemoryMLAExpander instance"
|
|
805
927
|
if expander is None:
|
|
@@ -1011,10 +1133,11 @@ class ConvMemory:
|
|
|
1011
1133
|
RerankResult(
|
|
1012
1134
|
memory_id=result.memory_id,
|
|
1013
1135
|
score=result.score,
|
|
1014
|
-
raw_score=result.raw_score,
|
|
1015
|
-
rank=rank,
|
|
1016
|
-
text=result.text,
|
|
1017
|
-
|
|
1136
|
+
raw_score=result.raw_score,
|
|
1137
|
+
rank=rank,
|
|
1138
|
+
text=result.text,
|
|
1139
|
+
validity=result.validity,
|
|
1140
|
+
)
|
|
1018
1141
|
for rank, result in enumerate(results, start=1)
|
|
1019
1142
|
]
|
|
1020
1143
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Optional Hugging Face Hub path resolution helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from huggingface_hub import HfApi as _HfApi
|
|
11
|
+
from huggingface_hub import hf_hub_download as _hf_hub_download
|
|
12
|
+
from huggingface_hub import snapshot_download as _hf_snapshot_download
|
|
13
|
+
except Exception: # pragma: no cover - exercised when optional dep is absent
|
|
14
|
+
_HfApi = None
|
|
15
|
+
_hf_hub_download = None
|
|
16
|
+
_hf_snapshot_download = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def looks_like_hub_id(path: str | Path) -> bool:
|
|
20
|
+
"""Return whether a missing path looks like a `namespace/repo` Hub id."""
|
|
21
|
+
|
|
22
|
+
text = str(path).replace("\\", "/").strip()
|
|
23
|
+
if not text or "://" in text or ":" in text:
|
|
24
|
+
return False
|
|
25
|
+
if text.startswith(("/", "./", "../", "~")):
|
|
26
|
+
return False
|
|
27
|
+
parts = text.split("/")
|
|
28
|
+
return len(parts) == 2 and all(parts)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_checkpoint_path(path: str | Path, *, repo_type: str = "model") -> Path:
|
|
32
|
+
"""Resolve a local checkpoint path or download a Hugging Face Hub repo id."""
|
|
33
|
+
|
|
34
|
+
candidate = Path(path)
|
|
35
|
+
if candidate.exists():
|
|
36
|
+
return candidate
|
|
37
|
+
if not looks_like_hub_id(path):
|
|
38
|
+
return candidate
|
|
39
|
+
if _hf_snapshot_download is None:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"Checkpoint path does not exist and looks like a Hugging Face Hub "
|
|
42
|
+
"repo id, but `huggingface_hub` is not installed. Install it with "
|
|
43
|
+
"`pip install huggingface_hub` or pass a local checkpoint path."
|
|
44
|
+
)
|
|
45
|
+
try:
|
|
46
|
+
return Path(_hf_snapshot_download(repo_id=str(path), repo_type=repo_type))
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
try:
|
|
49
|
+
return _download_hub_repo_without_snapshot_symlinks(str(path), repo_type=repo_type)
|
|
50
|
+
except Exception as fallback_exc:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Could not download Hugging Face Hub checkpoint repo '{path}'. "
|
|
53
|
+
"Pass a local checkpoint path or verify repo access."
|
|
54
|
+
) from fallback_exc
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _download_hub_repo_without_snapshot_symlinks(repo_id: str, *, repo_type: str = "model") -> Path:
|
|
58
|
+
"""Download a small checkpoint repo without relying on snapshot symlinks.
|
|
59
|
+
|
|
60
|
+
Some Windows environments do not allow symlink creation. Hugging Face Hub's
|
|
61
|
+
snapshot cache can fail there before it has a chance to fall back cleanly, so
|
|
62
|
+
ConvMemory downloads each file into a plain local directory as a compatibility
|
|
63
|
+
path.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
if _HfApi is None or _hf_hub_download is None:
|
|
67
|
+
raise RuntimeError("huggingface_hub is not installed")
|
|
68
|
+
cache_root = os.environ.get("CONVMEMORY_CACHE")
|
|
69
|
+
if cache_root:
|
|
70
|
+
base = Path(cache_root)
|
|
71
|
+
else:
|
|
72
|
+
base = Path.home() / ".cache" / "convmemory"
|
|
73
|
+
digest = hashlib.sha1(repo_id.encode("utf-8")).hexdigest()[:12]
|
|
74
|
+
target = base / "hub" / f"{repo_id.replace('/', '--')}-{digest}"
|
|
75
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
|
|
77
|
+
files = _HfApi().list_repo_files(repo_id=repo_id, repo_type=repo_type)
|
|
78
|
+
for filename in files:
|
|
79
|
+
if filename.endswith("/"):
|
|
80
|
+
continue
|
|
81
|
+
_hf_hub_download(
|
|
82
|
+
repo_id=repo_id,
|
|
83
|
+
filename=filename,
|
|
84
|
+
repo_type=repo_type,
|
|
85
|
+
local_dir=target,
|
|
86
|
+
)
|
|
87
|
+
return target
|