graphrefly 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.
Files changed (47) hide show
  1. graphrefly/__init__.py +160 -0
  2. graphrefly/compat/__init__.py +18 -0
  3. graphrefly/compat/async_utils.py +228 -0
  4. graphrefly/compat/asyncio_runner.py +89 -0
  5. graphrefly/compat/trio_runner.py +81 -0
  6. graphrefly/core/__init__.py +142 -0
  7. graphrefly/core/clock.py +20 -0
  8. graphrefly/core/dynamic_node.py +749 -0
  9. graphrefly/core/guard.py +277 -0
  10. graphrefly/core/meta.py +149 -0
  11. graphrefly/core/node.py +963 -0
  12. graphrefly/core/protocol.py +460 -0
  13. graphrefly/core/runner.py +107 -0
  14. graphrefly/core/subgraph_locks.py +296 -0
  15. graphrefly/core/sugar.py +138 -0
  16. graphrefly/core/versioning.py +193 -0
  17. graphrefly/extra/__init__.py +313 -0
  18. graphrefly/extra/adapters.py +2149 -0
  19. graphrefly/extra/backoff.py +287 -0
  20. graphrefly/extra/backpressure.py +113 -0
  21. graphrefly/extra/checkpoint.py +307 -0
  22. graphrefly/extra/composite.py +303 -0
  23. graphrefly/extra/cron.py +133 -0
  24. graphrefly/extra/data_structures.py +707 -0
  25. graphrefly/extra/resilience.py +727 -0
  26. graphrefly/extra/sources.py +766 -0
  27. graphrefly/extra/tier1.py +1067 -0
  28. graphrefly/extra/tier2.py +1802 -0
  29. graphrefly/graph/__init__.py +31 -0
  30. graphrefly/graph/graph.py +2249 -0
  31. graphrefly/integrations/__init__.py +1 -0
  32. graphrefly/integrations/fastapi.py +767 -0
  33. graphrefly/patterns/__init__.py +5 -0
  34. graphrefly/patterns/ai.py +2132 -0
  35. graphrefly/patterns/cqrs.py +515 -0
  36. graphrefly/patterns/memory.py +639 -0
  37. graphrefly/patterns/messaging.py +553 -0
  38. graphrefly/patterns/orchestration.py +536 -0
  39. graphrefly/patterns/reactive_layout/__init__.py +81 -0
  40. graphrefly/patterns/reactive_layout/measurement_adapters.py +276 -0
  41. graphrefly/patterns/reactive_layout/reactive_block_layout.py +434 -0
  42. graphrefly/patterns/reactive_layout/reactive_layout.py +943 -0
  43. graphrefly/py.typed +1 -0
  44. graphrefly-0.1.0.dist-info/METADATA +253 -0
  45. graphrefly-0.1.0.dist-info/RECORD +47 -0
  46. graphrefly-0.1.0.dist-info/WHEEL +4 -0
  47. graphrefly-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,639 @@
1
+ """Memory patterns (roadmap §4.3).
2
+
3
+ Domain-layer helpers composed from GraphRefly primitives. ``vector_index`` uses
4
+ exact cosine search by default; HNSW can be wired as an optional dependency
5
+ through ``hnsw_factory``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from dataclasses import dataclass
12
+ from types import MappingProxyType
13
+ from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeVar
14
+
15
+ from graphrefly.core.clock import monotonic_ns
16
+ from graphrefly.core.protocol import MessageType
17
+ from graphrefly.core.sugar import derived, state
18
+ from graphrefly.graph.graph import Graph
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Callable, Mapping, Sequence
22
+
23
+ from graphrefly.core.node import Node, NodeImpl
24
+
25
+ T = TypeVar("T")
26
+ TEntity = TypeVar("TEntity")
27
+ TRelation = TypeVar("TRelation", bound=str)
28
+ TMeta = TypeVar("TMeta")
29
+
30
+ CollectionPolicy = Literal["fifo", "lru"]
31
+ VectorBackend = Literal["flat", "hnsw"]
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class LightCollectionEntry[T]:
36
+ """Collection item metadata used by ``light_collection``."""
37
+
38
+ id: str
39
+ value: T
40
+ created_at_ns: int
41
+ last_access_ns: int
42
+
43
+
44
+ def decay(
45
+ base_score: float, age_seconds: float, rate_per_second: float, min_score: float = 0.0
46
+ ) -> float:
47
+ """Exponential score decay with floor."""
48
+ if not math.isfinite(base_score):
49
+ return float(min_score)
50
+ if not math.isfinite(age_seconds) or age_seconds <= 0:
51
+ return max(min_score, float(base_score))
52
+ if not math.isfinite(rate_per_second) or rate_per_second <= 0:
53
+ return max(min_score, float(base_score))
54
+ decayed = float(base_score) * math.exp(-rate_per_second * age_seconds)
55
+ return max(min_score, decayed)
56
+
57
+
58
+ class LightCollection[T]:
59
+ """FIFO/LRU collection without reactive scoring."""
60
+
61
+ __slots__ = ("_entries", "_entries_node", "_max_size", "_policy")
62
+
63
+ def __init__(
64
+ self,
65
+ *,
66
+ max_size: int | None = None,
67
+ policy: CollectionPolicy = "fifo",
68
+ name: str | None = None,
69
+ ) -> None:
70
+ if max_size is not None and max_size < 1:
71
+ raise ValueError("max_size must be >= 1")
72
+ self._max_size = max_size
73
+ self._policy = policy
74
+ self._entries: dict[str, LightCollectionEntry[T]] = {}
75
+ self._entries_node: NodeImpl[Mapping[str, LightCollectionEntry[T]]] = state(
76
+ MappingProxyType({}),
77
+ name=name,
78
+ describe_kind="state",
79
+ )
80
+
81
+ @property
82
+ def entries(self) -> Node[Mapping[str, LightCollectionEntry[T]]]:
83
+ return self._entries_node
84
+
85
+ def _commit(self) -> None:
86
+ snap = MappingProxyType(dict(self._entries))
87
+ self._entries_node.down([(MessageType.DATA, snap)])
88
+
89
+ def _evict_if_needed(self) -> None:
90
+ if self._max_size is None:
91
+ return
92
+ while len(self._entries) > self._max_size:
93
+ victim: LightCollectionEntry[T] | None = None
94
+ for entry in self._entries.values():
95
+ if victim is None:
96
+ victim = entry
97
+ continue
98
+ lhs = entry.last_access_ns if self._policy == "lru" else entry.created_at_ns
99
+ rhs = victim.last_access_ns if self._policy == "lru" else victim.created_at_ns
100
+ if lhs < rhs:
101
+ victim = entry
102
+ if victim is None:
103
+ return
104
+ self._entries.pop(victim.id, None)
105
+
106
+ def upsert(self, item_id: str, value: T) -> None:
107
+ now = monotonic_ns()
108
+ prev = self._entries.get(item_id)
109
+ self._entries[item_id] = LightCollectionEntry(
110
+ id=item_id,
111
+ value=value,
112
+ created_at_ns=prev.created_at_ns if prev is not None else now,
113
+ last_access_ns=now,
114
+ )
115
+ self._evict_if_needed()
116
+ self._commit()
117
+
118
+ def remove(self, item_id: str) -> None:
119
+ if item_id not in self._entries:
120
+ return
121
+ del self._entries[item_id]
122
+ self._commit()
123
+
124
+ def clear(self) -> None:
125
+ if not self._entries:
126
+ return
127
+ self._entries.clear()
128
+ self._commit()
129
+
130
+ def get(self, item_id: str) -> T | None:
131
+ found = self._entries.get(item_id)
132
+ if found is None:
133
+ return None
134
+ if self._policy == "lru":
135
+ refreshed = LightCollectionEntry(
136
+ id=found.id,
137
+ value=found.value,
138
+ created_at_ns=found.created_at_ns,
139
+ last_access_ns=monotonic_ns(),
140
+ )
141
+ self._entries[item_id] = refreshed
142
+ self._commit()
143
+ return refreshed.value
144
+ return found.value
145
+
146
+ def has(self, item_id: str) -> bool:
147
+ return item_id in self._entries
148
+
149
+
150
+ @dataclass(frozen=True, slots=True)
151
+ class CollectionEntry[T]:
152
+ id: str
153
+ value: T
154
+ base_score: float
155
+ created_at_ns: int
156
+ last_access_ns: int
157
+
158
+
159
+ @dataclass(frozen=True, slots=True)
160
+ class RankedCollectionEntry[T]:
161
+ id: str
162
+ value: T
163
+ base_score: float
164
+ score: float
165
+ created_at_ns: int
166
+ last_access_ns: int
167
+
168
+
169
+ class CollectionGraph[T](Graph):
170
+ """Scored memory collection represented as a graph."""
171
+
172
+ __slots__ = (
173
+ "_items",
174
+ "_items_node",
175
+ "_max_size",
176
+ "_policy",
177
+ "_score_fn",
178
+ "_decay_rate",
179
+ "_min_score",
180
+ )
181
+
182
+ def __init__(
183
+ self,
184
+ name: str,
185
+ *,
186
+ max_size: int | None = None,
187
+ policy: CollectionPolicy = "lru",
188
+ score: Callable[[T], float] | None = None,
189
+ decay_rate: float = 0.0,
190
+ min_score: float = 0.0,
191
+ opts: dict[str, Any] | None = None,
192
+ ) -> None:
193
+ super().__init__(name, opts)
194
+ if max_size is not None and max_size < 1:
195
+ raise ValueError("max_size must be >= 1")
196
+ self._max_size = max_size
197
+ self._policy = policy
198
+ self._score_fn = score if score is not None else (lambda _v: 1.0)
199
+ self._decay_rate = decay_rate
200
+ self._min_score = min_score
201
+ self._items: dict[str, CollectionEntry[T]] = {}
202
+ self._items_node: NodeImpl[Mapping[str, CollectionEntry[T]]] = state(
203
+ MappingProxyType({}),
204
+ name="items",
205
+ describe_kind="state",
206
+ )
207
+ ranked = derived(
208
+ [self._items_node],
209
+ lambda deps, _a: self._ranked(deps[0]),
210
+ initial=[],
211
+ name="ranked",
212
+ )
213
+ size = derived(
214
+ [self._items_node],
215
+ lambda deps, _a: len(deps[0]),
216
+ initial=0,
217
+ name="size",
218
+ )
219
+ ranked.subscribe(lambda _msgs: None)
220
+ size.subscribe(lambda _msgs: None)
221
+ self.add("items", self._items_node)
222
+ self.add("ranked", ranked)
223
+ self.add("size", size)
224
+ self.connect("items", "ranked")
225
+ self.connect("items", "size")
226
+
227
+ def _commit(self) -> None:
228
+ self._items_node.down([(MessageType.DATA, MappingProxyType(dict(self._items)))])
229
+
230
+ def _effective(self, entry: CollectionEntry[T], now_ns: int) -> float:
231
+ age_seconds = (now_ns - entry.last_access_ns) / 1_000_000_000
232
+ return decay(entry.base_score, age_seconds, self._decay_rate, self._min_score)
233
+
234
+ def _ranked(
235
+ self,
236
+ snapshot: Mapping[str, CollectionEntry[T]],
237
+ ) -> list[RankedCollectionEntry[T]]:
238
+ now = monotonic_ns()
239
+ out = [
240
+ RankedCollectionEntry(
241
+ id=row.id,
242
+ value=row.value,
243
+ base_score=row.base_score,
244
+ score=self._effective(row, now),
245
+ created_at_ns=row.created_at_ns,
246
+ last_access_ns=row.last_access_ns,
247
+ )
248
+ for row in snapshot.values()
249
+ ]
250
+ out.sort(key=lambda row: (row.score, row.last_access_ns), reverse=True)
251
+ return out
252
+
253
+ def _evict_if_needed(self) -> None:
254
+ if self._max_size is None:
255
+ return
256
+ while len(self._items) > self._max_size:
257
+ now = monotonic_ns()
258
+ victim: CollectionEntry[T] | None = None
259
+ victim_score = float("inf")
260
+ for row in self._items.values():
261
+ score = self._effective(row, now)
262
+ if score < victim_score:
263
+ victim = row
264
+ victim_score = score
265
+ continue
266
+ if score == victim_score and victim is not None:
267
+ lhs = row.last_access_ns if self._policy == "lru" else row.created_at_ns
268
+ rhs = victim.last_access_ns if self._policy == "lru" else victim.created_at_ns
269
+ if lhs < rhs:
270
+ victim = row
271
+ if victim is None:
272
+ return
273
+ self._items.pop(victim.id, None)
274
+
275
+ def upsert(self, item_id: str, value: T, *, score: float | None = None) -> None:
276
+ now = monotonic_ns()
277
+ prev = self._items.get(item_id)
278
+ base_score = float(score if score is not None else self._score_fn(value))
279
+ self._items[item_id] = CollectionEntry(
280
+ id=item_id,
281
+ value=value,
282
+ base_score=base_score,
283
+ created_at_ns=prev.created_at_ns if prev is not None else now,
284
+ last_access_ns=now,
285
+ )
286
+ self._evict_if_needed()
287
+ self._commit()
288
+
289
+ def remove(self, item_id: str) -> None:
290
+ if item_id not in self._items:
291
+ return
292
+ del self._items[item_id]
293
+ self._commit()
294
+
295
+ def clear(self) -> None:
296
+ if not self._items:
297
+ return
298
+ self._items.clear()
299
+ self._commit()
300
+
301
+ def get_item(self, item_id: str) -> CollectionEntry[T] | None:
302
+ row = self._items.get(item_id)
303
+ if row is None:
304
+ return None
305
+ if self._policy == "lru":
306
+ refreshed = CollectionEntry(
307
+ id=row.id,
308
+ value=row.value,
309
+ base_score=row.base_score,
310
+ created_at_ns=row.created_at_ns,
311
+ last_access_ns=monotonic_ns(),
312
+ )
313
+ self._items[item_id] = refreshed
314
+ self._commit()
315
+ return refreshed
316
+ return row
317
+
318
+
319
+ @dataclass(frozen=True, slots=True)
320
+ class VectorRecord[TMeta]:
321
+ id: str
322
+ vector: tuple[float, ...]
323
+ meta: TMeta | None
324
+
325
+
326
+ @dataclass(frozen=True, slots=True)
327
+ class VectorSearchResult[TMeta]:
328
+ id: str
329
+ score: float
330
+ meta: TMeta | None
331
+
332
+
333
+ class HnswAdapter[TMeta](Protocol):
334
+ def upsert(self, item_id: str, vector: Sequence[float], meta: TMeta | None = None) -> None: ...
335
+
336
+ def remove(self, item_id: str) -> None: ...
337
+
338
+ def clear(self) -> None: ...
339
+
340
+ def search(self, query: Sequence[float], k: int) -> Sequence[VectorSearchResult[TMeta]]: ...
341
+
342
+
343
+ class VectorIndex[TMeta]:
344
+ """Vector index with optional HNSW backend."""
345
+
346
+ __slots__ = ("_backend", "_dimension", "_entries", "_entries_node", "_hnsw")
347
+
348
+ def __init__(
349
+ self,
350
+ *,
351
+ backend: VectorBackend = "flat",
352
+ dimension: int | None = None,
353
+ hnsw_factory: Callable[[], HnswAdapter[TMeta]] | None = None,
354
+ ) -> None:
355
+ self._backend = backend
356
+ self._dimension = dimension
357
+ self._entries: dict[str, VectorRecord[TMeta]] = {}
358
+ self._entries_node: NodeImpl[Mapping[str, VectorRecord[TMeta]]] = state(
359
+ MappingProxyType({}),
360
+ name="vector_index",
361
+ describe_kind="state",
362
+ )
363
+ self._hnsw = hnsw_factory() if backend == "hnsw" and hnsw_factory is not None else None
364
+ if backend == "hnsw" and self._hnsw is None:
365
+ msg = (
366
+ 'vector_index backend "hnsw" requires an optional dependency adapter; '
367
+ "install your HNSW package and provide hnsw_factory."
368
+ )
369
+ raise ValueError(msg)
370
+
371
+ @property
372
+ def backend(self) -> VectorBackend:
373
+ return self._backend
374
+
375
+ @property
376
+ def entries(self) -> Node[Mapping[str, VectorRecord[TMeta]]]:
377
+ return self._entries_node
378
+
379
+ def _commit(self) -> None:
380
+ self._entries_node.down([(MessageType.DATA, MappingProxyType(dict(self._entries)))])
381
+
382
+ def _assert_dimension(self, vector: Sequence[float]) -> None:
383
+ if self._dimension is not None and len(vector) != self._dimension:
384
+ raise ValueError(
385
+ f"vector dimension mismatch: expected {self._dimension}, got {len(vector)}"
386
+ )
387
+
388
+ def upsert(self, item_id: str, vector: Sequence[float], meta: TMeta | None = None) -> None:
389
+ self._assert_dimension(vector)
390
+ row = VectorRecord(id=item_id, vector=tuple(float(v) for v in vector), meta=meta)
391
+ self._entries[item_id] = row
392
+ if self._backend == "hnsw":
393
+ self._hnsw.upsert(item_id, vector, meta) # type: ignore[union-attr]
394
+ self._commit()
395
+
396
+ def remove(self, item_id: str) -> None:
397
+ if item_id not in self._entries:
398
+ return
399
+ del self._entries[item_id]
400
+ if self._backend == "hnsw":
401
+ self._hnsw.remove(item_id) # type: ignore[union-attr]
402
+ self._commit()
403
+
404
+ def clear(self) -> None:
405
+ if not self._entries:
406
+ return
407
+ self._entries.clear()
408
+ if self._backend == "hnsw":
409
+ self._hnsw.clear() # type: ignore[union-attr]
410
+ self._commit()
411
+
412
+ def search(self, query: Sequence[float], k: int = 5) -> list[VectorSearchResult[TMeta]]:
413
+ self._assert_dimension(query)
414
+ if k <= 0:
415
+ return []
416
+ if self._backend == "hnsw":
417
+ return list(self._hnsw.search(query, k)) # type: ignore[union-attr]
418
+ query_tuple = tuple(float(v) for v in query)
419
+ scored = [
420
+ VectorSearchResult(
421
+ id=row.id, score=_cosine_similarity(query_tuple, row.vector), meta=row.meta
422
+ )
423
+ for row in self._entries.values()
424
+ ]
425
+ scored.sort(key=lambda row: row.score, reverse=True)
426
+ return scored[:k]
427
+
428
+
429
+ @dataclass(frozen=True, slots=True)
430
+ class KnowledgeEdge[TRelation: str]:
431
+ from_id: str
432
+ to_id: str
433
+ relation: TRelation
434
+ weight: float
435
+
436
+
437
+ class KnowledgeGraph[TEntity, TRelation: str](Graph):
438
+ """Entity + relation graph represented as a GraphRefly graph."""
439
+
440
+ __slots__ = ("_entities", "_relations", "_entities_node", "_edges_node")
441
+
442
+ def __init__(self, name: str, opts: dict[str, Any] | None = None) -> None:
443
+ super().__init__(name, opts)
444
+ self._entities: dict[str, TEntity] = {}
445
+ self._relations: list[KnowledgeEdge[TRelation]] = []
446
+ self._entities_node: NodeImpl[Mapping[str, TEntity]] = state(
447
+ MappingProxyType({}),
448
+ name="entities",
449
+ describe_kind="state",
450
+ )
451
+ self._edges_node: NodeImpl[tuple[KnowledgeEdge[TRelation], ...]] = state(
452
+ (),
453
+ name="edges",
454
+ describe_kind="state",
455
+ )
456
+ adjacency = derived(
457
+ [self._edges_node],
458
+ lambda deps, _a: _build_adjacency(deps[0]),
459
+ initial=MappingProxyType({}),
460
+ name="adjacency",
461
+ )
462
+ adjacency.subscribe(lambda _msgs: None)
463
+ self.add("entities", self._entities_node)
464
+ self.add("edges", self._edges_node)
465
+ self.add("adjacency", adjacency)
466
+ self.connect("edges", "adjacency")
467
+
468
+ def _commit_entities(self) -> None:
469
+ self._entities_node.down([(MessageType.DATA, MappingProxyType(dict(self._entities)))])
470
+
471
+ def _commit_edges(self) -> None:
472
+ self._edges_node.down([(MessageType.DATA, tuple(self._relations))])
473
+
474
+ def upsert_entity(self, entity_id: str, value: TEntity) -> None:
475
+ self._entities[entity_id] = value
476
+ self._commit_entities()
477
+
478
+ def remove_entity(self, entity_id: str) -> None:
479
+ had_entity = entity_id in self._entities
480
+ if had_entity:
481
+ del self._entities[entity_id]
482
+ next_edges = [
483
+ edge
484
+ for edge in self._relations
485
+ if edge.from_id != entity_id and edge.to_id != entity_id
486
+ ]
487
+ if not had_entity and len(next_edges) == len(self._relations):
488
+ return
489
+ self._relations = next_edges
490
+ self._commit_entities()
491
+ self._commit_edges()
492
+
493
+ def link(self, from_id: str, to_id: str, relation: TRelation, weight: float = 1.0) -> None:
494
+ key = (from_id, to_id, relation)
495
+ replaced = False
496
+ next_edges: list[KnowledgeEdge[TRelation]] = []
497
+ for edge in self._relations:
498
+ if (edge.from_id, edge.to_id, edge.relation) == key:
499
+ next_edges.append(
500
+ KnowledgeEdge(
501
+ from_id=from_id, to_id=to_id, relation=relation, weight=float(weight)
502
+ )
503
+ )
504
+ replaced = True
505
+ else:
506
+ next_edges.append(edge)
507
+ if not replaced:
508
+ next_edges.append(
509
+ KnowledgeEdge(from_id=from_id, to_id=to_id, relation=relation, weight=float(weight))
510
+ )
511
+ self._relations = next_edges
512
+ self._commit_edges()
513
+
514
+ def unlink(self, from_id: str, to_id: str, relation: TRelation | None = None) -> None:
515
+ next_edges = [
516
+ edge
517
+ for edge in self._relations
518
+ if not (
519
+ edge.from_id == from_id
520
+ and edge.to_id == to_id
521
+ and (relation is None or edge.relation == relation)
522
+ )
523
+ ]
524
+ if len(next_edges) == len(self._relations):
525
+ return
526
+ self._relations = next_edges
527
+ self._commit_edges()
528
+
529
+ def related(
530
+ self, entity_id: str, relation: TRelation | None = None
531
+ ) -> list[KnowledgeEdge[TRelation]]:
532
+ return [
533
+ edge
534
+ for edge in self._relations
535
+ if (edge.from_id == entity_id or edge.to_id == entity_id)
536
+ and (relation is None or edge.relation == relation)
537
+ ]
538
+
539
+
540
+ def light_collection(
541
+ *,
542
+ max_size: int | None = None,
543
+ policy: CollectionPolicy = "fifo",
544
+ name: str | None = None,
545
+ ) -> LightCollection[Any]:
546
+ """Create an unscored FIFO/LRU collection."""
547
+ return LightCollection(max_size=max_size, policy=policy, name=name)
548
+
549
+
550
+ def collection(
551
+ name: str,
552
+ *,
553
+ max_size: int | None = None,
554
+ policy: CollectionPolicy = "lru",
555
+ score: Callable[[Any], float] | None = None,
556
+ decay_rate: float = 0.0,
557
+ min_score: float = 0.0,
558
+ opts: dict[str, Any] | None = None,
559
+ ) -> CollectionGraph[Any]:
560
+ """Create a scored memory collection graph."""
561
+ return CollectionGraph(
562
+ name,
563
+ max_size=max_size,
564
+ policy=policy,
565
+ score=score,
566
+ decay_rate=decay_rate,
567
+ min_score=min_score,
568
+ opts=opts,
569
+ )
570
+
571
+
572
+ def vector_index(
573
+ *,
574
+ backend: VectorBackend = "flat",
575
+ dimension: int | None = None,
576
+ hnsw_factory: Callable[[], HnswAdapter[Any]] | None = None,
577
+ ) -> VectorIndex[Any]:
578
+ """Create a vector index with optional HNSW backend adapter."""
579
+ return VectorIndex(
580
+ backend=backend,
581
+ dimension=dimension,
582
+ hnsw_factory=hnsw_factory,
583
+ )
584
+
585
+
586
+ def knowledge_graph(
587
+ name: str,
588
+ *,
589
+ opts: dict[str, Any] | None = None,
590
+ ) -> KnowledgeGraph[Any, str]:
591
+ """Create a knowledge graph domain graph."""
592
+ return KnowledgeGraph(name, opts=opts)
593
+
594
+
595
+ def _cosine_similarity(a: Sequence[float], b: Sequence[float]) -> float:
596
+ n = max(len(a), len(b))
597
+ dot = 0.0
598
+ na = 0.0
599
+ nb = 0.0
600
+ for i in range(n):
601
+ avf = float(a[i]) if i < len(a) else 0.0
602
+ bvf = float(b[i]) if i < len(b) else 0.0
603
+ dot += avf * bvf
604
+ na += avf * avf
605
+ nb += bvf * bvf
606
+ if na == 0.0 or nb == 0.0:
607
+ return 0.0
608
+ return dot / math.sqrt(na * nb)
609
+
610
+
611
+ def _build_adjacency(
612
+ edges: Sequence[KnowledgeEdge[Any]],
613
+ ) -> Mapping[str, tuple[KnowledgeEdge[Any], ...]]:
614
+ raw: dict[str, list[KnowledgeEdge[Any]]] = {}
615
+ for edge in edges:
616
+ raw.setdefault(edge.from_id, []).append(edge)
617
+ return MappingProxyType({key: tuple(vals) for key, vals in raw.items()})
618
+
619
+
620
+ __all__ = [
621
+ "CollectionEntry",
622
+ "CollectionGraph",
623
+ "CollectionPolicy",
624
+ "HnswAdapter",
625
+ "KnowledgeEdge",
626
+ "KnowledgeGraph",
627
+ "LightCollection",
628
+ "LightCollectionEntry",
629
+ "RankedCollectionEntry",
630
+ "VectorBackend",
631
+ "VectorIndex",
632
+ "VectorRecord",
633
+ "VectorSearchResult",
634
+ "collection",
635
+ "decay",
636
+ "knowledge_graph",
637
+ "light_collection",
638
+ "vector_index",
639
+ ]