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/extraction.py
ADDED
|
@@ -0,0 +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)
|