synapse-framework 0.2.5__tar.gz → 0.2.6__tar.gz
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_framework-0.2.5 → synapse_framework-0.2.6}/PKG-INFO +3 -3
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/pyproject.toml +3 -3
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/__init__.py +1 -1
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/app.py +136 -69
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/export/__init__.py +2 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/models/ollama.py +125 -32
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/PKG-INFO +3 -3
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/requires.txt +2 -2
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/LICENSE +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/README.md +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/setup.cfg +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/cli.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/config.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/base.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/factory.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/gmail.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/mcp_wrapper.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/notion.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/photos.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/database/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/database/base.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/database/chroma.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/database/memory.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/database/pgvector.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/database/qdrant.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/embeddings/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/embeddings/base.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/export/mcp_exporter.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/graph/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/graph/base.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/graph/memory.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/graph/neo4j_store.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/graph/networkx_store.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/inspector/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/inspector/template.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/models/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/models/base.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/models/gemini.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/models/openai.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/oauth/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/oauth/google.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/oauth/notion.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/py.typed +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/rag/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/rag/chunker.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/rag/hybrid_rag.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/resource/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/resource/resource.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/resources/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/SKILL.md +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/knowledge_graph/SKILL.md +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/mcp_orchestrator/SKILL.md +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/mlflow_evaluator/SKILL.md +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/rag_analyst/SKILL.md +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/template/SKILL.md +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/skill.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/storage/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/storage/graph_exporter.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/storage/graph_store.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/storage/vector_store.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/telemetry/__init__.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/telemetry/mlflow_tracker.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/SOURCES.txt +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/dependency_links.txt +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/entry_points.txt +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/top_level.txt +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_chunker.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_connectors.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_embeddings.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_export.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_graph.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_mcp_validation.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_rag.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_skills_and_resources.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_storage_pluggability.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_synapse_library.py +0 -0
- {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_telemetry_mlflow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synapse-framework
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Modular Multi-MCP & Hybrid RAG AI Application Framework
|
|
5
5
|
Author: Synapse Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -32,9 +32,9 @@ Requires-Dist: langchain-chroma>=1.1.0
|
|
|
32
32
|
Requires-Dist: langchain-community>=0.4.2
|
|
33
33
|
Requires-Dist: langchain-core>=1.6.7
|
|
34
34
|
Requires-Dist: langchain-ollama>=1.1.0
|
|
35
|
-
Requires-Dist: matplotlib>=3.
|
|
35
|
+
Requires-Dist: matplotlib>=3.8.0
|
|
36
36
|
Requires-Dist: mcp>=2.3.0
|
|
37
|
-
Requires-Dist: networkx>=3.
|
|
37
|
+
Requires-Dist: networkx>=3.2
|
|
38
38
|
Requires-Dist: pydantic>=2.13.5
|
|
39
39
|
Requires-Dist: uvicorn>=0.30.0
|
|
40
40
|
Provides-Extra: mlflow
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "synapse-framework"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.6"
|
|
8
8
|
description = "Modular Multi-MCP & Hybrid RAG AI Application Framework"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -50,9 +50,9 @@ dependencies = [
|
|
|
50
50
|
"langchain-community>=0.4.2",
|
|
51
51
|
"langchain-core>=1.6.7",
|
|
52
52
|
"langchain-ollama>=1.1.0",
|
|
53
|
-
"matplotlib>=3.
|
|
53
|
+
"matplotlib>=3.8.0",
|
|
54
54
|
"mcp>=2.3.0",
|
|
55
|
-
"networkx>=3.
|
|
55
|
+
"networkx>=3.2",
|
|
56
56
|
"pydantic>=2.13.5",
|
|
57
57
|
"uvicorn>=0.30.0",
|
|
58
58
|
]
|
|
@@ -53,9 +53,7 @@ class ChatRequestPayload(BaseModel):
|
|
|
53
53
|
history: list[ChatMessagePayload] | None = Field(
|
|
54
54
|
default_factory=list, description="Conversation history."
|
|
55
55
|
)
|
|
56
|
-
use_rag: bool = Field(
|
|
57
|
-
default=True, description="Enable Hybrid RAG and Multi-MCP tool calls."
|
|
58
|
-
)
|
|
56
|
+
use_rag: bool = Field(default=True, description="Enable Hybrid RAG and Multi-MCP tool calls.")
|
|
59
57
|
|
|
60
58
|
|
|
61
59
|
class SyncRequestPayload(BaseModel):
|
|
@@ -63,7 +61,9 @@ class SyncRequestPayload(BaseModel):
|
|
|
63
61
|
|
|
64
62
|
query: str | None = Field(default=None, description="Optional search query filter.")
|
|
65
63
|
max_results: int = Field(default=5, description="Maximum items to index.")
|
|
66
|
-
extract_graph: bool = Field(
|
|
64
|
+
extract_graph: bool = Field(
|
|
65
|
+
default=True, description="Extract semantic knowledge graph triples."
|
|
66
|
+
)
|
|
67
67
|
tool_name: str | None = Field(default=None, description="Optional explicit tool name.")
|
|
68
68
|
arguments: dict[str, Any] | None = Field(default=None, description="Optional tool arguments.")
|
|
69
69
|
|
|
@@ -97,9 +97,9 @@ class BaseApp:
|
|
|
97
97
|
self,
|
|
98
98
|
name: str = "Synapse AI",
|
|
99
99
|
description: str = "Multi-MCP & Hybrid RAG Framework",
|
|
100
|
-
version: str = "0.2.
|
|
100
|
+
version: str = "0.2.6",
|
|
101
101
|
model: BaseModelProvider | None = None,
|
|
102
|
-
connectors: list[BaseConnector] | None = None,
|
|
102
|
+
connectors: list[BaseConnector] | BaseConnector | None = None,
|
|
103
103
|
skills: Skill | Sequence[Skill] | str | Path | None = None,
|
|
104
104
|
resources: Resource | Sequence[Resource] | str | Path | Any | None = None,
|
|
105
105
|
vector_store: VectorStorage | BaseVectorDB | bool | None = None,
|
|
@@ -111,6 +111,7 @@ class BaseApp:
|
|
|
111
111
|
*,
|
|
112
112
|
title: str | None = None,
|
|
113
113
|
app_name: str | None = None,
|
|
114
|
+
connector: list[BaseConnector] | BaseConnector | None = None,
|
|
114
115
|
resource: Resource | Sequence[Resource] | str | Path | Any | None = None,
|
|
115
116
|
persistent_storage: VectorStorage | BaseVectorDB | bool | None = None,
|
|
116
117
|
Visualize_graph: BaseGraphStore | bool | None = None,
|
|
@@ -150,9 +151,7 @@ class BaseApp:
|
|
|
150
151
|
self.vector_store = VectorStorage(database=storage_arg)
|
|
151
152
|
elif storage_arg is True:
|
|
152
153
|
resolved_data_dir.mkdir(parents=True, exist_ok=True)
|
|
153
|
-
self.vector_store = VectorStorage(
|
|
154
|
-
database=chromadb(path=self.data_dir / "chroma_db")
|
|
155
|
-
)
|
|
154
|
+
self.vector_store = VectorStorage(database=chromadb(path=self.data_dir / "chroma_db"))
|
|
156
155
|
else:
|
|
157
156
|
self.vector_store = None
|
|
158
157
|
|
|
@@ -179,8 +178,19 @@ class BaseApp:
|
|
|
179
178
|
)
|
|
180
179
|
|
|
181
180
|
self._connectors: dict[str, BaseConnector] = {}
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
active_connectors = connectors if connectors is not None else connector
|
|
182
|
+
if active_connectors is not None:
|
|
183
|
+
if isinstance(active_connectors, BaseConnector):
|
|
184
|
+
self.add_connector(active_connectors)
|
|
185
|
+
elif isinstance(active_connectors, (list, tuple, set)):
|
|
186
|
+
for conn in active_connectors:
|
|
187
|
+
self.add_connector(conn)
|
|
188
|
+
else:
|
|
189
|
+
try:
|
|
190
|
+
for conn in active_connectors:
|
|
191
|
+
self.add_connector(conn)
|
|
192
|
+
except TypeError:
|
|
193
|
+
self.add_connector(active_connectors)
|
|
184
194
|
|
|
185
195
|
self.sync_state: dict[str, Any] = {
|
|
186
196
|
"is_syncing": False,
|
|
@@ -193,7 +203,13 @@ class BaseApp:
|
|
|
193
203
|
|
|
194
204
|
self._skills: list[Skill] = []
|
|
195
205
|
if skills is not None:
|
|
196
|
-
if
|
|
206
|
+
if callable(skills) and not isinstance(skills, Skill):
|
|
207
|
+
for sk_name in ("rag-analyst", "knowledge-graph", "mcp-orchestrator"):
|
|
208
|
+
try:
|
|
209
|
+
self.add_skill(load_premade_skill(sk_name))
|
|
210
|
+
except Exception:
|
|
211
|
+
pass # nosec B110
|
|
212
|
+
elif isinstance(skills, (list, tuple)):
|
|
197
213
|
for s in skills:
|
|
198
214
|
self.add_skill(s)
|
|
199
215
|
else:
|
|
@@ -269,18 +285,22 @@ class BaseApp:
|
|
|
269
285
|
collected.extend(conn._cached_schemas)
|
|
270
286
|
elif hasattr(conn, "tools_dict") and isinstance(conn.tools_dict, dict):
|
|
271
287
|
for tname in conn.tools_dict.keys():
|
|
272
|
-
collected.append(
|
|
288
|
+
collected.append(
|
|
289
|
+
{
|
|
290
|
+
"server": conn.name,
|
|
291
|
+
"name": tname,
|
|
292
|
+
"connector": conn.name,
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
elif getattr(conn, "default_tool", None):
|
|
296
|
+
collected.append(
|
|
297
|
+
{
|
|
273
298
|
"server": conn.name,
|
|
274
|
-
"name":
|
|
299
|
+
"name": conn.default_tool,
|
|
300
|
+
"description": conn.description,
|
|
275
301
|
"connector": conn.name,
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
collected.append({
|
|
279
|
-
"server": conn.name,
|
|
280
|
-
"name": conn.default_tool,
|
|
281
|
-
"description": conn.description,
|
|
282
|
-
"connector": conn.name,
|
|
283
|
-
})
|
|
302
|
+
}
|
|
303
|
+
)
|
|
284
304
|
return collected
|
|
285
305
|
|
|
286
306
|
async def list_tools(self) -> list[dict[str, Any]]:
|
|
@@ -296,18 +316,22 @@ class BaseApp:
|
|
|
296
316
|
if isinstance(tools, list):
|
|
297
317
|
collected.extend(tools)
|
|
298
318
|
elif getattr(conn, "default_tool", None):
|
|
299
|
-
collected.append(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
319
|
+
collected.append(
|
|
320
|
+
{
|
|
321
|
+
"server": conn.name,
|
|
322
|
+
"name": conn.default_tool,
|
|
323
|
+
"description": conn.description,
|
|
324
|
+
"connector": conn.name,
|
|
325
|
+
}
|
|
326
|
+
)
|
|
305
327
|
return collected
|
|
306
328
|
|
|
307
329
|
def add_skill(self, skill: Skill | str | Path | list[Any] | tuple[Any, ...]) -> Any:
|
|
308
330
|
"""Registers a Skill or sequence of skills into this application."""
|
|
309
331
|
if isinstance(skill, (list, tuple)):
|
|
310
332
|
return [self.add_skill(s) for s in skill]
|
|
333
|
+
if callable(skill) and not isinstance(skill, Skill):
|
|
334
|
+
return None
|
|
311
335
|
sk = skill if isinstance(skill, Skill) else makeskills(skill)
|
|
312
336
|
self._skills.append(sk)
|
|
313
337
|
return sk
|
|
@@ -353,7 +377,6 @@ class BaseApp:
|
|
|
353
377
|
"""Returns all registered resource collections."""
|
|
354
378
|
return self.list_resources()
|
|
355
379
|
|
|
356
|
-
|
|
357
380
|
def get_resource(self, name: str) -> Resource | None:
|
|
358
381
|
"""Retrieves a resource collection by name."""
|
|
359
382
|
for r in self._resources:
|
|
@@ -378,9 +401,11 @@ class BaseApp:
|
|
|
378
401
|
|
|
379
402
|
records: list[dict[str, Any]] = []
|
|
380
403
|
if isinstance(raw_result, list):
|
|
381
|
-
records = [r
|
|
404
|
+
records = [r if isinstance(r, dict) else {"result": r} for r in raw_result]
|
|
382
405
|
elif isinstance(raw_result, dict):
|
|
383
406
|
records = [raw_result]
|
|
407
|
+
elif raw_result is not None:
|
|
408
|
+
records = [{"result": raw_result}]
|
|
384
409
|
|
|
385
410
|
formatted_items: list[str] = []
|
|
386
411
|
live_chunks: list[dict[str, Any]] = []
|
|
@@ -445,7 +470,9 @@ class BaseApp:
|
|
|
445
470
|
self.sync_state.get("items_synced", 0), self.vector_store.count()
|
|
446
471
|
)
|
|
447
472
|
else:
|
|
448
|
-
self.sync_state["items_synced"] = self.sync_state.get("items_synced", 0) + len(
|
|
473
|
+
self.sync_state["items_synced"] = self.sync_state.get("items_synced", 0) + len(
|
|
474
|
+
records
|
|
475
|
+
)
|
|
449
476
|
except Exception:
|
|
450
477
|
self.sync_state["items_synced"] = self.sync_state.get("items_synced", 0) + len(records)
|
|
451
478
|
|
|
@@ -476,11 +503,28 @@ class BaseApp:
|
|
|
476
503
|
"""Routes a prompt to appropriate connector tools and executes them."""
|
|
477
504
|
clean_msg = (user_message or "").strip().lower()
|
|
478
505
|
conversational_greetings = {
|
|
479
|
-
"hi",
|
|
480
|
-
"
|
|
481
|
-
"
|
|
482
|
-
"
|
|
483
|
-
"
|
|
506
|
+
"hi",
|
|
507
|
+
"hello",
|
|
508
|
+
"hey",
|
|
509
|
+
"hola",
|
|
510
|
+
"greetings",
|
|
511
|
+
"good morning",
|
|
512
|
+
"good afternoon",
|
|
513
|
+
"good evening",
|
|
514
|
+
"how are you",
|
|
515
|
+
"who are you",
|
|
516
|
+
"what can you do",
|
|
517
|
+
"which model is this",
|
|
518
|
+
"which model is getting used",
|
|
519
|
+
"what model is this",
|
|
520
|
+
"what model is being used",
|
|
521
|
+
"help",
|
|
522
|
+
"thanks",
|
|
523
|
+
"thank you",
|
|
524
|
+
"bye",
|
|
525
|
+
"goodbye",
|
|
526
|
+
"test",
|
|
527
|
+
"ping",
|
|
484
528
|
}
|
|
485
529
|
if clean_msg in conversational_greetings:
|
|
486
530
|
return {
|
|
@@ -512,18 +556,22 @@ class BaseApp:
|
|
|
512
556
|
for conn, tools in zip(matching_active_conns, tool_lists):
|
|
513
557
|
if isinstance(tools, list):
|
|
514
558
|
for t in tools:
|
|
515
|
-
candidate_tools.append(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
559
|
+
candidate_tools.append(
|
|
560
|
+
{
|
|
561
|
+
"server": conn.name,
|
|
562
|
+
"name": t.get("name", conn.default_tool),
|
|
563
|
+
"description": t.get("description", conn.description),
|
|
564
|
+
"inputSchema": t.get("inputSchema", {}),
|
|
565
|
+
}
|
|
566
|
+
)
|
|
521
567
|
if conn.default_tool and conn.intent_regex:
|
|
522
|
-
fallback_calls.append(
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
568
|
+
fallback_calls.append(
|
|
569
|
+
{
|
|
570
|
+
"server": conn.name,
|
|
571
|
+
"tool": conn.default_tool,
|
|
572
|
+
"arguments": conn.default_args_builder(user_message),
|
|
573
|
+
}
|
|
574
|
+
)
|
|
527
575
|
|
|
528
576
|
unauth_notes: list[str] = []
|
|
529
577
|
for c_name in unauthenticated_connectors:
|
|
@@ -574,16 +622,16 @@ class BaseApp:
|
|
|
574
622
|
context_sections.append(res["section_text"])
|
|
575
623
|
live_chunks.extend(res["live_chunks"])
|
|
576
624
|
live_triples.extend(res["live_triples"])
|
|
577
|
-
executed.append(
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
context_sections.append(
|
|
585
|
-
f"### {srv.upper()} NOTE:\nLive fetch ({tl}) error: {err}."
|
|
625
|
+
executed.append(
|
|
626
|
+
{
|
|
627
|
+
"server": srv,
|
|
628
|
+
"tool": tl,
|
|
629
|
+
"arguments": args,
|
|
630
|
+
"items_fetched": len(res["records"]),
|
|
631
|
+
}
|
|
586
632
|
)
|
|
633
|
+
except Exception as err:
|
|
634
|
+
context_sections.append(f"### {srv.upper()} NOTE:\nLive fetch ({tl}) error: {err}.")
|
|
587
635
|
|
|
588
636
|
return {
|
|
589
637
|
"tool_calls": executed,
|
|
@@ -640,9 +688,7 @@ class BaseApp:
|
|
|
640
688
|
yield {"type": "context", "data": context_data}
|
|
641
689
|
|
|
642
690
|
context_str = (
|
|
643
|
-
"\n\n".join(context_sections)
|
|
644
|
-
if context_sections
|
|
645
|
-
else "No external context required."
|
|
691
|
+
"\n\n".join(context_sections) if context_sections else "No external context required."
|
|
646
692
|
)
|
|
647
693
|
|
|
648
694
|
skills_section = ""
|
|
@@ -680,8 +726,12 @@ class BaseApp:
|
|
|
680
726
|
response="".join(full_tokens),
|
|
681
727
|
latency_ms=latency_ms,
|
|
682
728
|
model=getattr(self.model, "model_name", "custom"),
|
|
683
|
-
chunks_retrieved=len(merged_chunks)
|
|
684
|
-
|
|
729
|
+
chunks_retrieved=len(merged_chunks)
|
|
730
|
+
if "merged_chunks" in locals()
|
|
731
|
+
else len(live_tool_chunks),
|
|
732
|
+
graph_triples=len(merged_triples)
|
|
733
|
+
if "merged_triples" in locals()
|
|
734
|
+
else len(live_tool_triples),
|
|
685
735
|
tools_executed=executed_tools,
|
|
686
736
|
)
|
|
687
737
|
|
|
@@ -735,9 +785,7 @@ class BaseApp:
|
|
|
735
785
|
async def chat_route(req: ChatRequestPayload):
|
|
736
786
|
async def event_generator():
|
|
737
787
|
try:
|
|
738
|
-
async for ev in app_ref.chat_stream(
|
|
739
|
-
req.message, req.history, req.use_rag
|
|
740
|
-
):
|
|
788
|
+
async for ev in app_ref.chat_stream(req.message, req.history, req.use_rag):
|
|
741
789
|
if ev["type"] == "context":
|
|
742
790
|
yield f"event: context\ndata: {json.dumps(ev['data'])}\n\n"
|
|
743
791
|
elif ev["type"] == "token":
|
|
@@ -785,7 +833,9 @@ class BaseApp:
|
|
|
785
833
|
):
|
|
786
834
|
conn = app_ref.get_connector(connector_name)
|
|
787
835
|
if not conn:
|
|
788
|
-
raise HTTPException(
|
|
836
|
+
raise HTTPException(
|
|
837
|
+
status_code=404, detail=f"Connector '{connector_name}' not found."
|
|
838
|
+
)
|
|
789
839
|
|
|
790
840
|
tool = req.tool_name or conn.default_tool
|
|
791
841
|
if req.arguments:
|
|
@@ -817,7 +867,10 @@ class BaseApp:
|
|
|
817
867
|
app_ref.sync_state["is_syncing"] = False
|
|
818
868
|
|
|
819
869
|
background_tasks.add_task(_bg_task)
|
|
820
|
-
return {
|
|
870
|
+
return {
|
|
871
|
+
"status": "started",
|
|
872
|
+
"message": f"Background sync initiated for '{connector_name}'.",
|
|
873
|
+
}
|
|
821
874
|
|
|
822
875
|
# ---------------- Auth Routes ----------------
|
|
823
876
|
@api.get("/auth/status", tags=["Auth"])
|
|
@@ -828,7 +881,11 @@ class BaseApp:
|
|
|
828
881
|
if hasattr(c, "oauth") and hasattr(c.oauth, "has_credentials"):
|
|
829
882
|
has_creds = c.oauth.has_credentials()
|
|
830
883
|
elif hasattr(c, "has_credentials"):
|
|
831
|
-
has_creds =
|
|
884
|
+
has_creds = (
|
|
885
|
+
c.has_credentials()
|
|
886
|
+
if callable(c.has_credentials)
|
|
887
|
+
else bool(c.has_credentials)
|
|
888
|
+
)
|
|
832
889
|
else:
|
|
833
890
|
has_creds = c.is_authenticated()
|
|
834
891
|
|
|
@@ -992,6 +1049,7 @@ class BaseApp:
|
|
|
992
1049
|
base_url = getattr(app_ref.model, "base_url", "http://localhost:11434")
|
|
993
1050
|
try:
|
|
994
1051
|
import httpx
|
|
1052
|
+
|
|
995
1053
|
async with httpx.AsyncClient(timeout=0.4) as client:
|
|
996
1054
|
res = await client.get(f"{base_url}/api/tags")
|
|
997
1055
|
model_status = "ready" if res.status_code == 200 else "unreachable"
|
|
@@ -1028,7 +1086,11 @@ class BaseApp:
|
|
|
1028
1086
|
|
|
1029
1087
|
storage_status = "disabled"
|
|
1030
1088
|
if app_ref.vector_store is not None:
|
|
1031
|
-
storage_status =
|
|
1089
|
+
storage_status = (
|
|
1090
|
+
"ready"
|
|
1091
|
+
if (chroma_path.exists() or hasattr(app_ref.vector_store, "database"))
|
|
1092
|
+
else "initialized"
|
|
1093
|
+
)
|
|
1032
1094
|
|
|
1033
1095
|
services_dict = {
|
|
1034
1096
|
provider_type: model_status,
|
|
@@ -1074,7 +1136,11 @@ class BaseApp:
|
|
|
1074
1136
|
await self.model.close()
|
|
1075
1137
|
except Exception:
|
|
1076
1138
|
pass # nosec B110
|
|
1077
|
-
if
|
|
1139
|
+
if (
|
|
1140
|
+
self.graph_store is not None
|
|
1141
|
+
and hasattr(self.graph_store, "driver")
|
|
1142
|
+
and self.graph_store.driver
|
|
1143
|
+
):
|
|
1078
1144
|
try:
|
|
1079
1145
|
self.graph_store.driver.close()
|
|
1080
1146
|
except Exception:
|
|
@@ -1094,6 +1160,7 @@ class BaseApp:
|
|
|
1094
1160
|
def to_mcp_server(self, name: str | None = None) -> Any:
|
|
1095
1161
|
"""Converts all registered connectors into an official MCP Server instance."""
|
|
1096
1162
|
from synapse.export.mcp_exporter import MCPExporter
|
|
1163
|
+
|
|
1097
1164
|
return MCPExporter.create_mcp_server(self, name=name or self.name)
|
|
1098
1165
|
|
|
1099
1166
|
def run_mcp(self, transport: str = "stdio") -> None:
|
|
@@ -5,10 +5,12 @@ from synapse.storage.graph_exporter import GraphExporter
|
|
|
5
5
|
|
|
6
6
|
# Developer-friendly alias
|
|
7
7
|
graphexporter = GraphExporter
|
|
8
|
+
mcp_exporter = mcpexporter
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
10
11
|
"GraphExporter",
|
|
11
12
|
"MCPExporter",
|
|
12
13
|
"graphexporter",
|
|
14
|
+
"mcp_exporter",
|
|
13
15
|
"mcpexporter",
|
|
14
16
|
]
|
|
@@ -34,7 +34,7 @@ class OllamaModel(BaseModelProvider):
|
|
|
34
34
|
temperature: float = DEFAULT_TEMPERATURE,
|
|
35
35
|
num_ctx: int = 4096,
|
|
36
36
|
timeout: float = 60.0,
|
|
37
|
-
num_predict: int =
|
|
37
|
+
num_predict: int = 1024,
|
|
38
38
|
) -> None:
|
|
39
39
|
"""Initializes the Ollama model provider.
|
|
40
40
|
|
|
@@ -44,7 +44,7 @@ class OllamaModel(BaseModelProvider):
|
|
|
44
44
|
temperature: Sampling temperature. Defaults to 0.1.
|
|
45
45
|
num_ctx: Context window size. Defaults to 4096.
|
|
46
46
|
timeout: HTTP request timeout in seconds. Defaults to 60.0.
|
|
47
|
-
num_predict: Maximum tokens to generate per response turn. Defaults to
|
|
47
|
+
num_predict: Maximum tokens to generate per response turn. Defaults to 1024.
|
|
48
48
|
"""
|
|
49
49
|
super().__init__(model_name=model, temperature=temperature)
|
|
50
50
|
self.base_url = base_url.rstrip("/")
|
|
@@ -116,6 +116,7 @@ class OllamaModel(BaseModelProvider):
|
|
|
116
116
|
prompt: str,
|
|
117
117
|
temperature: float | None = None,
|
|
118
118
|
format: str | None = None,
|
|
119
|
+
num_predict: int | None = None,
|
|
119
120
|
) -> str:
|
|
120
121
|
"""Generates a complete non-streaming response from Ollama.
|
|
121
122
|
|
|
@@ -123,11 +124,13 @@ class OllamaModel(BaseModelProvider):
|
|
|
123
124
|
prompt: Text prompt string.
|
|
124
125
|
temperature: Optional temperature override.
|
|
125
126
|
format: Optional output format (e.g. 'json').
|
|
127
|
+
num_predict: Optional token generation limit override.
|
|
126
128
|
|
|
127
129
|
Returns:
|
|
128
130
|
The complete response text string.
|
|
129
131
|
"""
|
|
130
132
|
temp = self.temperature if temperature is None else temperature
|
|
133
|
+
pred = num_predict if num_predict is not None else self.num_predict
|
|
131
134
|
payload: dict[str, Any] = {
|
|
132
135
|
"model": self.model_name,
|
|
133
136
|
"prompt": prompt,
|
|
@@ -135,7 +138,7 @@ class OllamaModel(BaseModelProvider):
|
|
|
135
138
|
"options": {
|
|
136
139
|
"temperature": temp,
|
|
137
140
|
"num_ctx": self.num_ctx,
|
|
138
|
-
"num_predict":
|
|
141
|
+
"num_predict": pred,
|
|
139
142
|
},
|
|
140
143
|
}
|
|
141
144
|
if format:
|
|
@@ -147,9 +150,20 @@ class OllamaModel(BaseModelProvider):
|
|
|
147
150
|
f"{self.base_url}/api/generate",
|
|
148
151
|
json=payload,
|
|
149
152
|
)
|
|
153
|
+
if res.status_code == 501 and format:
|
|
154
|
+
# MLX runner or models that do not support format: json
|
|
155
|
+
payload.pop("format", None)
|
|
156
|
+
res = await client.post(
|
|
157
|
+
f"{self.base_url}/api/generate",
|
|
158
|
+
json=payload,
|
|
159
|
+
)
|
|
150
160
|
res.raise_for_status()
|
|
151
161
|
data = res.json()
|
|
152
|
-
|
|
162
|
+
resp_text = str(data.get("response", "")).strip()
|
|
163
|
+
if not resp_text and data.get("thinking"):
|
|
164
|
+
# Fallback for reasoning/thinking models if response is empty
|
|
165
|
+
resp_text = str(data.get("thinking", "")).strip()
|
|
166
|
+
return resp_text
|
|
153
167
|
except (httpx.ConnectError, httpx.ConnectTimeout):
|
|
154
168
|
return f"[Error: Cannot reach Ollama at {self.base_url}. Ensure Ollama is running.]"
|
|
155
169
|
except Exception as e:
|
|
@@ -193,7 +207,15 @@ class OllamaModel(BaseModelProvider):
|
|
|
193
207
|
cleaned = match.group(0)
|
|
194
208
|
parsed = json.loads(cleaned)
|
|
195
209
|
if isinstance(parsed, dict):
|
|
196
|
-
parsed =
|
|
210
|
+
parsed = (
|
|
211
|
+
parsed.get("triples")
|
|
212
|
+
or parsed.get("data")
|
|
213
|
+
or (
|
|
214
|
+
list(parsed.values())[0]
|
|
215
|
+
if parsed.values() and isinstance(list(parsed.values())[0], list)
|
|
216
|
+
else []
|
|
217
|
+
)
|
|
218
|
+
)
|
|
197
219
|
|
|
198
220
|
if isinstance(parsed, list):
|
|
199
221
|
valid_triples: list[dict[str, str]] = []
|
|
@@ -204,26 +226,21 @@ class OllamaModel(BaseModelProvider):
|
|
|
204
226
|
and "relation" in item
|
|
205
227
|
and "target" in item
|
|
206
228
|
):
|
|
207
|
-
valid_triples.append(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
str(item.get("target_type", "Default"))
|
|
223
|
-
.strip()
|
|
224
|
-
.title()
|
|
225
|
-
),
|
|
226
|
-
})
|
|
229
|
+
valid_triples.append(
|
|
230
|
+
{
|
|
231
|
+
"source": str(item["source"]).strip(),
|
|
232
|
+
"relation": (
|
|
233
|
+
str(item["relation"]).strip().upper().replace(" ", "_")
|
|
234
|
+
),
|
|
235
|
+
"target": str(item["target"]).strip(),
|
|
236
|
+
"source_type": (
|
|
237
|
+
str(item.get("source_type", "Default")).strip().title()
|
|
238
|
+
),
|
|
239
|
+
"target_type": (
|
|
240
|
+
str(item.get("target_type", "Default")).strip().title()
|
|
241
|
+
),
|
|
242
|
+
}
|
|
243
|
+
)
|
|
227
244
|
return valid_triples
|
|
228
245
|
except Exception:
|
|
229
246
|
return []
|
|
@@ -266,6 +283,73 @@ class OllamaModel(BaseModelProvider):
|
|
|
266
283
|
valid_keys[("", name.lower())] = t
|
|
267
284
|
valid_keys[(srv.lower(), f"{srv.lower()}_{name.lower()}")] = t
|
|
268
285
|
|
|
286
|
+
# 1. Primary path: Attempt native Ollama function/tool calling via /api/chat
|
|
287
|
+
try:
|
|
288
|
+
formatted_tools: list[dict[str, Any]] = []
|
|
289
|
+
for t in candidate_tools:
|
|
290
|
+
t_name = str(t.get("name") or "").strip()
|
|
291
|
+
t_desc = str(t.get("description") or "").strip()
|
|
292
|
+
t_params = (
|
|
293
|
+
t.get("inputSchema")
|
|
294
|
+
or t.get("parameters")
|
|
295
|
+
or {"type": "object", "properties": {}}
|
|
296
|
+
)
|
|
297
|
+
formatted_tools.append(
|
|
298
|
+
{
|
|
299
|
+
"type": "function",
|
|
300
|
+
"function": {
|
|
301
|
+
"name": t_name,
|
|
302
|
+
"description": t_desc,
|
|
303
|
+
"parameters": t_params,
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
309
|
+
chat_res = await client.post(
|
|
310
|
+
f"{self.base_url}/api/chat",
|
|
311
|
+
json={
|
|
312
|
+
"model": self.model_name,
|
|
313
|
+
"messages": [{"role": "user", "content": text}],
|
|
314
|
+
"tools": formatted_tools,
|
|
315
|
+
"stream": False,
|
|
316
|
+
"options": {
|
|
317
|
+
"temperature": 0.0,
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
if chat_res.status_code == 200:
|
|
322
|
+
chat_data = chat_res.json()
|
|
323
|
+
native_calls = chat_data.get("message", {}).get("tool_calls") or []
|
|
324
|
+
if isinstance(native_calls, list) and len(native_calls) > 0:
|
|
325
|
+
validated_native: list[dict[str, Any]] = []
|
|
326
|
+
for call in native_calls:
|
|
327
|
+
fn = call.get("function") or {}
|
|
328
|
+
tl_name = str(fn.get("name") or "").strip()
|
|
329
|
+
args = fn.get("arguments")
|
|
330
|
+
if not isinstance(args, dict):
|
|
331
|
+
args = {}
|
|
332
|
+
spec = valid_keys.get(("", tl_name)) or valid_keys.get(
|
|
333
|
+
("", tl_name.lower())
|
|
334
|
+
)
|
|
335
|
+
if not spec:
|
|
336
|
+
for (_k_srv, k_name), s_val in valid_keys.items():
|
|
337
|
+
if k_name.lower() == tl_name.lower():
|
|
338
|
+
spec = s_val
|
|
339
|
+
break
|
|
340
|
+
if spec:
|
|
341
|
+
validated_native.append(
|
|
342
|
+
{
|
|
343
|
+
"server": str(spec.get("server") or ""),
|
|
344
|
+
"tool": str(spec.get("name") or tl_name),
|
|
345
|
+
"arguments": args,
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
if validated_native:
|
|
349
|
+
return validated_native
|
|
350
|
+
except Exception:
|
|
351
|
+
pass # nosec B110
|
|
352
|
+
|
|
269
353
|
catalog_text = "\n".join(catalog_lines)
|
|
270
354
|
tool_prompt = (
|
|
271
355
|
"You are a multi-tool router for a personal AI assistant.\n"
|
|
@@ -283,7 +367,12 @@ class OllamaModel(BaseModelProvider):
|
|
|
283
367
|
)
|
|
284
368
|
|
|
285
369
|
try:
|
|
286
|
-
raw_text = await self.generate(
|
|
370
|
+
raw_text = await self.generate(
|
|
371
|
+
tool_prompt,
|
|
372
|
+
temperature=0.0,
|
|
373
|
+
format="json",
|
|
374
|
+
num_predict=max(self.num_predict, 1024),
|
|
375
|
+
)
|
|
287
376
|
if not raw_text or raw_text.startswith("[Error:"):
|
|
288
377
|
return []
|
|
289
378
|
cleaned = re.sub(
|
|
@@ -292,7 +381,9 @@ class OllamaModel(BaseModelProvider):
|
|
|
292
381
|
raw_text,
|
|
293
382
|
flags=re.MULTILINE,
|
|
294
383
|
).strip()
|
|
295
|
-
match = re.search(r"\{.*\}", cleaned, re.DOTALL)
|
|
384
|
+
match = re.search(r"\{\s*\"tool_calls\".*\}", cleaned, re.DOTALL) or re.search(
|
|
385
|
+
r"\{.*\}", cleaned, re.DOTALL
|
|
386
|
+
)
|
|
296
387
|
if match:
|
|
297
388
|
cleaned = match.group(0)
|
|
298
389
|
parsed = json.loads(cleaned)
|
|
@@ -323,11 +414,13 @@ class OllamaModel(BaseModelProvider):
|
|
|
323
414
|
args = item.get("arguments")
|
|
324
415
|
if not isinstance(args, dict):
|
|
325
416
|
args = {}
|
|
326
|
-
validated.append(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
417
|
+
validated.append(
|
|
418
|
+
{
|
|
419
|
+
"server": resolved_srv,
|
|
420
|
+
"tool": resolved_tl,
|
|
421
|
+
"arguments": args,
|
|
422
|
+
}
|
|
423
|
+
)
|
|
331
424
|
return validated
|
|
332
425
|
except (json.JSONDecodeError, KeyError, TypeError, ValueError, httpx.HTTPError):
|
|
333
426
|
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synapse-framework
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Modular Multi-MCP & Hybrid RAG AI Application Framework
|
|
5
5
|
Author: Synapse Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -32,9 +32,9 @@ Requires-Dist: langchain-chroma>=1.1.0
|
|
|
32
32
|
Requires-Dist: langchain-community>=0.4.2
|
|
33
33
|
Requires-Dist: langchain-core>=1.6.7
|
|
34
34
|
Requires-Dist: langchain-ollama>=1.1.0
|
|
35
|
-
Requires-Dist: matplotlib>=3.
|
|
35
|
+
Requires-Dist: matplotlib>=3.8.0
|
|
36
36
|
Requires-Dist: mcp>=2.3.0
|
|
37
|
-
Requires-Dist: networkx>=3.
|
|
37
|
+
Requires-Dist: networkx>=3.2
|
|
38
38
|
Requires-Dist: pydantic>=2.13.5
|
|
39
39
|
Requires-Dist: uvicorn>=0.30.0
|
|
40
40
|
Provides-Extra: mlflow
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/knowledge_graph/SKILL.md
RENAMED
|
File without changes
|
{synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/mcp_orchestrator/SKILL.md
RENAMED
|
File without changes
|
{synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/mlflow_evaluator/SKILL.md
RENAMED
|
File without changes
|
{synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/rag_analyst/SKILL.md
RENAMED
|
File without changes
|
{synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/template/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|