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,530 @@
|
|
|
1
|
+
"""Interactive Web Inspector HTML/CSS/JS template for Synapse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
INSPECTOR_HTML: str = """<!DOCTYPE html>
|
|
6
|
+
<html lang="en">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="UTF-8" />
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
10
|
+
<title>Synapse // Interactive Inspector</title>
|
|
11
|
+
<style>
|
|
12
|
+
:root {
|
|
13
|
+
--canvas: #09090b;
|
|
14
|
+
--panel: #18181b;
|
|
15
|
+
--panel-hover: #27272a;
|
|
16
|
+
--inset: #121215;
|
|
17
|
+
--border-subtle: #27272a;
|
|
18
|
+
--border-strong: #3f3f46;
|
|
19
|
+
--text-pri: #f4f4f5;
|
|
20
|
+
--text-sec: #a1a1aa;
|
|
21
|
+
--text-muted: #71717a;
|
|
22
|
+
--accent: #8b8cf8;
|
|
23
|
+
--emerald: #43a077;
|
|
24
|
+
--amber: #c98a3e;
|
|
25
|
+
--font: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
26
|
+
}
|
|
27
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
28
|
+
body {
|
|
29
|
+
background: var(--canvas);
|
|
30
|
+
color: var(--text-pri);
|
|
31
|
+
font-family: var(--font);
|
|
32
|
+
height: 100vh;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
-webkit-font-smoothing: antialiased;
|
|
37
|
+
}
|
|
38
|
+
header {
|
|
39
|
+
height: 52px;
|
|
40
|
+
background: var(--panel);
|
|
41
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: space-between;
|
|
45
|
+
padding: 0 20px;
|
|
46
|
+
flex-shrink: 0;
|
|
47
|
+
}
|
|
48
|
+
.brand {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: 10px;
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
font-size: 0.95rem;
|
|
54
|
+
letter-spacing: -0.01em;
|
|
55
|
+
}
|
|
56
|
+
.status-dot {
|
|
57
|
+
width: 8px;
|
|
58
|
+
height: 8px;
|
|
59
|
+
border-radius: 50%;
|
|
60
|
+
background: var(--emerald);
|
|
61
|
+
box-shadow: 0 0 8px rgba(67, 160, 119, 0.4);
|
|
62
|
+
}
|
|
63
|
+
.tabs {
|
|
64
|
+
display: flex;
|
|
65
|
+
gap: 6px;
|
|
66
|
+
}
|
|
67
|
+
.tab-btn {
|
|
68
|
+
background: transparent;
|
|
69
|
+
border: 1px solid transparent;
|
|
70
|
+
color: var(--text-sec);
|
|
71
|
+
font-family: inherit;
|
|
72
|
+
font-size: 0.82rem;
|
|
73
|
+
font-weight: 500;
|
|
74
|
+
padding: 6px 14px;
|
|
75
|
+
border-radius: 6px;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
transition: all 0.15s ease;
|
|
78
|
+
}
|
|
79
|
+
.tab-btn:hover {
|
|
80
|
+
color: var(--text-pri);
|
|
81
|
+
background: var(--panel-hover);
|
|
82
|
+
}
|
|
83
|
+
.tab-btn.active {
|
|
84
|
+
color: var(--text-pri);
|
|
85
|
+
background: var(--inset);
|
|
86
|
+
border-color: var(--border-subtle);
|
|
87
|
+
}
|
|
88
|
+
main {
|
|
89
|
+
flex: 1;
|
|
90
|
+
display: flex;
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
position: relative;
|
|
93
|
+
}
|
|
94
|
+
.tab-content {
|
|
95
|
+
display: none;
|
|
96
|
+
width: 100%;
|
|
97
|
+
height: 100%;
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
}
|
|
100
|
+
.tab-content.active {
|
|
101
|
+
display: flex;
|
|
102
|
+
}
|
|
103
|
+
/* Chat View */
|
|
104
|
+
.chat-layout {
|
|
105
|
+
width: 100%;
|
|
106
|
+
display: flex;
|
|
107
|
+
height: 100%;
|
|
108
|
+
}
|
|
109
|
+
.chat-main {
|
|
110
|
+
flex: 1;
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
height: 100%;
|
|
114
|
+
border-right: 1px solid var(--border-subtle);
|
|
115
|
+
}
|
|
116
|
+
.messages-pane {
|
|
117
|
+
flex: 1;
|
|
118
|
+
overflow-y: auto;
|
|
119
|
+
padding: 24px;
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
gap: 16px;
|
|
123
|
+
}
|
|
124
|
+
.msg {
|
|
125
|
+
max-width: 80%;
|
|
126
|
+
padding: 12px 16px;
|
|
127
|
+
border-radius: 8px;
|
|
128
|
+
font-size: 0.88rem;
|
|
129
|
+
line-height: 1.5;
|
|
130
|
+
word-break: break-word;
|
|
131
|
+
}
|
|
132
|
+
.msg.user {
|
|
133
|
+
align-self: flex-end;
|
|
134
|
+
background: var(--panel-hover);
|
|
135
|
+
border: 1px solid var(--border-strong);
|
|
136
|
+
color: var(--text-pri);
|
|
137
|
+
}
|
|
138
|
+
.msg.assistant {
|
|
139
|
+
align-self: flex-start;
|
|
140
|
+
background: var(--panel);
|
|
141
|
+
border: 1px solid var(--border-subtle);
|
|
142
|
+
color: var(--text-pri);
|
|
143
|
+
}
|
|
144
|
+
.msg pre {
|
|
145
|
+
background: var(--inset);
|
|
146
|
+
padding: 8px 12px;
|
|
147
|
+
border-radius: 4px;
|
|
148
|
+
overflow-x: auto;
|
|
149
|
+
margin: 8px 0;
|
|
150
|
+
font-family: monospace;
|
|
151
|
+
font-size: 0.82rem;
|
|
152
|
+
}
|
|
153
|
+
.input-pane {
|
|
154
|
+
padding: 16px 20px;
|
|
155
|
+
background: var(--panel);
|
|
156
|
+
border-top: 1px solid var(--border-subtle);
|
|
157
|
+
display: flex;
|
|
158
|
+
gap: 10px;
|
|
159
|
+
}
|
|
160
|
+
.chat-input {
|
|
161
|
+
flex: 1;
|
|
162
|
+
background: var(--inset);
|
|
163
|
+
border: 1px solid var(--border-subtle);
|
|
164
|
+
color: var(--text-pri);
|
|
165
|
+
font-family: inherit;
|
|
166
|
+
font-size: 0.88rem;
|
|
167
|
+
padding: 10px 14px;
|
|
168
|
+
border-radius: 6px;
|
|
169
|
+
outline: none;
|
|
170
|
+
}
|
|
171
|
+
.chat-input:focus {
|
|
172
|
+
border-color: var(--accent);
|
|
173
|
+
}
|
|
174
|
+
.send-btn {
|
|
175
|
+
background: var(--accent);
|
|
176
|
+
color: #09090b;
|
|
177
|
+
font-weight: 600;
|
|
178
|
+
border: none;
|
|
179
|
+
padding: 0 18px;
|
|
180
|
+
border-radius: 6px;
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
font-size: 0.85rem;
|
|
183
|
+
}
|
|
184
|
+
/* Side Inspector Pane */
|
|
185
|
+
.inspector-side {
|
|
186
|
+
width: 380px;
|
|
187
|
+
background: var(--panel);
|
|
188
|
+
display: flex;
|
|
189
|
+
flex-direction: column;
|
|
190
|
+
overflow-y: auto;
|
|
191
|
+
padding: 20px;
|
|
192
|
+
gap: 18px;
|
|
193
|
+
}
|
|
194
|
+
.card {
|
|
195
|
+
background: var(--inset);
|
|
196
|
+
border: 1px solid var(--border-subtle);
|
|
197
|
+
border-radius: 6px;
|
|
198
|
+
padding: 14px;
|
|
199
|
+
}
|
|
200
|
+
.card-title {
|
|
201
|
+
font-size: 0.78rem;
|
|
202
|
+
font-weight: 600;
|
|
203
|
+
text-transform: uppercase;
|
|
204
|
+
letter-spacing: 0.05em;
|
|
205
|
+
color: var(--text-sec);
|
|
206
|
+
margin-bottom: 10px;
|
|
207
|
+
display: flex;
|
|
208
|
+
justify-content: space-between;
|
|
209
|
+
align-items: center;
|
|
210
|
+
}
|
|
211
|
+
.badge {
|
|
212
|
+
display: inline-block;
|
|
213
|
+
padding: 2px 7px;
|
|
214
|
+
font-size: 0.72rem;
|
|
215
|
+
border-radius: 4px;
|
|
216
|
+
font-weight: 500;
|
|
217
|
+
}
|
|
218
|
+
.badge.green { background: rgba(67,160,119,0.15); color: var(--emerald); }
|
|
219
|
+
.badge.amber { background: rgba(201,138,62,0.15); color: var(--amber); }
|
|
220
|
+
.pill-list {
|
|
221
|
+
display: flex;
|
|
222
|
+
flex-wrap: wrap;
|
|
223
|
+
gap: 6px;
|
|
224
|
+
}
|
|
225
|
+
.pill {
|
|
226
|
+
background: var(--panel);
|
|
227
|
+
border: 1px solid var(--border-subtle);
|
|
228
|
+
font-size: 0.75rem;
|
|
229
|
+
padding: 4px 8px;
|
|
230
|
+
border-radius: 4px;
|
|
231
|
+
color: var(--text-sec);
|
|
232
|
+
}
|
|
233
|
+
/* Connectors & Graph View */
|
|
234
|
+
.full-pane {
|
|
235
|
+
width: 100%;
|
|
236
|
+
height: 100%;
|
|
237
|
+
overflow-y: auto;
|
|
238
|
+
padding: 28px;
|
|
239
|
+
}
|
|
240
|
+
.grid-2 {
|
|
241
|
+
display: grid;
|
|
242
|
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
243
|
+
gap: 16px;
|
|
244
|
+
margin-top: 16px;
|
|
245
|
+
}
|
|
246
|
+
.action-btn {
|
|
247
|
+
background: var(--panel-hover);
|
|
248
|
+
border: 1px solid var(--border-subtle);
|
|
249
|
+
color: var(--text-pri);
|
|
250
|
+
padding: 8px 14px;
|
|
251
|
+
border-radius: 6px;
|
|
252
|
+
font-size: 0.8rem;
|
|
253
|
+
cursor: pointer;
|
|
254
|
+
font-family: inherit;
|
|
255
|
+
}
|
|
256
|
+
.action-btn:hover { background: var(--border-strong); }
|
|
257
|
+
iframe.graph-frame {
|
|
258
|
+
width: 100%;
|
|
259
|
+
height: 100%;
|
|
260
|
+
border: none;
|
|
261
|
+
}
|
|
262
|
+
</style>
|
|
263
|
+
</head>
|
|
264
|
+
<body>
|
|
265
|
+
<header>
|
|
266
|
+
<div class="brand">
|
|
267
|
+
<div class="status-dot"></div>
|
|
268
|
+
<span>Synapse // Inspector</span>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="tabs">
|
|
271
|
+
<button class="tab-btn active" onclick="switchTab('chat')">Chat Playground</button>
|
|
272
|
+
<button class="tab-btn" onclick="switchTab('connectors')">Connectors</button>
|
|
273
|
+
<button class="tab-btn" onclick="switchTab('graph')">3D Knowledge Graph</button>
|
|
274
|
+
</div>
|
|
275
|
+
</header>
|
|
276
|
+
|
|
277
|
+
<main>
|
|
278
|
+
<!-- TAB 1: Chat Playground -->
|
|
279
|
+
<div id="tab-chat" class="tab-content active">
|
|
280
|
+
<div class="chat-layout">
|
|
281
|
+
<div class="chat-main">
|
|
282
|
+
<div id="messages-pane" class="messages-pane">
|
|
283
|
+
<div class="msg assistant">
|
|
284
|
+
Hello! Synapse is online with Multi-MCP tool routing and Hybrid RAG active. Try asking:
|
|
285
|
+
<br><br>
|
|
286
|
+
• <em>"Check my email to find any unread messages"</em><br>
|
|
287
|
+
• <em>"Search my Notion notes"</em><br>
|
|
288
|
+
• <em>"Explain how Hybrid RAG combines ChromaDB with NetworkX"</em>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
<div class="input-pane">
|
|
292
|
+
<input type="text" id="chat-input" class="chat-input" placeholder="Type a message or tool request..." onkeydown="if(event.key==='Enter') sendMessage()" />
|
|
293
|
+
<button class="send-btn" onclick="sendMessage()">Send</button>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div class="inspector-side">
|
|
298
|
+
<div class="card">
|
|
299
|
+
<div class="card-title">Active Model Provider</div>
|
|
300
|
+
<div id="model-badge" style="font-size:0.85rem; font-weight:500; color:var(--text-pri);">{{MODEL_DISPLAY}}</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<div class="card">
|
|
304
|
+
<div class="card-title">
|
|
305
|
+
<span>Executed Tools</span>
|
|
306
|
+
<span id="tool-count" class="badge green">0 calls</span>
|
|
307
|
+
</div>
|
|
308
|
+
<div id="live-tool-display" style="font-size:0.8rem; color:var(--text-sec);">No tools invoked yet.</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div class="card">
|
|
312
|
+
<div class="card-title">Retrieved Chunks</div>
|
|
313
|
+
<div id="chunks-pill-list" class="pill-list">
|
|
314
|
+
<span style="font-size:0.75rem; color:var(--text-muted);">None</span>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<div class="card">
|
|
319
|
+
<div class="card-title">Graph Triples</div>
|
|
320
|
+
<div id="triples-pill-list" class="pill-list">
|
|
321
|
+
<span style="font-size:0.75rem; color:var(--text-muted);">None</span>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<!-- TAB 2: Connectors -->
|
|
329
|
+
<div id="tab-connectors" class="tab-content">
|
|
330
|
+
<div class="full-pane">
|
|
331
|
+
<h2 style="font-size: 1.15rem; font-weight:600; margin-bottom: 6px;">Registered Connectors</h2>
|
|
332
|
+
<p style="font-size: 0.85rem; color: var(--text-sec);">Multi-MCP connectors registered in this Synapse instance.</p>
|
|
333
|
+
<div id="connectors-grid" class="grid-2">
|
|
334
|
+
<!-- Dynamically filled -->
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<!-- TAB 3: 3D Knowledge Graph -->
|
|
340
|
+
<div id="tab-graph" class="tab-content">
|
|
341
|
+
<iframe src="/graph/export" class="graph-frame"></iframe>
|
|
342
|
+
</div>
|
|
343
|
+
</main>
|
|
344
|
+
|
|
345
|
+
<script>
|
|
346
|
+
function switchTab(name) {
|
|
347
|
+
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
348
|
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
349
|
+
event.target.classList.add('active');
|
|
350
|
+
document.getElementById('tab-' + name).classList.add('active');
|
|
351
|
+
if (name === 'connectors') loadConnectors();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function loadConnectors() {
|
|
355
|
+
try {
|
|
356
|
+
const res = await fetch('/sync/servers');
|
|
357
|
+
const data = await res.json();
|
|
358
|
+
const grid = document.getElementById('connectors-grid');
|
|
359
|
+
grid.innerHTML = '';
|
|
360
|
+
(data.servers || []).forEach(s => {
|
|
361
|
+
const card = document.createElement('div');
|
|
362
|
+
card.className = 'card';
|
|
363
|
+
card.innerHTML = `
|
|
364
|
+
<div class="card-title">
|
|
365
|
+
<span>${s.name.toUpperCase()}</span>
|
|
366
|
+
<span class="badge ${s.is_ready ? 'green' : 'amber'}">${s.is_ready ? 'Connected' : 'Needs Config'}</span>
|
|
367
|
+
</div>
|
|
368
|
+
<p style="font-size:0.8rem; color:var(--text-sec); margin-bottom:12px;">${s.description || 'MCP Connector'}</p>
|
|
369
|
+
<div style="font-size:0.75rem; color:var(--text-muted); margin-bottom:12px;">Default tool: <code>${s.default_tool || 'N/A'}</code></div>
|
|
370
|
+
<button class="action-btn" onclick="syncConnector('${s.name}')">Trigger Background Sync</button>
|
|
371
|
+
`;
|
|
372
|
+
grid.appendChild(card);
|
|
373
|
+
});
|
|
374
|
+
} catch (e) {
|
|
375
|
+
console.error(e);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function syncConnector(name) {
|
|
380
|
+
try {
|
|
381
|
+
const res = await fetch('/sync/' + name, {
|
|
382
|
+
method: 'POST',
|
|
383
|
+
headers: {'Content-Type': 'application/json'},
|
|
384
|
+
body: JSON.stringify({extract_graph: true})
|
|
385
|
+
});
|
|
386
|
+
const data = await res.json();
|
|
387
|
+
alert(data.message || 'Sync initiated.');
|
|
388
|
+
} catch (e) {
|
|
389
|
+
alert('Sync error: ' + e);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function sendMessage() {
|
|
394
|
+
const input = document.getElementById('chat-input');
|
|
395
|
+
const text = input.value.trim();
|
|
396
|
+
if (!text) return;
|
|
397
|
+
input.value = '';
|
|
398
|
+
|
|
399
|
+
const pane = document.getElementById('messages-pane');
|
|
400
|
+
const userMsg = document.createElement('div');
|
|
401
|
+
userMsg.className = 'msg user';
|
|
402
|
+
userMsg.textContent = text;
|
|
403
|
+
pane.appendChild(userMsg);
|
|
404
|
+
|
|
405
|
+
const assistantMsg = document.createElement('div');
|
|
406
|
+
assistantMsg.className = 'msg assistant';
|
|
407
|
+
assistantMsg.textContent = '...';
|
|
408
|
+
pane.appendChild(assistantMsg);
|
|
409
|
+
pane.scrollTop = pane.scrollHeight;
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
const res = await fetch('/chat', {
|
|
413
|
+
method: 'POST',
|
|
414
|
+
headers: {'Content-Type': 'application/json'},
|
|
415
|
+
body: JSON.stringify({message: text, history: []})
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const reader = res.body.getReader();
|
|
419
|
+
const decoder = new TextDecoder();
|
|
420
|
+
let buffer = '';
|
|
421
|
+
assistantMsg.textContent = '';
|
|
422
|
+
const tokenContainer = document.createElement('div');
|
|
423
|
+
tokenContainer.className = 'msg-body';
|
|
424
|
+
assistantMsg.appendChild(tokenContainer);
|
|
425
|
+
|
|
426
|
+
while (true) {
|
|
427
|
+
const {done, value} = await reader.read();
|
|
428
|
+
if (done) break;
|
|
429
|
+
buffer += decoder.decode(value, {stream: true});
|
|
430
|
+
const lines = buffer.split('\\n');
|
|
431
|
+
buffer = lines.pop();
|
|
432
|
+
|
|
433
|
+
for (let i = 0; i < lines.length; i++) {
|
|
434
|
+
const line = lines[i];
|
|
435
|
+
if (line.startsWith('event: context')) {
|
|
436
|
+
const nextLine = lines[i+1];
|
|
437
|
+
if (nextLine && nextLine.startsWith('data: ')) {
|
|
438
|
+
const ctx = JSON.parse(nextLine.slice(6));
|
|
439
|
+
renderContext(ctx);
|
|
440
|
+
if (ctx.tool_calls && ctx.tool_calls.length > 0) {
|
|
441
|
+
ctx.tool_calls.forEach(call => {
|
|
442
|
+
const badge = document.createElement('div');
|
|
443
|
+
badge.style.cssText = 'display:flex; align-items:center; gap:6px; background:rgba(16,185,129,0.12); border:1px solid rgba(16,185,129,0.3); color:#10b981; border-radius:6px; padding:6px 10px; font-size:0.75rem; font-family:monospace; margin-bottom:8px;';
|
|
444
|
+
const argsFormatted = JSON.stringify(call.arguments || {});
|
|
445
|
+
badge.innerHTML = `<span>⚡</span> <span>Invoked Tool: <strong>${call.server}.${call.tool}</strong>(${argsFormatted})</span>`;
|
|
446
|
+
assistantMsg.insertBefore(badge, tokenContainer);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
i++;
|
|
450
|
+
}
|
|
451
|
+
} else if (line.startsWith('data: ')) {
|
|
452
|
+
try {
|
|
453
|
+
const payload = JSON.parse(line.slice(6));
|
|
454
|
+
if (payload.token) {
|
|
455
|
+
tokenContainer.textContent += payload.token;
|
|
456
|
+
pane.scrollTop = pane.scrollHeight;
|
|
457
|
+
}
|
|
458
|
+
} catch(e) {}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} catch (err) {
|
|
463
|
+
assistantMsg.textContent = 'Error: ' + err;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function renderContext(ctx) {
|
|
469
|
+
if (ctx.tool_calls && ctx.tool_calls.length > 0) {
|
|
470
|
+
document.getElementById('tool-count').textContent = ctx.tool_calls.length + ' call(s)';
|
|
471
|
+
document.getElementById('live-tool-display').innerHTML = ctx.tool_calls.map(c => `<code>${c.server}.${c.tool}</code> (${c.items_fetched || 0} items)`).join('<br>');
|
|
472
|
+
} else {
|
|
473
|
+
document.getElementById('tool-count').textContent = '0 calls';
|
|
474
|
+
document.getElementById('live-tool-display').textContent = 'No live tool called.';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const chunkList = document.getElementById('chunks-pill-list');
|
|
478
|
+
chunkList.innerHTML = '';
|
|
479
|
+
if (ctx.chunks && ctx.chunks.length > 0) {
|
|
480
|
+
ctx.chunks.forEach(c => {
|
|
481
|
+
const pill = document.createElement('span');
|
|
482
|
+
pill.className = 'pill';
|
|
483
|
+
pill.textContent = (c.subject || c.source || 'Chunk').slice(0, 32);
|
|
484
|
+
chunkList.appendChild(pill);
|
|
485
|
+
});
|
|
486
|
+
} else {
|
|
487
|
+
chunkList.innerHTML = '<span style="font-size:0.75rem; color:var(--text-muted);">None</span>';
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const tripleList = document.getElementById('triples-pill-list');
|
|
491
|
+
tripleList.innerHTML = '';
|
|
492
|
+
if (ctx.graph_triples && ctx.graph_triples.length > 0) {
|
|
493
|
+
ctx.graph_triples.forEach(t => {
|
|
494
|
+
const pill = document.createElement('span');
|
|
495
|
+
pill.className = 'pill';
|
|
496
|
+
pill.textContent = String(t).slice(0, 36);
|
|
497
|
+
tripleList.appendChild(pill);
|
|
498
|
+
});
|
|
499
|
+
} else {
|
|
500
|
+
tripleList.innerHTML = '<span style="font-size:0.75rem; color:var(--text-muted);">None</span>';
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function loadAppInfo() {
|
|
505
|
+
try {
|
|
506
|
+
const res = await fetch('/info');
|
|
507
|
+
if (!res.ok) return;
|
|
508
|
+
const data = await res.json();
|
|
509
|
+
if (data.model_display) {
|
|
510
|
+
const badge = document.getElementById('model-badge');
|
|
511
|
+
if (badge) badge.textContent = data.model_display;
|
|
512
|
+
}
|
|
513
|
+
} catch (e) {
|
|
514
|
+
console.warn('Could not load app info:', e);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
519
|
+
loadAppInfo();
|
|
520
|
+
});
|
|
521
|
+
</script>
|
|
522
|
+
</body>
|
|
523
|
+
</html>
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def render_inspector_html(model_display: str = "Ollama (qwen2.5:0.5b)") -> str:
|
|
528
|
+
"""Renders the Inspector HTML template with dynamic metadata."""
|
|
529
|
+
return INSPECTOR_HTML.replace("{{MODEL_DISPLAY}}", model_display)
|
|
530
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Language model providers and factory functions for Synapse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from synapse.models.base import BaseModelProvider
|
|
8
|
+
from synapse.models.gemini import GeminiModel
|
|
9
|
+
from synapse.models.ollama import OllamaModel
|
|
10
|
+
from synapse.models.openai import OpenAIModel
|
|
11
|
+
|
|
12
|
+
# Developer-friendly aliases matching different casing conventions
|
|
13
|
+
ollama = OllamaModel
|
|
14
|
+
Ollama = OllamaModel
|
|
15
|
+
openAI = OpenAIModel
|
|
16
|
+
openai = OpenAIModel
|
|
17
|
+
OpenAI = OpenAIModel
|
|
18
|
+
Gemini = GeminiModel
|
|
19
|
+
gemini = GeminiModel
|
|
20
|
+
Gemimi = GeminiModel
|
|
21
|
+
gemimi = GeminiModel
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_model(
|
|
25
|
+
provider_or_name: str | BaseModelProvider = "ollama",
|
|
26
|
+
**kwargs: Any,
|
|
27
|
+
) -> BaseModelProvider:
|
|
28
|
+
"""Factory function returning a configured `BaseModelProvider`.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
provider_or_name: Either a provider string ('ollama', 'openai', 'gemini')
|
|
32
|
+
or an existing `BaseModelProvider` instance.
|
|
33
|
+
**kwargs: Provider-specific configuration arguments (model, api_key, base_url, etc.).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A concrete `BaseModelProvider` instance.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
```python
|
|
40
|
+
model = get_model("openai", api_key="sk-...", model="gpt-4o")
|
|
41
|
+
```
|
|
42
|
+
"""
|
|
43
|
+
if isinstance(provider_or_name, BaseModelProvider):
|
|
44
|
+
return provider_or_name
|
|
45
|
+
|
|
46
|
+
name = str(provider_or_name).lower().strip()
|
|
47
|
+
if name in {"ollama", "local", "qwen"}:
|
|
48
|
+
return OllamaModel(**kwargs)
|
|
49
|
+
elif name in {"openai", "chatgpt", "gpt"}:
|
|
50
|
+
return OpenAIModel(**kwargs)
|
|
51
|
+
elif name in {"gemini", "google"}:
|
|
52
|
+
return GeminiModel(**kwargs)
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"Unknown model provider: '{provider_or_name}'. "
|
|
56
|
+
"Supported providers: 'ollama', 'openai', 'gemini'."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"BaseModelProvider",
|
|
62
|
+
"Gemini",
|
|
63
|
+
"GeminiModel",
|
|
64
|
+
"Ollama",
|
|
65
|
+
"OllamaModel",
|
|
66
|
+
"OpenAI",
|
|
67
|
+
"OpenAIModel",
|
|
68
|
+
"gemini",
|
|
69
|
+
"get_model",
|
|
70
|
+
"ollama",
|
|
71
|
+
"openAI",
|
|
72
|
+
"openai",
|
|
73
|
+
]
|
synapse/models/base.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Abstract base class for Synapse language model providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import AsyncGenerator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseModelProvider(ABC):
|
|
11
|
+
"""Abstract interface for LLM providers supported by the Synapse framework.
|
|
12
|
+
|
|
13
|
+
Subclasses must implement streaming chat, single prompt completion,
|
|
14
|
+
knowledge graph triple extraction, and multi-MCP tool routing.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, model_name: str, temperature: float = 0.1) -> None:
|
|
18
|
+
"""Initializes the base model provider.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
model_name: Identifier of the target model (e.g. 'qwen2.5:0.5b', 'gpt-4o').
|
|
22
|
+
temperature: Sampling temperature in the range [0.0, 1.0].
|
|
23
|
+
"""
|
|
24
|
+
self.model_name = model_name
|
|
25
|
+
self.temperature = temperature
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
async def chat_stream(
|
|
29
|
+
self,
|
|
30
|
+
messages: list[dict[str, str]],
|
|
31
|
+
temperature: float | None = None,
|
|
32
|
+
) -> AsyncGenerator[str, None]:
|
|
33
|
+
"""Streams response tokens for a multi-turn chat sequence.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
messages: List of message dictionaries with 'role' and 'content' keys.
|
|
37
|
+
temperature: Optional per-request temperature override.
|
|
38
|
+
|
|
39
|
+
Yields:
|
|
40
|
+
Incremental string tokens as they are produced by the model.
|
|
41
|
+
"""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
async def generate(
|
|
46
|
+
self,
|
|
47
|
+
prompt: str,
|
|
48
|
+
temperature: float | None = None,
|
|
49
|
+
) -> str:
|
|
50
|
+
"""Generates a complete non-streaming response for a single prompt.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
prompt: Text prompt string.
|
|
54
|
+
temperature: Optional per-request temperature override.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The complete response text string.
|
|
58
|
+
"""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def extract_triples(self, text: str) -> list[dict[str, str]]:
|
|
63
|
+
"""Extracts relational entity triples from unstructured text.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
text: Unstructured text content.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A list of dictionary triples containing 'source', 'relation', 'target',
|
|
70
|
+
'source_type', and 'target_type'.
|
|
71
|
+
"""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
async def decide_tool_calls(
|
|
76
|
+
self,
|
|
77
|
+
user_message: str,
|
|
78
|
+
candidate_tools: list[dict[str, Any]],
|
|
79
|
+
fallback_calls: list[dict[str, Any]] | None = None,
|
|
80
|
+
) -> list[dict[str, Any]]:
|
|
81
|
+
"""Selects which candidate tool(s) to execute for a user message.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
user_message: Natural language prompt submitted by the user.
|
|
85
|
+
candidate_tools: List of tool schemas across registered connectors.
|
|
86
|
+
fallback_calls: Optional fallback tool calls if the model fails or times out.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
A list of validated tool calls: [{'server': '...', 'tool': '...', 'arguments': {...}}].
|
|
90
|
+
"""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
async def close(self) -> None:
|
|
94
|
+
"""Releases any client connections or background resources."""
|
|
95
|
+
return
|