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.
Files changed (31) hide show
  1. {convmemory-0.5.0 → convmemory-0.6.0}/PKG-INFO +30 -4
  2. {convmemory-0.5.0 → convmemory-0.6.0}/README.md +33 -8
  3. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/__init__.py +14 -10
  4. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/api.py +129 -6
  5. convmemory-0.6.0/convmemory/hub.py +87 -0
  6. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/reranker.py +1 -0
  7. convmemory-0.6.0/convmemory/validity.py +438 -0
  8. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory.egg-info/PKG-INFO +30 -4
  9. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory.egg-info/SOURCES.txt +3 -1
  10. {convmemory-0.5.0 → convmemory-0.6.0}/pyproject.toml +2 -2
  11. convmemory-0.6.0/tests/test_validity_context.py +365 -0
  12. convmemory-0.5.0/convmemory/hub.py +0 -45
  13. {convmemory-0.5.0 → convmemory-0.6.0}/LICENSE +0 -0
  14. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/ccge.py +0 -0
  15. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/encoder.py +0 -0
  16. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/evidence_reranker.py +0 -0
  17. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/memory_mla.py +0 -0
  18. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/metrics.py +0 -0
  19. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/models.py +0 -0
  20. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/routing.py +0 -0
  21. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory/scoring.py +0 -0
  22. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory.egg-info/dependency_links.txt +0 -0
  23. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory.egg-info/requires.txt +0 -0
  24. {convmemory-0.5.0 → convmemory-0.6.0}/convmemory.egg-info/top_level.txt +0 -0
  25. {convmemory-0.5.0 → convmemory-0.6.0}/setup.cfg +0 -0
  26. {convmemory-0.5.0 → convmemory-0.6.0}/tests/test_api_smoke.py +0 -0
  27. {convmemory-0.5.0 → convmemory-0.6.0}/tests/test_ccge.py +0 -0
  28. {convmemory-0.5.0 → convmemory-0.6.0}/tests/test_evidence_reranker.py +0 -0
  29. {convmemory-0.5.0 → convmemory-0.6.0}/tests/test_hub_loading.py +0 -0
  30. {convmemory-0.5.0 → convmemory-0.6.0}/tests/test_memory_mla.py +0 -0
  31. {convmemory-0.5.0 → convmemory-0.6.0}/tests/test_reranker.py +0 -0
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: convmemory
3
- Version: 0.5.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.5.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.5.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
- ### Experimental Memory-MLA Recall Expander
288
-
289
- This is **not** the v0.5.0 evidence reranker. See "ConvMemory v2: Evidence
290
- Reranker" above for the v2 release.
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
- "build_ccge_features",
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
- memory_ids, memory_texts = self._parse_memories(memories)
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
- memory_ids, memory_texts = self._parse_memories(memories)
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
@@ -31,6 +31,7 @@ class RerankResult:
31
31
  raw_score: float
32
32
  rank: int
33
33
  text: Optional[str] = None
34
+ validity: Optional[dict] = None
34
35
 
35
36
 
36
37
  def sliding_windows(num_items: int, window_size: int, stride: int):