clm-kernel 0.0.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.
- clm/__init__.py +230 -0
- clm/__main__.py +25 -0
- clm/cross_cutting/__init__.py +50 -0
- clm/cross_cutting/_test_runner.py +36 -0
- clm/cross_cutting/async_utils.py +25 -0
- clm/cross_cutting/cel_builder.py +123 -0
- clm/cross_cutting/cel_converter.py +697 -0
- clm/cross_cutting/cel_engine.py +252 -0
- clm/cross_cutting/cel_ops.py +72 -0
- clm/cross_cutting/cel_ops_db.py +157 -0
- clm/cross_cutting/cel_ops_files.py +99 -0
- clm/cross_cutting/cel_ops_state.py +71 -0
- clm/cross_cutting/cel_ops_strings.py +64 -0
- clm/cross_cutting/cel_resolver.py +63 -0
- clm/cross_cutting/cel_slices.py +56 -0
- clm/cross_cutting/cel_store.py +330 -0
- clm/cross_cutting/clm_logger.py +140 -0
- clm/cross_cutting/config/__init__.py +155 -0
- clm/cross_cutting/config/config_constants.py +392 -0
- clm/cross_cutting/config/env_parameters.py +90 -0
- clm/cross_cutting/config/logging.py +378 -0
- clm/cross_cutting/config/settings.py +504 -0
- clm/cross_cutting/context.py +5 -0
- clm/cross_cutting/domain_models.py +55 -0
- clm/cross_cutting/domain_types.py +61 -0
- clm/cross_cutting/effects.py +67 -0
- clm/cross_cutting/errors.py +153 -0
- clm/cross_cutting/governance.py +331 -0
- clm/cross_cutting/guards.py +41 -0
- clm/cross_cutting/mcard.py +321 -0
- clm/cross_cutting/native.py +185 -0
- clm/cross_cutting/observability.py +289 -0
- clm/cross_cutting/pcard.py +434 -0
- clm/cross_cutting/registry.py +71 -0
- clm/cross_cutting/resources.py +84 -0
- clm/cross_cutting/secrets.py +44 -0
- clm/cross_cutting/subprocess_utils.py +118 -0
- clm/cross_cutting/telemetry.py +164 -0
- clm/cross_cutting/testing/__init__.py +8 -0
- clm/cross_cutting/testing/comparator.py +187 -0
- clm/cross_cutting/testing/datasets.py +87 -0
- clm/cross_cutting/timeutil.py +20 -0
- clm/cross_cutting/types/new_type_check.py +0 -0
- clm/cross_cutting/utils/__init__.py +1 -0
- clm/cross_cutting/utils/url_safety.py +50 -0
- clm/cross_cutting/vcard.py +560 -0
- clm/cross_cutting/writer.py +118 -0
- clm/layer0/README.md +50 -0
- clm/layer0/__init__.py +223 -0
- clm/layer0/algebra.py +60 -0
- clm/layer0/algebra_baldwin.py +133 -0
- clm/layer0/algebra_baldwin_generators.py +36 -0
- clm/layer0/algebra_equivalence.py +262 -0
- clm/layer0/algebra_ingest.py +306 -0
- clm/layer0/algebra_manifest_io.py +156 -0
- clm/layer0/assembler.py +481 -0
- clm/layer0/cel/__init__.py +23 -0
- clm/layer0/cel_eval.py +491 -0
- clm/layer0/codec.py +106 -0
- clm/layer0/context.py +122 -0
- clm/layer0/db.py +160 -0
- clm/layer0/exceptions.py +73 -0
- clm/layer0/fibration.py +106 -0
- clm/layer0/gates.py +50 -0
- clm/layer0/hash.py +80 -0
- clm/layer0/loader.py +826 -0
- clm/layer0/math_utils.py +163 -0
- clm/layer0/mcard.py +96 -0
- clm/layer0/model/__init__.py +0 -0
- clm/layer0/model/action.py +223 -0
- clm/layer0/model/card.py +280 -0
- clm/layer0/model/card_triad.py +427 -0
- clm/layer0/model/compatibility.py +60 -0
- clm/layer0/model/dictionary.py +46 -0
- clm/layer0/model/dots.py +331 -0
- clm/layer0/model/event_producer.py +88 -0
- clm/layer0/model/g_time.py +89 -0
- clm/layer0/model/handle.py +163 -0
- clm/layer0/model/hash/__init__.py +6 -0
- clm/layer0/model/hash/algorithms/__init__.py +3 -0
- clm/layer0/model/hash/algorithms/custom_hash.py +7 -0
- clm/layer0/model/hash/algorithms/local_sha256.py +178 -0
- clm/layer0/model/hash/constants.py +53 -0
- clm/layer0/model/hash/enums.py +60 -0
- clm/layer0/model/hash/validator.py +307 -0
- clm/layer0/model/interpreter.py +275 -0
- clm/layer0/model/pagination.py +45 -0
- clm/layer0/model/pcard.py +716 -0
- clm/layer0/model/schema.py +44 -0
- clm/layer0/model/utils/__init__.py +1 -0
- clm/layer0/model/utils/content_analyzer.py +152 -0
- clm/layer0/model/validators/__init__.py +1 -0
- clm/layer0/model/validators/base_validator.py +44 -0
- clm/layer0/model/validators/binary_validator.py +66 -0
- clm/layer0/model/validators/text_validator.py +82 -0
- clm/layer0/model/validators/validation_registry.py +49 -0
- clm/layer0/model/vcard.py +823 -0
- clm/layer0/model/vcard_ext/__init__.py +39 -0
- clm/layer0/model/vcard_ext/core.py +60 -0
- clm/layer0/model/vcard_ext/network.py +23 -0
- clm/layer0/model/vcard_ext/observability.py +63 -0
- clm/layer0/model/vcard_ext/storage.py +60 -0
- clm/layer0/model/vcard_ext/vendors.py +211 -0
- clm/layer0/model/vcard_sandwich.py +119 -0
- clm/layer0/model/vcard_vocabulary.py +463 -0
- clm/layer0/model/workflow.py +274 -0
- clm/layer0/narrative.py +431 -0
- clm/layer0/ontology/collection.py +125 -0
- clm/layer0/parser.py +429 -0
- clm/layer0/relations.py +73 -0
- clm/layer0/rule_evaluator.py +251 -0
- clm/layer0/schema/__init__.py +38 -0
- clm/layer0/schema/ddl.py +97 -0
- clm/layer0/templates/abstract.yaml +99 -0
- clm/layer0/templates/balanced.yaml +253 -0
- clm/layer0/templates/concrete.yaml +172 -0
- clm/layer0/type_registry.py +223 -0
- clm/layer0/types.py +204 -0
- clm/layer0/utils.py +886 -0
- clm/layer0/verifier_core.py +262 -0
- clm/layer1/__init__.py +31 -0
- clm/layer1/action_dispatcher.py +275 -0
- clm/layer1/adapter_registry.py +233 -0
- clm/layer1/algebra_baldwin_generators.py +192 -0
- clm/layer1/arrow_compiler.py +525 -0
- clm/layer1/batch_processor.py +152 -0
- clm/layer1/cel/__init__.py +27 -0
- clm/layer1/cel/builder.py +173 -0
- clm/layer1/engine.py +759 -0
- clm/layer1/engine_async.py +89 -0
- clm/layer1/eval_callback.py +49 -0
- clm/layer1/gate_composer.py +229 -0
- clm/layer1/hoare_sandwich.py +298 -0
- clm/layer1/identity_guard.py +348 -0
- clm/layer1/matrix.py +138 -0
- clm/layer1/neural_runtime.py +221 -0
- clm/layer1/operations/__init__.py +123 -0
- clm/layer1/operations/augment.py +156 -0
- clm/layer1/operations/base.py +109 -0
- clm/layer1/operations/builtins.py +338 -0
- clm/layer1/operations/dispatch.py +144 -0
- clm/layer1/operations/exclude.py +107 -0
- clm/layer1/operations/handle.py +197 -0
- clm/layer1/operations/identity.py +139 -0
- clm/layer1/operations/invert.py +66 -0
- clm/layer1/operations/loader.py +116 -0
- clm/layer1/operations/manipulators/__init__.py +23 -0
- clm/layer1/operations/manipulators/document.py +211 -0
- clm/layer1/operations/manipulators/media.py +134 -0
- clm/layer1/operations/manipulators/structured.py +159 -0
- clm/layer1/operations/manipulators/tabular.py +334 -0
- clm/layer1/operations/port.py +65 -0
- clm/layer1/operations/services.py +529 -0
- clm/layer1/operations/split.py +117 -0
- clm/layer1/operations/substitute.py +75 -0
- clm/layer1/petri.py +1052 -0
- clm/layer1/pocketflow_orchestrator.py +408 -0
- clm/layer1/pocketflow_scheduler.py +519 -0
- clm/layer1/protocols.py +197 -0
- clm/layer1/runtime_adapter.py +718 -0
- clm/layer1/runtimes/__init__.py +71 -0
- clm/layer1/runtimes/_generated_assertions.py +125 -0
- clm/layer1/runtimes/_generated_types.py +42 -0
- clm/layer1/runtimes/base.py +265 -0
- clm/layer1/runtimes/binary.py +101 -0
- clm/layer1/runtimes/factory.py +203 -0
- clm/layer1/runtimes/javascript.py +321 -0
- clm/layer1/runtimes/javascript_runtime.js +33 -0
- clm/layer1/runtimes/js_scripts/isomorphism.mjs +22 -0
- clm/layer1/runtimes/js_scripts/loader.mjs +52 -0
- clm/layer1/runtimes/lambda_calc.py +126 -0
- clm/layer1/runtimes/python.py +288 -0
- clm/layer1/runtimes/python_runtime.py +41 -0
- clm/layer1/runtimes/script.py +114 -0
- clm/layer1/sandbox.py +334 -0
- clm/layer1/savepoint_guard.py +136 -0
- clm/layer1/sparse_net.py +122 -0
- clm/layer1/spec_tester.py +165 -0
- clm/layer1/store.py +295 -0
- clm/layer1/test_runner.py +498 -0
- clm/layer1/type_inspector.py +166 -0
- clm/layer1/verifier.py +772 -0
- clm/layer1/vm.py +91 -0
- clm/layer2/__init__.py +78 -0
- clm/layer2/card_collection.py +678 -0
- clm/layer2/classifier.py +479 -0
- clm/layer2/engine/__init__.py +1 -0
- clm/layer2/engine/abstract_sql_engine.py +320 -0
- clm/layer2/engine/base.py +193 -0
- clm/layer2/engine/duckdb_engine.py +576 -0
- clm/layer2/engine/sqlite_engine.py +518 -0
- clm/layer2/fiber/__init__.py +31 -0
- clm/layer2/fiber/context.py +89 -0
- clm/layer2/fiber/disposable.py +163 -0
- clm/layer2/fiber/fiber.py +125 -0
- clm/layer2/fiber/hoare.py +191 -0
- clm/layer2/file_io.py +376 -0
- clm/layer2/file_registrar.py +77 -0
- clm/layer2/ingest.py +202 -0
- clm/layer2/lineage.py +322 -0
- clm/layer2/mcard_fs.py +262 -0
- clm/layer2/merkle.py +11 -0
- clm/layer2/mime.py +244 -0
- clm/layer2/mime_detector.py +245 -0
- clm/layer2/private_collection.py +118 -0
- clm/layer2/shared_store.py +11 -0
- clm/layer2/storage/__init__.py +190 -0
- clm/layer2/storage/db_bitemporal.py +141 -0
- clm/layer2/storage/db_driver.py +331 -0
- clm/layer2/storage/db_hyperlinks.py +214 -0
- clm/layer2/storage/db_pool.py +139 -0
- clm/layer2/storage/db_repository.py +229 -0
- clm/layer2/storage/savepoint.py +78 -0
- clm/layer2/storage/sqlite_wal.py +74 -0
- clm/layer2/tridb.py +245 -0
- clm/layer2/vcard_receipt.py +71 -0
- clm/layer2/vfs.py +17 -0
- clm/layer3/__init__.py +141 -0
- clm/layer3/agency/__init__.py +19 -0
- clm/layer3/agency/config.py +220 -0
- clm/layer3/agency/providers/__init__.py +15 -0
- clm/layer3/agency/providers/base.py +91 -0
- clm/layer3/agency/providers/mlc_llm.py +169 -0
- clm/layer3/agency/providers/ollama.py +203 -0
- clm/layer3/agency/router.py +71 -0
- clm/layer3/agency/runtime.py +524 -0
- clm/layer3/gateway.py +353 -0
- clm/layer3/open_interpreter_adapter.py +159 -0
- clm/layer3/rag/__init__.py +70 -0
- clm/layer3/rag/cli.py +228 -0
- clm/layer3/rag/config.py +153 -0
- clm/layer3/rag/embeddings/__init__.py +16 -0
- clm/layer3/rag/embeddings/base.py +79 -0
- clm/layer3/rag/embeddings/ollama.py +211 -0
- clm/layer3/rag/embeddings/vision.py +290 -0
- clm/layer3/rag/engine.py +276 -0
- clm/layer3/rag/graph/__init__.py +22 -0
- clm/layer3/rag/graph/community.py +198 -0
- clm/layer3/rag/graph/engine.py +433 -0
- clm/layer3/rag/graph/extractor.py +255 -0
- clm/layer3/rag/graph/schema.py +115 -0
- clm/layer3/rag/graph/store.py +641 -0
- clm/layer3/rag/indexer.py +333 -0
- clm/layer3/rag/llm_providers.json +52 -0
- clm/layer3/rag/semantic_versioning.py +313 -0
- clm/layer3/rag/vector/__init__.py +28 -0
- clm/layer3/rag/vector/handle_vector_store.py +749 -0
- clm/layer3/rag/vector/schema.py +151 -0
- clm/layer3/rag/vector/store.py +632 -0
- clm/layer3/reticulum/__init__.py +55 -0
- clm/layer3/reticulum/bridge.py +74 -0
- clm/layer3/reticulum/daemon.py +88 -0
- clm/layer3/reticulum/discovery.py +164 -0
- clm/layer3/reticulum/identity.py +107 -0
- clm/layer3/reticulum/media.py +148 -0
- clm/layer3/reticulum/transport.py +133 -0
- clm/layer3/satori/__init__.py +209 -0
- clm/layer3/satori/alpha_conversion.py +168 -0
- clm/layer3/satori/beta_reduction.py +359 -0
- clm/layer3/satori/continuation.py +38 -0
- clm/layer3/satori/eta_conversion.py +205 -0
- clm/layer3/satori/free_variables.py +163 -0
- clm/layer3/satori/io_effects.py +370 -0
- clm/layer3/satori/lambda_runtime.py +726 -0
- clm/layer3/satori/lambda_term.py +230 -0
- clm/layer3/satori/sheaf_audit.py +261 -0
- clm/layer3/satori/speech_act.py +279 -0
- clm/layer4/README.md +9 -0
- clm/layer4/__init__.py +67 -0
- clm/layer4/_sovereign_executor.py +580 -0
- clm/layer4/action_commands.py +719 -0
- clm/layer4/api.py +236 -0
- clm/layer4/bootstrap/__init__.py +13 -0
- clm/layer4/bootstrap/bootstrapper.py +118 -0
- clm/layer4/bootstrap/cleanup.py +44 -0
- clm/layer4/bootstrap/clm_hooks.py +49 -0
- clm/layer4/bootstrap/genesis.py +77 -0
- clm/layer4/bootstrap/injection.py +125 -0
- clm/layer4/cli.py +549 -0
- clm/layer4/collection_manager.py +553 -0
- clm/layer4/commands/__init__.py +81 -0
- clm/layer4/commands/action_cmd.py +42 -0
- clm/layer4/commands/check_cmd.py +82 -0
- clm/layer4/commands/envelope.py +73 -0
- clm/layer4/commands/evaluate_cmd.py +224 -0
- clm/layer4/commands/mcard_cmd.py +24 -0
- clm/layer4/commands/petri_cmd.py +92 -0
- clm/layer4/commands/tridb_cmd.py +319 -0
- clm/layer4/commands/version_cmd.py +27 -0
- clm/layer4/diff_commands.py +167 -0
- clm/layer4/eoa_sponsor.py +232 -0
- clm/layer4/events.py +69 -0
- clm/layer4/flux_gateway.py +133 -0
- clm/layer4/gateway.py +91 -0
- clm/layer4/loader.py +307 -0
- clm/layer4/mcard_commands.py +166 -0
- clm/layer4/membrane.py +273 -0
- clm/layer4/navigation.py +50 -0
- clm/layer4/protocol/protocol_entry_python.py +108 -0
- clm/layer4/protocol/python_sync.py +126 -0
- clm/layer4/runner.py +150 -0
- clm/layer4/satori.py +262 -0
- clm/layer4/server/python_sync_client.py +150 -0
- clm/layer4/server/python_sync_server.py +139 -0
- clm/layer4/server/service_api.py +315 -0
- clm/layer4/server/start_servers.py +107 -0
- clm/layer4/server/stop_servers.py +24 -0
- clm/layer4/simulated_optimization_fix.py +0 -0
- clm/layer4/storage.py +463 -0
- clm/layer4/tridb_admin.py +268 -0
- clm/layer4/websocket/test_ws_client.py +104 -0
- clm/layer4/websocket/test_ws_server.py +138 -0
- clm/layer4/websocket/ws_server_local.py +160 -0
- clm/layer5/__init__.py +51 -0
- clm/layer5/async_batch_writer.py +133 -0
- clm/layer5/learning.py +276 -0
- clm/layer5/telemetry_hook.py +107 -0
- clm/layer5/type_lattice.py +238 -0
- clm/py.typed +0 -0
- clm/schemas/__init__.py +652 -0
- clm/schemas/abstract_face_schema.yaml +77 -0
- clm/schemas/cel_schema.yaml +416 -0
- clm/schemas/lifecycle/__init__.py +45 -0
- clm/schemas/lifecycle/models.py +123 -0
- clm/schemas/lifecycle/service.py +364 -0
- clm/schemas/mcard_schema.sql +192 -0
- clm/schemas/mcard_vector_schema.sql +283 -0
- clm/schemas/mlp-context-v1.json +91 -0
- clm_kernel-0.0.0.dist-info/METADATA +121 -0
- clm_kernel-0.0.0.dist-info/RECORD +333 -0
- clm_kernel-0.0.0.dist-info/WHEEL +4 -0
- clm_kernel-0.0.0.dist-info/entry_points.txt +3 -0
- clm_kernel-0.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Graph Store
|
|
3
|
+
|
|
4
|
+
SQLite-based knowledge graph storage with traversal capabilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import sqlite3
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .extractor import Entity, ExtractionResult
|
|
14
|
+
from .schema import GRAPH_SCHEMAS
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
# Graph Store
|
|
21
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GraphStore:
|
|
25
|
+
"""
|
|
26
|
+
SQLite-based storage for knowledge graph.
|
|
27
|
+
|
|
28
|
+
Features:
|
|
29
|
+
- Entity and relationship storage
|
|
30
|
+
- Graph traversal (BFS)
|
|
31
|
+
- Multi-hop pathfinding
|
|
32
|
+
- Community management
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
store = GraphStore(db_path="graph.db")
|
|
36
|
+
|
|
37
|
+
# Add entities
|
|
38
|
+
entity_id = store.add_entity(entity, source_hash)
|
|
39
|
+
|
|
40
|
+
# Add relationships
|
|
41
|
+
store.add_relationship(source_id, target_id, "relates_to", source_hash)
|
|
42
|
+
|
|
43
|
+
# Find related entities
|
|
44
|
+
related = store.find_related("MCard", hops=2)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, db_path: str = ":memory:"):
|
|
48
|
+
"""
|
|
49
|
+
Initialize graph store.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
db_path: Path to SQLite database
|
|
53
|
+
"""
|
|
54
|
+
self.db_path = db_path
|
|
55
|
+
self.conn = self._init_database()
|
|
56
|
+
|
|
57
|
+
def _init_database(self) -> sqlite3.Connection:
|
|
58
|
+
"""Initialize SQLite connection and create tables."""
|
|
59
|
+
conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
60
|
+
cursor = conn.cursor()
|
|
61
|
+
|
|
62
|
+
# Create all graph tables
|
|
63
|
+
for _name, schema in GRAPH_SCHEMAS.items():
|
|
64
|
+
cursor.execute(schema)
|
|
65
|
+
|
|
66
|
+
conn.commit()
|
|
67
|
+
logger.debug(f"Initialized graph store at {self.db_path}")
|
|
68
|
+
return conn
|
|
69
|
+
|
|
70
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
71
|
+
# Entity Operations
|
|
72
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def add_entity(
|
|
75
|
+
self, entity: Entity, source_hash: str, embedding: bytes = None
|
|
76
|
+
) -> int:
|
|
77
|
+
"""
|
|
78
|
+
Add an entity to the graph.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
entity: Entity to add
|
|
82
|
+
source_hash: Source MCard hash
|
|
83
|
+
embedding: Optional entity embedding
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Entity ID
|
|
87
|
+
"""
|
|
88
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
89
|
+
cursor = self.conn.cursor()
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
cursor.execute(
|
|
93
|
+
"""
|
|
94
|
+
INSERT INTO graph_entities (name, type, description, source_hash, embedding, created_at)
|
|
95
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
96
|
+
""",
|
|
97
|
+
(
|
|
98
|
+
entity.name,
|
|
99
|
+
entity.type,
|
|
100
|
+
entity.description,
|
|
101
|
+
source_hash,
|
|
102
|
+
embedding,
|
|
103
|
+
now,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
self.conn.commit()
|
|
107
|
+
return cursor.lastrowid
|
|
108
|
+
|
|
109
|
+
except sqlite3.IntegrityError:
|
|
110
|
+
# Entity already exists, get its ID
|
|
111
|
+
cursor.execute(
|
|
112
|
+
"""
|
|
113
|
+
SELECT id FROM graph_entities
|
|
114
|
+
WHERE name = ? AND type = ? AND source_hash = ?
|
|
115
|
+
""",
|
|
116
|
+
(entity.name, entity.type, source_hash),
|
|
117
|
+
)
|
|
118
|
+
row = cursor.fetchone()
|
|
119
|
+
return row[0] if row else -1
|
|
120
|
+
|
|
121
|
+
def get_entity_by_name(self, name: str) -> dict[str, Any] | None:
|
|
122
|
+
"""Get entity by name (case-insensitive)."""
|
|
123
|
+
cursor = self.conn.cursor()
|
|
124
|
+
cursor.execute(
|
|
125
|
+
"""
|
|
126
|
+
SELECT id, name, type, description, source_hash, created_at
|
|
127
|
+
FROM graph_entities
|
|
128
|
+
WHERE LOWER(name) = LOWER(?)
|
|
129
|
+
LIMIT 1
|
|
130
|
+
""",
|
|
131
|
+
(name,),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
row = cursor.fetchone()
|
|
135
|
+
if row:
|
|
136
|
+
return {
|
|
137
|
+
"id": row[0],
|
|
138
|
+
"name": row[1],
|
|
139
|
+
"type": row[2],
|
|
140
|
+
"description": row[3],
|
|
141
|
+
"source_hash": row[4],
|
|
142
|
+
"created_at": row[5],
|
|
143
|
+
}
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def get_entity_by_id(self, entity_id: int) -> dict[str, Any] | None:
|
|
147
|
+
"""Get entity by ID."""
|
|
148
|
+
cursor = self.conn.cursor()
|
|
149
|
+
cursor.execute(
|
|
150
|
+
"""
|
|
151
|
+
SELECT id, name, type, description, source_hash, created_at
|
|
152
|
+
FROM graph_entities
|
|
153
|
+
WHERE id = ?
|
|
154
|
+
""",
|
|
155
|
+
(entity_id,),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
row = cursor.fetchone()
|
|
159
|
+
if row:
|
|
160
|
+
return {
|
|
161
|
+
"id": row[0],
|
|
162
|
+
"name": row[1],
|
|
163
|
+
"type": row[2],
|
|
164
|
+
"description": row[3],
|
|
165
|
+
"source_hash": row[4],
|
|
166
|
+
"created_at": row[5],
|
|
167
|
+
}
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
def search_entities(
|
|
171
|
+
self, query: str, type_filter: str = None, limit: int = 10
|
|
172
|
+
) -> list[dict[str, Any]]:
|
|
173
|
+
"""Search entities by name pattern."""
|
|
174
|
+
cursor = self.conn.cursor()
|
|
175
|
+
|
|
176
|
+
if type_filter:
|
|
177
|
+
cursor.execute(
|
|
178
|
+
"""
|
|
179
|
+
SELECT id, name, type, description, source_hash
|
|
180
|
+
FROM graph_entities
|
|
181
|
+
WHERE name LIKE ? AND type = ?
|
|
182
|
+
LIMIT ?
|
|
183
|
+
""",
|
|
184
|
+
(f"%{query}%", type_filter, limit),
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
cursor.execute(
|
|
188
|
+
"""
|
|
189
|
+
SELECT id, name, type, description, source_hash
|
|
190
|
+
FROM graph_entities
|
|
191
|
+
WHERE name LIKE ?
|
|
192
|
+
LIMIT ?
|
|
193
|
+
""",
|
|
194
|
+
(f"%{query}%", limit),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return [
|
|
198
|
+
{
|
|
199
|
+
"id": r[0],
|
|
200
|
+
"name": r[1],
|
|
201
|
+
"type": r[2],
|
|
202
|
+
"description": r[3],
|
|
203
|
+
"source_hash": r[4],
|
|
204
|
+
}
|
|
205
|
+
for r in cursor.fetchall()
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
def get_entities_by_source(self, source_hash: str) -> list[dict[str, Any]]:
|
|
209
|
+
"""Get all entities from a source MCard."""
|
|
210
|
+
cursor = self.conn.cursor()
|
|
211
|
+
cursor.execute(
|
|
212
|
+
"""
|
|
213
|
+
SELECT id, name, type, description
|
|
214
|
+
FROM graph_entities
|
|
215
|
+
WHERE source_hash = ?
|
|
216
|
+
""",
|
|
217
|
+
(source_hash,),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return [
|
|
221
|
+
{"id": r[0], "name": r[1], "type": r[2], "description": r[3]}
|
|
222
|
+
for r in cursor.fetchall()
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
226
|
+
# Community Operations
|
|
227
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
def add_community(
|
|
230
|
+
self,
|
|
231
|
+
title: str,
|
|
232
|
+
summary: str,
|
|
233
|
+
member_ids: list[int],
|
|
234
|
+
level: int = 0,
|
|
235
|
+
parent_id: int | None = None,
|
|
236
|
+
embedding: bytes = None,
|
|
237
|
+
) -> int:
|
|
238
|
+
"""Add a community summary."""
|
|
239
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
240
|
+
cursor = self.conn.cursor()
|
|
241
|
+
|
|
242
|
+
cursor.execute(
|
|
243
|
+
"""
|
|
244
|
+
INSERT INTO graph_communities
|
|
245
|
+
(title, summary, member_entity_ids, level, parent_community_id, embedding, created_at)
|
|
246
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
247
|
+
""",
|
|
248
|
+
(title, summary, json.dumps(member_ids), level, parent_id, embedding, now),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
self.conn.commit()
|
|
252
|
+
return cursor.lastrowid
|
|
253
|
+
|
|
254
|
+
def get_communities(self, level: int = 0) -> list[dict[str, Any]]:
|
|
255
|
+
"""Get communities by level."""
|
|
256
|
+
cursor = self.conn.cursor()
|
|
257
|
+
cursor.execute(
|
|
258
|
+
"""
|
|
259
|
+
SELECT id, title, summary, member_entity_ids, level
|
|
260
|
+
FROM graph_communities
|
|
261
|
+
WHERE level = ?
|
|
262
|
+
""",
|
|
263
|
+
(level,),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
results = []
|
|
267
|
+
for row in cursor.fetchall():
|
|
268
|
+
results.append(
|
|
269
|
+
{
|
|
270
|
+
"id": row[0],
|
|
271
|
+
"title": row[1],
|
|
272
|
+
"summary": row[2],
|
|
273
|
+
"member_ids": json.loads(row[3]) if row[3] else [],
|
|
274
|
+
"level": row[4],
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
return results
|
|
278
|
+
|
|
279
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
280
|
+
# Relationship Operations
|
|
281
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
def add_relationship(
|
|
284
|
+
self,
|
|
285
|
+
source_entity_id: int,
|
|
286
|
+
target_entity_id: int,
|
|
287
|
+
relationship: str,
|
|
288
|
+
source_hash: str,
|
|
289
|
+
description: str = "",
|
|
290
|
+
weight: float = 1.0,
|
|
291
|
+
) -> int:
|
|
292
|
+
"""
|
|
293
|
+
Add a relationship between entities.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
source_entity_id: Source entity ID
|
|
297
|
+
target_entity_id: Target entity ID
|
|
298
|
+
relationship: Relationship type/verb
|
|
299
|
+
source_hash: Source MCard hash
|
|
300
|
+
description: Optional description
|
|
301
|
+
weight: Relationship weight
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Relationship ID
|
|
305
|
+
"""
|
|
306
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
307
|
+
cursor = self.conn.cursor()
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
cursor.execute(
|
|
311
|
+
"""
|
|
312
|
+
INSERT INTO graph_relationships
|
|
313
|
+
(source_entity_id, target_entity_id, relationship, description, weight, source_hash, created_at)
|
|
314
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
315
|
+
""",
|
|
316
|
+
(
|
|
317
|
+
source_entity_id,
|
|
318
|
+
target_entity_id,
|
|
319
|
+
relationship,
|
|
320
|
+
description,
|
|
321
|
+
weight,
|
|
322
|
+
source_hash,
|
|
323
|
+
now,
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
self.conn.commit()
|
|
327
|
+
return cursor.lastrowid
|
|
328
|
+
|
|
329
|
+
except sqlite3.IntegrityError:
|
|
330
|
+
# Relationship already exists
|
|
331
|
+
return -1
|
|
332
|
+
|
|
333
|
+
def get_relationships_from(self, entity_id: int) -> list[dict[str, Any]]:
|
|
334
|
+
"""Get outgoing relationships from an entity."""
|
|
335
|
+
cursor = self.conn.cursor()
|
|
336
|
+
cursor.execute(
|
|
337
|
+
"""
|
|
338
|
+
SELECT r.id, r.relationship, r.description, r.weight,
|
|
339
|
+
e.id, e.name, e.type
|
|
340
|
+
FROM graph_relationships r
|
|
341
|
+
JOIN graph_entities e ON r.target_entity_id = e.id
|
|
342
|
+
WHERE r.source_entity_id = ?
|
|
343
|
+
""",
|
|
344
|
+
(entity_id,),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return [
|
|
348
|
+
{
|
|
349
|
+
"rel_id": r[0],
|
|
350
|
+
"relationship": r[1],
|
|
351
|
+
"description": r[2],
|
|
352
|
+
"weight": r[3],
|
|
353
|
+
"target_id": r[4],
|
|
354
|
+
"target_name": r[5],
|
|
355
|
+
"target_type": r[6],
|
|
356
|
+
}
|
|
357
|
+
for r in cursor.fetchall()
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
def get_relationships_to(self, entity_id: int) -> list[dict[str, Any]]:
|
|
361
|
+
"""Get incoming relationships to an entity."""
|
|
362
|
+
cursor = self.conn.cursor()
|
|
363
|
+
cursor.execute(
|
|
364
|
+
"""
|
|
365
|
+
SELECT r.id, r.relationship, r.description, r.weight,
|
|
366
|
+
e.id, e.name, e.type
|
|
367
|
+
FROM graph_relationships r
|
|
368
|
+
JOIN graph_entities e ON r.source_entity_id = e.id
|
|
369
|
+
WHERE r.target_entity_id = ?
|
|
370
|
+
""",
|
|
371
|
+
(entity_id,),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
return [
|
|
375
|
+
{
|
|
376
|
+
"rel_id": r[0],
|
|
377
|
+
"relationship": r[1],
|
|
378
|
+
"description": r[2],
|
|
379
|
+
"weight": r[3],
|
|
380
|
+
"source_id": r[4],
|
|
381
|
+
"source_name": r[5],
|
|
382
|
+
"source_type": r[6],
|
|
383
|
+
}
|
|
384
|
+
for r in cursor.fetchall()
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
388
|
+
# Graph Traversal
|
|
389
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
def find_related(
|
|
392
|
+
self, entity_name: str, hops: int = 2, direction: str = "both"
|
|
393
|
+
) -> list[dict[str, Any]]:
|
|
394
|
+
"""
|
|
395
|
+
Find entities within N hops of a given entity.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
entity_name: Starting entity name
|
|
399
|
+
hops: Maximum traversal depth
|
|
400
|
+
direction: 'outgoing', 'incoming', or 'both'
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
List of related entities with their paths
|
|
404
|
+
"""
|
|
405
|
+
# Find starting entity
|
|
406
|
+
start = self.get_entity_by_name(entity_name)
|
|
407
|
+
if not start:
|
|
408
|
+
return []
|
|
409
|
+
|
|
410
|
+
visited: set[int] = {start["id"]}
|
|
411
|
+
results = []
|
|
412
|
+
frontier = [(start["id"], 0, [start["name"]])] # (id, depth, path)
|
|
413
|
+
|
|
414
|
+
while frontier:
|
|
415
|
+
current_id, depth, path = frontier.pop(0)
|
|
416
|
+
|
|
417
|
+
if depth >= hops:
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
# Get adjacent entities
|
|
421
|
+
neighbors = []
|
|
422
|
+
|
|
423
|
+
if direction in ("outgoing", "both"):
|
|
424
|
+
for rel in self.get_relationships_from(current_id):
|
|
425
|
+
neighbors.append(
|
|
426
|
+
(
|
|
427
|
+
rel["target_id"],
|
|
428
|
+
rel["target_name"],
|
|
429
|
+
rel["relationship"],
|
|
430
|
+
"outgoing",
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if direction in ("incoming", "both"):
|
|
435
|
+
for rel in self.get_relationships_to(current_id):
|
|
436
|
+
neighbors.append(
|
|
437
|
+
(
|
|
438
|
+
rel["source_id"],
|
|
439
|
+
rel["source_name"],
|
|
440
|
+
rel["relationship"],
|
|
441
|
+
"incoming",
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
for neighbor_id, neighbor_name, rel, _dir in neighbors:
|
|
446
|
+
if neighbor_id not in visited:
|
|
447
|
+
visited.add(neighbor_id)
|
|
448
|
+
new_path = path + [f"--{rel}->", neighbor_name]
|
|
449
|
+
|
|
450
|
+
entity = self.get_entity_by_id(neighbor_id)
|
|
451
|
+
if entity:
|
|
452
|
+
results.append(
|
|
453
|
+
{
|
|
454
|
+
"entity": entity,
|
|
455
|
+
"depth": depth + 1,
|
|
456
|
+
"path": new_path,
|
|
457
|
+
"relationship": rel,
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
frontier.append((neighbor_id, depth + 1, new_path))
|
|
462
|
+
|
|
463
|
+
return results
|
|
464
|
+
|
|
465
|
+
def find_path(
|
|
466
|
+
self, source_name: str, target_name: str, max_depth: int = 4
|
|
467
|
+
) -> list[str] | None:
|
|
468
|
+
"""
|
|
469
|
+
Find shortest path between two entities.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
source_name: Starting entity name
|
|
473
|
+
target_name: Target entity name
|
|
474
|
+
max_depth: Maximum path length
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
List of entity names in path, or None if no path
|
|
478
|
+
"""
|
|
479
|
+
source = self.get_entity_by_name(source_name)
|
|
480
|
+
target = self.get_entity_by_name(target_name)
|
|
481
|
+
|
|
482
|
+
if not source or not target:
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
if source["id"] == target["id"]:
|
|
486
|
+
return [source_name]
|
|
487
|
+
|
|
488
|
+
# BFS for shortest path
|
|
489
|
+
visited = {source["id"]}
|
|
490
|
+
queue = [(source["id"], [source["name"]])]
|
|
491
|
+
|
|
492
|
+
while queue:
|
|
493
|
+
current_id, path = queue.pop(0)
|
|
494
|
+
|
|
495
|
+
if len(path) > max_depth:
|
|
496
|
+
continue
|
|
497
|
+
|
|
498
|
+
for rel in self.get_relationships_from(current_id):
|
|
499
|
+
neighbor_id = rel["target_id"]
|
|
500
|
+
|
|
501
|
+
if neighbor_id == target["id"]:
|
|
502
|
+
return path + [f"--{rel['relationship']}->", target_name]
|
|
503
|
+
|
|
504
|
+
if neighbor_id not in visited:
|
|
505
|
+
visited.add(neighbor_id)
|
|
506
|
+
queue.append(
|
|
507
|
+
(
|
|
508
|
+
neighbor_id,
|
|
509
|
+
path + [f"--{rel['relationship']}->", rel["target_name"]],
|
|
510
|
+
)
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
return None
|
|
514
|
+
|
|
515
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
516
|
+
# Extraction Tracking
|
|
517
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
518
|
+
|
|
519
|
+
def mark_extracted(self, source_hash: str, entity_count: int, rel_count: int):
|
|
520
|
+
"""Mark a source as having been extracted."""
|
|
521
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
522
|
+
cursor = self.conn.cursor()
|
|
523
|
+
|
|
524
|
+
cursor.execute(
|
|
525
|
+
"""
|
|
526
|
+
INSERT OR REPLACE INTO graph_extractions
|
|
527
|
+
(hash, entity_count, relationship_count, extracted_at)
|
|
528
|
+
VALUES (?, ?, ?, ?)
|
|
529
|
+
""",
|
|
530
|
+
(source_hash, entity_count, rel_count, now),
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
self.conn.commit()
|
|
534
|
+
|
|
535
|
+
def is_extracted(self, source_hash: str) -> bool:
|
|
536
|
+
"""Check if a source has been extracted."""
|
|
537
|
+
cursor = self.conn.cursor()
|
|
538
|
+
cursor.execute("SELECT 1 FROM graph_extractions WHERE hash = ?", (source_hash,))
|
|
539
|
+
return cursor.fetchone() is not None
|
|
540
|
+
|
|
541
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
542
|
+
# Statistics
|
|
543
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
def count_entities(self) -> int:
|
|
546
|
+
"""Get total entity count."""
|
|
547
|
+
cursor = self.conn.cursor()
|
|
548
|
+
cursor.execute("SELECT COUNT(*) FROM graph_entities")
|
|
549
|
+
return cursor.fetchone()[0]
|
|
550
|
+
|
|
551
|
+
def count_relationships(self) -> int:
|
|
552
|
+
"""Get total relationship count."""
|
|
553
|
+
cursor = self.conn.cursor()
|
|
554
|
+
cursor.execute("SELECT COUNT(*) FROM graph_relationships")
|
|
555
|
+
return cursor.fetchone()[0]
|
|
556
|
+
|
|
557
|
+
def count_extractions(self) -> int:
|
|
558
|
+
"""Get count of extracted sources."""
|
|
559
|
+
cursor = self.conn.cursor()
|
|
560
|
+
cursor.execute("SELECT COUNT(*) FROM graph_extractions")
|
|
561
|
+
return cursor.fetchone()[0]
|
|
562
|
+
|
|
563
|
+
def get_entity_types(self) -> dict[str, int]:
|
|
564
|
+
"""Get counts by entity type."""
|
|
565
|
+
cursor = self.conn.cursor()
|
|
566
|
+
cursor.execute(
|
|
567
|
+
"""
|
|
568
|
+
SELECT type, COUNT(*) FROM graph_entities
|
|
569
|
+
GROUP BY type
|
|
570
|
+
ORDER BY COUNT(*) DESC
|
|
571
|
+
"""
|
|
572
|
+
)
|
|
573
|
+
return {row[0]: row[1] for row in cursor.fetchall()}
|
|
574
|
+
|
|
575
|
+
def get_stats(self) -> dict[str, Any]:
|
|
576
|
+
"""Get graph statistics."""
|
|
577
|
+
return {
|
|
578
|
+
"entity_count": self.count_entities(),
|
|
579
|
+
"relationship_count": self.count_relationships(),
|
|
580
|
+
"extraction_count": self.count_extractions(),
|
|
581
|
+
"entity_types": self.get_entity_types(),
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
def clear(self):
|
|
585
|
+
"""Clear all graph data."""
|
|
586
|
+
cursor = self.conn.cursor()
|
|
587
|
+
cursor.execute("DELETE FROM graph_relationships")
|
|
588
|
+
cursor.execute("DELETE FROM graph_entities")
|
|
589
|
+
cursor.execute("DELETE FROM graph_communities")
|
|
590
|
+
cursor.execute("DELETE FROM graph_extractions")
|
|
591
|
+
self.conn.commit()
|
|
592
|
+
logger.info("Graph store cleared")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
596
|
+
# Batch Operations
|
|
597
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def store_extraction_result(
|
|
601
|
+
store: GraphStore, result: ExtractionResult, source_hash: str
|
|
602
|
+
) -> tuple[int, int]:
|
|
603
|
+
"""
|
|
604
|
+
Store an extraction result in the graph.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
store: GraphStore instance
|
|
608
|
+
result: ExtractionResult from GraphExtractor
|
|
609
|
+
source_hash: Source MCard hash
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
Tuple of (entity_count, relationship_count) stored
|
|
613
|
+
"""
|
|
614
|
+
if not result.success:
|
|
615
|
+
return 0, 0
|
|
616
|
+
|
|
617
|
+
entity_map = {} # name -> id
|
|
618
|
+
|
|
619
|
+
# Add entities
|
|
620
|
+
for entity in result.entities:
|
|
621
|
+
entity_id = store.add_entity(entity, source_hash)
|
|
622
|
+
if entity_id > 0:
|
|
623
|
+
entity_map[entity.name.lower()] = entity_id
|
|
624
|
+
|
|
625
|
+
# Add relationships
|
|
626
|
+
rel_count = 0
|
|
627
|
+
for rel in result.relationships:
|
|
628
|
+
source_id = entity_map.get(rel.source.lower())
|
|
629
|
+
target_id = entity_map.get(rel.target.lower())
|
|
630
|
+
|
|
631
|
+
if source_id and target_id:
|
|
632
|
+
rel_id = store.add_relationship(
|
|
633
|
+
source_id, target_id, rel.relationship, source_hash, rel.description
|
|
634
|
+
)
|
|
635
|
+
if rel_id > 0:
|
|
636
|
+
rel_count += 1
|
|
637
|
+
|
|
638
|
+
# Mark as extracted
|
|
639
|
+
store.mark_extracted(source_hash, len(entity_map), rel_count)
|
|
640
|
+
|
|
641
|
+
return len(entity_map), rel_count
|