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.
Files changed (77) hide show
  1. alma/__init__.py +99 -29
  2. alma/confidence/__init__.py +47 -0
  3. alma/confidence/engine.py +540 -0
  4. alma/confidence/types.py +351 -0
  5. alma/config/loader.py +3 -2
  6. alma/consolidation/__init__.py +23 -0
  7. alma/consolidation/engine.py +678 -0
  8. alma/consolidation/prompts.py +84 -0
  9. alma/core.py +15 -15
  10. alma/domains/__init__.py +6 -6
  11. alma/domains/factory.py +12 -9
  12. alma/domains/schemas.py +17 -3
  13. alma/domains/types.py +8 -4
  14. alma/events/__init__.py +75 -0
  15. alma/events/emitter.py +284 -0
  16. alma/events/storage_mixin.py +246 -0
  17. alma/events/types.py +126 -0
  18. alma/events/webhook.py +425 -0
  19. alma/exceptions.py +49 -0
  20. alma/extraction/__init__.py +31 -0
  21. alma/extraction/auto_learner.py +264 -0
  22. alma/extraction/extractor.py +420 -0
  23. alma/graph/__init__.py +81 -0
  24. alma/graph/backends/__init__.py +18 -0
  25. alma/graph/backends/memory.py +236 -0
  26. alma/graph/backends/neo4j.py +417 -0
  27. alma/graph/base.py +159 -0
  28. alma/graph/extraction.py +198 -0
  29. alma/graph/store.py +860 -0
  30. alma/harness/__init__.py +4 -4
  31. alma/harness/base.py +18 -9
  32. alma/harness/domains.py +27 -11
  33. alma/initializer/__init__.py +37 -0
  34. alma/initializer/initializer.py +418 -0
  35. alma/initializer/types.py +250 -0
  36. alma/integration/__init__.py +9 -9
  37. alma/integration/claude_agents.py +10 -10
  38. alma/integration/helena.py +32 -22
  39. alma/integration/victor.py +57 -33
  40. alma/learning/__init__.py +27 -27
  41. alma/learning/forgetting.py +198 -148
  42. alma/learning/heuristic_extractor.py +40 -24
  43. alma/learning/protocols.py +62 -14
  44. alma/learning/validation.py +7 -2
  45. alma/mcp/__init__.py +4 -4
  46. alma/mcp/__main__.py +2 -1
  47. alma/mcp/resources.py +17 -16
  48. alma/mcp/server.py +102 -44
  49. alma/mcp/tools.py +174 -37
  50. alma/progress/__init__.py +3 -3
  51. alma/progress/tracker.py +26 -20
  52. alma/progress/types.py +8 -12
  53. alma/py.typed +0 -0
  54. alma/retrieval/__init__.py +11 -11
  55. alma/retrieval/cache.py +20 -21
  56. alma/retrieval/embeddings.py +4 -4
  57. alma/retrieval/engine.py +114 -35
  58. alma/retrieval/scoring.py +73 -63
  59. alma/session/__init__.py +2 -2
  60. alma/session/manager.py +5 -5
  61. alma/session/types.py +5 -4
  62. alma/storage/__init__.py +41 -0
  63. alma/storage/azure_cosmos.py +107 -31
  64. alma/storage/base.py +157 -4
  65. alma/storage/chroma.py +1443 -0
  66. alma/storage/file_based.py +56 -20
  67. alma/storage/pinecone.py +1080 -0
  68. alma/storage/postgresql.py +1452 -0
  69. alma/storage/qdrant.py +1306 -0
  70. alma/storage/sqlite_local.py +376 -31
  71. alma/types.py +62 -14
  72. alma_memory-0.5.0.dist-info/METADATA +905 -0
  73. alma_memory-0.5.0.dist-info/RECORD +76 -0
  74. {alma_memory-0.3.0.dist-info → alma_memory-0.5.0.dist-info}/WHEEL +1 -1
  75. alma_memory-0.3.0.dist-info/METADATA +0 -438
  76. alma_memory-0.3.0.dist-info/RECORD +0 -46
  77. {alma_memory-0.3.0.dist-info → alma_memory-0.5.0.dist-info}/top_level.txt +0 -0
@@ -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)