alma-memory 0.3.0__py3-none-any.whl → 0.5.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.
- alma/__init__.py +99 -29
- alma/confidence/__init__.py +47 -0
- alma/confidence/engine.py +540 -0
- alma/confidence/types.py +351 -0
- alma/config/loader.py +3 -2
- alma/consolidation/__init__.py +23 -0
- alma/consolidation/engine.py +678 -0
- alma/consolidation/prompts.py +84 -0
- alma/core.py +15 -15
- alma/domains/__init__.py +6 -6
- alma/domains/factory.py +12 -9
- alma/domains/schemas.py +17 -3
- alma/domains/types.py +8 -4
- alma/events/__init__.py +75 -0
- alma/events/emitter.py +284 -0
- alma/events/storage_mixin.py +246 -0
- alma/events/types.py +126 -0
- alma/events/webhook.py +425 -0
- alma/exceptions.py +49 -0
- alma/extraction/__init__.py +31 -0
- alma/extraction/auto_learner.py +264 -0
- alma/extraction/extractor.py +420 -0
- alma/graph/__init__.py +81 -0
- alma/graph/backends/__init__.py +18 -0
- alma/graph/backends/memory.py +236 -0
- alma/graph/backends/neo4j.py +417 -0
- alma/graph/base.py +159 -0
- alma/graph/extraction.py +198 -0
- alma/graph/store.py +860 -0
- alma/harness/__init__.py +4 -4
- alma/harness/base.py +18 -9
- alma/harness/domains.py +27 -11
- alma/initializer/__init__.py +37 -0
- alma/initializer/initializer.py +418 -0
- alma/initializer/types.py +250 -0
- alma/integration/__init__.py +9 -9
- alma/integration/claude_agents.py +10 -10
- alma/integration/helena.py +32 -22
- alma/integration/victor.py +57 -33
- alma/learning/__init__.py +27 -27
- alma/learning/forgetting.py +198 -148
- alma/learning/heuristic_extractor.py +40 -24
- alma/learning/protocols.py +62 -14
- alma/learning/validation.py +7 -2
- alma/mcp/__init__.py +4 -4
- alma/mcp/__main__.py +2 -1
- alma/mcp/resources.py +17 -16
- alma/mcp/server.py +102 -44
- alma/mcp/tools.py +174 -37
- alma/progress/__init__.py +3 -3
- alma/progress/tracker.py +26 -20
- alma/progress/types.py +8 -12
- alma/py.typed +0 -0
- alma/retrieval/__init__.py +11 -11
- alma/retrieval/cache.py +20 -21
- alma/retrieval/embeddings.py +4 -4
- alma/retrieval/engine.py +114 -35
- alma/retrieval/scoring.py +73 -63
- alma/session/__init__.py +2 -2
- alma/session/manager.py +5 -5
- alma/session/types.py +5 -4
- alma/storage/__init__.py +41 -0
- alma/storage/azure_cosmos.py +107 -31
- alma/storage/base.py +157 -4
- alma/storage/chroma.py +1443 -0
- alma/storage/file_based.py +56 -20
- alma/storage/pinecone.py +1080 -0
- alma/storage/postgresql.py +1452 -0
- alma/storage/qdrant.py +1306 -0
- alma/storage/sqlite_local.py +376 -31
- alma/types.py +62 -14
- alma_memory-0.5.0.dist-info/METADATA +905 -0
- alma_memory-0.5.0.dist-info/RECORD +76 -0
- {alma_memory-0.3.0.dist-info → alma_memory-0.5.0.dist-info}/WHEEL +1 -1
- alma_memory-0.3.0.dist-info/METADATA +0 -438
- alma_memory-0.3.0.dist-info/RECORD +0 -46
- {alma_memory-0.3.0.dist-info → alma_memory-0.5.0.dist-info}/top_level.txt +0 -0
alma/graph/__init__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Graph Memory Module.
|
|
3
|
+
|
|
4
|
+
Graph-based memory for capturing relationships between entities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from alma.graph.base import GraphBackend
|
|
8
|
+
from alma.graph.extraction import (
|
|
9
|
+
EntityExtractor,
|
|
10
|
+
ExtractionConfig,
|
|
11
|
+
)
|
|
12
|
+
from alma.graph.store import (
|
|
13
|
+
BackendGraphStore,
|
|
14
|
+
Entity,
|
|
15
|
+
GraphQuery,
|
|
16
|
+
GraphResult,
|
|
17
|
+
GraphStore,
|
|
18
|
+
InMemoryGraphStore,
|
|
19
|
+
Neo4jGraphStore,
|
|
20
|
+
Relationship,
|
|
21
|
+
create_graph_store,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Backend abstract base class
|
|
26
|
+
"GraphBackend",
|
|
27
|
+
# Store classes (high-level API)
|
|
28
|
+
"GraphStore",
|
|
29
|
+
"Neo4jGraphStore",
|
|
30
|
+
"InMemoryGraphStore",
|
|
31
|
+
"BackendGraphStore",
|
|
32
|
+
# Data classes
|
|
33
|
+
"Entity",
|
|
34
|
+
"Relationship",
|
|
35
|
+
"GraphQuery",
|
|
36
|
+
"GraphResult",
|
|
37
|
+
# Extraction
|
|
38
|
+
"EntityExtractor",
|
|
39
|
+
"ExtractionConfig",
|
|
40
|
+
# Factory functions
|
|
41
|
+
"create_graph_store",
|
|
42
|
+
"create_graph_backend",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_graph_backend(backend: str = "neo4j", **config) -> GraphBackend:
|
|
47
|
+
"""
|
|
48
|
+
Factory function to create a graph backend.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
backend: Backend type ("neo4j" or "memory")
|
|
52
|
+
**config: Backend-specific configuration options
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Configured GraphBackend instance
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: If an unknown backend type is specified
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
# Create Neo4j backend
|
|
62
|
+
backend = create_graph_backend(
|
|
63
|
+
backend="neo4j",
|
|
64
|
+
uri="bolt://localhost:7687",
|
|
65
|
+
username="neo4j",
|
|
66
|
+
password="password"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Create in-memory backend for testing
|
|
70
|
+
backend = create_graph_backend(backend="memory")
|
|
71
|
+
"""
|
|
72
|
+
if backend == "neo4j":
|
|
73
|
+
from alma.graph.backends.neo4j import Neo4jBackend
|
|
74
|
+
|
|
75
|
+
return Neo4jBackend(**config)
|
|
76
|
+
elif backend == "memory":
|
|
77
|
+
from alma.graph.backends.memory import InMemoryBackend
|
|
78
|
+
|
|
79
|
+
return InMemoryBackend()
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError(f"Unknown graph backend: {backend}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Graph Memory Backends.
|
|
3
|
+
|
|
4
|
+
This package contains implementations of the GraphBackend interface
|
|
5
|
+
for various graph database systems.
|
|
6
|
+
|
|
7
|
+
Available backends:
|
|
8
|
+
- neo4j: Neo4j graph database backend
|
|
9
|
+
- memory: In-memory backend for testing and development
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from alma.graph.backends.memory import InMemoryBackend
|
|
13
|
+
from alma.graph.backends.neo4j import Neo4jBackend
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Neo4jBackend",
|
|
17
|
+
"InMemoryBackend",
|
|
18
|
+
]
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Graph Memory - In-Memory Backend.
|
|
3
|
+
|
|
4
|
+
In-memory implementation of the GraphBackend interface for testing and development.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, List, Optional, Set
|
|
9
|
+
|
|
10
|
+
from alma.graph.base import GraphBackend
|
|
11
|
+
from alma.graph.store import Entity, Relationship
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InMemoryBackend(GraphBackend):
|
|
17
|
+
"""
|
|
18
|
+
In-memory graph database backend.
|
|
19
|
+
|
|
20
|
+
Suitable for testing, development, and small-scale use cases
|
|
21
|
+
where persistence is not required.
|
|
22
|
+
|
|
23
|
+
No external dependencies required.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""Initialize empty in-memory storage."""
|
|
28
|
+
self._entities: Dict[str, Entity] = {}
|
|
29
|
+
self._relationships: Dict[str, Relationship] = {}
|
|
30
|
+
self._outgoing: Dict[str, List[str]] = {} # entity_id -> [rel_ids]
|
|
31
|
+
self._incoming: Dict[str, List[str]] = {} # entity_id -> [rel_ids]
|
|
32
|
+
|
|
33
|
+
def add_entity(self, entity: Entity) -> str:
|
|
34
|
+
"""Add or update an entity."""
|
|
35
|
+
self._entities[entity.id] = entity
|
|
36
|
+
if entity.id not in self._outgoing:
|
|
37
|
+
self._outgoing[entity.id] = []
|
|
38
|
+
if entity.id not in self._incoming:
|
|
39
|
+
self._incoming[entity.id] = []
|
|
40
|
+
return entity.id
|
|
41
|
+
|
|
42
|
+
def add_relationship(self, relationship: Relationship) -> str:
|
|
43
|
+
"""Add or update a relationship."""
|
|
44
|
+
self._relationships[relationship.id] = relationship
|
|
45
|
+
|
|
46
|
+
if relationship.source_id not in self._outgoing:
|
|
47
|
+
self._outgoing[relationship.source_id] = []
|
|
48
|
+
if relationship.id not in self._outgoing[relationship.source_id]:
|
|
49
|
+
self._outgoing[relationship.source_id].append(relationship.id)
|
|
50
|
+
|
|
51
|
+
if relationship.target_id not in self._incoming:
|
|
52
|
+
self._incoming[relationship.target_id] = []
|
|
53
|
+
if relationship.id not in self._incoming[relationship.target_id]:
|
|
54
|
+
self._incoming[relationship.target_id].append(relationship.id)
|
|
55
|
+
|
|
56
|
+
return relationship.id
|
|
57
|
+
|
|
58
|
+
def get_entity(self, entity_id: str) -> Optional[Entity]:
|
|
59
|
+
"""Get an entity by ID."""
|
|
60
|
+
return self._entities.get(entity_id)
|
|
61
|
+
|
|
62
|
+
def get_entities(
|
|
63
|
+
self,
|
|
64
|
+
entity_type: Optional[str] = None,
|
|
65
|
+
project_id: Optional[str] = None,
|
|
66
|
+
agent: Optional[str] = None,
|
|
67
|
+
limit: int = 100,
|
|
68
|
+
) -> List[Entity]:
|
|
69
|
+
"""Get entities with optional filtering."""
|
|
70
|
+
results = []
|
|
71
|
+
for entity in self._entities.values():
|
|
72
|
+
if entity_type and entity.entity_type != entity_type:
|
|
73
|
+
continue
|
|
74
|
+
if project_id and entity.properties.get("project_id") != project_id:
|
|
75
|
+
continue
|
|
76
|
+
if agent and entity.properties.get("agent") != agent:
|
|
77
|
+
continue
|
|
78
|
+
results.append(entity)
|
|
79
|
+
if len(results) >= limit:
|
|
80
|
+
break
|
|
81
|
+
return results
|
|
82
|
+
|
|
83
|
+
def get_relationships(self, entity_id: str) -> List[Relationship]:
|
|
84
|
+
"""Get all relationships for an entity (both directions)."""
|
|
85
|
+
rel_ids: Set[str] = set()
|
|
86
|
+
rel_ids.update(self._outgoing.get(entity_id, []))
|
|
87
|
+
rel_ids.update(self._incoming.get(entity_id, []))
|
|
88
|
+
|
|
89
|
+
return [
|
|
90
|
+
self._relationships[rid] for rid in rel_ids if rid in self._relationships
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
def search_entities(
|
|
94
|
+
self,
|
|
95
|
+
query: str,
|
|
96
|
+
embedding: Optional[List[float]] = None,
|
|
97
|
+
top_k: int = 10,
|
|
98
|
+
) -> List[Entity]:
|
|
99
|
+
"""
|
|
100
|
+
Search for entities by name.
|
|
101
|
+
|
|
102
|
+
Note: Vector similarity search is not implemented for in-memory backend.
|
|
103
|
+
Falls back to case-insensitive text search.
|
|
104
|
+
"""
|
|
105
|
+
query_lower = query.lower()
|
|
106
|
+
results = []
|
|
107
|
+
for entity in self._entities.values():
|
|
108
|
+
if query_lower in entity.name.lower():
|
|
109
|
+
results.append(entity)
|
|
110
|
+
if len(results) >= top_k:
|
|
111
|
+
break
|
|
112
|
+
return results
|
|
113
|
+
|
|
114
|
+
def delete_entity(self, entity_id: str) -> bool:
|
|
115
|
+
"""Delete an entity and all its relationships."""
|
|
116
|
+
if entity_id not in self._entities:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
# Delete outgoing relationships
|
|
120
|
+
for rel_id in list(self._outgoing.get(entity_id, [])):
|
|
121
|
+
if rel_id in self._relationships:
|
|
122
|
+
rel = self._relationships[rel_id]
|
|
123
|
+
# Remove from target's incoming
|
|
124
|
+
if rel.target_id in self._incoming:
|
|
125
|
+
if rel_id in self._incoming[rel.target_id]:
|
|
126
|
+
self._incoming[rel.target_id].remove(rel_id)
|
|
127
|
+
del self._relationships[rel_id]
|
|
128
|
+
|
|
129
|
+
# Delete incoming relationships
|
|
130
|
+
for rel_id in list(self._incoming.get(entity_id, [])):
|
|
131
|
+
if rel_id in self._relationships:
|
|
132
|
+
rel = self._relationships[rel_id]
|
|
133
|
+
# Remove from source's outgoing
|
|
134
|
+
if rel.source_id in self._outgoing:
|
|
135
|
+
if rel_id in self._outgoing[rel.source_id]:
|
|
136
|
+
self._outgoing[rel.source_id].remove(rel_id)
|
|
137
|
+
del self._relationships[rel_id]
|
|
138
|
+
|
|
139
|
+
# Delete entity
|
|
140
|
+
del self._entities[entity_id]
|
|
141
|
+
self._outgoing.pop(entity_id, None)
|
|
142
|
+
self._incoming.pop(entity_id, None)
|
|
143
|
+
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
def delete_relationship(self, relationship_id: str) -> bool:
|
|
147
|
+
"""Delete a specific relationship by ID."""
|
|
148
|
+
if relationship_id not in self._relationships:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
rel = self._relationships[relationship_id]
|
|
152
|
+
|
|
153
|
+
# Remove from source's outgoing
|
|
154
|
+
if rel.source_id in self._outgoing:
|
|
155
|
+
if relationship_id in self._outgoing[rel.source_id]:
|
|
156
|
+
self._outgoing[rel.source_id].remove(relationship_id)
|
|
157
|
+
|
|
158
|
+
# Remove from target's incoming
|
|
159
|
+
if rel.target_id in self._incoming:
|
|
160
|
+
if relationship_id in self._incoming[rel.target_id]:
|
|
161
|
+
self._incoming[rel.target_id].remove(relationship_id)
|
|
162
|
+
|
|
163
|
+
del self._relationships[relationship_id]
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
def close(self) -> None:
|
|
167
|
+
"""Clear all data (no-op for in-memory backend)."""
|
|
168
|
+
# In-memory backend doesn't need explicit cleanup
|
|
169
|
+
# but we can optionally clear data
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
def clear(self) -> None:
|
|
173
|
+
"""Clear all stored data."""
|
|
174
|
+
self._entities.clear()
|
|
175
|
+
self._relationships.clear()
|
|
176
|
+
self._outgoing.clear()
|
|
177
|
+
self._incoming.clear()
|
|
178
|
+
|
|
179
|
+
# Additional methods for compatibility with existing GraphStore API
|
|
180
|
+
|
|
181
|
+
def find_entities(
|
|
182
|
+
self,
|
|
183
|
+
name: Optional[str] = None,
|
|
184
|
+
entity_type: Optional[str] = None,
|
|
185
|
+
limit: int = 10,
|
|
186
|
+
) -> List[Entity]:
|
|
187
|
+
"""
|
|
188
|
+
Find entities by name or type.
|
|
189
|
+
|
|
190
|
+
This method provides compatibility with the existing GraphStore API.
|
|
191
|
+
"""
|
|
192
|
+
results = []
|
|
193
|
+
for entity in self._entities.values():
|
|
194
|
+
if name and name.lower() not in entity.name.lower():
|
|
195
|
+
continue
|
|
196
|
+
if entity_type and entity.entity_type != entity_type:
|
|
197
|
+
continue
|
|
198
|
+
results.append(entity)
|
|
199
|
+
if len(results) >= limit:
|
|
200
|
+
break
|
|
201
|
+
return results
|
|
202
|
+
|
|
203
|
+
def get_relationships_directional(
|
|
204
|
+
self,
|
|
205
|
+
entity_id: str,
|
|
206
|
+
direction: str = "both",
|
|
207
|
+
relation_type: Optional[str] = None,
|
|
208
|
+
) -> List[Relationship]:
|
|
209
|
+
"""
|
|
210
|
+
Get relationships for an entity with direction control.
|
|
211
|
+
|
|
212
|
+
This method provides compatibility with the existing GraphStore API.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
entity_id: The entity ID.
|
|
216
|
+
direction: "outgoing", "incoming", or "both".
|
|
217
|
+
relation_type: Optional filter by relationship type.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List of matching relationships.
|
|
221
|
+
"""
|
|
222
|
+
rel_ids: Set[str] = set()
|
|
223
|
+
|
|
224
|
+
if direction in ("outgoing", "both"):
|
|
225
|
+
rel_ids.update(self._outgoing.get(entity_id, []))
|
|
226
|
+
if direction in ("incoming", "both"):
|
|
227
|
+
rel_ids.update(self._incoming.get(entity_id, []))
|
|
228
|
+
|
|
229
|
+
results = []
|
|
230
|
+
for rel_id in rel_ids:
|
|
231
|
+
rel = self._relationships.get(rel_id)
|
|
232
|
+
if rel:
|
|
233
|
+
if relation_type and rel.relation_type != relation_type:
|
|
234
|
+
continue
|
|
235
|
+
results.append(rel)
|
|
236
|
+
return results
|