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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Google Photos connector for the Synapse framework."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from synapse.connectors.base import BaseConnector
|
|
9
|
+
from synapse.oauth.google import GoogleOAuthService
|
|
10
|
+
|
|
11
|
+
PHOTOS_READONLY_SCOPE = (
|
|
12
|
+
"https://www.googleapis.com/auth/photoslibrary.readonly"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GooglePhotosConnector(BaseConnector):
|
|
17
|
+
"""Google Photos connector for searching and referencing photo media items.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
oauth: Reusable GoogleOAuthService managing Google Photos scopes.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
credentials_path: str | Path = "credentials.json",
|
|
26
|
+
token_path: str | Path = "token.json", # nosec B107
|
|
27
|
+
oauth_service: GoogleOAuthService | None = None,
|
|
28
|
+
scopes: list[str] | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Initializes the GooglePhotosConnector.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
credentials_path: Path to OAuth client secrets JSON.
|
|
34
|
+
token_path: Path to cached token JSON.
|
|
35
|
+
oauth_service: Optional pre-configured GoogleOAuthService instance.
|
|
36
|
+
scopes: Optional list of OAuth scopes.
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(
|
|
39
|
+
name="photos",
|
|
40
|
+
description="Google Photos media library, photos, albums, and image metadata",
|
|
41
|
+
intent_regex=(
|
|
42
|
+
r"\b(photo|photos|image|images|picture|pictures|album|albums|"
|
|
43
|
+
r"snapshot|snapshots|gallery|google\s+photos)\b"
|
|
44
|
+
),
|
|
45
|
+
default_tool="search_photos",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
active_scopes = list(scopes or [PHOTOS_READONLY_SCOPE])
|
|
49
|
+
if oauth_service:
|
|
50
|
+
self.oauth = oauth_service
|
|
51
|
+
self.oauth.add_scopes(active_scopes)
|
|
52
|
+
else:
|
|
53
|
+
self.oauth = GoogleOAuthService(
|
|
54
|
+
credentials_path=credentials_path,
|
|
55
|
+
token_path=token_path,
|
|
56
|
+
scopes=active_scopes,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def is_authenticated(self) -> bool:
|
|
60
|
+
"""Checks if a valid OAuth token exists for Google Photos."""
|
|
61
|
+
return self.oauth.is_authenticated()
|
|
62
|
+
|
|
63
|
+
async def list_tools(self) -> list[dict[str, Any]]:
|
|
64
|
+
"""Returns the Google Photos tool schemas."""
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
"server": self.name,
|
|
68
|
+
"name": "search_photos",
|
|
69
|
+
"description": "Search photos and media items in user's Google Photos library.",
|
|
70
|
+
"inputSchema": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {
|
|
73
|
+
"query": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"default": "",
|
|
76
|
+
"description": "Optional search filter or category description.",
|
|
77
|
+
},
|
|
78
|
+
"max_results": {
|
|
79
|
+
"type": "integer",
|
|
80
|
+
"default": 10,
|
|
81
|
+
"description": "Maximum number of photos to return.",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
async def call_tool(
|
|
89
|
+
self,
|
|
90
|
+
tool_name: str,
|
|
91
|
+
arguments: dict[str, Any] | None = None,
|
|
92
|
+
) -> Any:
|
|
93
|
+
"""Executes a Google Photos tool."""
|
|
94
|
+
args = arguments or {}
|
|
95
|
+
if tool_name == "search_photos":
|
|
96
|
+
query = str(args.get("query", ""))
|
|
97
|
+
max_results = int(args.get("max_results", 10))
|
|
98
|
+
return self.search_photos(query=query, max_results=max_results)
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"Unknown tool '{tool_name}' for connector '{self.name}'"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def search_photos(
|
|
104
|
+
self,
|
|
105
|
+
query: str = "",
|
|
106
|
+
max_results: int = 10,
|
|
107
|
+
) -> list[dict[str, Any]]:
|
|
108
|
+
"""Searches media items via Google Photos Library API."""
|
|
109
|
+
if not self.is_authenticated():
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
service = self.oauth.get_service(
|
|
114
|
+
api_name="photoslibrary",
|
|
115
|
+
version="v1",
|
|
116
|
+
scopes=[PHOTOS_READONLY_SCOPE],
|
|
117
|
+
)
|
|
118
|
+
body: dict[str, Any] = {"pageSize": max(1, min(max_results, 50))}
|
|
119
|
+
response = service.mediaItems().search(body=body).execute()
|
|
120
|
+
items = response.get("mediaItems", [])
|
|
121
|
+
|
|
122
|
+
results: list[dict[str, Any]] = []
|
|
123
|
+
for item in items:
|
|
124
|
+
results.append({
|
|
125
|
+
"id": item.get("id", ""),
|
|
126
|
+
"title": item.get("filename", "Photo"),
|
|
127
|
+
"url": item.get("productUrl", ""),
|
|
128
|
+
"mime_type": item.get("mimeType", ""),
|
|
129
|
+
"date": (
|
|
130
|
+
item.get("mediaMetadata", {}).get("creationTime", "")
|
|
131
|
+
),
|
|
132
|
+
"description": item.get("description", ""),
|
|
133
|
+
"type": "Photo",
|
|
134
|
+
})
|
|
135
|
+
return results
|
|
136
|
+
except Exception:
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Developer-friendly alias matching user request
|
|
141
|
+
GooglePhotosConnectors = GooglePhotosConnector
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Pluggable vector database backends for Synapse."""
|
|
2
|
+
|
|
3
|
+
from synapse.database.base import BaseVectorDB
|
|
4
|
+
from synapse.database.chroma import ChromaDB, chromadb
|
|
5
|
+
from synapse.database.memory import InMemoryDB, in_memory
|
|
6
|
+
from synapse.database.pgvector import PGVectorDB, pgvector
|
|
7
|
+
from synapse.database.qdrant import QdrantDB, qdrant
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BaseVectorDB",
|
|
11
|
+
"ChromaDB",
|
|
12
|
+
"InMemoryDB",
|
|
13
|
+
"PGVectorDB",
|
|
14
|
+
"QdrantDB",
|
|
15
|
+
"chromadb",
|
|
16
|
+
"in_memory",
|
|
17
|
+
"pgvector",
|
|
18
|
+
"qdrant",
|
|
19
|
+
]
|
synapse/database/base.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Abstract base interface for pluggable vector databases in Synapse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from langchain_core.documents import Document
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseVectorDB(ABC):
|
|
12
|
+
"""Abstract interface that all Synapse vector database backends must implement.
|
|
13
|
+
|
|
14
|
+
Ensures seamless interchangeability between local ChromaDB, in-memory storage,
|
|
15
|
+
Qdrant, pgvector, or remote hosted vector databases.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def add_documents(
|
|
20
|
+
self,
|
|
21
|
+
documents: list[Document],
|
|
22
|
+
embeddings: list[list[float]] | None = None,
|
|
23
|
+
) -> list[str]:
|
|
24
|
+
"""Upserts a list of LangChain Document objects into the vector database.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
documents: List of Document instances with page_content and metadata.
|
|
28
|
+
embeddings: Optional precomputed vector embeddings.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
List of generated or assigned document ID strings.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def similarity_search(
|
|
36
|
+
self,
|
|
37
|
+
query: str,
|
|
38
|
+
k: int = 4,
|
|
39
|
+
filter_dict: dict[str, Any] | None = None,
|
|
40
|
+
query_embedding: list[float] | None = None,
|
|
41
|
+
) -> list[Document]:
|
|
42
|
+
"""Performs semantic similarity search for a query string.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
query: Natural language query string.
|
|
46
|
+
k: Maximum number of nearest neighbors to return.
|
|
47
|
+
filter_dict: Optional metadata filter dictionary.
|
|
48
|
+
query_embedding: Optional precomputed query embedding vector.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of top-k closest Document instances.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def count(self) -> int:
|
|
56
|
+
"""Returns the total number of document chunks currently indexed."""
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def clear(self) -> None:
|
|
60
|
+
"""Clears all records in the collection/index."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = ["BaseVectorDB"]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""ChromaDB vector database provider for Synapse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import chromadb as native_chromadb
|
|
9
|
+
from langchain_chroma import Chroma
|
|
10
|
+
from langchain_core.documents import Document
|
|
11
|
+
|
|
12
|
+
from synapse.config import (
|
|
13
|
+
DEFAULT_CHROMA_COLLECTION,
|
|
14
|
+
DEFAULT_CHROMA_DIR,
|
|
15
|
+
)
|
|
16
|
+
from synapse.database.base import BaseVectorDB
|
|
17
|
+
from synapse.embeddings import resolve_embedding
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ChromaDB(BaseVectorDB):
|
|
21
|
+
"""ChromaDB vector database adapter supporting local disk, in-memory, or remote HTTP."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
path: str | Path | None = None,
|
|
26
|
+
host: str | None = None,
|
|
27
|
+
port: int | None = None,
|
|
28
|
+
ssl: bool = False,
|
|
29
|
+
headers: dict[str, str] | None = None,
|
|
30
|
+
in_memory: bool = False,
|
|
31
|
+
collection_name: str = DEFAULT_CHROMA_COLLECTION,
|
|
32
|
+
embedding: str | Any | None = None,
|
|
33
|
+
embedding_model: str | None = None,
|
|
34
|
+
embedding_function: Any | None = None,
|
|
35
|
+
**kwargs: Any,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initializes ChromaDB adapter.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
path: Local storage path. If ':memory:' or in_memory=True, runs purely in RAM.
|
|
41
|
+
host: Hostname for online/remote ChromaDB server (e.g. 'localhost' or 'api.chroma.io').
|
|
42
|
+
port: Port for remote ChromaDB server (e.g. 8000).
|
|
43
|
+
ssl: Whether to use HTTPS for remote ChromaDB server.
|
|
44
|
+
headers: Optional HTTP headers for remote ChromaDB server (e.g. auth tokens).
|
|
45
|
+
in_memory: If True, operates in ephemeral RAM with zero disk writes.
|
|
46
|
+
collection_name: ChromaDB collection name. Defaults to 'synapse_memory'.
|
|
47
|
+
embedding: Embedding model name ('fastembed', 'minilm', 'openai', etc.) or Embeddings object.
|
|
48
|
+
embedding_model: Backward-compatible alias for embedding model name.
|
|
49
|
+
embedding_function: Optional custom embedding function.
|
|
50
|
+
**kwargs: Extra parameters forwarded to the embedding resolver.
|
|
51
|
+
"""
|
|
52
|
+
self.collection_name = collection_name
|
|
53
|
+
self.embedding_function = resolve_embedding(
|
|
54
|
+
embedding=embedding or embedding_function or embedding_model,
|
|
55
|
+
**kwargs,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if host is not None:
|
|
59
|
+
# Online / remote ChromaDB server
|
|
60
|
+
self.client = native_chromadb.HttpClient(
|
|
61
|
+
host=host,
|
|
62
|
+
port=port or 8000,
|
|
63
|
+
ssl=ssl,
|
|
64
|
+
headers=headers,
|
|
65
|
+
)
|
|
66
|
+
elif in_memory or path == ":memory:":
|
|
67
|
+
# Pure in-memory ephemeral client (0 disk writes)
|
|
68
|
+
self.client = native_chromadb.EphemeralClient()
|
|
69
|
+
else:
|
|
70
|
+
# Local persistent on-disk ChromaDB
|
|
71
|
+
resolved_path = Path(path or DEFAULT_CHROMA_DIR).resolve()
|
|
72
|
+
resolved_path.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
self.client = native_chromadb.PersistentClient(path=str(resolved_path))
|
|
74
|
+
|
|
75
|
+
self.store = Chroma(
|
|
76
|
+
client=self.client,
|
|
77
|
+
collection_name=collection_name,
|
|
78
|
+
embedding_function=self.embedding_function,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def add_documents(
|
|
82
|
+
self,
|
|
83
|
+
documents: list[Document],
|
|
84
|
+
embeddings: list[list[float]] | None = None,
|
|
85
|
+
) -> list[str]:
|
|
86
|
+
"""Upserts documents into ChromaDB."""
|
|
87
|
+
if not documents:
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
ids: list[str] | None = None
|
|
91
|
+
has_chunk_ids = all("chunk_id" in doc.metadata for doc in documents)
|
|
92
|
+
if has_chunk_ids:
|
|
93
|
+
ids = [str(doc.metadata["chunk_id"]) for doc in documents]
|
|
94
|
+
|
|
95
|
+
return self.store.add_documents(documents=documents, ids=ids)
|
|
96
|
+
|
|
97
|
+
def similarity_search(
|
|
98
|
+
self,
|
|
99
|
+
query: str,
|
|
100
|
+
k: int = 4,
|
|
101
|
+
filter_dict: dict[str, Any] | None = None,
|
|
102
|
+
query_embedding: list[float] | None = None,
|
|
103
|
+
) -> list[Document]:
|
|
104
|
+
"""Performs semantic search in ChromaDB."""
|
|
105
|
+
return self.store.similarity_search(query=query, k=k, filter=filter_dict)
|
|
106
|
+
|
|
107
|
+
def count(self) -> int:
|
|
108
|
+
"""Returns total document count in the collection."""
|
|
109
|
+
try:
|
|
110
|
+
col = self.client.get_collection(self.collection_name)
|
|
111
|
+
return col.count()
|
|
112
|
+
except Exception:
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
def clear(self) -> None:
|
|
116
|
+
"""Deletes and recreates the collection."""
|
|
117
|
+
try:
|
|
118
|
+
self.client.delete_collection(self.collection_name)
|
|
119
|
+
except Exception:
|
|
120
|
+
pass # nosec B110
|
|
121
|
+
self.store = Chroma(
|
|
122
|
+
client=self.client,
|
|
123
|
+
collection_name=self.collection_name,
|
|
124
|
+
embedding_function=self.embedding_function,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def chromadb(
|
|
129
|
+
path: str | Path | None = None,
|
|
130
|
+
host: str | None = None,
|
|
131
|
+
port: int | None = None,
|
|
132
|
+
ssl: bool = False,
|
|
133
|
+
headers: dict[str, str] | None = None,
|
|
134
|
+
in_memory: bool = False,
|
|
135
|
+
collection_name: str = DEFAULT_CHROMA_COLLECTION,
|
|
136
|
+
embedding: str | Any | None = None,
|
|
137
|
+
embedding_model: str | None = None,
|
|
138
|
+
embedding_function: Any | None = None,
|
|
139
|
+
**kwargs: Any,
|
|
140
|
+
) -> ChromaDB:
|
|
141
|
+
"""Factory helper to configure a ChromaDB vector database instance.
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
```python
|
|
145
|
+
from synapse.database import chromadb
|
|
146
|
+
from synapse.storage import vectorStorage
|
|
147
|
+
|
|
148
|
+
# Local persistent with MiniLM:
|
|
149
|
+
db = chromadb("data/chroma_db", embedding="minilm")
|
|
150
|
+
|
|
151
|
+
# In-memory ephemeral with FastEmbed BGE:
|
|
152
|
+
db = chromadb(in_memory=True, embedding="fastembed")
|
|
153
|
+
|
|
154
|
+
# Online remote server with OpenAI embeddings:
|
|
155
|
+
db = chromadb(host="my-chroma.internal", port=8000, embedding="openai")
|
|
156
|
+
|
|
157
|
+
storage = vectorStorage(database=db)
|
|
158
|
+
```
|
|
159
|
+
"""
|
|
160
|
+
return ChromaDB(
|
|
161
|
+
path=path,
|
|
162
|
+
host=host,
|
|
163
|
+
port=port,
|
|
164
|
+
ssl=ssl,
|
|
165
|
+
headers=headers,
|
|
166
|
+
in_memory=in_memory,
|
|
167
|
+
collection_name=collection_name,
|
|
168
|
+
embedding=embedding,
|
|
169
|
+
embedding_model=embedding_model,
|
|
170
|
+
embedding_function=embedding_function,
|
|
171
|
+
**kwargs,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
__all__ = ["ChromaDB", "chromadb"]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Pure in-memory vector database backend with zero disk dependencies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any, Union
|
|
8
|
+
|
|
9
|
+
from langchain_core.documents import Document
|
|
10
|
+
|
|
11
|
+
from synapse.database.base import BaseVectorDB
|
|
12
|
+
from synapse.embeddings import resolve_embedding
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InMemoryDB(BaseVectorDB):
|
|
16
|
+
"""Pure in-memory vector database using cosine similarity in RAM."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
embedding: Union[str, Any] | None = None,
|
|
21
|
+
embedding_model: str | None = None,
|
|
22
|
+
embedding_function: Any | None = None,
|
|
23
|
+
**kwargs: Any,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Initializes the in-memory vector database."""
|
|
26
|
+
self.embedding_function = resolve_embedding(
|
|
27
|
+
embedding=embedding or embedding_function or embedding_model,
|
|
28
|
+
**kwargs,
|
|
29
|
+
)
|
|
30
|
+
self._entries: list[tuple[str, list[float], Document]] = []
|
|
31
|
+
|
|
32
|
+
def _cosine_similarity(self, a: list[float], b: list[float]) -> float:
|
|
33
|
+
"""Calculates cosine similarity between two vector embeddings."""
|
|
34
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
35
|
+
norm_a = math.sqrt(sum(x * x for x in a))
|
|
36
|
+
norm_b = math.sqrt(sum(y * y for y in b))
|
|
37
|
+
if norm_a == 0.0 or norm_b == 0.0:
|
|
38
|
+
return 0.0
|
|
39
|
+
return dot / (norm_a * norm_b)
|
|
40
|
+
|
|
41
|
+
def add_documents(
|
|
42
|
+
self,
|
|
43
|
+
documents: list[Document],
|
|
44
|
+
embeddings: list[list[float]] | None = None,
|
|
45
|
+
) -> list[str]:
|
|
46
|
+
"""Upserts documents into memory."""
|
|
47
|
+
if not documents:
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
texts = [doc.page_content for doc in documents]
|
|
51
|
+
vecs = embeddings or self.embedding_function.embed_documents(texts)
|
|
52
|
+
assigned_ids: list[str] = []
|
|
53
|
+
|
|
54
|
+
for doc, vec in zip(documents, vecs):
|
|
55
|
+
doc_id = str(doc.metadata.get("chunk_id") or uuid.uuid4())
|
|
56
|
+
# Replace if same ID exists
|
|
57
|
+
self._entries = [e for e in self._entries if e[0] != doc_id]
|
|
58
|
+
self._entries.append((doc_id, vec, doc))
|
|
59
|
+
assigned_ids.append(doc_id)
|
|
60
|
+
|
|
61
|
+
return assigned_ids
|
|
62
|
+
|
|
63
|
+
def similarity_search(
|
|
64
|
+
self,
|
|
65
|
+
query: str,
|
|
66
|
+
k: int = 4,
|
|
67
|
+
filter_dict: dict[str, Any] | None = None,
|
|
68
|
+
query_embedding: list[float] | None = None,
|
|
69
|
+
) -> list[Document]:
|
|
70
|
+
"""Performs nearest-neighbor search in RAM using cosine similarity."""
|
|
71
|
+
if not self._entries:
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
q_vec = query_embedding or self.embedding_function.embed_query(query)
|
|
75
|
+
scored: list[tuple[float, Document]] = []
|
|
76
|
+
|
|
77
|
+
for _, vec, doc in self._entries:
|
|
78
|
+
# Check optional metadata filter
|
|
79
|
+
if filter_dict:
|
|
80
|
+
match = all(
|
|
81
|
+
doc.metadata.get(kf) == vf for kf, vf in filter_dict.items()
|
|
82
|
+
)
|
|
83
|
+
if not match:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
score = self._cosine_similarity(q_vec, vec)
|
|
87
|
+
scored.append((score, doc))
|
|
88
|
+
|
|
89
|
+
# Sort descending by cosine similarity score
|
|
90
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
91
|
+
return [doc for _, doc in scored[:k]]
|
|
92
|
+
|
|
93
|
+
def count(self) -> int:
|
|
94
|
+
"""Returns the number of documents stored in RAM."""
|
|
95
|
+
return len(self._entries)
|
|
96
|
+
|
|
97
|
+
def clear(self) -> None:
|
|
98
|
+
"""Clears all in-memory entries."""
|
|
99
|
+
self._entries.clear()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def in_memory(
|
|
103
|
+
embedding: Union[str, Any] | None = None,
|
|
104
|
+
embedding_model: str | None = None,
|
|
105
|
+
embedding_function: Any | None = None,
|
|
106
|
+
**kwargs: Any,
|
|
107
|
+
) -> InMemoryDB:
|
|
108
|
+
"""Factory helper creating an ephemeral in-memory vector database.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
```python
|
|
112
|
+
from synapse.database import in_memory
|
|
113
|
+
from synapse.storage import vectorStorage
|
|
114
|
+
|
|
115
|
+
# In-memory with MiniLM:
|
|
116
|
+
db = in_memory(embedding="minilm")
|
|
117
|
+
|
|
118
|
+
# In-memory with FastEmbed BGE:
|
|
119
|
+
db = in_memory(embedding="fastembed")
|
|
120
|
+
|
|
121
|
+
storage = vectorStorage(database=db)
|
|
122
|
+
```
|
|
123
|
+
"""
|
|
124
|
+
return InMemoryDB(
|
|
125
|
+
embedding=embedding,
|
|
126
|
+
embedding_model=embedding_model,
|
|
127
|
+
embedding_function=embedding_function,
|
|
128
|
+
**kwargs,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = ["InMemoryDB", "in_memory"]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""PostgreSQL PGVector database provider for Synapse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Union
|
|
6
|
+
|
|
7
|
+
from langchain_core.documents import Document
|
|
8
|
+
|
|
9
|
+
from synapse.database.base import BaseVectorDB
|
|
10
|
+
from synapse.embeddings import resolve_embedding
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PGVectorDB(BaseVectorDB):
|
|
14
|
+
"""PostgreSQL pgvector database adapter for enterprise deployments."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
connection_string: str,
|
|
19
|
+
collection_name: str = "synapse_embeddings",
|
|
20
|
+
embedding: Union[str, Any] | None = None,
|
|
21
|
+
embedding_model: str | None = None,
|
|
22
|
+
embedding_function: Any | None = None,
|
|
23
|
+
**kwargs: Any,
|
|
24
|
+
) -> None:
|
|
25
|
+
self.connection_string = connection_string
|
|
26
|
+
self.collection_name = collection_name
|
|
27
|
+
self.embedding_function = resolve_embedding(
|
|
28
|
+
embedding=embedding or embedding_function or embedding_model,
|
|
29
|
+
**kwargs,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from langchain_community.vectorstores import PGVector
|
|
34
|
+
|
|
35
|
+
self.store = PGVector(
|
|
36
|
+
connection_string=connection_string,
|
|
37
|
+
collection_name=collection_name,
|
|
38
|
+
embedding_function=self.embedding_function,
|
|
39
|
+
)
|
|
40
|
+
except ImportError:
|
|
41
|
+
from synapse.database.memory import InMemoryDB
|
|
42
|
+
self._fallback = InMemoryDB(
|
|
43
|
+
embedding_model=embedding_model,
|
|
44
|
+
embedding_function=self.embedding_function,
|
|
45
|
+
)
|
|
46
|
+
self.store = None
|
|
47
|
+
|
|
48
|
+
def add_documents(
|
|
49
|
+
self,
|
|
50
|
+
documents: list[Document],
|
|
51
|
+
embeddings: list[list[float]] | None = None,
|
|
52
|
+
) -> list[str]:
|
|
53
|
+
if self.store:
|
|
54
|
+
return self.store.add_documents(documents)
|
|
55
|
+
return self._fallback.add_documents(documents, embeddings)
|
|
56
|
+
|
|
57
|
+
def similarity_search(
|
|
58
|
+
self,
|
|
59
|
+
query: str,
|
|
60
|
+
k: int = 4,
|
|
61
|
+
filter_dict: dict[str, Any] | None = None,
|
|
62
|
+
query_embedding: list[float] | None = None,
|
|
63
|
+
) -> list[Document]:
|
|
64
|
+
if self.store:
|
|
65
|
+
return self.store.similarity_search(query=query, k=k)
|
|
66
|
+
return self._fallback.similarity_search(query=query, k=k, filter_dict=filter_dict)
|
|
67
|
+
|
|
68
|
+
def count(self) -> int:
|
|
69
|
+
if self.store:
|
|
70
|
+
return 0
|
|
71
|
+
return self._fallback.count()
|
|
72
|
+
|
|
73
|
+
def clear(self) -> None:
|
|
74
|
+
if self.store:
|
|
75
|
+
pass
|
|
76
|
+
else:
|
|
77
|
+
self._fallback.clear()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def pgvector(
|
|
81
|
+
connection_string: str,
|
|
82
|
+
collection_name: str = "synapse_embeddings",
|
|
83
|
+
embedding: Union[str, Any] | None = None,
|
|
84
|
+
embedding_model: str | None = None,
|
|
85
|
+
embedding_function: Any | None = None,
|
|
86
|
+
**kwargs: Any,
|
|
87
|
+
) -> PGVectorDB:
|
|
88
|
+
"""Factory helper to configure a PostgreSQL PGVector database instance."""
|
|
89
|
+
return PGVectorDB(
|
|
90
|
+
connection_string=connection_string,
|
|
91
|
+
collection_name=collection_name,
|
|
92
|
+
embedding=embedding,
|
|
93
|
+
embedding_model=embedding_model,
|
|
94
|
+
embedding_function=embedding_function,
|
|
95
|
+
**kwargs,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
__all__ = ["PGVectorDB", "pgvector"]
|