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/rag/chunker.py ADDED
@@ -0,0 +1,219 @@
1
+ """Text chunking utilities for RAG ingestion in Synapse."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from typing import Any
7
+
8
+ from langchain_core.documents import Document
9
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
10
+
11
+ from synapse.config import DEFAULT_CHUNK_OVERLAP, DEFAULT_CHUNK_SIZE
12
+
13
+
14
+ class DocumentChunker:
15
+ """Splits arbitrary text and connector payloads into deterministic LangChain Document chunks."""
16
+
17
+ def __init__(
18
+ self,
19
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
20
+ chunk_overlap: int = DEFAULT_CHUNK_OVERLAP,
21
+ ) -> None:
22
+ """Initializes the document chunker.
23
+
24
+ Args:
25
+ chunk_size: Maximum character length per chunk. Defaults to 280.
26
+ chunk_overlap: Character overlap between consecutive chunks. Defaults to 30.
27
+ """
28
+ self.chunk_size = chunk_size
29
+ self.chunk_overlap = chunk_overlap
30
+ self.splitter = RecursiveCharacterTextSplitter(
31
+ chunk_size=self.chunk_size,
32
+ chunk_overlap=self.chunk_overlap,
33
+ separators=["\n\n", "\n", ". ", "? ", "! ", " ", ""],
34
+ length_function=len,
35
+ )
36
+
37
+ @staticmethod
38
+ def _generate_chunk_id(source: str, parent_id: str, index: int) -> str:
39
+ seed = f"{source}:{parent_id}:{index}"
40
+ return hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16]
41
+
42
+ def chunk_text(
43
+ self,
44
+ text: str,
45
+ source: str = "text",
46
+ metadata: dict[str, Any] | None = None,
47
+ parent_id: str | None = None,
48
+ ) -> list[Document]:
49
+ """Splits raw text into indexed LangChain Document chunks."""
50
+ clean_text = text.strip()
51
+ if not clean_text:
52
+ return []
53
+
54
+ base_meta = metadata.copy() if metadata else {}
55
+ base_meta["source"] = source
56
+ doc_id = (
57
+ parent_id
58
+ or hashlib.sha256(clean_text[:128].encode("utf-8")).hexdigest()[:12]
59
+ )
60
+
61
+ raw_chunks = self.splitter.split_text(clean_text)
62
+ documents: list[Document] = []
63
+ for idx, chunk in enumerate(raw_chunks):
64
+ chunk_meta = {
65
+ **base_meta,
66
+ "parent_id": doc_id,
67
+ "chunk_index": idx,
68
+ "chunk_id": self._generate_chunk_id(source, doc_id, idx),
69
+ }
70
+ documents.append(Document(page_content=chunk, metadata=chunk_meta))
71
+
72
+ return documents
73
+
74
+ def chunk_email(self, email_data: dict[str, Any]) -> list[Document]:
75
+ """Formats and splits an email dictionary into Document chunks."""
76
+ email_id = str(email_data.get("id", ""))
77
+ subject = str(email_data.get("subject", "No Subject")).strip()
78
+ sender = str(
79
+ email_data.get("from") or email_data.get("sender") or "Unknown Sender"
80
+ ).strip()
81
+ recipients = str(email_data.get("to", "Me")).strip()
82
+ date_str = str(email_data.get("date", "")).strip()
83
+ body = str(email_data.get("body", "")).strip()
84
+ is_unread = bool(email_data.get("is_unread", False))
85
+ read_tag = "UNREAD" if is_unread else "READ"
86
+
87
+ if not body and not subject:
88
+ return []
89
+
90
+ header_context = (
91
+ f"[EMAIL | {read_tag}] Subject: {subject} | From: {sender} | To: {recipients}"
92
+ )
93
+ full_text = f"{header_context}\nDate: {date_str}\n\n{body}"
94
+
95
+ base_meta: dict[str, Any] = {
96
+ "source": "gmail",
97
+ "email_id": email_id,
98
+ "subject": subject,
99
+ "sender": sender,
100
+ "recipients": recipients,
101
+ "date": date_str,
102
+ "is_unread": is_unread,
103
+ "type": "Email",
104
+ }
105
+
106
+ raw_chunks = self.splitter.split_text(full_text)
107
+ documents: list[Document] = []
108
+ for idx, chunk in enumerate(raw_chunks):
109
+ chunk_meta = {
110
+ **base_meta,
111
+ "chunk_index": idx,
112
+ "chunk_id": self._generate_chunk_id("gmail", email_id, idx),
113
+ }
114
+ documents.append(Document(page_content=chunk, metadata=chunk_meta))
115
+
116
+ return documents
117
+
118
+ def chunk_chat_turn(
119
+ self,
120
+ user_message: str,
121
+ assistant_response: str,
122
+ session_id: str,
123
+ turn_index: int,
124
+ ) -> list[Document]:
125
+ """Splits a single conversation turn into Document chunks."""
126
+ content = (
127
+ f"User: {user_message.strip()}\nAssistant: {assistant_response.strip()}"
128
+ )
129
+ base_meta: dict[str, Any] = {
130
+ "source": "chat_history",
131
+ "session_id": session_id,
132
+ "turn_index": turn_index,
133
+ "type": "Conversation",
134
+ }
135
+ return self.chunk_text(
136
+ text=content,
137
+ source="chat_history",
138
+ metadata=base_meta,
139
+ parent_id=f"{session_id}_{turn_index}",
140
+ )
141
+
142
+ def chunk_mcp_record(
143
+ self,
144
+ server_name: str,
145
+ tool_name: str,
146
+ record: dict[str, Any],
147
+ ) -> list[Document]:
148
+ """Normalizes and splits a record from any connector/MCP tool."""
149
+ if server_name == "gmail":
150
+ return self.chunk_email(record)
151
+
152
+ rec_id = str(
153
+ record.get("id")
154
+ or record.get("page_id")
155
+ or record.get("url")
156
+ or hashlib.sha256(str(record).encode("utf-8")).hexdigest()[:12]
157
+ )
158
+ title = str(
159
+ record.get("title")
160
+ or record.get("subject")
161
+ or record.get("name")
162
+ or record.get("summary")
163
+ or f"{server_name.upper()} Record"
164
+ ).strip()
165
+ author = str(
166
+ record.get("sender")
167
+ or record.get("from")
168
+ or record.get("author")
169
+ or record.get("owner")
170
+ or record.get("creator")
171
+ or server_name
172
+ ).strip()
173
+ date_str = str(
174
+ record.get("date")
175
+ or record.get("last_edited_time")
176
+ or record.get("created_at")
177
+ or record.get("updated_at")
178
+ or ""
179
+ ).strip()
180
+ body = str(
181
+ record.get("body")
182
+ or record.get("content")
183
+ or record.get("text")
184
+ or record.get("description")
185
+ or record.get("snippet")
186
+ or ""
187
+ ).strip()
188
+ url = str(record.get("url") or "").strip()
189
+
190
+ if not body and not title:
191
+ return []
192
+
193
+ header = f"[{server_name.upper()} | {tool_name}] Title: {title} | Source: {author}"
194
+ if url:
195
+ header += f" | URL: {url}"
196
+ full_text = f"{header}\nDate: {date_str}\n\n{body}".strip()
197
+
198
+ base_meta: dict[str, Any] = {
199
+ "source": server_name,
200
+ "tool": tool_name,
201
+ "record_id": rec_id,
202
+ "subject": title,
203
+ "sender": author,
204
+ "date": date_str,
205
+ "url": url,
206
+ "type": str(record.get("type") or server_name.capitalize()),
207
+ }
208
+
209
+ raw_chunks = self.splitter.split_text(full_text)
210
+ documents: list[Document] = []
211
+ for idx, chunk in enumerate(raw_chunks):
212
+ chunk_meta = {
213
+ **base_meta,
214
+ "chunk_index": idx,
215
+ "chunk_id": self._generate_chunk_id(server_name, rec_id, idx),
216
+ }
217
+ documents.append(Document(page_content=chunk, metadata=chunk_meta))
218
+
219
+ return documents
@@ -0,0 +1,122 @@
1
+ """Hybrid vector and knowledge graph context retriever."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from langchain_core.documents import Document
9
+
10
+ from synapse.config import DEFAULT_TOP_K
11
+
12
+
13
+ class HybridRAGEngine:
14
+ """Combines dense semantic similarity search with knowledge graph multi-hop relations."""
15
+
16
+ def __init__(
17
+ self,
18
+ vector_store: Any | None = None,
19
+ graph_store: Any | None = None,
20
+ ) -> None:
21
+ """Initializes the Hybrid RAG engine."""
22
+ self.vector_store = vector_store
23
+ self.graph_store = graph_store
24
+
25
+ def _extract_candidate_entities(self, query: str) -> list[str]:
26
+ candidates = set()
27
+ if self.graph_store and hasattr(self.graph_store, "graph") and hasattr(self.graph_store.graph, "nodes"):
28
+ existing_nodes = list(self.graph_store.graph.nodes)
29
+ query_lower = query.lower()
30
+ for node in existing_nodes:
31
+ if str(node).lower() in query_lower:
32
+ candidates.add(str(node))
33
+
34
+ matches = re.findall(r"\b[A-Z][a-zA-Z0-9_-]+\b", query)
35
+ for match in matches:
36
+ candidates.add(match)
37
+
38
+ return list(candidates)
39
+
40
+ def retrieve_context(
41
+ self,
42
+ query: str,
43
+ k: int = DEFAULT_TOP_K,
44
+ filter_dict: dict[str, Any] | None = None,
45
+ ) -> dict[str, Any]:
46
+ """Retrieves matching vector chunks and relational facts for a query."""
47
+ vector_docs: list[Document] = []
48
+ if self.vector_store is not None:
49
+ try:
50
+ vector_docs = self.vector_store.similarity_search(
51
+ query=query, k=k, filter_dict=filter_dict
52
+ )
53
+ except Exception:
54
+ vector_docs = []
55
+
56
+ candidate_entities: list[str] = []
57
+ graph_facts: list[str] = []
58
+ if self.graph_store is not None:
59
+ try:
60
+ candidate_entities = self._extract_candidate_entities(query)
61
+ for entity in candidate_entities:
62
+ facts = self.graph_store.find_relation_for(entity)
63
+ graph_facts.extend(facts)
64
+ except Exception:
65
+ pass # nosec B110
66
+
67
+ unique_facts = list(dict.fromkeys(graph_facts))
68
+ formatted_context = self._format_prompt_context(
69
+ vector_docs, unique_facts
70
+ )
71
+
72
+ serialized_chunks = [
73
+ {
74
+ "page_content": doc.page_content,
75
+ "metadata": doc.metadata,
76
+ "source": doc.metadata.get("source", "chroma_db"),
77
+ "subject": doc.metadata.get("subject", ""),
78
+ "chunk_id": doc.metadata.get("chunk_id", ""),
79
+ }
80
+ for doc in vector_docs
81
+ ]
82
+
83
+ return {
84
+ "query": query,
85
+ "vector_documents": vector_docs,
86
+ "chunks": serialized_chunks,
87
+ "graph_facts": unique_facts,
88
+ "graph_triples": unique_facts,
89
+ "entities_detected": candidate_entities,
90
+ "formatted_context": formatted_context,
91
+ }
92
+
93
+ @staticmethod
94
+ def _format_prompt_context(
95
+ vector_docs: list[Document],
96
+ graph_facts: list[str],
97
+ ) -> str:
98
+ sections: list[str] = []
99
+
100
+ if graph_facts:
101
+ facts_block = "\n".join(f"- {fact}" for fact in graph_facts)
102
+ sections.append(
103
+ f"### RELATIONAL KNOWLEDGE GRAPH FACTS:\n{facts_block}"
104
+ )
105
+
106
+ if vector_docs:
107
+ doc_chunks: list[str] = []
108
+ for idx, doc in enumerate(vector_docs, 1):
109
+ src = doc.metadata.get("source", "unknown")
110
+ subj = doc.metadata.get("subject", "")
111
+ header = f"[{src.upper()}" + (f" - {subj}" if subj else "") + "]"
112
+ doc_chunks.append(
113
+ f"{idx}. {header}\n{doc.page_content.strip()}"
114
+ )
115
+ sections.append(
116
+ "### RELEVANT DOCUMENT EXCERPTS:\n" + "\n\n".join(doc_chunks)
117
+ )
118
+
119
+ if not sections:
120
+ return "No relevant past context or graph facts found."
121
+
122
+ return "\n\n".join(sections)
@@ -0,0 +1,5 @@
1
+ """Resource module for managing document collections and RAG ingestion."""
2
+
3
+ from synapse.resource.resource import Resource
4
+
5
+ __all__ = ["Resource"]
@@ -0,0 +1,304 @@
1
+ """Resource management and document collection ingestion for Synapse."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from collections.abc import Sequence
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from langchain_core.documents import Document
12
+
13
+ from synapse.rag.chunker import DocumentChunker
14
+
15
+
16
+ class Resource:
17
+ """Manages collections of static or dynamic documents for Synapse RAG retrieval.
18
+
19
+ Supports ingesting raw strings, local files (.txt, .md, .pdf, .json, .csv),
20
+ directories, and LangChain Document instances. Automatically chunks and embeds
21
+ content for instant vector search and knowledge graph indexing.
22
+
23
+ Example:
24
+ ```python
25
+ from synapse.resource import Resource
26
+
27
+ # Ingest files or text
28
+ res = Resource("knowledge_base")
29
+ res.add("README.md")
30
+ res.add("Quick note about architecture", title="Architecture")
31
+ res.add(["docs/guide.md", "data/faq.txt"])
32
+
33
+ # Pass to BaseApp:
34
+ app = BaseApp(resource=res)
35
+ ```
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ name: str = "resource",
41
+ documents: Document | str | Path | Sequence[Any] | None = None,
42
+ chunker: DocumentChunker | None = None,
43
+ metadata: dict[str, Any] | None = None,
44
+ ) -> None:
45
+ """Initializes a Resource container.
46
+
47
+ Args:
48
+ name: Logical identifier for this resource collection.
49
+ documents: Optional initial document, file path, or sequence of documents.
50
+ chunker: Optional custom DocumentChunker instance.
51
+ metadata: Arbitrary metadata associated with this resource.
52
+ """
53
+ self.name = name
54
+ self.chunker = chunker or DocumentChunker()
55
+ self.metadata = dict(metadata or {})
56
+ self._raw_documents: list[Document] = []
57
+ self._is_indexed: bool = False
58
+
59
+ if documents is not None:
60
+ self.add(documents)
61
+
62
+ @property
63
+ def documents(self) -> list[Document]:
64
+ """Returns the list of raw Document objects in this resource."""
65
+ return list(self._raw_documents)
66
+
67
+ def __len__(self) -> int:
68
+ return len(self._raw_documents)
69
+
70
+ def __repr__(self) -> str:
71
+ return f"<Resource name='{self.name}' documents={len(self._raw_documents)}>"
72
+
73
+ def add(
74
+ self,
75
+ *items: Any,
76
+ **extra_metadata: Any,
77
+ ) -> Resource:
78
+ """Adds one or more documents, text strings, files, or directories to this resource.
79
+
80
+ Args:
81
+ *items: Variadic items to add. Each item can be a LangChain Document,
82
+ raw text string, file Path, directory Path, or a list/tuple of them.
83
+ **extra_metadata: Metadata merged into all items added during this call.
84
+
85
+ Returns:
86
+ The current Resource instance for fluent method chaining.
87
+ """
88
+ for item in items:
89
+ self._ingest_item(item, extra_metadata)
90
+ self._is_indexed = False
91
+ return self
92
+
93
+ def _ingest_item(self, item: Any, extra_metadata: dict[str, Any]) -> None:
94
+ """Internal dispatcher for handling diverse input types."""
95
+ if item is None:
96
+ return
97
+
98
+ if isinstance(item, (list, tuple, set)):
99
+ for sub_item in item:
100
+ self._ingest_item(sub_item, extra_metadata)
101
+ return
102
+
103
+ if isinstance(item, Document):
104
+ merged_meta = {**item.metadata, **extra_metadata, "resource": self.name}
105
+ self._raw_documents.append(
106
+ Document(page_content=item.page_content, metadata=merged_meta)
107
+ )
108
+ return
109
+
110
+ if isinstance(item, Path):
111
+ self._ingest_path(item, extra_metadata)
112
+ return
113
+
114
+ if isinstance(item, str):
115
+ clean_str = item.strip()
116
+ # Check if this string points to an existing file or directory
117
+ candidate = Path(clean_str)
118
+ if candidate.exists():
119
+ self._ingest_path(candidate, extra_metadata)
120
+ return
121
+
122
+ # Treat as raw text
123
+ doc_id = hashlib.sha256(clean_str[:256].encode("utf-8")).hexdigest()[:12]
124
+ meta = {
125
+ "source": "resource_text",
126
+ "resource": self.name,
127
+ "record_id": doc_id,
128
+ **extra_metadata,
129
+ }
130
+ self._raw_documents.append(
131
+ Document(page_content=clean_str, metadata=meta)
132
+ )
133
+ return
134
+
135
+ if isinstance(item, dict):
136
+ content = str(
137
+ item.get("content")
138
+ or item.get("text")
139
+ or item.get("body")
140
+ or item.get("page_content")
141
+ or json.dumps(item)
142
+ )
143
+ meta = {
144
+ "source": "resource_dict",
145
+ "resource": self.name,
146
+ **item.get("metadata", {}),
147
+ **extra_metadata,
148
+ }
149
+ self._raw_documents.append(Document(page_content=content, metadata=meta))
150
+ return
151
+
152
+ # Fallback for any other object: convert to string
153
+ self._raw_documents.append(
154
+ Document(
155
+ page_content=str(item),
156
+ metadata={"source": "resource_object", "resource": self.name, **extra_metadata},
157
+ )
158
+ )
159
+
160
+ def _ingest_path(self, path: Path, extra_metadata: dict[str, Any]) -> None:
161
+ """Reads a file or directory from disk."""
162
+ resolved = path.resolve()
163
+ if not resolved.exists():
164
+ raise FileNotFoundError(f"Resource path does not exist: {resolved}")
165
+
166
+ if resolved.is_dir():
167
+ # Ingest all supported files in directory
168
+ extensions = {".md", ".txt", ".json", ".csv", ".py", ".html", ".log", ".yaml", ".yml"}
169
+ for child in sorted(resolved.rglob("*")):
170
+ if child.is_file() and child.suffix.lower() in extensions:
171
+ self._ingest_file(child, extra_metadata)
172
+ return
173
+
174
+ if resolved.is_file():
175
+ self._ingest_file(resolved, extra_metadata)
176
+
177
+ def _ingest_file(self, file_path: Path, extra_metadata: dict[str, Any]) -> None:
178
+ """Reads a single file into a Document."""
179
+ suffix = file_path.suffix.lower()
180
+ title = file_path.stem.replace("_", " ").replace("-", " ").title()
181
+
182
+ try:
183
+ content = file_path.read_text(encoding="utf-8", errors="ignore")
184
+ except Exception as e:
185
+ content = f"Error reading file {file_path.name}: {e}"
186
+
187
+ meta = {
188
+ "source": str(file_path),
189
+ "filename": file_path.name,
190
+ "title": title,
191
+ "subject": title,
192
+ "resource": self.name,
193
+ "file_type": suffix.lstrip("."),
194
+ **extra_metadata,
195
+ }
196
+
197
+ self._raw_documents.append(Document(page_content=content, metadata=meta))
198
+
199
+ def add_text(
200
+ self,
201
+ text: str,
202
+ title: str | None = None,
203
+ source: str = "text",
204
+ **metadata: Any,
205
+ ) -> Resource:
206
+ """Convenience helper to add raw text with a title."""
207
+ meta = {"source": source, **metadata}
208
+ if title:
209
+ meta["title"] = title
210
+ meta["subject"] = title
211
+ return self.add(text, **meta)
212
+
213
+ def add_file(
214
+ self,
215
+ file_path: str | Path,
216
+ **metadata: Any,
217
+ ) -> Resource:
218
+ """Convenience helper to add a single file."""
219
+ return self.add(Path(file_path), **metadata)
220
+
221
+ def add_directory(
222
+ self,
223
+ dir_path: str | Path,
224
+ **metadata: Any,
225
+ ) -> Resource:
226
+ """Convenience helper to ingest an entire directory."""
227
+ return self.add(Path(dir_path), **metadata)
228
+
229
+ def get_chunks(self) -> list[Document]:
230
+ """Splits all ingested raw documents into indexable chunks using DocumentChunker."""
231
+ all_chunks: list[Document] = []
232
+ for doc in self._raw_documents:
233
+ src = doc.metadata.get("source", self.name)
234
+ rec_id = doc.metadata.get("record_id") or hashlib.sha256(
235
+ doc.page_content[:128].encode("utf-8")
236
+ ).hexdigest()[:12]
237
+ chunks = self.chunker.chunk_text(
238
+ text=doc.page_content,
239
+ source=src,
240
+ metadata=doc.metadata,
241
+ parent_id=rec_id,
242
+ )
243
+ all_chunks.extend(chunks)
244
+ return all_chunks
245
+
246
+ def index_into(
247
+ self,
248
+ vector_store: Any | None = None,
249
+ graph_store: Any | None = None,
250
+ ) -> int:
251
+ """Chunks and indexes this resource into vector store and knowledge graph.
252
+
253
+ Args:
254
+ vector_store: Optional vector store instance (LocalVectorStore, VectorStorage, BaseVectorDB).
255
+ graph_store: Optional graph store instance (PersistentGraphStore, BaseGraphStore).
256
+
257
+ Returns:
258
+ The total number of chunks successfully processed.
259
+ """
260
+ chunks = self.get_chunks()
261
+ if not chunks:
262
+ return 0
263
+
264
+ if vector_store is not None:
265
+ vector_store.add_documents(chunks)
266
+
267
+ if graph_store is not None:
268
+ # Register resource node and links in knowledge graph
269
+ graph_store.add_node_entity(
270
+ name=self.name,
271
+ entity_type="Resource",
272
+ attributes={"total_docs": len(self._raw_documents), "chunks": len(chunks)},
273
+ )
274
+ for doc in self._raw_documents:
275
+ title = (
276
+ doc.metadata.get("title")
277
+ or doc.metadata.get("filename")
278
+ or doc.metadata.get("subject")
279
+ or (doc.page_content[:35].strip() + "...")
280
+ )
281
+ if title:
282
+ graph_store.add_relation(
283
+ source=self.name,
284
+ relation="CONTAINS",
285
+ target=str(title),
286
+ source_type="Resource",
287
+ target_type="Document",
288
+ )
289
+
290
+ self._is_indexed = True
291
+ return len(chunks)
292
+
293
+ def to_dict(self) -> dict[str, Any]:
294
+ """Serializes resource statistics into a dictionary."""
295
+ return {
296
+ "name": self.name,
297
+ "document_count": len(self._raw_documents),
298
+ "chunk_count": len(self.get_chunks()),
299
+ "is_indexed": self._is_indexed,
300
+ "metadata": self.metadata,
301
+ }
302
+
303
+
304
+ __all__ = ["Resource"]
@@ -0,0 +1,5 @@
1
+ """Alias package for synapse.resource."""
2
+
3
+ from synapse.resource.resource import Resource
4
+
5
+ __all__ = ["Resource"]
@@ -0,0 +1,5 @@
1
+ """Skills module for specialized agent instructions and domain personas."""
2
+
3
+ from synapse.skills.skill import Skill, make_skill, makeskills
4
+
5
+ __all__ = ["Skill", "make_skill", "makeskills"]