hb-cortex-memory 0.1.0__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.
@@ -0,0 +1,158 @@
1
+ """
2
+ cortex_memory.domains — DomainTreeBase + retrieval-weight registry.
3
+
4
+ Each memory domain (Knowledge / Episodic / Experience / Intelligence) is a typed
5
+ view over the CORTEX substrate. This module captures three things:
6
+
7
+ 1. The *signal vector* every domain ranks against: ``semantic``, ``recency``,
8
+ ``user_match``, ``success``.
9
+ 2. The per-domain **retrieval weights** (constants below).
10
+ 3. A pure ``score_signals`` helper that turns a signal dict into a single
11
+ weighted score.
12
+
13
+ A self-contained tree primitive (no host dependency); moved out of the host
14
+ (Phase 12 `04`). The host re-exports it from ``ai.memory.domains``.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from abc import ABC
20
+ from dataclasses import dataclass, field
21
+ from typing import Any, ClassVar, Optional
22
+ from uuid import UUID
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ _REQUIRED_SIGNALS = ("semantic", "recency", "user_match", "success")
28
+
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Canonical per-domain weights.
32
+ # ---------------------------------------------------------------------------
33
+
34
+
35
+ KnowledgeWeights: dict[str, float] = {
36
+ "semantic": 1.0, "recency": 0.4, "user_match": 0.0, "success": 0.0,
37
+ }
38
+ ExperienceWeights: dict[str, float] = {
39
+ "semantic": 0.7, "recency": 0.5, "user_match": 0.2, "success": 0.6,
40
+ }
41
+ IntelligenceWeights: dict[str, float] = {
42
+ "semantic": 0.6, "recency": 0.3, "user_match": 0.0, "success": 0.0,
43
+ }
44
+ EpisodicWeights: dict[str, float] = {
45
+ "semantic": 0.5, "recency": 0.7, "user_match": 0.6, "success": 0.5,
46
+ }
47
+
48
+ DEFAULT_DOMAIN_WEIGHTS: dict[str, dict[str, float]] = {
49
+ "knowledge": KnowledgeWeights,
50
+ "experience": ExperienceWeights,
51
+ "intelligence": IntelligenceWeights,
52
+ "episodic": EpisodicWeights,
53
+ }
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Pure scoring helper
58
+ # ---------------------------------------------------------------------------
59
+
60
+
61
+ def score_signals(
62
+ weights: dict[str, float],
63
+ signals: dict[str, float],
64
+ ) -> float:
65
+ """Weighted average over the canonical signal vector.
66
+
67
+ Missing signals are treated as 0; the denominator uses the declared weights
68
+ (not the present signals) so domains that always want recency see
69
+ partial-credit penalties when recency is missing. Returns 0.0 when the
70
+ weight vector is all zero (defensive).
71
+ """
72
+ total_weight = sum(max(0.0, w) for w in weights.values())
73
+ if total_weight <= 0:
74
+ return 0.0
75
+ accum = 0.0
76
+ for key in _REQUIRED_SIGNALS:
77
+ w = max(0.0, float(weights.get(key, 0.0)))
78
+ s = max(0.0, float(signals.get(key, 0.0)))
79
+ accum += w * s
80
+ return accum / total_weight
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Typed retrieval result
85
+ # ---------------------------------------------------------------------------
86
+
87
+
88
+ @dataclass
89
+ class DomainItem:
90
+ """One retrievable item from a domain tree (post-scoring)."""
91
+
92
+ node_id: UUID
93
+ title: str
94
+ summary: Optional[str]
95
+ domain: str
96
+ score: float = 0.0
97
+ signals: dict[str, float] = field(default_factory=dict)
98
+ payload: dict[str, Any] = field(default_factory=dict)
99
+
100
+ def to_dict(self) -> dict[str, Any]:
101
+ return {
102
+ "node_id": str(self.node_id),
103
+ "title": self.title,
104
+ "summary": self.summary,
105
+ "domain": self.domain,
106
+ "score": self.score,
107
+ "signals": dict(self.signals),
108
+ "payload": dict(self.payload),
109
+ }
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Base class
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ class DomainTreeBase(ABC):
118
+ """Optional base for domain tree services. New domain code SHOULD subclass
119
+ to inherit the retrieval-weight contract and the pure scorer."""
120
+
121
+ DOMAIN: ClassVar[str] = "general"
122
+ ROOT_TITLE: ClassVar[str] = "Tree"
123
+ SECTIONS: ClassVar[dict[str, str]] = {}
124
+ RETRIEVAL_WEIGHTS: ClassVar[dict[str, float]] = KnowledgeWeights
125
+
126
+ def __init__(self, db: Any, company_id: UUID):
127
+ self.db = db
128
+ self.company_id = company_id
129
+
130
+ async def ensure_tree(self, *, scope_id: UUID, scope_level: str) -> Any: # pragma: no cover
131
+ raise NotImplementedError
132
+
133
+ async def ensure_section(self, tree: Any, section_type: str) -> Any: # pragma: no cover
134
+ raise NotImplementedError
135
+
136
+ async def write_item(self, **kwargs: Any) -> Any: # pragma: no cover
137
+ raise NotImplementedError
138
+
139
+ async def find(
140
+ self, *, tree: Any, query: str, top_k: int = 5,
141
+ filters: Optional[dict[str, Any]] = None,
142
+ ) -> list[DomainItem]: # pragma: no cover
143
+ raise NotImplementedError
144
+
145
+ def score(self, signals: dict[str, float]) -> float:
146
+ return score_signals(self.RETRIEVAL_WEIGHTS, signals)
147
+
148
+
149
+ __all__ = [
150
+ "DomainItem",
151
+ "DomainTreeBase",
152
+ "DEFAULT_DOMAIN_WEIGHTS",
153
+ "KnowledgeWeights",
154
+ "EpisodicWeights",
155
+ "ExperienceWeights",
156
+ "IntelligenceWeights",
157
+ "score_signals",
158
+ ]