genxai-framework 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.
- cli/__init__.py +3 -0
- cli/commands/__init__.py +6 -0
- cli/commands/approval.py +85 -0
- cli/commands/audit.py +127 -0
- cli/commands/metrics.py +25 -0
- cli/commands/tool.py +389 -0
- cli/main.py +32 -0
- genxai/__init__.py +81 -0
- genxai/api/__init__.py +5 -0
- genxai/api/app.py +21 -0
- genxai/config/__init__.py +5 -0
- genxai/config/settings.py +37 -0
- genxai/connectors/__init__.py +19 -0
- genxai/connectors/base.py +122 -0
- genxai/connectors/kafka.py +92 -0
- genxai/connectors/postgres_cdc.py +95 -0
- genxai/connectors/registry.py +44 -0
- genxai/connectors/sqs.py +94 -0
- genxai/connectors/webhook.py +73 -0
- genxai/core/__init__.py +37 -0
- genxai/core/agent/__init__.py +32 -0
- genxai/core/agent/base.py +206 -0
- genxai/core/agent/config_io.py +59 -0
- genxai/core/agent/registry.py +98 -0
- genxai/core/agent/runtime.py +970 -0
- genxai/core/communication/__init__.py +6 -0
- genxai/core/communication/collaboration.py +44 -0
- genxai/core/communication/message_bus.py +192 -0
- genxai/core/communication/protocols.py +35 -0
- genxai/core/execution/__init__.py +22 -0
- genxai/core/execution/metadata.py +181 -0
- genxai/core/execution/queue.py +201 -0
- genxai/core/graph/__init__.py +30 -0
- genxai/core/graph/checkpoints.py +77 -0
- genxai/core/graph/edges.py +131 -0
- genxai/core/graph/engine.py +813 -0
- genxai/core/graph/executor.py +516 -0
- genxai/core/graph/nodes.py +161 -0
- genxai/core/graph/trigger_runner.py +40 -0
- genxai/core/memory/__init__.py +19 -0
- genxai/core/memory/base.py +72 -0
- genxai/core/memory/embedding.py +327 -0
- genxai/core/memory/episodic.py +448 -0
- genxai/core/memory/long_term.py +467 -0
- genxai/core/memory/manager.py +543 -0
- genxai/core/memory/persistence.py +297 -0
- genxai/core/memory/procedural.py +461 -0
- genxai/core/memory/semantic.py +526 -0
- genxai/core/memory/shared.py +62 -0
- genxai/core/memory/short_term.py +303 -0
- genxai/core/memory/vector_store.py +508 -0
- genxai/core/memory/working.py +211 -0
- genxai/core/state/__init__.py +6 -0
- genxai/core/state/manager.py +293 -0
- genxai/core/state/schema.py +115 -0
- genxai/llm/__init__.py +14 -0
- genxai/llm/base.py +150 -0
- genxai/llm/factory.py +329 -0
- genxai/llm/providers/__init__.py +1 -0
- genxai/llm/providers/anthropic.py +249 -0
- genxai/llm/providers/cohere.py +274 -0
- genxai/llm/providers/google.py +334 -0
- genxai/llm/providers/ollama.py +147 -0
- genxai/llm/providers/openai.py +257 -0
- genxai/llm/routing.py +83 -0
- genxai/observability/__init__.py +6 -0
- genxai/observability/logging.py +327 -0
- genxai/observability/metrics.py +494 -0
- genxai/observability/tracing.py +372 -0
- genxai/performance/__init__.py +39 -0
- genxai/performance/cache.py +256 -0
- genxai/performance/pooling.py +289 -0
- genxai/security/audit.py +304 -0
- genxai/security/auth.py +315 -0
- genxai/security/cost_control.py +528 -0
- genxai/security/default_policies.py +44 -0
- genxai/security/jwt.py +142 -0
- genxai/security/oauth.py +226 -0
- genxai/security/pii.py +366 -0
- genxai/security/policy_engine.py +82 -0
- genxai/security/rate_limit.py +341 -0
- genxai/security/rbac.py +247 -0
- genxai/security/validation.py +218 -0
- genxai/tools/__init__.py +21 -0
- genxai/tools/base.py +383 -0
- genxai/tools/builtin/__init__.py +131 -0
- genxai/tools/builtin/communication/__init__.py +15 -0
- genxai/tools/builtin/communication/email_sender.py +159 -0
- genxai/tools/builtin/communication/notification_manager.py +167 -0
- genxai/tools/builtin/communication/slack_notifier.py +118 -0
- genxai/tools/builtin/communication/sms_sender.py +118 -0
- genxai/tools/builtin/communication/webhook_caller.py +136 -0
- genxai/tools/builtin/computation/__init__.py +15 -0
- genxai/tools/builtin/computation/calculator.py +101 -0
- genxai/tools/builtin/computation/code_executor.py +183 -0
- genxai/tools/builtin/computation/data_validator.py +259 -0
- genxai/tools/builtin/computation/hash_generator.py +129 -0
- genxai/tools/builtin/computation/regex_matcher.py +201 -0
- genxai/tools/builtin/data/__init__.py +15 -0
- genxai/tools/builtin/data/csv_processor.py +213 -0
- genxai/tools/builtin/data/data_transformer.py +299 -0
- genxai/tools/builtin/data/json_processor.py +233 -0
- genxai/tools/builtin/data/text_analyzer.py +288 -0
- genxai/tools/builtin/data/xml_processor.py +175 -0
- genxai/tools/builtin/database/__init__.py +15 -0
- genxai/tools/builtin/database/database_inspector.py +157 -0
- genxai/tools/builtin/database/mongodb_query.py +196 -0
- genxai/tools/builtin/database/redis_cache.py +167 -0
- genxai/tools/builtin/database/sql_query.py +145 -0
- genxai/tools/builtin/database/vector_search.py +163 -0
- genxai/tools/builtin/file/__init__.py +17 -0
- genxai/tools/builtin/file/directory_scanner.py +214 -0
- genxai/tools/builtin/file/file_compressor.py +237 -0
- genxai/tools/builtin/file/file_reader.py +102 -0
- genxai/tools/builtin/file/file_writer.py +122 -0
- genxai/tools/builtin/file/image_processor.py +186 -0
- genxai/tools/builtin/file/pdf_parser.py +144 -0
- genxai/tools/builtin/test/__init__.py +15 -0
- genxai/tools/builtin/test/async_simulator.py +62 -0
- genxai/tools/builtin/test/data_transformer.py +99 -0
- genxai/tools/builtin/test/error_generator.py +82 -0
- genxai/tools/builtin/test/simple_math.py +94 -0
- genxai/tools/builtin/test/string_processor.py +72 -0
- genxai/tools/builtin/web/__init__.py +15 -0
- genxai/tools/builtin/web/api_caller.py +161 -0
- genxai/tools/builtin/web/html_parser.py +330 -0
- genxai/tools/builtin/web/http_client.py +187 -0
- genxai/tools/builtin/web/url_validator.py +162 -0
- genxai/tools/builtin/web/web_scraper.py +170 -0
- genxai/tools/custom/my_test_tool_2.py +9 -0
- genxai/tools/dynamic.py +105 -0
- genxai/tools/mcp_server.py +167 -0
- genxai/tools/persistence/__init__.py +6 -0
- genxai/tools/persistence/models.py +55 -0
- genxai/tools/persistence/service.py +322 -0
- genxai/tools/registry.py +227 -0
- genxai/tools/security/__init__.py +11 -0
- genxai/tools/security/limits.py +214 -0
- genxai/tools/security/policy.py +20 -0
- genxai/tools/security/sandbox.py +248 -0
- genxai/tools/templates.py +435 -0
- genxai/triggers/__init__.py +19 -0
- genxai/triggers/base.py +104 -0
- genxai/triggers/file_watcher.py +75 -0
- genxai/triggers/queue.py +68 -0
- genxai/triggers/registry.py +82 -0
- genxai/triggers/schedule.py +66 -0
- genxai/triggers/webhook.py +68 -0
- genxai/utils/__init__.py +1 -0
- genxai/utils/tokens.py +295 -0
- genxai_framework-0.1.0.dist-info/METADATA +495 -0
- genxai_framework-0.1.0.dist-info/RECORD +156 -0
- genxai_framework-0.1.0.dist-info/WHEEL +5 -0
- genxai_framework-0.1.0.dist-info/entry_points.txt +2 -0
- genxai_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- genxai_framework-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"""Semantic memory implementation for storing facts and knowledge."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from genxai.core.memory.persistence import (
|
|
9
|
+
JsonMemoryStore,
|
|
10
|
+
MemoryPersistenceConfig,
|
|
11
|
+
SqliteMemoryStore,
|
|
12
|
+
create_memory_store,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Fact:
|
|
19
|
+
"""Represents a single fact in semantic memory."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
id: str,
|
|
24
|
+
subject: str,
|
|
25
|
+
predicate: str,
|
|
26
|
+
object: str,
|
|
27
|
+
confidence: float = 1.0,
|
|
28
|
+
source: Optional[str] = None,
|
|
29
|
+
timestamp: Optional[datetime] = None,
|
|
30
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Initialize fact.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
id: Unique fact ID
|
|
36
|
+
subject: Subject entity
|
|
37
|
+
predicate: Relationship/property
|
|
38
|
+
object: Object entity/value
|
|
39
|
+
confidence: Confidence score (0.0 to 1.0)
|
|
40
|
+
source: Source of the fact
|
|
41
|
+
timestamp: When fact was learned
|
|
42
|
+
metadata: Additional metadata
|
|
43
|
+
"""
|
|
44
|
+
self.id = id
|
|
45
|
+
self.subject = subject
|
|
46
|
+
self.predicate = predicate
|
|
47
|
+
self.object = object
|
|
48
|
+
self.confidence = confidence
|
|
49
|
+
self.source = source
|
|
50
|
+
self.timestamp = timestamp or datetime.now()
|
|
51
|
+
self.metadata = metadata or {}
|
|
52
|
+
|
|
53
|
+
def to_triple(self) -> Tuple[str, str, str]:
|
|
54
|
+
"""Convert to RDF-style triple."""
|
|
55
|
+
return (self.subject, self.predicate, self.object)
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
58
|
+
"""Convert to dictionary."""
|
|
59
|
+
return {
|
|
60
|
+
"id": self.id,
|
|
61
|
+
"subject": self.subject,
|
|
62
|
+
"predicate": self.predicate,
|
|
63
|
+
"object": self.object,
|
|
64
|
+
"confidence": self.confidence,
|
|
65
|
+
"source": self.source,
|
|
66
|
+
"timestamp": self.timestamp.isoformat(),
|
|
67
|
+
"metadata": self.metadata,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Fact":
|
|
72
|
+
"""Create fact from dictionary."""
|
|
73
|
+
return cls(
|
|
74
|
+
id=data["id"],
|
|
75
|
+
subject=data["subject"],
|
|
76
|
+
predicate=data["predicate"],
|
|
77
|
+
object=data["object"],
|
|
78
|
+
confidence=data.get("confidence", 1.0),
|
|
79
|
+
source=data.get("source"),
|
|
80
|
+
timestamp=datetime.fromisoformat(data["timestamp"]) if data.get("timestamp") else None,
|
|
81
|
+
metadata=data.get("metadata", {}),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
"""String representation."""
|
|
86
|
+
return f"Fact({self.subject} {self.predicate} {self.object})"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SemanticMemory:
|
|
90
|
+
"""Semantic memory for storing facts and knowledge.
|
|
91
|
+
|
|
92
|
+
Stores structured knowledge as subject-predicate-object triples:
|
|
93
|
+
- Facts about entities
|
|
94
|
+
- Relationships between entities
|
|
95
|
+
- Properties and attributes
|
|
96
|
+
- General knowledge
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
graph_db: Optional[Any] = None,
|
|
102
|
+
persistence: Optional[MemoryPersistenceConfig] = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Initialize semantic memory.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
graph_db: Graph database client (Neo4j, etc.)
|
|
108
|
+
"""
|
|
109
|
+
self._graph_db = graph_db
|
|
110
|
+
self._use_graph = graph_db is not None
|
|
111
|
+
self._persistence = persistence
|
|
112
|
+
if persistence:
|
|
113
|
+
self._store = create_memory_store(persistence)
|
|
114
|
+
else:
|
|
115
|
+
self._store = None
|
|
116
|
+
|
|
117
|
+
# Fallback to in-memory storage
|
|
118
|
+
self._facts: Dict[str, Fact] = {}
|
|
119
|
+
self._subject_index: Dict[str, Set[str]] = {} # subject -> fact_ids
|
|
120
|
+
self._predicate_index: Dict[str, Set[str]] = {} # predicate -> fact_ids
|
|
121
|
+
self._object_index: Dict[str, Set[str]] = {} # object -> fact_ids
|
|
122
|
+
|
|
123
|
+
if self._use_graph:
|
|
124
|
+
logger.info("Initialized semantic memory with graph database")
|
|
125
|
+
else:
|
|
126
|
+
logger.warning(
|
|
127
|
+
"Graph database not provided. Using in-memory storage. "
|
|
128
|
+
"Facts will not persist across restarts."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if self._store and self._persistence and self._persistence.enabled:
|
|
132
|
+
self._load_from_disk()
|
|
133
|
+
|
|
134
|
+
async def store_fact(
|
|
135
|
+
self,
|
|
136
|
+
subject: str,
|
|
137
|
+
predicate: str,
|
|
138
|
+
object: str,
|
|
139
|
+
confidence: float = 1.0,
|
|
140
|
+
source: Optional[str] = None,
|
|
141
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
142
|
+
) -> Fact:
|
|
143
|
+
"""Store a new fact.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
subject: Subject entity
|
|
147
|
+
predicate: Relationship/property
|
|
148
|
+
object: Object entity/value
|
|
149
|
+
confidence: Confidence score
|
|
150
|
+
source: Source of the fact
|
|
151
|
+
metadata: Additional metadata
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Created fact
|
|
155
|
+
"""
|
|
156
|
+
# Check if fact already exists
|
|
157
|
+
existing = await self._find_exact_fact(subject, predicate, object)
|
|
158
|
+
if existing:
|
|
159
|
+
# Update confidence if higher
|
|
160
|
+
if confidence > existing.confidence:
|
|
161
|
+
existing.confidence = confidence
|
|
162
|
+
existing.timestamp = datetime.now()
|
|
163
|
+
logger.debug(f"Updated fact confidence: {existing}")
|
|
164
|
+
return existing
|
|
165
|
+
|
|
166
|
+
# Create new fact
|
|
167
|
+
fact = Fact(
|
|
168
|
+
id=str(uuid.uuid4()),
|
|
169
|
+
subject=subject,
|
|
170
|
+
predicate=predicate,
|
|
171
|
+
object=object,
|
|
172
|
+
confidence=confidence,
|
|
173
|
+
source=source,
|
|
174
|
+
metadata=metadata,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if self._use_graph:
|
|
178
|
+
await self._store_in_graph(fact)
|
|
179
|
+
else:
|
|
180
|
+
# In-memory storage
|
|
181
|
+
self._facts[fact.id] = fact
|
|
182
|
+
|
|
183
|
+
# Update indexes
|
|
184
|
+
if subject not in self._subject_index:
|
|
185
|
+
self._subject_index[subject] = set()
|
|
186
|
+
self._subject_index[subject].add(fact.id)
|
|
187
|
+
|
|
188
|
+
if predicate not in self._predicate_index:
|
|
189
|
+
self._predicate_index[predicate] = set()
|
|
190
|
+
self._predicate_index[predicate].add(fact.id)
|
|
191
|
+
|
|
192
|
+
if object not in self._object_index:
|
|
193
|
+
self._object_index[object] = set()
|
|
194
|
+
self._object_index[object].add(fact.id)
|
|
195
|
+
|
|
196
|
+
self._persist()
|
|
197
|
+
|
|
198
|
+
logger.debug(f"Stored fact: {fact}")
|
|
199
|
+
return fact
|
|
200
|
+
|
|
201
|
+
async def retrieve_by_subject(
|
|
202
|
+
self,
|
|
203
|
+
subject: str,
|
|
204
|
+
predicate: Optional[str] = None,
|
|
205
|
+
) -> List[Fact]:
|
|
206
|
+
"""Retrieve facts about a subject.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
subject: Subject entity
|
|
210
|
+
predicate: Optional predicate filter
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
List of facts
|
|
214
|
+
"""
|
|
215
|
+
if self._use_graph:
|
|
216
|
+
return await self._retrieve_by_subject_from_graph(subject, predicate)
|
|
217
|
+
|
|
218
|
+
# In-memory retrieval
|
|
219
|
+
fact_ids = self._subject_index.get(subject, set())
|
|
220
|
+
facts = [self._facts[fid] for fid in fact_ids]
|
|
221
|
+
|
|
222
|
+
if predicate:
|
|
223
|
+
facts = [f for f in facts if f.predicate == predicate]
|
|
224
|
+
|
|
225
|
+
return facts
|
|
226
|
+
|
|
227
|
+
async def retrieve_by_predicate(
|
|
228
|
+
self,
|
|
229
|
+
predicate: str,
|
|
230
|
+
subject: Optional[str] = None,
|
|
231
|
+
object: Optional[str] = None,
|
|
232
|
+
) -> List[Fact]:
|
|
233
|
+
"""Retrieve facts with a specific predicate.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
predicate: Predicate/relationship
|
|
237
|
+
subject: Optional subject filter
|
|
238
|
+
object: Optional object filter
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
List of facts
|
|
242
|
+
"""
|
|
243
|
+
if self._use_graph:
|
|
244
|
+
return await self._retrieve_by_predicate_from_graph(
|
|
245
|
+
predicate, subject, object
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# In-memory retrieval
|
|
249
|
+
fact_ids = self._predicate_index.get(predicate, set())
|
|
250
|
+
facts = [self._facts[fid] for fid in fact_ids]
|
|
251
|
+
|
|
252
|
+
if subject:
|
|
253
|
+
facts = [f for f in facts if f.subject == subject]
|
|
254
|
+
if object:
|
|
255
|
+
facts = [f for f in facts if f.object == object]
|
|
256
|
+
|
|
257
|
+
return facts
|
|
258
|
+
|
|
259
|
+
async def retrieve_by_object(
|
|
260
|
+
self,
|
|
261
|
+
object: str,
|
|
262
|
+
predicate: Optional[str] = None,
|
|
263
|
+
) -> List[Fact]:
|
|
264
|
+
"""Retrieve facts with a specific object.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
object: Object entity/value
|
|
268
|
+
predicate: Optional predicate filter
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List of facts
|
|
272
|
+
"""
|
|
273
|
+
if self._use_graph:
|
|
274
|
+
return await self._retrieve_by_object_from_graph(object, predicate)
|
|
275
|
+
|
|
276
|
+
# In-memory retrieval
|
|
277
|
+
fact_ids = self._object_index.get(object, set())
|
|
278
|
+
facts = [self._facts[fid] for fid in fact_ids]
|
|
279
|
+
|
|
280
|
+
if predicate:
|
|
281
|
+
facts = [f for f in facts if f.predicate == predicate]
|
|
282
|
+
|
|
283
|
+
return facts
|
|
284
|
+
|
|
285
|
+
async def query(
|
|
286
|
+
self,
|
|
287
|
+
subject: Optional[str] = None,
|
|
288
|
+
predicate: Optional[str] = None,
|
|
289
|
+
object: Optional[str] = None,
|
|
290
|
+
min_confidence: float = 0.0,
|
|
291
|
+
) -> List[Fact]:
|
|
292
|
+
"""Query facts with flexible filters.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
subject: Optional subject filter
|
|
296
|
+
predicate: Optional predicate filter
|
|
297
|
+
object: Optional object filter
|
|
298
|
+
min_confidence: Minimum confidence threshold
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
List of matching facts
|
|
302
|
+
"""
|
|
303
|
+
facts = list(self._facts.values())
|
|
304
|
+
|
|
305
|
+
# Apply filters
|
|
306
|
+
if subject:
|
|
307
|
+
facts = [f for f in facts if f.subject == subject]
|
|
308
|
+
if predicate:
|
|
309
|
+
facts = [f for f in facts if f.predicate == predicate]
|
|
310
|
+
if object:
|
|
311
|
+
facts = [f for f in facts if f.object == object]
|
|
312
|
+
if min_confidence > 0.0:
|
|
313
|
+
facts = [f for f in facts if f.confidence >= min_confidence]
|
|
314
|
+
|
|
315
|
+
return facts
|
|
316
|
+
|
|
317
|
+
async def get_related_entities(
|
|
318
|
+
self,
|
|
319
|
+
entity: str,
|
|
320
|
+
max_depth: int = 2,
|
|
321
|
+
) -> Set[str]:
|
|
322
|
+
"""Get entities related to a given entity.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
entity: Starting entity
|
|
326
|
+
max_depth: Maximum relationship depth
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Set of related entities
|
|
330
|
+
"""
|
|
331
|
+
related = set()
|
|
332
|
+
to_explore = {entity}
|
|
333
|
+
explored = set()
|
|
334
|
+
|
|
335
|
+
for _ in range(max_depth):
|
|
336
|
+
if not to_explore:
|
|
337
|
+
break
|
|
338
|
+
|
|
339
|
+
current = to_explore.pop()
|
|
340
|
+
explored.add(current)
|
|
341
|
+
|
|
342
|
+
# Get facts where entity is subject
|
|
343
|
+
subject_facts = await self.retrieve_by_subject(current)
|
|
344
|
+
for fact in subject_facts:
|
|
345
|
+
related.add(fact.object)
|
|
346
|
+
if fact.object not in explored:
|
|
347
|
+
to_explore.add(fact.object)
|
|
348
|
+
|
|
349
|
+
# Get facts where entity is object
|
|
350
|
+
object_facts = await self.retrieve_by_object(current)
|
|
351
|
+
for fact in object_facts:
|
|
352
|
+
related.add(fact.subject)
|
|
353
|
+
if fact.subject not in explored:
|
|
354
|
+
to_explore.add(fact.subject)
|
|
355
|
+
|
|
356
|
+
return related - {entity}
|
|
357
|
+
|
|
358
|
+
async def get_entity_properties(
|
|
359
|
+
self,
|
|
360
|
+
entity: str,
|
|
361
|
+
) -> Dict[str, Any]:
|
|
362
|
+
"""Get all properties of an entity.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
entity: Entity to get properties for
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Dictionary of properties
|
|
369
|
+
"""
|
|
370
|
+
facts = await self.retrieve_by_subject(entity)
|
|
371
|
+
|
|
372
|
+
properties = {}
|
|
373
|
+
for fact in facts:
|
|
374
|
+
if fact.predicate not in properties:
|
|
375
|
+
properties[fact.predicate] = []
|
|
376
|
+
properties[fact.predicate].append({
|
|
377
|
+
"value": fact.object,
|
|
378
|
+
"confidence": fact.confidence,
|
|
379
|
+
"source": fact.source,
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
return properties
|
|
383
|
+
|
|
384
|
+
async def delete_fact(self, fact_id: str) -> bool:
|
|
385
|
+
"""Delete a fact by ID.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
fact_id: Fact ID
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
True if deleted, False if not found
|
|
392
|
+
"""
|
|
393
|
+
if fact_id not in self._facts:
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
fact = self._facts[fact_id]
|
|
397
|
+
|
|
398
|
+
# Remove from indexes
|
|
399
|
+
self._subject_index[fact.subject].discard(fact_id)
|
|
400
|
+
self._predicate_index[fact.predicate].discard(fact_id)
|
|
401
|
+
self._object_index[fact.object].discard(fact_id)
|
|
402
|
+
|
|
403
|
+
# Remove fact
|
|
404
|
+
del self._facts[fact_id]
|
|
405
|
+
|
|
406
|
+
self._persist()
|
|
407
|
+
|
|
408
|
+
logger.debug(f"Deleted fact: {fact}")
|
|
409
|
+
return True
|
|
410
|
+
|
|
411
|
+
async def clear(self) -> None:
|
|
412
|
+
"""Clear all facts."""
|
|
413
|
+
self._facts.clear()
|
|
414
|
+
self._subject_index.clear()
|
|
415
|
+
self._predicate_index.clear()
|
|
416
|
+
self._object_index.clear()
|
|
417
|
+
logger.info("Cleared all facts")
|
|
418
|
+
|
|
419
|
+
self._persist()
|
|
420
|
+
|
|
421
|
+
async def get_stats(self) -> Dict[str, Any]:
|
|
422
|
+
"""Get semantic memory statistics.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Statistics dictionary
|
|
426
|
+
"""
|
|
427
|
+
if not self._facts:
|
|
428
|
+
return {
|
|
429
|
+
"total_facts": 0,
|
|
430
|
+
"backend": "graph" if self._use_graph else "in-memory",
|
|
431
|
+
"persistence": bool(self._persistence and self._persistence.enabled),
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
facts = list(self._facts.values())
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
"total_facts": len(facts),
|
|
438
|
+
"unique_subjects": len(self._subject_index),
|
|
439
|
+
"unique_predicates": len(self._predicate_index),
|
|
440
|
+
"unique_objects": len(self._object_index),
|
|
441
|
+
"avg_confidence": sum(f.confidence for f in facts) / len(facts),
|
|
442
|
+
"oldest_fact": min(f.timestamp for f in facts).isoformat(),
|
|
443
|
+
"newest_fact": max(f.timestamp for f in facts).isoformat(),
|
|
444
|
+
"backend": "graph" if self._use_graph else "in-memory",
|
|
445
|
+
"persistence": bool(self._persistence and self._persistence.enabled),
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
def _persist(self) -> None:
|
|
449
|
+
if not self._store:
|
|
450
|
+
return
|
|
451
|
+
self._store.save_list("semantic_memory.json", [fact.to_dict() for fact in self._facts.values()])
|
|
452
|
+
|
|
453
|
+
def _load_from_disk(self) -> None:
|
|
454
|
+
if not self._store:
|
|
455
|
+
return
|
|
456
|
+
data = self._store.load_list("semantic_memory.json")
|
|
457
|
+
if not data:
|
|
458
|
+
return
|
|
459
|
+
self._facts = {}
|
|
460
|
+
self._subject_index = {}
|
|
461
|
+
self._predicate_index = {}
|
|
462
|
+
self._object_index = {}
|
|
463
|
+
for item in data:
|
|
464
|
+
fact = Fact.from_dict(item)
|
|
465
|
+
self._facts[fact.id] = fact
|
|
466
|
+
self._subject_index.setdefault(fact.subject, set()).add(fact.id)
|
|
467
|
+
self._predicate_index.setdefault(fact.predicate, set()).add(fact.id)
|
|
468
|
+
self._object_index.setdefault(fact.object, set()).add(fact.id)
|
|
469
|
+
|
|
470
|
+
async def _find_exact_fact(
|
|
471
|
+
self,
|
|
472
|
+
subject: str,
|
|
473
|
+
predicate: str,
|
|
474
|
+
object: str,
|
|
475
|
+
) -> Optional[Fact]:
|
|
476
|
+
"""Find exact matching fact."""
|
|
477
|
+
for fact in self._facts.values():
|
|
478
|
+
if (fact.subject == subject and
|
|
479
|
+
fact.predicate == predicate and
|
|
480
|
+
fact.object == object):
|
|
481
|
+
return fact
|
|
482
|
+
return None
|
|
483
|
+
|
|
484
|
+
async def _store_in_graph(self, fact: Fact) -> None:
|
|
485
|
+
"""Store fact in graph database (placeholder)."""
|
|
486
|
+
# TODO: Implement Neo4j storage
|
|
487
|
+
logger.warning("Graph database storage not yet implemented")
|
|
488
|
+
# Fallback to in-memory
|
|
489
|
+
self._facts[fact.id] = fact
|
|
490
|
+
|
|
491
|
+
async def _retrieve_by_subject_from_graph(
|
|
492
|
+
self,
|
|
493
|
+
subject: str,
|
|
494
|
+
predicate: Optional[str],
|
|
495
|
+
) -> List[Fact]:
|
|
496
|
+
"""Retrieve from graph database (placeholder)."""
|
|
497
|
+
# TODO: Implement Neo4j query
|
|
498
|
+
return await self.retrieve_by_subject(subject, predicate)
|
|
499
|
+
|
|
500
|
+
async def _retrieve_by_predicate_from_graph(
|
|
501
|
+
self,
|
|
502
|
+
predicate: str,
|
|
503
|
+
subject: Optional[str],
|
|
504
|
+
object: Optional[str],
|
|
505
|
+
) -> List[Fact]:
|
|
506
|
+
"""Retrieve from graph database (placeholder)."""
|
|
507
|
+
# TODO: Implement Neo4j query
|
|
508
|
+
return await self.retrieve_by_predicate(predicate, subject, object)
|
|
509
|
+
|
|
510
|
+
async def _retrieve_by_object_from_graph(
|
|
511
|
+
self,
|
|
512
|
+
object: str,
|
|
513
|
+
predicate: Optional[str],
|
|
514
|
+
) -> List[Fact]:
|
|
515
|
+
"""Retrieve from graph database (placeholder)."""
|
|
516
|
+
# TODO: Implement Neo4j query
|
|
517
|
+
return await self.retrieve_by_object(object, predicate)
|
|
518
|
+
|
|
519
|
+
def __len__(self) -> int:
|
|
520
|
+
"""Get number of stored facts."""
|
|
521
|
+
return len(self._facts)
|
|
522
|
+
|
|
523
|
+
def __repr__(self) -> str:
|
|
524
|
+
"""String representation."""
|
|
525
|
+
backend = "Graph" if self._use_graph else "In-Memory"
|
|
526
|
+
return f"SemanticMemory(backend={backend}, facts={len(self._facts)})"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Shared memory bus for cross-agent collaboration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Dict, List, Optional, Callable, Awaitable
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from genxai.security.rbac import get_current_user, Permission
|
|
12
|
+
from genxai.security.policy_engine import get_policy_engine
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class SharedMemoryEntry:
|
|
19
|
+
key: str
|
|
20
|
+
value: Any
|
|
21
|
+
updated_at: datetime = field(default_factory=datetime.utcnow)
|
|
22
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SharedMemoryBus:
|
|
26
|
+
"""In-memory shared memory store with pub/sub hooks."""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._store: Dict[str, SharedMemoryEntry] = {}
|
|
30
|
+
self._subscribers: Dict[str, List[Callable[[SharedMemoryEntry], Awaitable[None]]]] = {}
|
|
31
|
+
self._lock = asyncio.Lock()
|
|
32
|
+
|
|
33
|
+
async def set(self, key: str, value: Any, metadata: Optional[Dict[str, Any]] = None) -> None:
|
|
34
|
+
user = get_current_user()
|
|
35
|
+
if user is not None:
|
|
36
|
+
get_policy_engine().check(user, f"memory:{key}", Permission.MEMORY_WRITE)
|
|
37
|
+
async with self._lock:
|
|
38
|
+
entry = SharedMemoryEntry(key=key, value=value, metadata=metadata or {})
|
|
39
|
+
self._store[key] = entry
|
|
40
|
+
await self._notify(key, entry)
|
|
41
|
+
|
|
42
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
43
|
+
user = get_current_user()
|
|
44
|
+
if user is not None:
|
|
45
|
+
get_policy_engine().check(user, f"memory:{key}", Permission.MEMORY_READ)
|
|
46
|
+
entry = self._store.get(key)
|
|
47
|
+
return entry.value if entry else default
|
|
48
|
+
|
|
49
|
+
def list_keys(self) -> List[str]:
|
|
50
|
+
return list(self._store.keys())
|
|
51
|
+
|
|
52
|
+
def subscribe(
|
|
53
|
+
self, key: str, callback: Callable[[SharedMemoryEntry], Awaitable[None]]
|
|
54
|
+
) -> None:
|
|
55
|
+
self._subscribers.setdefault(key, []).append(callback)
|
|
56
|
+
|
|
57
|
+
async def _notify(self, key: str, entry: SharedMemoryEntry) -> None:
|
|
58
|
+
for callback in self._subscribers.get(key, []):
|
|
59
|
+
try:
|
|
60
|
+
await callback(entry)
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
logger.error("Shared memory notify error for %s: %s", key, exc)
|