alma-memory 0.5.0__py3-none-any.whl → 0.7.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.
Files changed (111) hide show
  1. alma/__init__.py +296 -194
  2. alma/compression/__init__.py +33 -0
  3. alma/compression/pipeline.py +980 -0
  4. alma/confidence/__init__.py +47 -47
  5. alma/confidence/engine.py +540 -540
  6. alma/confidence/types.py +351 -351
  7. alma/config/loader.py +157 -157
  8. alma/consolidation/__init__.py +23 -23
  9. alma/consolidation/engine.py +678 -678
  10. alma/consolidation/prompts.py +84 -84
  11. alma/core.py +1189 -322
  12. alma/domains/__init__.py +30 -30
  13. alma/domains/factory.py +359 -359
  14. alma/domains/schemas.py +448 -448
  15. alma/domains/types.py +272 -272
  16. alma/events/__init__.py +75 -75
  17. alma/events/emitter.py +285 -284
  18. alma/events/storage_mixin.py +246 -246
  19. alma/events/types.py +126 -126
  20. alma/events/webhook.py +425 -425
  21. alma/exceptions.py +49 -49
  22. alma/extraction/__init__.py +31 -31
  23. alma/extraction/auto_learner.py +265 -264
  24. alma/extraction/extractor.py +420 -420
  25. alma/graph/__init__.py +106 -81
  26. alma/graph/backends/__init__.py +32 -18
  27. alma/graph/backends/kuzu.py +624 -0
  28. alma/graph/backends/memgraph.py +432 -0
  29. alma/graph/backends/memory.py +236 -236
  30. alma/graph/backends/neo4j.py +417 -417
  31. alma/graph/base.py +159 -159
  32. alma/graph/extraction.py +198 -198
  33. alma/graph/store.py +860 -860
  34. alma/harness/__init__.py +35 -35
  35. alma/harness/base.py +386 -386
  36. alma/harness/domains.py +705 -705
  37. alma/initializer/__init__.py +37 -37
  38. alma/initializer/initializer.py +418 -418
  39. alma/initializer/types.py +250 -250
  40. alma/integration/__init__.py +62 -62
  41. alma/integration/claude_agents.py +444 -432
  42. alma/integration/helena.py +423 -423
  43. alma/integration/victor.py +471 -471
  44. alma/learning/__init__.py +101 -86
  45. alma/learning/decay.py +878 -0
  46. alma/learning/forgetting.py +1446 -1446
  47. alma/learning/heuristic_extractor.py +390 -390
  48. alma/learning/protocols.py +374 -374
  49. alma/learning/validation.py +346 -346
  50. alma/mcp/__init__.py +123 -45
  51. alma/mcp/__main__.py +156 -156
  52. alma/mcp/resources.py +122 -122
  53. alma/mcp/server.py +955 -591
  54. alma/mcp/tools.py +3254 -511
  55. alma/observability/__init__.py +91 -0
  56. alma/observability/config.py +302 -0
  57. alma/observability/guidelines.py +170 -0
  58. alma/observability/logging.py +424 -0
  59. alma/observability/metrics.py +583 -0
  60. alma/observability/tracing.py +440 -0
  61. alma/progress/__init__.py +21 -21
  62. alma/progress/tracker.py +607 -607
  63. alma/progress/types.py +250 -250
  64. alma/retrieval/__init__.py +134 -53
  65. alma/retrieval/budget.py +525 -0
  66. alma/retrieval/cache.py +1304 -1061
  67. alma/retrieval/embeddings.py +202 -202
  68. alma/retrieval/engine.py +850 -366
  69. alma/retrieval/modes.py +365 -0
  70. alma/retrieval/progressive.py +560 -0
  71. alma/retrieval/scoring.py +344 -344
  72. alma/retrieval/trust_scoring.py +637 -0
  73. alma/retrieval/verification.py +797 -0
  74. alma/session/__init__.py +19 -19
  75. alma/session/manager.py +442 -399
  76. alma/session/types.py +288 -288
  77. alma/storage/__init__.py +101 -61
  78. alma/storage/archive.py +233 -0
  79. alma/storage/azure_cosmos.py +1259 -1048
  80. alma/storage/base.py +1083 -525
  81. alma/storage/chroma.py +1443 -1443
  82. alma/storage/constants.py +103 -0
  83. alma/storage/file_based.py +614 -619
  84. alma/storage/migrations/__init__.py +21 -0
  85. alma/storage/migrations/base.py +321 -0
  86. alma/storage/migrations/runner.py +323 -0
  87. alma/storage/migrations/version_stores.py +337 -0
  88. alma/storage/migrations/versions/__init__.py +11 -0
  89. alma/storage/migrations/versions/v1_0_0.py +373 -0
  90. alma/storage/migrations/versions/v1_1_0_workflow_context.py +551 -0
  91. alma/storage/pinecone.py +1080 -1080
  92. alma/storage/postgresql.py +1948 -1452
  93. alma/storage/qdrant.py +1306 -1306
  94. alma/storage/sqlite_local.py +3041 -1358
  95. alma/testing/__init__.py +46 -0
  96. alma/testing/factories.py +301 -0
  97. alma/testing/mocks.py +389 -0
  98. alma/types.py +292 -264
  99. alma/utils/__init__.py +19 -0
  100. alma/utils/tokenizer.py +521 -0
  101. alma/workflow/__init__.py +83 -0
  102. alma/workflow/artifacts.py +170 -0
  103. alma/workflow/checkpoint.py +311 -0
  104. alma/workflow/context.py +228 -0
  105. alma/workflow/outcomes.py +189 -0
  106. alma/workflow/reducers.py +393 -0
  107. {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/METADATA +244 -72
  108. alma_memory-0.7.0.dist-info/RECORD +112 -0
  109. alma_memory-0.5.0.dist-info/RECORD +0 -76
  110. {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
  111. {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
alma/graph/extraction.py CHANGED
@@ -1,198 +1,198 @@
1
- """
2
- ALMA Graph Memory - Entity Extraction.
3
-
4
- LLM-powered extraction of entities and relationships from text.
5
- """
6
-
7
- import logging
8
- from dataclasses import dataclass
9
- from typing import Dict, List, Optional, Tuple
10
-
11
- from alma.graph.store import Entity, Relationship
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
-
16
- @dataclass
17
- class ExtractionConfig:
18
- """Configuration for entity extraction."""
19
-
20
- provider: str = "openai"
21
- model: str = "gpt-4o-mini"
22
- temperature: float = 0.1
23
-
24
-
25
- class EntityExtractor:
26
- """
27
- LLM-powered entity and relationship extraction.
28
-
29
- Extracts entities (people, organizations, tools, concepts) and
30
- relationships between them from text.
31
- """
32
-
33
- EXTRACTION_PROMPT = """Extract entities and relationships from the following text.
34
-
35
- Entities are things like:
36
- - People (names, roles)
37
- - Organizations (companies, teams)
38
- - Tools/Technologies (software, frameworks)
39
- - Concepts (methodologies, patterns)
40
- - Locations (places, regions)
41
-
42
- Relationships describe how entities are connected:
43
- - WORKS_AT (person -> organization)
44
- - USES (entity -> tool)
45
- - KNOWS (person -> person)
46
- - CREATED_BY (thing -> person/org)
47
- - PART_OF (entity -> larger entity)
48
- - RELATED_TO (general relationship)
49
-
50
- TEXT:
51
- {text}
52
-
53
- Respond in JSON format:
54
- ```json
55
- {{
56
- "entities": [
57
- {{"id": "unique-id", "name": "Entity Name", "type": "person|organization|tool|concept|location"}}
58
- ],
59
- "relationships": [
60
- {{"source": "entity-id", "target": "entity-id", "type": "RELATIONSHIP_TYPE", "properties": {{}}}}
61
- ]
62
- }}
63
- ```
64
-
65
- Only extract entities and relationships that are explicitly mentioned or strongly implied.
66
- """
67
-
68
- def __init__(self, config: Optional[ExtractionConfig] = None):
69
- self.config = config or ExtractionConfig()
70
- self._client = None
71
-
72
- def _get_client(self):
73
- """Lazy initialization of LLM client."""
74
- if self._client is None:
75
- if self.config.provider == "openai":
76
- from openai import OpenAI
77
-
78
- self._client = OpenAI()
79
- elif self.config.provider == "anthropic":
80
- from anthropic import Anthropic
81
-
82
- self._client = Anthropic()
83
- return self._client
84
-
85
- def extract(
86
- self,
87
- text: str,
88
- existing_entities: Optional[List[Entity]] = None,
89
- ) -> Tuple[List[Entity], List[Relationship]]:
90
- """
91
- Extract entities and relationships from text.
92
-
93
- Args:
94
- text: Text to extract from
95
- existing_entities: Optional list of known entities for linking
96
-
97
- Returns:
98
- Tuple of (entities, relationships)
99
- """
100
- import json
101
- import re
102
- import uuid
103
-
104
- prompt = self.EXTRACTION_PROMPT.format(text=text)
105
-
106
- client = self._get_client()
107
-
108
- if self.config.provider == "openai":
109
- response = client.chat.completions.create(
110
- model=self.config.model,
111
- messages=[{"role": "user", "content": prompt}],
112
- temperature=self.config.temperature,
113
- )
114
- raw_response = response.choices[0].message.content
115
- elif self.config.provider == "anthropic":
116
- response = client.messages.create(
117
- model=self.config.model,
118
- max_tokens=2000,
119
- messages=[{"role": "user", "content": prompt}],
120
- )
121
- raw_response = response.content[0].text
122
- else:
123
- raise ValueError(f"Unsupported provider: {self.config.provider}")
124
-
125
- # Parse JSON from response
126
- json_match = re.search(r"```json\s*(.*?)\s*```", raw_response, re.DOTALL)
127
- if json_match:
128
- json_str = json_match.group(1)
129
- else:
130
- json_match = re.search(r"\{.*\}", raw_response, re.DOTALL)
131
- json_str = json_match.group(0) if json_match else "{}"
132
-
133
- try:
134
- data = json.loads(json_str)
135
- except json.JSONDecodeError:
136
- logger.warning(f"Failed to parse extraction response: {raw_response[:200]}")
137
- return [], []
138
-
139
- # Build entities
140
- entities = []
141
- entity_map = {} # id -> Entity
142
-
143
- for e in data.get("entities", []):
144
- entity_id = e.get("id") or str(uuid.uuid4())[:8]
145
- entity = Entity(
146
- id=entity_id,
147
- name=e.get("name", "Unknown"),
148
- entity_type=e.get("type", "concept"),
149
- )
150
- entities.append(entity)
151
- entity_map[entity_id] = entity
152
-
153
- # Also map by name for relationship linking
154
- entity_map[e.get("name", "").lower()] = entity
155
-
156
- # Build relationships
157
- relationships = []
158
- for r in data.get("relationships", []):
159
- source_id = r.get("source")
160
- target_id = r.get("target")
161
-
162
- # Try to resolve IDs
163
- source = entity_map.get(source_id) or entity_map.get(
164
- source_id.lower() if source_id else ""
165
- )
166
- target = entity_map.get(target_id) or entity_map.get(
167
- target_id.lower() if target_id else ""
168
- )
169
-
170
- if source and target:
171
- rel = Relationship(
172
- id=f"{source.id}-{r.get('type', 'RELATED')}-{target.id}",
173
- source_id=source.id,
174
- target_id=target.id,
175
- relation_type=r.get("type", "RELATED_TO"),
176
- properties=r.get("properties", {}),
177
- )
178
- relationships.append(rel)
179
-
180
- return entities, relationships
181
-
182
- def extract_from_conversation(
183
- self,
184
- messages: List[Dict[str, str]],
185
- ) -> Tuple[List[Entity], List[Relationship]]:
186
- """
187
- Extract entities and relationships from a conversation.
188
-
189
- Args:
190
- messages: List of {"role": "...", "content": "..."}
191
-
192
- Returns:
193
- Tuple of (entities, relationships)
194
- """
195
- # Combine messages into text
196
- text = "\n".join(f"{msg['role'].upper()}: {msg['content']}" for msg in messages)
197
-
198
- return self.extract(text)
1
+ """
2
+ ALMA Graph Memory - Entity Extraction.
3
+
4
+ LLM-powered extraction of entities and relationships from text.
5
+ """
6
+
7
+ import logging
8
+ from dataclasses import dataclass
9
+ from typing import Dict, List, Optional, Tuple
10
+
11
+ from alma.graph.store import Entity, Relationship
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass
17
+ class ExtractionConfig:
18
+ """Configuration for entity extraction."""
19
+
20
+ provider: str = "openai"
21
+ model: str = "gpt-4o-mini"
22
+ temperature: float = 0.1
23
+
24
+
25
+ class EntityExtractor:
26
+ """
27
+ LLM-powered entity and relationship extraction.
28
+
29
+ Extracts entities (people, organizations, tools, concepts) and
30
+ relationships between them from text.
31
+ """
32
+
33
+ EXTRACTION_PROMPT = """Extract entities and relationships from the following text.
34
+
35
+ Entities are things like:
36
+ - People (names, roles)
37
+ - Organizations (companies, teams)
38
+ - Tools/Technologies (software, frameworks)
39
+ - Concepts (methodologies, patterns)
40
+ - Locations (places, regions)
41
+
42
+ Relationships describe how entities are connected:
43
+ - WORKS_AT (person -> organization)
44
+ - USES (entity -> tool)
45
+ - KNOWS (person -> person)
46
+ - CREATED_BY (thing -> person/org)
47
+ - PART_OF (entity -> larger entity)
48
+ - RELATED_TO (general relationship)
49
+
50
+ TEXT:
51
+ {text}
52
+
53
+ Respond in JSON format:
54
+ ```json
55
+ {{
56
+ "entities": [
57
+ {{"id": "unique-id", "name": "Entity Name", "type": "person|organization|tool|concept|location"}}
58
+ ],
59
+ "relationships": [
60
+ {{"source": "entity-id", "target": "entity-id", "type": "RELATIONSHIP_TYPE", "properties": {{}}}}
61
+ ]
62
+ }}
63
+ ```
64
+
65
+ Only extract entities and relationships that are explicitly mentioned or strongly implied.
66
+ """
67
+
68
+ def __init__(self, config: Optional[ExtractionConfig] = None):
69
+ self.config = config or ExtractionConfig()
70
+ self._client = None
71
+
72
+ def _get_client(self):
73
+ """Lazy initialization of LLM client."""
74
+ if self._client is None:
75
+ if self.config.provider == "openai":
76
+ from openai import OpenAI
77
+
78
+ self._client = OpenAI()
79
+ elif self.config.provider == "anthropic":
80
+ from anthropic import Anthropic
81
+
82
+ self._client = Anthropic()
83
+ return self._client
84
+
85
+ def extract(
86
+ self,
87
+ text: str,
88
+ existing_entities: Optional[List[Entity]] = None,
89
+ ) -> Tuple[List[Entity], List[Relationship]]:
90
+ """
91
+ Extract entities and relationships from text.
92
+
93
+ Args:
94
+ text: Text to extract from
95
+ existing_entities: Optional list of known entities for linking
96
+
97
+ Returns:
98
+ Tuple of (entities, relationships)
99
+ """
100
+ import json
101
+ import re
102
+ import uuid
103
+
104
+ prompt = self.EXTRACTION_PROMPT.format(text=text)
105
+
106
+ client = self._get_client()
107
+
108
+ if self.config.provider == "openai":
109
+ response = client.chat.completions.create(
110
+ model=self.config.model,
111
+ messages=[{"role": "user", "content": prompt}],
112
+ temperature=self.config.temperature,
113
+ )
114
+ raw_response = response.choices[0].message.content
115
+ elif self.config.provider == "anthropic":
116
+ response = client.messages.create(
117
+ model=self.config.model,
118
+ max_tokens=2000,
119
+ messages=[{"role": "user", "content": prompt}],
120
+ )
121
+ raw_response = response.content[0].text
122
+ else:
123
+ raise ValueError(f"Unsupported provider: {self.config.provider}")
124
+
125
+ # Parse JSON from response
126
+ json_match = re.search(r"```json\s*(.*?)\s*```", raw_response, re.DOTALL)
127
+ if json_match:
128
+ json_str = json_match.group(1)
129
+ else:
130
+ json_match = re.search(r"\{.*\}", raw_response, re.DOTALL)
131
+ json_str = json_match.group(0) if json_match else "{}"
132
+
133
+ try:
134
+ data = json.loads(json_str)
135
+ except json.JSONDecodeError:
136
+ logger.warning(f"Failed to parse extraction response: {raw_response[:200]}")
137
+ return [], []
138
+
139
+ # Build entities
140
+ entities = []
141
+ entity_map = {} # id -> Entity
142
+
143
+ for e in data.get("entities", []):
144
+ entity_id = e.get("id") or str(uuid.uuid4())[:8]
145
+ entity = Entity(
146
+ id=entity_id,
147
+ name=e.get("name", "Unknown"),
148
+ entity_type=e.get("type", "concept"),
149
+ )
150
+ entities.append(entity)
151
+ entity_map[entity_id] = entity
152
+
153
+ # Also map by name for relationship linking
154
+ entity_map[e.get("name", "").lower()] = entity
155
+
156
+ # Build relationships
157
+ relationships = []
158
+ for r in data.get("relationships", []):
159
+ source_id = r.get("source")
160
+ target_id = r.get("target")
161
+
162
+ # Try to resolve IDs
163
+ source = entity_map.get(source_id) or entity_map.get(
164
+ source_id.lower() if source_id else ""
165
+ )
166
+ target = entity_map.get(target_id) or entity_map.get(
167
+ target_id.lower() if target_id else ""
168
+ )
169
+
170
+ if source and target:
171
+ rel = Relationship(
172
+ id=f"{source.id}-{r.get('type', 'RELATED')}-{target.id}",
173
+ source_id=source.id,
174
+ target_id=target.id,
175
+ relation_type=r.get("type", "RELATED_TO"),
176
+ properties=r.get("properties", {}),
177
+ )
178
+ relationships.append(rel)
179
+
180
+ return entities, relationships
181
+
182
+ def extract_from_conversation(
183
+ self,
184
+ messages: List[Dict[str, str]],
185
+ ) -> Tuple[List[Entity], List[Relationship]]:
186
+ """
187
+ Extract entities and relationships from a conversation.
188
+
189
+ Args:
190
+ messages: List of {"role": "...", "content": "..."}
191
+
192
+ Returns:
193
+ Tuple of (entities, relationships)
194
+ """
195
+ # Combine messages into text
196
+ text = "\n".join(f"{msg['role'].upper()}: {msg['content']}" for msg in messages)
197
+
198
+ return self.extract(text)