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.
Files changed (79) hide show
  1. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/PKG-INFO +3 -3
  2. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/pyproject.toml +3 -3
  3. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/__init__.py +1 -1
  4. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/app.py +136 -69
  5. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/export/__init__.py +2 -0
  6. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/models/ollama.py +125 -32
  7. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/PKG-INFO +3 -3
  8. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/requires.txt +2 -2
  9. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/LICENSE +0 -0
  10. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/README.md +0 -0
  11. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/setup.cfg +0 -0
  12. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/cli.py +0 -0
  13. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/config.py +0 -0
  14. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/__init__.py +0 -0
  15. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/base.py +0 -0
  16. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/factory.py +0 -0
  17. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/gmail.py +0 -0
  18. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/mcp_wrapper.py +0 -0
  19. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/notion.py +0 -0
  20. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/connectors/photos.py +0 -0
  21. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/database/__init__.py +0 -0
  22. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/database/base.py +0 -0
  23. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/database/chroma.py +0 -0
  24. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/database/memory.py +0 -0
  25. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/database/pgvector.py +0 -0
  26. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/database/qdrant.py +0 -0
  27. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/embeddings/__init__.py +0 -0
  28. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/embeddings/base.py +0 -0
  29. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/export/mcp_exporter.py +0 -0
  30. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/graph/__init__.py +0 -0
  31. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/graph/base.py +0 -0
  32. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/graph/memory.py +0 -0
  33. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/graph/neo4j_store.py +0 -0
  34. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/graph/networkx_store.py +0 -0
  35. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/inspector/__init__.py +0 -0
  36. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/inspector/template.py +0 -0
  37. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/models/__init__.py +0 -0
  38. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/models/base.py +0 -0
  39. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/models/gemini.py +0 -0
  40. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/models/openai.py +0 -0
  41. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/oauth/__init__.py +0 -0
  42. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/oauth/google.py +0 -0
  43. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/oauth/notion.py +0 -0
  44. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/py.typed +0 -0
  45. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/rag/__init__.py +0 -0
  46. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/rag/chunker.py +0 -0
  47. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/rag/hybrid_rag.py +0 -0
  48. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/resource/__init__.py +0 -0
  49. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/resource/resource.py +0 -0
  50. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/resources/__init__.py +0 -0
  51. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/__init__.py +0 -0
  52. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/SKILL.md +0 -0
  53. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/knowledge_graph/SKILL.md +0 -0
  54. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/mcp_orchestrator/SKILL.md +0 -0
  55. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/mlflow_evaluator/SKILL.md +0 -0
  56. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/rag_analyst/SKILL.md +0 -0
  57. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/premade/template/SKILL.md +0 -0
  58. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/skills/skill.py +0 -0
  59. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/storage/__init__.py +0 -0
  60. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/storage/graph_exporter.py +0 -0
  61. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/storage/graph_store.py +0 -0
  62. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/storage/vector_store.py +0 -0
  63. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/telemetry/__init__.py +0 -0
  64. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse/telemetry/mlflow_tracker.py +0 -0
  65. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/SOURCES.txt +0 -0
  66. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/dependency_links.txt +0 -0
  67. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/entry_points.txt +0 -0
  68. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/synapse_framework.egg-info/top_level.txt +0 -0
  69. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_chunker.py +0 -0
  70. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_connectors.py +0 -0
  71. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_embeddings.py +0 -0
  72. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_export.py +0 -0
  73. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_graph.py +0 -0
  74. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_mcp_validation.py +0 -0
  75. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_rag.py +0 -0
  76. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_skills_and_resources.py +0 -0
  77. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_storage_pluggability.py +0 -0
  78. {synapse_framework-0.2.5 → synapse_framework-0.2.6}/tests/test_synapse_library.py +0 -0
  79. {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.5
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.11.2
35
+ Requires-Dist: matplotlib>=3.8.0
36
36
  Requires-Dist: mcp>=2.3.0
37
- Requires-Dist: networkx>=3.7
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.5"
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.11.2",
53
+ "matplotlib>=3.8.0",
54
54
  "mcp>=2.3.0",
55
- "networkx>=3.7",
55
+ "networkx>=3.2",
56
56
  "pydantic>=2.13.5",
57
57
  "uvicorn>=0.30.0",
58
58
  ]
@@ -33,7 +33,7 @@ from synapse import telemetry
33
33
  from synapse import telemetry as mlflow
34
34
  from synapse.app import BaseApp, SynapseApp
35
35
 
36
- __version__ = "0.2.5"
36
+ __version__ = "0.2.6"
37
37
 
38
38
  __all__ = [
39
39
  "BaseApp",
@@ -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(default=True, description="Extract semantic knowledge graph triples.")
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.5",
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
- for conn in connectors or []:
183
- self.add_connector(conn)
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 isinstance(skills, (list, tuple)):
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": tname,
299
+ "name": conn.default_tool,
300
+ "description": conn.description,
275
301
  "connector": conn.name,
276
- })
277
- elif getattr(conn, "default_tool", None):
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
- "server": conn.name,
301
- "name": conn.default_tool,
302
- "description": conn.description,
303
- "connector": conn.name,
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 for r in raw_result if isinstance(r, dict)]
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(records)
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", "hello", "hey", "hola", "greetings", "good morning",
480
- "good afternoon", "good evening", "how are you", "who are you",
481
- "what can you do", "which model is this", "which model is getting used",
482
- "what model is this", "what model is being used", "help", "thanks",
483
- "thank you", "bye", "goodbye", "test", "ping",
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
- "server": conn.name,
517
- "name": t.get("name", conn.default_tool),
518
- "description": t.get("description", conn.description),
519
- "inputSchema": t.get("inputSchema", {}),
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
- "server": conn.name,
524
- "tool": conn.default_tool,
525
- "arguments": conn.default_args_builder(user_message),
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
- "server": srv,
579
- "tool": tl,
580
- "arguments": args,
581
- "items_fetched": len(res["records"]),
582
- })
583
- except Exception as err:
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) if "merged_chunks" in locals() else len(live_tool_chunks),
684
- graph_triples=len(merged_triples) if "merged_triples" in locals() else len(live_tool_triples),
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(status_code=404, detail=f"Connector '{connector_name}' not found.")
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 {"status": "started", "message": f"Background sync initiated for '{connector_name}'."}
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 = c.has_credentials() if callable(c.has_credentials) else bool(c.has_credentials)
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 = "ready" if (chroma_path.exists() or hasattr(app_ref.vector_store, "database")) else "initialized"
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 self.graph_store is not None and hasattr(self.graph_store, "driver") and self.graph_store.driver:
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 = 256,
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 256.
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": self.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
- return str(data.get("response", "")).strip()
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 = parsed.get("triples") or parsed.get("data") or (list(parsed.values())[0] if parsed.values() and isinstance(list(parsed.values())[0], list) else [])
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
- "source": str(item["source"]).strip(),
209
- "relation": (
210
- str(item["relation"])
211
- .strip()
212
- .upper()
213
- .replace(" ", "_")
214
- ),
215
- "target": str(item["target"]).strip(),
216
- "source_type": (
217
- str(item.get("source_type", "Default"))
218
- .strip()
219
- .title()
220
- ),
221
- "target_type": (
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(tool_prompt, temperature=0.0, format="json")
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
- "server": resolved_srv,
328
- "tool": resolved_tl,
329
- "arguments": args,
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.5
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.11.2
35
+ Requires-Dist: matplotlib>=3.8.0
36
36
  Requires-Dist: mcp>=2.3.0
37
- Requires-Dist: networkx>=3.7
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
@@ -10,9 +10,9 @@ langchain-chroma>=1.1.0
10
10
  langchain-community>=0.4.2
11
11
  langchain-core>=1.6.7
12
12
  langchain-ollama>=1.1.0
13
- matplotlib>=3.11.2
13
+ matplotlib>=3.8.0
14
14
  mcp>=2.3.0
15
- networkx>=3.7
15
+ networkx>=3.2
16
16
  pydantic>=2.13.5
17
17
  uvicorn>=0.30.0
18
18