synapse-framework 0.2.4__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.
- synapse/__init__.py +207 -0
- synapse/app.py +942 -0
- synapse/cli.py +88 -0
- synapse/config.py +103 -0
- synapse/connectors/__init__.py +29 -0
- synapse/connectors/base.py +222 -0
- synapse/connectors/factory.py +202 -0
- synapse/connectors/gmail.py +292 -0
- synapse/connectors/mcp_wrapper.py +153 -0
- synapse/connectors/notion.py +161 -0
- synapse/connectors/photos.py +141 -0
- synapse/database/__init__.py +19 -0
- synapse/database/base.py +63 -0
- synapse/database/chroma.py +175 -0
- synapse/database/memory.py +132 -0
- synapse/database/pgvector.py +99 -0
- synapse/database/qdrant.py +138 -0
- synapse/embeddings/__init__.py +21 -0
- synapse/embeddings/base.py +262 -0
- synapse/export/__init__.py +14 -0
- synapse/export/mcp_exporter.py +202 -0
- synapse/graph/__init__.py +21 -0
- synapse/graph/base.py +130 -0
- synapse/graph/memory.py +20 -0
- synapse/graph/neo4j_store.py +176 -0
- synapse/graph/networkx_store.py +221 -0
- synapse/inspector/__init__.py +5 -0
- synapse/inspector/template.py +530 -0
- synapse/models/__init__.py +73 -0
- synapse/models/base.py +95 -0
- synapse/models/gemini.py +328 -0
- synapse/models/ollama.py +322 -0
- synapse/models/openai.py +289 -0
- synapse/oauth/__init__.py +7 -0
- synapse/oauth/google.py +194 -0
- synapse/oauth/notion.py +35 -0
- synapse/rag/__init__.py +8 -0
- synapse/rag/chunker.py +219 -0
- synapse/rag/hybrid_rag.py +122 -0
- synapse/resource/__init__.py +5 -0
- synapse/resource/resource.py +304 -0
- synapse/resources/__init__.py +5 -0
- synapse/skills/__init__.py +5 -0
- synapse/skills/skill.py +321 -0
- synapse/storage/__init__.py +24 -0
- synapse/storage/graph_exporter.py +624 -0
- synapse/storage/graph_store.py +162 -0
- synapse/storage/vector_store.py +103 -0
- synapse_framework-0.2.4.dist-info/METADATA +415 -0
- synapse_framework-0.2.4.dist-info/RECORD +54 -0
- synapse_framework-0.2.4.dist-info/WHEEL +5 -0
- synapse_framework-0.2.4.dist-info/entry_points.txt +2 -0
- synapse_framework-0.2.4.dist-info/licenses/LICENSE +21 -0
- synapse_framework-0.2.4.dist-info/top_level.txt +1 -0
synapse/graph/base.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Abstract base interface for pluggable knowledge graph stores in Synapse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseGraphStore(ABC):
|
|
10
|
+
"""Abstract interface that all Synapse knowledge graph backends must implement.
|
|
11
|
+
|
|
12
|
+
Ensures interchangeability between NetworkX, in-memory graphs, Neo4j,
|
|
13
|
+
or other graph databases.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def add_node_entity(
|
|
18
|
+
self,
|
|
19
|
+
name: str,
|
|
20
|
+
entity_type: str = "Default",
|
|
21
|
+
color: str | None = None,
|
|
22
|
+
attributes: dict[str, Any] | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Adds or updates an entity node in the graph."""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def add_relation(
|
|
28
|
+
self,
|
|
29
|
+
source: str,
|
|
30
|
+
relation: str,
|
|
31
|
+
target: str,
|
|
32
|
+
source_type: str = "Default",
|
|
33
|
+
target_type: str = "Default",
|
|
34
|
+
metadata: dict[str, Any] | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Adds a directed relation edge between source and target entities."""
|
|
37
|
+
|
|
38
|
+
def add_node(
|
|
39
|
+
self,
|
|
40
|
+
name: str,
|
|
41
|
+
entity_type: str = "Default",
|
|
42
|
+
color: str | None = None,
|
|
43
|
+
attributes: dict[str, Any] | None = None,
|
|
44
|
+
**kwargs: Any,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Convenience alias for add_node_entity."""
|
|
47
|
+
attrs = dict(attributes or {})
|
|
48
|
+
attrs.update(kwargs)
|
|
49
|
+
self.add_node_entity(name=name, entity_type=entity_type, color=color, attributes=attrs)
|
|
50
|
+
|
|
51
|
+
def add_edge(
|
|
52
|
+
self,
|
|
53
|
+
source: str,
|
|
54
|
+
target_or_rel: str,
|
|
55
|
+
target: str | None = None,
|
|
56
|
+
relation: str = "CONNECTED_TO",
|
|
57
|
+
source_type: str = "Default",
|
|
58
|
+
target_type: str = "Default",
|
|
59
|
+
metadata: dict[str, Any] | None = None,
|
|
60
|
+
**kwargs: Any,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Convenience alias for add_relation.
|
|
63
|
+
|
|
64
|
+
Supports both:
|
|
65
|
+
add_edge(u, v, relation="CONNECTED_TO")
|
|
66
|
+
add_edge(u, "RELATION", v)
|
|
67
|
+
"""
|
|
68
|
+
meta = dict(metadata or {})
|
|
69
|
+
meta.update(kwargs)
|
|
70
|
+
if target is not None:
|
|
71
|
+
# 3 positional args: source, target_or_rel, target
|
|
72
|
+
# e.g., add_edge("alice", "WORKS_AT", "synapse") -> relation="WORKS_AT", target="synapse"
|
|
73
|
+
# or add_edge("alice", "synapse", "WORKS_AT") -> relation="WORKS_AT", target="synapse"
|
|
74
|
+
if target_or_rel.isupper() and not target.isupper():
|
|
75
|
+
actual_rel = target_or_rel
|
|
76
|
+
actual_target = target
|
|
77
|
+
else:
|
|
78
|
+
actual_target = target_or_rel
|
|
79
|
+
actual_rel = target
|
|
80
|
+
self.add_relation(
|
|
81
|
+
source=source,
|
|
82
|
+
relation=actual_rel,
|
|
83
|
+
target=actual_target,
|
|
84
|
+
source_type=source_type,
|
|
85
|
+
target_type=target_type,
|
|
86
|
+
metadata=meta,
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
# 2 positional args: source, target
|
|
90
|
+
self.add_relation(
|
|
91
|
+
source=source,
|
|
92
|
+
relation=relation,
|
|
93
|
+
target=target_or_rel,
|
|
94
|
+
source_type=source_type,
|
|
95
|
+
target_type=target_type,
|
|
96
|
+
metadata=meta,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def find_relation_for(self, entity: str) -> list[str]:
|
|
102
|
+
"""Finds all incoming and outgoing relational facts for a given entity."""
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def get_3d_payload(self) -> dict[str, Any]:
|
|
106
|
+
"""Returns node and link data formatted for 3D force-directed visualization."""
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def get_stats(self) -> dict[str, Any]:
|
|
110
|
+
"""Returns summary node and edge counts."""
|
|
111
|
+
|
|
112
|
+
def count_nodes(self) -> int:
|
|
113
|
+
"""Returns the total number of nodes in the graph."""
|
|
114
|
+
return int(self.get_stats().get("total_nodes", 0))
|
|
115
|
+
|
|
116
|
+
def count_edges(self) -> int:
|
|
117
|
+
"""Returns the total number of edges in the graph."""
|
|
118
|
+
return int(self.get_stats().get("total_edges", 0))
|
|
119
|
+
|
|
120
|
+
def clear(self) -> None:
|
|
121
|
+
"""Clears all nodes and edges from the graph."""
|
|
122
|
+
|
|
123
|
+
def load(self) -> None:
|
|
124
|
+
"""Loads state from persistent storage if applicable."""
|
|
125
|
+
|
|
126
|
+
def save(self) -> None:
|
|
127
|
+
"""Persists state to storage if applicable."""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = ["BaseGraphStore"]
|
synapse/graph/memory.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Pure in-memory knowledge graph implementation for Synapse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from synapse.graph.networkx_store import NetworkXGraph
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InMemoryGraph(NetworkXGraph):
|
|
9
|
+
"""Ephemeral knowledge graph residing purely in RAM with zero disk footprints."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, palette: dict[str, str] | None = None) -> None:
|
|
12
|
+
super().__init__(filepath=None, palette=palette)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def in_memory_graph(palette: dict[str, str] | None = None) -> InMemoryGraph:
|
|
16
|
+
"""Factory helper creating an in-memory knowledge graph."""
|
|
17
|
+
return InMemoryGraph(palette=palette)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["InMemoryGraph", "in_memory_graph"]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Neo4j graph database adapter for Synapse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from synapse.config import DESIGN_TOKENS
|
|
8
|
+
from synapse.graph.base import BaseGraphStore
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Neo4jGraph(BaseGraphStore):
|
|
12
|
+
"""Neo4j graph database adapter connecting to enterprise or AuraDB clusters."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
uri: str = "bolt://localhost:7687",
|
|
17
|
+
auth: tuple[str, str] | Any | None = None,
|
|
18
|
+
database: str = "neo4j",
|
|
19
|
+
palette: dict[str, str] | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
self.uri = uri
|
|
22
|
+
self.database = database
|
|
23
|
+
self.palette = palette or DESIGN_TOKENS["palettes"]["zinc"]
|
|
24
|
+
self.driver = None
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from neo4j import GraphDatabase
|
|
28
|
+
|
|
29
|
+
self.driver = GraphDatabase.driver(uri, auth=auth)
|
|
30
|
+
except ImportError:
|
|
31
|
+
# Fallback in-memory representation if neo4j package is omitted
|
|
32
|
+
from synapse.graph.memory import InMemoryGraph
|
|
33
|
+
|
|
34
|
+
self._fallback = InMemoryGraph(palette=self.palette)
|
|
35
|
+
|
|
36
|
+
def add_node_entity(
|
|
37
|
+
self,
|
|
38
|
+
name: str,
|
|
39
|
+
entity_type: str = "Default",
|
|
40
|
+
color: str | None = None,
|
|
41
|
+
attributes: dict[str, Any] | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
if not self.driver:
|
|
44
|
+
return self._fallback.add_node_entity(name, entity_type, color, attributes)
|
|
45
|
+
|
|
46
|
+
assigned_color = color or self.palette.get(
|
|
47
|
+
entity_type, self.palette.get("Default", "#71717a")
|
|
48
|
+
)
|
|
49
|
+
props = {"name": name, "type": entity_type, "color": assigned_color}
|
|
50
|
+
if attributes:
|
|
51
|
+
props.update(attributes)
|
|
52
|
+
|
|
53
|
+
query = """
|
|
54
|
+
MERGE (n:Entity {name: $name})
|
|
55
|
+
SET n += $props
|
|
56
|
+
"""
|
|
57
|
+
with self.driver.session(database=self.database) as session:
|
|
58
|
+
session.run(query, name=name, props=props)
|
|
59
|
+
|
|
60
|
+
def add_relation(
|
|
61
|
+
self,
|
|
62
|
+
source: str,
|
|
63
|
+
relation: str,
|
|
64
|
+
target: str,
|
|
65
|
+
source_type: str = "Default",
|
|
66
|
+
target_type: str = "Default",
|
|
67
|
+
metadata: dict[str, Any] | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
if not self.driver:
|
|
70
|
+
return self._fallback.add_relation(
|
|
71
|
+
source, relation, target, source_type, target_type, metadata
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
self.add_node_entity(source, entity_type=source_type)
|
|
75
|
+
self.add_node_entity(target, entity_type=target_type)
|
|
76
|
+
|
|
77
|
+
clean_rel = relation.strip().upper().replace(" ", "_")
|
|
78
|
+
query = f"""
|
|
79
|
+
MATCH (s:Entity {{name: $source}})
|
|
80
|
+
MATCH (t:Entity {{name: $target}})
|
|
81
|
+
MERGE (s)-[r:{clean_rel}]->(t)
|
|
82
|
+
SET r += $props
|
|
83
|
+
"""
|
|
84
|
+
with self.driver.session(database=self.database) as session:
|
|
85
|
+
session.run(query, source=source, target=target, props=metadata or {})
|
|
86
|
+
|
|
87
|
+
def find_relation_for(self, entity: str) -> list[str]:
|
|
88
|
+
if not self.driver:
|
|
89
|
+
return self._fallback.find_relation_for(entity)
|
|
90
|
+
|
|
91
|
+
facts: list[str] = []
|
|
92
|
+
query = """
|
|
93
|
+
MATCH (s:Entity {name: $entity})-[r]->(t:Entity)
|
|
94
|
+
RETURN s.name as source, type(r) as relation, t.name as target
|
|
95
|
+
UNION
|
|
96
|
+
MATCH (s:Entity)-[r]->(t:Entity {name: $entity})
|
|
97
|
+
RETURN s.name as source, type(r) as relation, t.name as target
|
|
98
|
+
"""
|
|
99
|
+
with self.driver.session(database=self.database) as session:
|
|
100
|
+
result = session.run(query, entity=entity)
|
|
101
|
+
for record in result:
|
|
102
|
+
facts.append(
|
|
103
|
+
f"{record['source']} -> {record['relation']} -> {record['target']}"
|
|
104
|
+
)
|
|
105
|
+
return facts
|
|
106
|
+
|
|
107
|
+
def get_3d_payload(self) -> dict[str, Any]:
|
|
108
|
+
if not self.driver:
|
|
109
|
+
return self._fallback.get_3d_payload()
|
|
110
|
+
|
|
111
|
+
nodes: list[dict[str, Any]] = []
|
|
112
|
+
links: list[dict[str, Any]] = []
|
|
113
|
+
|
|
114
|
+
query = """
|
|
115
|
+
MATCH (s:Entity)-[r]->(t:Entity)
|
|
116
|
+
RETURN s, r, t LIMIT 500
|
|
117
|
+
"""
|
|
118
|
+
with self.driver.session(database=self.database) as session:
|
|
119
|
+
res = session.run(query)
|
|
120
|
+
seen_nodes = set()
|
|
121
|
+
for record in res:
|
|
122
|
+
s_node = record["s"]
|
|
123
|
+
t_node = record["t"]
|
|
124
|
+
r_rel = record["r"]
|
|
125
|
+
|
|
126
|
+
for n in (s_node, t_node):
|
|
127
|
+
nid = n["name"]
|
|
128
|
+
if nid not in seen_nodes:
|
|
129
|
+
seen_nodes.add(nid)
|
|
130
|
+
ntype = n.get("type", "Default")
|
|
131
|
+
color = n.get("color", self.palette.get(ntype, "#71717a"))
|
|
132
|
+
nodes.append({
|
|
133
|
+
"id": nid,
|
|
134
|
+
"name": nid,
|
|
135
|
+
"label": nid,
|
|
136
|
+
"type": ntype,
|
|
137
|
+
"color": color,
|
|
138
|
+
"val": 5.0,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
links.append({
|
|
142
|
+
"source": s_node["name"],
|
|
143
|
+
"target": t_node["name"],
|
|
144
|
+
"relation": r_rel.type,
|
|
145
|
+
"label": r_rel.type,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return {"nodes": nodes, "links": links}
|
|
149
|
+
|
|
150
|
+
def get_stats(self) -> dict[str, Any]:
|
|
151
|
+
if not self.driver:
|
|
152
|
+
return self._fallback.get_stats()
|
|
153
|
+
|
|
154
|
+
query = """
|
|
155
|
+
CALL { MATCH (n:Entity) RETURN count(n) as nodes }
|
|
156
|
+
CALL { MATCH ()-[r]->() RETURN count(r) as edges }
|
|
157
|
+
RETURN nodes, edges
|
|
158
|
+
"""
|
|
159
|
+
with self.driver.session(database=self.database) as session:
|
|
160
|
+
res = session.run(query).single()
|
|
161
|
+
if res:
|
|
162
|
+
return {"total_nodes": res["nodes"], "total_edges": res["edges"]}
|
|
163
|
+
return {"total_nodes": 0, "total_edges": 0}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def neo4j(
|
|
167
|
+
uri: str = "bolt://localhost:7687",
|
|
168
|
+
auth: tuple[str, str] | Any | None = None,
|
|
169
|
+
database: str = "neo4j",
|
|
170
|
+
palette: dict[str, str] | None = None,
|
|
171
|
+
) -> Neo4jGraph:
|
|
172
|
+
"""Factory helper to configure a Neo4j graph database adapter."""
|
|
173
|
+
return Neo4jGraph(uri=uri, auth=auth, database=database, palette=palette)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
__all__ = ["Neo4jGraph", "neo4j"]
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""NetworkX knowledge graph implementation for Synapse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import networkx as nx
|
|
10
|
+
|
|
11
|
+
from synapse.config import DESIGN_TOKENS
|
|
12
|
+
from synapse.graph.base import BaseGraphStore
|
|
13
|
+
|
|
14
|
+
PALETTE: dict[str, str] = DESIGN_TOKENS["palettes"]["zinc"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NetworkXGraph(BaseGraphStore):
|
|
18
|
+
"""NetworkX directed knowledge graph supporting file persistence or ephemeral RAM."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
filepath: str | Path | None = None,
|
|
23
|
+
palette: dict[str, str] | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Initializes NetworkX knowledge graph.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
filepath: Optional persistent JSON file path. If None, operates purely in RAM.
|
|
29
|
+
palette: Optional dictionary mapping entity types to hex color strings.
|
|
30
|
+
"""
|
|
31
|
+
self.filepath = Path(filepath).resolve() if filepath else None
|
|
32
|
+
self.palette = palette or PALETTE
|
|
33
|
+
self.graph: nx.DiGraph = nx.DiGraph()
|
|
34
|
+
if self.filepath:
|
|
35
|
+
self.load()
|
|
36
|
+
|
|
37
|
+
def add_node_entity(
|
|
38
|
+
self,
|
|
39
|
+
name: str,
|
|
40
|
+
entity_type: str = "Default",
|
|
41
|
+
color: str | None = None,
|
|
42
|
+
attributes: dict[str, Any] | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Adds or updates an entity node in the graph."""
|
|
45
|
+
clean_name = name.strip()
|
|
46
|
+
if not clean_name:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
assigned_color = color or self.palette.get(
|
|
50
|
+
entity_type, self.palette.get("Default", "#71717a")
|
|
51
|
+
)
|
|
52
|
+
node_data: dict[str, Any] = {
|
|
53
|
+
"name": clean_name,
|
|
54
|
+
"type": entity_type,
|
|
55
|
+
"color": assigned_color,
|
|
56
|
+
}
|
|
57
|
+
if attributes:
|
|
58
|
+
node_data.update(attributes)
|
|
59
|
+
|
|
60
|
+
if clean_name in self.graph:
|
|
61
|
+
self.graph.nodes[clean_name].update(node_data)
|
|
62
|
+
else:
|
|
63
|
+
self.graph.add_node(clean_name, **node_data)
|
|
64
|
+
|
|
65
|
+
self.save()
|
|
66
|
+
|
|
67
|
+
def add_relation(
|
|
68
|
+
self,
|
|
69
|
+
source: str,
|
|
70
|
+
relation: str,
|
|
71
|
+
target: str,
|
|
72
|
+
source_type: str = "Default",
|
|
73
|
+
target_type: str = "Default",
|
|
74
|
+
metadata: dict[str, Any] | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Adds a directed relation edge between source and target entities."""
|
|
77
|
+
s = source.strip()
|
|
78
|
+
t = target.strip()
|
|
79
|
+
r = relation.strip().upper().replace(" ", "_")
|
|
80
|
+
|
|
81
|
+
if not s or not t:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
self.add_node_entity(s, entity_type=source_type)
|
|
85
|
+
self.add_node_entity(t, entity_type=target_type)
|
|
86
|
+
|
|
87
|
+
edge_data: dict[str, Any] = {"relation": r}
|
|
88
|
+
if metadata:
|
|
89
|
+
edge_data.update(metadata)
|
|
90
|
+
|
|
91
|
+
self.graph.add_edge(s, t, **edge_data)
|
|
92
|
+
self.save()
|
|
93
|
+
|
|
94
|
+
def find_relation_for(self, entity: str) -> list[str]:
|
|
95
|
+
"""Finds all relational facts directly connected to an entity."""
|
|
96
|
+
facts: list[str] = []
|
|
97
|
+
clean = entity.strip()
|
|
98
|
+
if clean not in self.graph:
|
|
99
|
+
# Case-insensitive search
|
|
100
|
+
found = next(
|
|
101
|
+
(n for n in self.graph.nodes if str(n).lower() == clean.lower()),
|
|
102
|
+
None,
|
|
103
|
+
)
|
|
104
|
+
if not found:
|
|
105
|
+
return facts
|
|
106
|
+
clean = str(found)
|
|
107
|
+
|
|
108
|
+
# Outgoing edges
|
|
109
|
+
for _, target, data in self.graph.out_edges(clean, data=True):
|
|
110
|
+
rel = data.get("relation", "RELATION")
|
|
111
|
+
facts.append(f"{clean} -> {rel} -> {target}")
|
|
112
|
+
|
|
113
|
+
# Incoming edges
|
|
114
|
+
for source, _, data in self.graph.in_edges(clean, data=True):
|
|
115
|
+
rel = data.get("relation", "RELATION")
|
|
116
|
+
facts.append(f"{source} -> {rel} -> {clean}")
|
|
117
|
+
|
|
118
|
+
return facts
|
|
119
|
+
|
|
120
|
+
def get_3d_payload(self) -> dict[str, Any]:
|
|
121
|
+
"""Serializes nodes and edges into 3D force-directed layout format."""
|
|
122
|
+
nodes: list[dict[str, Any]] = []
|
|
123
|
+
for node_id, data in self.graph.nodes(data=True):
|
|
124
|
+
node_type = data.get("type", "Default")
|
|
125
|
+
color = data.get("color", self.palette.get(node_type, "#71717a"))
|
|
126
|
+
degree = self.graph.degree(node_id)
|
|
127
|
+
val = max(3.5, min(14.0, degree * 2.2))
|
|
128
|
+
nodes.append({
|
|
129
|
+
"id": str(node_id),
|
|
130
|
+
"name": str(node_id),
|
|
131
|
+
"label": str(node_id),
|
|
132
|
+
"type": node_type,
|
|
133
|
+
"color": color,
|
|
134
|
+
"degree": degree,
|
|
135
|
+
"val": val,
|
|
136
|
+
"metadata": data,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
links: list[dict[str, Any]] = []
|
|
140
|
+
for u, v, data in self.graph.edges(data=True):
|
|
141
|
+
rel = data.get("relation", "RELATION")
|
|
142
|
+
links.append({
|
|
143
|
+
"source": str(u),
|
|
144
|
+
"target": str(v),
|
|
145
|
+
"relation": rel,
|
|
146
|
+
"label": rel,
|
|
147
|
+
"metadata": data,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
return {"nodes": nodes, "links": links}
|
|
151
|
+
|
|
152
|
+
def get_stats(self) -> dict[str, Any]:
|
|
153
|
+
"""Returns summary node and edge counts."""
|
|
154
|
+
return {
|
|
155
|
+
"total_nodes": self.graph.number_of_nodes(),
|
|
156
|
+
"total_edges": self.graph.number_of_edges(),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
def count_nodes(self) -> int:
|
|
160
|
+
"""Returns the total number of nodes in the graph."""
|
|
161
|
+
return self.graph.number_of_nodes()
|
|
162
|
+
|
|
163
|
+
def count_edges(self) -> int:
|
|
164
|
+
"""Returns the total number of edges in the graph."""
|
|
165
|
+
return self.graph.number_of_edges()
|
|
166
|
+
|
|
167
|
+
def clear(self) -> None:
|
|
168
|
+
"""Clears all nodes and edges from the graph."""
|
|
169
|
+
self.graph.clear()
|
|
170
|
+
self.save()
|
|
171
|
+
|
|
172
|
+
def load(self) -> None:
|
|
173
|
+
"""Loads graph state from persistent JSON file if configured."""
|
|
174
|
+
if not self.filepath or not self.filepath.exists():
|
|
175
|
+
return
|
|
176
|
+
try:
|
|
177
|
+
content = self.filepath.read_text(encoding="utf-8")
|
|
178
|
+
if not content.strip():
|
|
179
|
+
return
|
|
180
|
+
data = json.loads(content)
|
|
181
|
+
self.graph = nx.node_link_graph(data, edges="links")
|
|
182
|
+
except Exception:
|
|
183
|
+
pass # nosec B110
|
|
184
|
+
|
|
185
|
+
def save(self) -> None:
|
|
186
|
+
"""Persists graph state to JSON file if configured."""
|
|
187
|
+
if not self.filepath:
|
|
188
|
+
return
|
|
189
|
+
try:
|
|
190
|
+
self.filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
payload = nx.node_link_data(self.graph, edges="links")
|
|
192
|
+
self.filepath.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
193
|
+
except Exception:
|
|
194
|
+
pass # nosec B110
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def networkx(
|
|
198
|
+
filepath: str | Path | None = None,
|
|
199
|
+
palette: dict[str, str] | None = None,
|
|
200
|
+
) -> NetworkXGraph:
|
|
201
|
+
"""Factory helper creating a NetworkX knowledge graph.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
filepath: Optional path to JSON persistence file. If None, runs purely in RAM.
|
|
205
|
+
palette: Optional dictionary mapping entity types to colors.
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
```python
|
|
209
|
+
from synapse.graph import networkx
|
|
210
|
+
|
|
211
|
+
# File-backed persistence:
|
|
212
|
+
graph = networkx("data/graph.json")
|
|
213
|
+
|
|
214
|
+
# In-memory ephemeral (zero disk files):
|
|
215
|
+
graph = networkx()
|
|
216
|
+
```
|
|
217
|
+
"""
|
|
218
|
+
return NetworkXGraph(filepath=filepath, palette=palette)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
__all__ = ["PALETTE", "NetworkXGraph", "networkx"]
|