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.
Files changed (54) hide show
  1. synapse/__init__.py +207 -0
  2. synapse/app.py +942 -0
  3. synapse/cli.py +88 -0
  4. synapse/config.py +103 -0
  5. synapse/connectors/__init__.py +29 -0
  6. synapse/connectors/base.py +222 -0
  7. synapse/connectors/factory.py +202 -0
  8. synapse/connectors/gmail.py +292 -0
  9. synapse/connectors/mcp_wrapper.py +153 -0
  10. synapse/connectors/notion.py +161 -0
  11. synapse/connectors/photos.py +141 -0
  12. synapse/database/__init__.py +19 -0
  13. synapse/database/base.py +63 -0
  14. synapse/database/chroma.py +175 -0
  15. synapse/database/memory.py +132 -0
  16. synapse/database/pgvector.py +99 -0
  17. synapse/database/qdrant.py +138 -0
  18. synapse/embeddings/__init__.py +21 -0
  19. synapse/embeddings/base.py +262 -0
  20. synapse/export/__init__.py +14 -0
  21. synapse/export/mcp_exporter.py +202 -0
  22. synapse/graph/__init__.py +21 -0
  23. synapse/graph/base.py +130 -0
  24. synapse/graph/memory.py +20 -0
  25. synapse/graph/neo4j_store.py +176 -0
  26. synapse/graph/networkx_store.py +221 -0
  27. synapse/inspector/__init__.py +5 -0
  28. synapse/inspector/template.py +530 -0
  29. synapse/models/__init__.py +73 -0
  30. synapse/models/base.py +95 -0
  31. synapse/models/gemini.py +328 -0
  32. synapse/models/ollama.py +322 -0
  33. synapse/models/openai.py +289 -0
  34. synapse/oauth/__init__.py +7 -0
  35. synapse/oauth/google.py +194 -0
  36. synapse/oauth/notion.py +35 -0
  37. synapse/rag/__init__.py +8 -0
  38. synapse/rag/chunker.py +219 -0
  39. synapse/rag/hybrid_rag.py +122 -0
  40. synapse/resource/__init__.py +5 -0
  41. synapse/resource/resource.py +304 -0
  42. synapse/resources/__init__.py +5 -0
  43. synapse/skills/__init__.py +5 -0
  44. synapse/skills/skill.py +321 -0
  45. synapse/storage/__init__.py +24 -0
  46. synapse/storage/graph_exporter.py +624 -0
  47. synapse/storage/graph_store.py +162 -0
  48. synapse/storage/vector_store.py +103 -0
  49. synapse_framework-0.2.4.dist-info/METADATA +415 -0
  50. synapse_framework-0.2.4.dist-info/RECORD +54 -0
  51. synapse_framework-0.2.4.dist-info/WHEEL +5 -0
  52. synapse_framework-0.2.4.dist-info/entry_points.txt +2 -0
  53. synapse_framework-0.2.4.dist-info/licenses/LICENSE +21 -0
  54. 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"]
@@ -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"]
@@ -0,0 +1,5 @@
1
+ """Interactive Inspector UI for Synapse."""
2
+
3
+ from synapse.inspector.template import INSPECTOR_HTML, render_inspector_html
4
+
5
+ __all__ = ["INSPECTOR_HTML", "render_inspector_html"]