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,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Community Detection and Summarization
|
|
3
|
+
|
|
4
|
+
Label Propagation Algorithm (LPA) for community detection
|
|
5
|
+
and LLM-based hierarchical summarization.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import random
|
|
11
|
+
import re
|
|
12
|
+
from collections import Counter, defaultdict
|
|
13
|
+
|
|
14
|
+
from .store import GraphStore
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
# Community Detection
|
|
21
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def detect_communities(store: GraphStore, max_iter: int = 20) -> list[list[int]]:
|
|
25
|
+
"""
|
|
26
|
+
Detect communities using asynchronous Label Propagation.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
store: GraphStore instance
|
|
30
|
+
max_iter: Maximum iterations
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of communities, where each community is a list of entity IDs
|
|
34
|
+
"""
|
|
35
|
+
logger.info("Starting community detection (LPA)...")
|
|
36
|
+
|
|
37
|
+
# 1. Build Adjacency List
|
|
38
|
+
adj = defaultdict(list)
|
|
39
|
+
nodes = set()
|
|
40
|
+
|
|
41
|
+
cursor = store.conn.cursor()
|
|
42
|
+
cursor.execute("SELECT source_entity_id, target_entity_id FROM graph_relationships")
|
|
43
|
+
|
|
44
|
+
rows = cursor.fetchall()
|
|
45
|
+
for src, tgt in rows:
|
|
46
|
+
adj[src].append(tgt)
|
|
47
|
+
adj[tgt].append(src) # Undirected for community detection
|
|
48
|
+
nodes.add(src)
|
|
49
|
+
nodes.add(tgt)
|
|
50
|
+
|
|
51
|
+
node_list = list(nodes)
|
|
52
|
+
if not node_list:
|
|
53
|
+
logger.warning("No nodes found for community detection")
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
logger.debug(f"Graph size: {len(nodes)} nodes, {len(rows)} edges")
|
|
57
|
+
|
|
58
|
+
# 2. Initialize Labels
|
|
59
|
+
labels = {node: node for node in nodes}
|
|
60
|
+
|
|
61
|
+
# 3. Propagate Labels
|
|
62
|
+
for i in range(max_iter):
|
|
63
|
+
changes = 0
|
|
64
|
+
random.shuffle(node_list)
|
|
65
|
+
|
|
66
|
+
for node in node_list:
|
|
67
|
+
if not adj[node]:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
neighbor_labels = [labels[neighbor] for neighbor in adj[node]]
|
|
71
|
+
if not neighbor_labels:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Find most frequent label (ties broken randomly)
|
|
75
|
+
counts = Counter(neighbor_labels)
|
|
76
|
+
max_freq = counts.most_common(1)[0][1]
|
|
77
|
+
best_labels = [lbl for lbl, count in counts.items() if count == max_freq]
|
|
78
|
+
new_label = random.choice(best_labels)
|
|
79
|
+
|
|
80
|
+
if labels[node] != new_label:
|
|
81
|
+
labels[node] = new_label
|
|
82
|
+
changes += 1
|
|
83
|
+
|
|
84
|
+
logger.debug(f"LPA Iteration {i + 1}: {changes} changes")
|
|
85
|
+
if changes == 0:
|
|
86
|
+
logger.info(f"LPA converged after {i + 1} iterations")
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
# 4. Group Communities
|
|
90
|
+
communities = defaultdict(list)
|
|
91
|
+
for node, label in labels.items():
|
|
92
|
+
communities[label].append(node)
|
|
93
|
+
|
|
94
|
+
result = list(communities.values())
|
|
95
|
+
logger.info(f"Detected {len(result)} communities")
|
|
96
|
+
|
|
97
|
+
# Sort for deterministic output (by size desc, then first ID)
|
|
98
|
+
result.sort(key=lambda c: (-len(c), min(c)))
|
|
99
|
+
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
# Community Summarization
|
|
105
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
SUMMARIZE_SYSTEM_PROMPT = """You are an expert graph analyst.
|
|
108
|
+
Your task is to summarize a "community" of related entities from a knowledge graph.
|
|
109
|
+
Focus on the common themes, purposes, or technologies that connect these entities.
|
|
110
|
+
Synthesize the descriptions into a cohesive whole."""
|
|
111
|
+
|
|
112
|
+
SUMMARIZE_USER_PROMPT = """Analyze the following list of entities and their descriptions which form a community in a knowledge graph.
|
|
113
|
+
Create a Title and valid JSON Summary.
|
|
114
|
+
|
|
115
|
+
--- BEGIN ENTITY LIST ---
|
|
116
|
+
{entity_text}
|
|
117
|
+
--- END ENTITY LIST ---
|
|
118
|
+
|
|
119
|
+
Requirement:
|
|
120
|
+
- Provide a short Title.
|
|
121
|
+
- Provide a detailed Summary of common themes.
|
|
122
|
+
- Output MUST be valid JSON in the format:
|
|
123
|
+
{{
|
|
124
|
+
"title": "Community Title",
|
|
125
|
+
"summary": "Detailed summary..."
|
|
126
|
+
}}
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class CommunitySummarizer:
|
|
131
|
+
"""Summarizes graph communities using LLM."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, store: GraphStore, model: str = "qwen3:0.6b"):
|
|
134
|
+
self.store = store
|
|
135
|
+
self.model = model
|
|
136
|
+
|
|
137
|
+
def summarize_and_store(self, communities: list[list[int]]) -> int:
|
|
138
|
+
"""
|
|
139
|
+
Summarize communities and store them in the DB.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Count of summaries generated
|
|
143
|
+
"""
|
|
144
|
+
count = 0
|
|
145
|
+
|
|
146
|
+
for comm_ids in communities:
|
|
147
|
+
# Prepare context
|
|
148
|
+
entity_text = self._prepare_context(comm_ids)
|
|
149
|
+
if not entity_text:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
title, summary = self._generate_summary(entity_text)
|
|
154
|
+
self.store.add_community(title, summary, comm_ids)
|
|
155
|
+
count += 1
|
|
156
|
+
logger.info(f"Generated community: {title}")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error(f"Failed to summarize community: {e}", exc_info=True)
|
|
159
|
+
|
|
160
|
+
return count
|
|
161
|
+
|
|
162
|
+
def _prepare_context(self, ids: list[int]) -> str:
|
|
163
|
+
lines = []
|
|
164
|
+
for eid in ids[:30]: # Limit context size
|
|
165
|
+
ent = self.store.get_entity_by_id(eid)
|
|
166
|
+
if ent:
|
|
167
|
+
lines.append(f"- {ent['name']} ({ent['type']}): {ent['description']}")
|
|
168
|
+
return "\n".join(lines)
|
|
169
|
+
|
|
170
|
+
def _generate_summary(self, entity_text: str) -> tuple[str, str]:
|
|
171
|
+
from clm.layer3.agency import chat_monad
|
|
172
|
+
|
|
173
|
+
prompt = SUMMARIZE_USER_PROMPT.format(entity_text=entity_text)
|
|
174
|
+
|
|
175
|
+
result = chat_monad(
|
|
176
|
+
prompt=prompt,
|
|
177
|
+
system_prompt=SUMMARIZE_SYSTEM_PROMPT,
|
|
178
|
+
model=self.model,
|
|
179
|
+
temperature=0.3,
|
|
180
|
+
max_tokens=1000,
|
|
181
|
+
).unsafe_run()
|
|
182
|
+
|
|
183
|
+
if result.is_left():
|
|
184
|
+
raise RuntimeError(f"LLM failure: {result.value}")
|
|
185
|
+
|
|
186
|
+
content = result.value.get("content", str(result.value))
|
|
187
|
+
|
|
188
|
+
# Parse JSON
|
|
189
|
+
json_match = re.search(r"\{[\s\S]*\}", content)
|
|
190
|
+
if json_match:
|
|
191
|
+
try:
|
|
192
|
+
data = json.loads(json_match.group())
|
|
193
|
+
return data.get("title", "Unknown Community"), data.get("summary", "")
|
|
194
|
+
except json.JSONDecodeError:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
# Fallback if no JSON
|
|
198
|
+
return "Community Summary", content[:500]
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GraphRAG Engine
|
|
3
|
+
|
|
4
|
+
Combines vector search with knowledge graph for enhanced RAG.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from clm.layer0.model.card import MCard
|
|
12
|
+
|
|
13
|
+
from ..config import DEFAULT_RAG_CONFIG, RAGConfig
|
|
14
|
+
from ..embeddings import OllamaEmbeddingProvider
|
|
15
|
+
from ..vector import MCardVectorStore
|
|
16
|
+
from .extractor import GraphExtractor
|
|
17
|
+
from .store import GraphStore, store_extraction_result
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
# GraphRAG Response
|
|
24
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class GraphRAGResponse:
|
|
29
|
+
"""Response from GraphRAG query."""
|
|
30
|
+
|
|
31
|
+
answer: str
|
|
32
|
+
sources: list[str] # Source MCard hashes
|
|
33
|
+
entities: list[dict[str, Any]] # Relevant entities
|
|
34
|
+
relationships: list[dict[str, Any]] # Relevant relationships
|
|
35
|
+
graph_context: str # Graph traversal context
|
|
36
|
+
vector_context: str # Vector search context
|
|
37
|
+
confidence: float
|
|
38
|
+
metadata: dict[str, Any] | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
# System Prompts
|
|
43
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
GRAPHRAG_SYSTEM_PROMPT = """You are a helpful assistant that answers questions using both document context and a knowledge graph.
|
|
46
|
+
|
|
47
|
+
You have access to:
|
|
48
|
+
1. DOCUMENT CONTEXT: Relevant text passages from the knowledge base
|
|
49
|
+
2. GRAPH CONTEXT: Entities and their relationships from the knowledge graph
|
|
50
|
+
|
|
51
|
+
Use both sources to provide comprehensive, accurate answers.
|
|
52
|
+
- Cite sources [1], [2], etc. for document context
|
|
53
|
+
- Reference entities by name when using graph information
|
|
54
|
+
- If the graph shows relationships that help answer the question, describe them
|
|
55
|
+
|
|
56
|
+
Be thorough but concise. Acknowledge if information is incomplete."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
GRAPH_QUERY_PROMPT = """Question: {question}
|
|
60
|
+
|
|
61
|
+
=== DOCUMENT CONTEXT ===
|
|
62
|
+
{vector_context}
|
|
63
|
+
|
|
64
|
+
=== KNOWLEDGE GRAPH ===
|
|
65
|
+
Entities: {entities}
|
|
66
|
+
Relationships: {relationships}
|
|
67
|
+
|
|
68
|
+
Please provide a comprehensive answer using both the document context and knowledge graph information."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
# GraphRAG Engine
|
|
73
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class GraphRAGEngine:
|
|
77
|
+
"""
|
|
78
|
+
GraphRAG Engine combining vector search with knowledge graphs.
|
|
79
|
+
|
|
80
|
+
Features:
|
|
81
|
+
- Entity extraction from MCard content
|
|
82
|
+
- Knowledge graph storage and traversal
|
|
83
|
+
- Multi-hop reasoning across entities
|
|
84
|
+
- Combined vector + graph context for LLM
|
|
85
|
+
|
|
86
|
+
Usage:
|
|
87
|
+
from clm.layer3.rag.graph import GraphRAGEngine
|
|
88
|
+
|
|
89
|
+
engine = GraphRAGEngine(vector_db_path=':memory:')
|
|
90
|
+
|
|
91
|
+
# Index with graph extraction
|
|
92
|
+
engine.index(mcard)
|
|
93
|
+
|
|
94
|
+
# Query with graph context
|
|
95
|
+
response = engine.query("How does MCard relate to PTR?")
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
config: RAGConfig = None,
|
|
101
|
+
vector_db_path: str = None,
|
|
102
|
+
graph_db_path: str = None,
|
|
103
|
+
llm_model: str = "qwen3:0.6b",
|
|
104
|
+
):
|
|
105
|
+
"""
|
|
106
|
+
Initialize GraphRAG engine.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
config: RAG configuration
|
|
110
|
+
vector_db_path: Path to vector database
|
|
111
|
+
graph_db_path: Path to graph database (default: same as vector)
|
|
112
|
+
llm_model: LLM model for generation
|
|
113
|
+
"""
|
|
114
|
+
self.config = config or DEFAULT_RAG_CONFIG
|
|
115
|
+
self.llm_model = llm_model
|
|
116
|
+
|
|
117
|
+
# Initialize embedding provider
|
|
118
|
+
self.embedder = OllamaEmbeddingProvider(
|
|
119
|
+
model=self.config.embedding_model, base_url=self.config.ollama_base_url
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Initialize vector store
|
|
123
|
+
self.vector_store = MCardVectorStore(
|
|
124
|
+
db_path=vector_db_path, config=self.config, embedding_provider=self.embedder
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Initialize graph store (same DB or separate)
|
|
128
|
+
graph_path = graph_db_path or vector_db_path or ":memory:"
|
|
129
|
+
self.graph_store = GraphStore(db_path=graph_path)
|
|
130
|
+
|
|
131
|
+
# Initialize extractor
|
|
132
|
+
self.extractor = GraphExtractor(model=llm_model)
|
|
133
|
+
|
|
134
|
+
logger.info("Initialized GraphRAG engine")
|
|
135
|
+
|
|
136
|
+
def index(
|
|
137
|
+
self, mcard: MCard, extract_graph: bool = True, force: bool = False
|
|
138
|
+
) -> dict[str, int]:
|
|
139
|
+
"""
|
|
140
|
+
Index an MCard with optional graph extraction.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
mcard: MCard to index
|
|
144
|
+
extract_graph: Extract entities and relationships
|
|
145
|
+
force: Re-index even if exists
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dict with indexing stats
|
|
149
|
+
"""
|
|
150
|
+
stats = {"vectors": 0, "entities": 0, "relationships": 0}
|
|
151
|
+
|
|
152
|
+
# Get content
|
|
153
|
+
content = mcard.get_content()
|
|
154
|
+
if isinstance(content, bytes):
|
|
155
|
+
try:
|
|
156
|
+
content = content.decode("utf-8")
|
|
157
|
+
except UnicodeDecodeError:
|
|
158
|
+
logger.warning(f"Cannot decode MCard {mcard.hash[:8]}")
|
|
159
|
+
return stats
|
|
160
|
+
|
|
161
|
+
# Vector indexing
|
|
162
|
+
if force or not self.vector_store.is_indexed(mcard.hash):
|
|
163
|
+
stats["vectors"] = self.vector_store.index(mcard)
|
|
164
|
+
|
|
165
|
+
# Graph extraction
|
|
166
|
+
if extract_graph and (force or not self.graph_store.is_extracted(mcard.hash)):
|
|
167
|
+
logger.debug(f"Extracting graph from {mcard.hash[:8]}...")
|
|
168
|
+
|
|
169
|
+
result = self.extractor.extract(content)
|
|
170
|
+
|
|
171
|
+
if result.success:
|
|
172
|
+
entity_count, rel_count = store_extraction_result(
|
|
173
|
+
self.graph_store, result, mcard.hash
|
|
174
|
+
)
|
|
175
|
+
stats["entities"] = entity_count
|
|
176
|
+
stats["relationships"] = rel_count
|
|
177
|
+
logger.debug(
|
|
178
|
+
f"Extracted {entity_count} entities, {rel_count} relationships"
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
logger.warning(f"Graph extraction failed: {result.error}")
|
|
182
|
+
|
|
183
|
+
return stats
|
|
184
|
+
|
|
185
|
+
def detect_and_summarize_communities(self) -> int:
|
|
186
|
+
"""
|
|
187
|
+
Detect communities and generate summaries.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Number of communities summarized
|
|
191
|
+
"""
|
|
192
|
+
from .community import CommunitySummarizer, detect_communities
|
|
193
|
+
|
|
194
|
+
logger.info("Detecting communities...")
|
|
195
|
+
communities = detect_communities(self.graph_store)
|
|
196
|
+
|
|
197
|
+
if not communities:
|
|
198
|
+
logger.info("No communities detected.")
|
|
199
|
+
return 0
|
|
200
|
+
|
|
201
|
+
logger.info(f"Summarizing {len(communities)} communities...")
|
|
202
|
+
summarizer = CommunitySummarizer(self.graph_store, model=self.llm_model)
|
|
203
|
+
count = summarizer.summarize_and_store(communities)
|
|
204
|
+
|
|
205
|
+
logger.info(f"Generated {count} community summaries.")
|
|
206
|
+
return count
|
|
207
|
+
|
|
208
|
+
def _get_vector_context(self, query: str, k: int) -> tuple[str, list[str]]:
|
|
209
|
+
"""Get vector search context."""
|
|
210
|
+
results = self.vector_store.hybrid_search(query, k)
|
|
211
|
+
|
|
212
|
+
context_parts = []
|
|
213
|
+
sources = []
|
|
214
|
+
|
|
215
|
+
for i, result in enumerate(results):
|
|
216
|
+
chunk = result.chunk_text or f"[Content from {result.hash[:8]}]"
|
|
217
|
+
context_parts.append(f"[{i + 1}] {chunk}")
|
|
218
|
+
sources.append(result.hash)
|
|
219
|
+
|
|
220
|
+
return "\n\n".join(context_parts), sources
|
|
221
|
+
|
|
222
|
+
def _get_graph_context(
|
|
223
|
+
self, query: str, source_hashes: list[str] = None
|
|
224
|
+
) -> tuple[list[dict], list[dict]]:
|
|
225
|
+
"""
|
|
226
|
+
Get relevant graph context.
|
|
227
|
+
|
|
228
|
+
Searches for entities mentioned in the query and finds related entities.
|
|
229
|
+
"""
|
|
230
|
+
entities = []
|
|
231
|
+
relationships = []
|
|
232
|
+
seen_entity_ids: set[int] = set()
|
|
233
|
+
|
|
234
|
+
# Search for entities matching query terms
|
|
235
|
+
for word in query.split():
|
|
236
|
+
if len(word) > 3:
|
|
237
|
+
found = self.graph_store.search_entities(word, limit=5)
|
|
238
|
+
for entity in found:
|
|
239
|
+
if entity["id"] not in seen_entity_ids:
|
|
240
|
+
seen_entity_ids.add(entity["id"])
|
|
241
|
+
entities.append(entity)
|
|
242
|
+
|
|
243
|
+
# Get relationships
|
|
244
|
+
rels_out = self.graph_store.get_relationships_from(entity["id"])
|
|
245
|
+
rels_in = self.graph_store.get_relationships_to(entity["id"])
|
|
246
|
+
|
|
247
|
+
for rel in rels_out:
|
|
248
|
+
relationships.append(
|
|
249
|
+
{
|
|
250
|
+
"source": entity["name"],
|
|
251
|
+
"relationship": rel["relationship"],
|
|
252
|
+
"target": rel["target_name"],
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
for rel in rels_in:
|
|
257
|
+
relationships.append(
|
|
258
|
+
{
|
|
259
|
+
"source": rel["source_name"],
|
|
260
|
+
"relationship": rel["relationship"],
|
|
261
|
+
"target": entity["name"],
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Also get entities from source documents
|
|
266
|
+
if source_hashes:
|
|
267
|
+
for hash_val in source_hashes[:3]: # Limit
|
|
268
|
+
source_entities = self.graph_store.get_entities_by_source(hash_val)
|
|
269
|
+
for entity in source_entities:
|
|
270
|
+
if entity["id"] not in seen_entity_ids:
|
|
271
|
+
seen_entity_ids.add(entity["id"])
|
|
272
|
+
entities.append(entity)
|
|
273
|
+
|
|
274
|
+
return entities, relationships
|
|
275
|
+
|
|
276
|
+
def query(
|
|
277
|
+
self, question: str, k: int = 5, use_graph: bool = True
|
|
278
|
+
) -> GraphRAGResponse:
|
|
279
|
+
"""
|
|
280
|
+
Query with GraphRAG: vector search + graph context.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
question: User question
|
|
284
|
+
k: Number of vector results
|
|
285
|
+
use_graph: Include graph context
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
GraphRAGResponse with answer and sources
|
|
289
|
+
"""
|
|
290
|
+
# Get vector context
|
|
291
|
+
vector_context, sources = self._get_vector_context(question, k)
|
|
292
|
+
|
|
293
|
+
# Get graph context
|
|
294
|
+
entities = []
|
|
295
|
+
relationships = []
|
|
296
|
+
graph_context = ""
|
|
297
|
+
|
|
298
|
+
if use_graph:
|
|
299
|
+
entities, relationships = self._get_graph_context(question, sources)
|
|
300
|
+
|
|
301
|
+
if entities:
|
|
302
|
+
entity_strs = [f"{e['name']} ({e['type']})" for e in entities[:10]]
|
|
303
|
+
rel_strs = [
|
|
304
|
+
f"{r['source']} --{r['relationship']}-> {r['target']}"
|
|
305
|
+
for r in relationships[:15]
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
graph_context = f"Entities: {', '.join(entity_strs)}\n"
|
|
309
|
+
graph_context += "Relationships:\n" + "\n".join(rel_strs)
|
|
310
|
+
|
|
311
|
+
# Generate answer
|
|
312
|
+
if not vector_context and not entities:
|
|
313
|
+
return GraphRAGResponse(
|
|
314
|
+
answer="I couldn't find relevant information.",
|
|
315
|
+
sources=[],
|
|
316
|
+
entities=[],
|
|
317
|
+
relationships=[],
|
|
318
|
+
graph_context="",
|
|
319
|
+
vector_context="",
|
|
320
|
+
confidence=0.0,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Build prompt
|
|
324
|
+
entity_str = (
|
|
325
|
+
", ".join(f"{e['name']} ({e['type']})" for e in entities)
|
|
326
|
+
if entities
|
|
327
|
+
else "None found"
|
|
328
|
+
)
|
|
329
|
+
rel_str = (
|
|
330
|
+
"\n".join(
|
|
331
|
+
f" - {r['source']} --{r['relationship']}-> {r['target']}"
|
|
332
|
+
for r in relationships
|
|
333
|
+
)
|
|
334
|
+
if relationships
|
|
335
|
+
else "None found"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
prompt = GRAPH_QUERY_PROMPT.format(
|
|
339
|
+
question=question,
|
|
340
|
+
vector_context=vector_context or "No document context available.",
|
|
341
|
+
entities=entity_str,
|
|
342
|
+
relationships=rel_str,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Call LLM
|
|
346
|
+
try:
|
|
347
|
+
from clm.layer3.agency import chat_monad
|
|
348
|
+
|
|
349
|
+
response = chat_monad(
|
|
350
|
+
prompt=prompt,
|
|
351
|
+
system_prompt=GRAPHRAG_SYSTEM_PROMPT,
|
|
352
|
+
model=self.llm_model,
|
|
353
|
+
temperature=0.3,
|
|
354
|
+
max_tokens=1500,
|
|
355
|
+
).unsafe_run()
|
|
356
|
+
|
|
357
|
+
if response.is_right():
|
|
358
|
+
answer = response.value.get("content", str(response.value))
|
|
359
|
+
else:
|
|
360
|
+
answer = f"Error generating response: {response.value}"
|
|
361
|
+
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.error(f"LLM generation failed: {e}", exc_info=True)
|
|
364
|
+
answer = f"Error: {e}"
|
|
365
|
+
|
|
366
|
+
# Calculate confidence
|
|
367
|
+
vec_results = self.vector_store.search(question, k)
|
|
368
|
+
avg_score = (
|
|
369
|
+
sum(r.score for r in vec_results) / len(vec_results) if vec_results else 0
|
|
370
|
+
)
|
|
371
|
+
graph_boost = 0.1 if entities else 0
|
|
372
|
+
|
|
373
|
+
return GraphRAGResponse(
|
|
374
|
+
answer=answer,
|
|
375
|
+
sources=sources,
|
|
376
|
+
entities=entities,
|
|
377
|
+
relationships=relationships,
|
|
378
|
+
graph_context=graph_context,
|
|
379
|
+
vector_context=vector_context,
|
|
380
|
+
confidence=min(avg_score + graph_boost, 1.0),
|
|
381
|
+
metadata={
|
|
382
|
+
"model": self.llm_model,
|
|
383
|
+
"entity_count": len(entities),
|
|
384
|
+
"relationship_count": len(relationships),
|
|
385
|
+
},
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
def explore_entity(self, entity_name: str, hops: int = 2) -> dict[str, Any]:
|
|
389
|
+
"""
|
|
390
|
+
Explore an entity and its neighborhood.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
entity_name: Entity to explore
|
|
394
|
+
hops: Traversal depth
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Dict with entity info and related entities
|
|
398
|
+
"""
|
|
399
|
+
# Try exact match first
|
|
400
|
+
entity = self.graph_store.get_entity_by_name(entity_name)
|
|
401
|
+
|
|
402
|
+
# If not found, try fuzzy search
|
|
403
|
+
if not entity:
|
|
404
|
+
candidates = self.graph_store.search_entities(entity_name, limit=1)
|
|
405
|
+
if candidates:
|
|
406
|
+
entity = candidates[0]
|
|
407
|
+
# Update outgoing/incoming/related to use the found entity name/id
|
|
408
|
+
|
|
409
|
+
if not entity:
|
|
410
|
+
return {"error": f"Entity '{entity_name}' not found"}
|
|
411
|
+
|
|
412
|
+
related = self.graph_store.find_related(entity["name"], hops=hops)
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
"entity": entity,
|
|
416
|
+
"related": related,
|
|
417
|
+
"outgoing": self.graph_store.get_relationships_from(entity["id"]),
|
|
418
|
+
"incoming": self.graph_store.get_relationships_to(entity["id"]),
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
def find_connection(self, source: str, target: str) -> list[str] | None:
|
|
422
|
+
"""Find path between two entities."""
|
|
423
|
+
return self.graph_store.find_path(source, target)
|
|
424
|
+
|
|
425
|
+
def get_stats(self) -> dict[str, Any]:
|
|
426
|
+
"""Get engine statistics."""
|
|
427
|
+
return {
|
|
428
|
+
"vector_count": self.vector_store.count(),
|
|
429
|
+
"unique_mcards": self.vector_store.count_unique(),
|
|
430
|
+
"graph": self.graph_store.get_stats(),
|
|
431
|
+
"llm_model": self.llm_model,
|
|
432
|
+
"embedding_model": self.config.embedding_model,
|
|
433
|
+
}
|