agent-api-server 2.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. agent_api_server/__init__.py +0 -0
  2. agent_api_server/api/__init__.py +0 -0
  3. agent_api_server/api/v1/__init__.py +0 -0
  4. agent_api_server/api/v1/api.py +25 -0
  5. agent_api_server/api/v1/config.py +57 -0
  6. agent_api_server/api/v1/graph.py +59 -0
  7. agent_api_server/api/v1/schema.py +57 -0
  8. agent_api_server/api/v1/thread.py +563 -0
  9. agent_api_server/cache/__init__.py +0 -0
  10. agent_api_server/cache/redis_cache.py +385 -0
  11. agent_api_server/callback_handler.py +18 -0
  12. agent_api_server/client/css/styles.css +1202 -0
  13. agent_api_server/client/favicon.ico +0 -0
  14. agent_api_server/client/index.html +102 -0
  15. agent_api_server/client/js/app.js +1499 -0
  16. agent_api_server/client/js/index.umd.js +824 -0
  17. agent_api_server/config_center/config_center.py +239 -0
  18. agent_api_server/configs/__init__.py +3 -0
  19. agent_api_server/configs/config.py +163 -0
  20. agent_api_server/dynamic_llm/__init__.py +0 -0
  21. agent_api_server/dynamic_llm/dynamic_llm.py +331 -0
  22. agent_api_server/listener.py +530 -0
  23. agent_api_server/log/__init__.py +0 -0
  24. agent_api_server/log/formatters.py +122 -0
  25. agent_api_server/log/logging.json +50 -0
  26. agent_api_server/mcp_convert/__init__.py +0 -0
  27. agent_api_server/mcp_convert/mcp_convert.py +375 -0
  28. agent_api_server/memeory/__init__.py +0 -0
  29. agent_api_server/memeory/postgres.py +233 -0
  30. agent_api_server/register/__init__.py +0 -0
  31. agent_api_server/register/register.py +65 -0
  32. agent_api_server/service.py +354 -0
  33. agent_api_server/service_hub/service_hub.py +233 -0
  34. agent_api_server/service_hub/service_hub_test.py +700 -0
  35. agent_api_server/shared/__init__.py +0 -0
  36. agent_api_server/shared/ase.py +54 -0
  37. agent_api_server/shared/base_model.py +103 -0
  38. agent_api_server/shared/common.py +110 -0
  39. agent_api_server/shared/decode_token.py +107 -0
  40. agent_api_server/shared/detect_message.py +410 -0
  41. agent_api_server/shared/get_model_info.py +491 -0
  42. agent_api_server/shared/message.py +419 -0
  43. agent_api_server/shared/util_func.py +372 -0
  44. agent_api_server/sso_service/__init__.py +1 -0
  45. agent_api_server/sso_service/sdk/__init__.py +1 -0
  46. agent_api_server/sso_service/sdk/client.py +224 -0
  47. agent_api_server/sso_service/sdk/credential.py +11 -0
  48. agent_api_server/sso_service/sdk/encoding.py +22 -0
  49. agent_api_server/sso_service/sso_service.py +177 -0
  50. agent_api_server-2.1.7.dist-info/METADATA +130 -0
  51. agent_api_server-2.1.7.dist-info/RECORD +52 -0
  52. agent_api_server-2.1.7.dist-info/WHEEL +4 -0
@@ -0,0 +1,1499 @@
1
+ let currentInterruptData = null; // 存储当前的 interrupt 数据
2
+
3
+ document.addEventListener('DOMContentLoaded', function() {
4
+ // DOM Elements
5
+ const createThreadBtn = document.getElementById('createThreadBtn');
6
+ const connectBtn = document.getElementById('connectBtn');
7
+ const disconnectBtn = document.getElementById('disconnectBtn');
8
+ const clearBtn = document.getElementById('clearBtn');
9
+ const graphNameInput = document.getElementById('graphName');
10
+ const threadIdInput = document.getElementById('threadId');
11
+ const apiUrlInput = document.getElementById('apiUrl');
12
+ const messageContentInput = document.getElementById('messageContent');
13
+ const streamContainer = document.getElementById('streamContainer');
14
+ const statusElement = document.getElementById('status');
15
+ const graphSelect = document.getElementById('graphSelect');
16
+ const refreshGraphsBtn = document.getElementById('refreshGraphsBtn');
17
+ const getConfigBtn = document.getElementById('getConfigBtn');
18
+
19
+ // Interrupt Modal Elements
20
+ const interruptModal = document.getElementById('interruptModal');
21
+ const interruptInput = document.getElementById('interruptInput');
22
+ const confirmInterruptBtn = document.getElementById('confirmInterruptBtn');
23
+ const cancelInterruptBtn = document.getElementById('cancelInterruptBtn');
24
+
25
+ // State
26
+ let eventSource = null;
27
+ let isConnected = false;
28
+ let currentThreadId = '';
29
+ let apiUrlTemplate = apiUrlInput.value;
30
+ let currentApiUrl = ''; // Store API URL during interrupt
31
+ let currentMessageContent = {}; // Store original message content during interrupt
32
+ let currentSchema = null;
33
+
34
+ // Token streaming state
35
+ let currentStreamingMessage = null;
36
+ let currentStreamingContent = '';
37
+ let streamingTimeout = null;
38
+ let streamingNodes = new Set(); // 记录正在流式输出的节点
39
+ let completedStreamingNodes = new Set(); // 记录已完成流式输出的节点
40
+ let pendingToolCalls = new Map(); // 存储待处理的 tool calls,key: node, value: tool calls
41
+
42
+ // 添加获取schema的函数
43
+ async function loadSchema() {
44
+ const graphName = graphNameInput.value.trim();
45
+ if (!graphName) {
46
+ resetInputForm();
47
+ return;
48
+ }
49
+
50
+ try {
51
+ const response = await fetch(`/api/v1/schema/?graph_name=${encodeURIComponent(graphName)}`, {
52
+ method: 'GET',
53
+ headers: {
54
+ 'Accept': 'application/json'
55
+ }
56
+ });
57
+
58
+ if (!response.ok) {
59
+ throw new Error(`HTTP error! status: ${response.status}`);
60
+ }
61
+
62
+ currentSchema = await response.json();
63
+ generateInputForm(currentSchema);
64
+
65
+ } catch (error) {
66
+ console.error('Error loading schema:', error);
67
+ currentSchema = null;
68
+ resetInputForm();
69
+ }
70
+ }
71
+
72
+ // 生成输入表单
73
+ function generateInputForm(schema) {
74
+ const formContainer = document.getElementById('inputFormContainer');
75
+ formContainer.innerHTML = '';
76
+
77
+ const properties = schema.properties || {};
78
+ const requiredFields = schema.required || [];
79
+
80
+ Object.entries(properties).forEach(([key, prop]) => {
81
+ const fieldContainer = document.createElement('div');
82
+ fieldContainer.className = 'form-field';
83
+
84
+ const label = document.createElement('label');
85
+ label.textContent = prop.title || key;
86
+ if (requiredFields.includes(key)) {
87
+ label.innerHTML += ' <span class="required">*</span>';
88
+ }
89
+ fieldContainer.appendChild(label);
90
+
91
+ const input = document.createElement('input');
92
+ input.type = 'text';
93
+ input.id = `input_${key}`;
94
+ input.placeholder = `Enter ${prop.title || key}`;
95
+ input.dataset.key = key;
96
+ input.required = requiredFields.includes(key);
97
+ fieldContainer.appendChild(input);
98
+ formContainer.appendChild(fieldContainer);
99
+ });
100
+ }
101
+
102
+ // 重置输入表单
103
+ function resetInputForm() {
104
+ const formContainer = document.getElementById('inputFormContainer');
105
+ formContainer.innerHTML = '<div class="empty-form">No schema loaded. Select a graph first.</div>';
106
+ }
107
+
108
+ // 从表单生成请求体
109
+ function generateRequestBody() {
110
+ if (!currentSchema) {
111
+ return { inputs: {} };
112
+ }
113
+
114
+ const inputs = {};
115
+ const requiredFields = currentSchema.required || [];
116
+ let isValid = true;
117
+
118
+ Object.entries(currentSchema.properties || {}).forEach(([key]) => {
119
+ const input = document.querySelector(`#input_${key}`);
120
+ if (input) {
121
+ const value = input.value.trim();
122
+
123
+ if (requiredFields.includes(key) && !value) {
124
+ input.classList.add('error');
125
+ isValid = false;
126
+ } else {
127
+ input.classList.remove('error');
128
+ inputs[key] = value;
129
+ }
130
+ }
131
+ });
132
+
133
+ if (!isValid) {
134
+ throw new Error('Please fill in all required fields');
135
+ }
136
+
137
+ return { inputs };
138
+ }
139
+
140
+ // Load available graphs from API
141
+ async function loadGraphs() {
142
+ try {
143
+ graphSelect.disabled = true;
144
+ refreshGraphsBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
145
+ graphSelect.innerHTML = '<option value="" disabled selected>Loading graphs...</option>';
146
+
147
+ const response = await fetch('/api/v1/graph/', {
148
+ method: 'GET',
149
+ headers: {
150
+ 'Accept': 'application/json'
151
+ }
152
+ });
153
+
154
+ if (!response.ok) {
155
+ throw new Error(`HTTP error! status: ${response.status}`);
156
+ }
157
+
158
+ const data = await response.json();
159
+ const graphs = data.graphs || [];
160
+ const graphCount = data.count || 0;
161
+
162
+ if (graphCount === 0 || graphs.length === 0) {
163
+ graphSelect.innerHTML = '<option value="" disabled selected>No graphs available</option>';
164
+ return;
165
+ }
166
+
167
+ graphSelect.innerHTML = '';
168
+ const defaultOption = document.createElement('option');
169
+ defaultOption.value = '';
170
+ defaultOption.textContent = '-- Select a Graph --';
171
+ defaultOption.disabled = true;
172
+ defaultOption.selected = true;
173
+ graphSelect.appendChild(defaultOption);
174
+
175
+ graphs.forEach(graph => {
176
+ const option = document.createElement('option');
177
+ option.value = graph;
178
+ option.textContent = graph;
179
+ graphSelect.appendChild(option);
180
+ });
181
+
182
+ graphSelect.addEventListener('change', function() {
183
+ if (this.value) {
184
+ graphNameInput.value = this.value;
185
+ }
186
+ });
187
+
188
+ const successMessage = {
189
+ node: 'system',
190
+ update_type: 'graphs_loaded',
191
+ timestamp: new Date().toISOString(),
192
+ update_content: {
193
+ content: `Successfully loaded ${graphCount} graphs from API`,
194
+ response_metadata: {}
195
+ }
196
+ };
197
+ addMessage(successMessage);
198
+
199
+ } catch (error) {
200
+ console.error('Error loading graphs:', error);
201
+ graphSelect.innerHTML = '<option value="" disabled selected>Error loading graphs</option>';
202
+
203
+ const errorMessage = {
204
+ node: 'system',
205
+ update_type: 'error',
206
+ timestamp: new Date().toISOString(),
207
+ update_content: {
208
+ content: `Failed to load graphs: ${error.message}`,
209
+ response_metadata: {}
210
+ }
211
+ };
212
+ addMessage(errorMessage);
213
+ } finally {
214
+ graphSelect.disabled = false;
215
+ refreshGraphsBtn.innerHTML = '<i class="fas fa-sync-alt"></i>';
216
+ }
217
+ }
218
+
219
+ // Format timestamp
220
+ function formatTimestamp(timestamp) {
221
+ if (!timestamp) return 'N/A';
222
+ const date = new Date(timestamp);
223
+ return date.toLocaleString('en-US', {
224
+ month: 'short',
225
+ day: 'numeric',
226
+ hour: '2-digit',
227
+ minute: '2-digit',
228
+ second: '2-digit',
229
+ hour12: false
230
+ });
231
+ }
232
+
233
+ // Format tool calls
234
+ function formatToolCalls(toolCalls) {
235
+ if (!toolCalls || toolCalls.length === 0) return '';
236
+
237
+ return toolCalls.map(tool => {
238
+ return `
239
+ <div class="tool-call">
240
+ <div class="tool-call-name">
241
+ <i class="fas fa-cog"></i> ${tool.name}
242
+ </div>
243
+ <div class="tool-call-args">
244
+ <pre>${JSON.stringify(tool.args, null, 2)}</pre>
245
+ </div>
246
+ </div>
247
+ `;
248
+ }).join('');
249
+ }
250
+
251
+ // Format token usage
252
+ function formatTokenUsage(metadata) {
253
+ if (!metadata || !metadata.token_usage) return '';
254
+
255
+ const usage = metadata.token_usage;
256
+ return `
257
+ <div class="token-usage">
258
+ <span class="token-prompt">
259
+ <i class="fas fa-keyboard"></i> Prompt: ${usage.prompt_tokens || 0}
260
+ </span>
261
+ <span class="token-completion">
262
+ <i class="fas fa-reply"></i> Completion: ${usage.completion_tokens || 0}
263
+ </span>
264
+ <span class="token-total">
265
+ <i class="fas fa-calculator"></i> Total: ${usage.total_tokens || 0}
266
+ </span>
267
+ </div>
268
+ `;
269
+ }
270
+
271
+ // Show interrupt dialog
272
+ function showInterruptDialog() {
273
+ if (!currentInterruptData) return;
274
+
275
+ const promptText = currentInterruptData.update_content?.content || "Please provide the required information to continue:";
276
+
277
+ interruptModal.style.display = 'block';
278
+ interruptInput.value = '';
279
+
280
+ const promptElement = document.createElement('div');
281
+ promptElement.className = 'interrupt-prompt';
282
+ promptElement.textContent = promptText;
283
+
284
+ const modalContent = interruptModal.querySelector('.modal-content');
285
+ const existingPrompt = modalContent.querySelector('.interrupt-prompt');
286
+ if (existingPrompt) {
287
+ existingPrompt.remove();
288
+ }
289
+
290
+ modalContent.insertBefore(promptElement, interruptInput);
291
+ interruptInput.focus();
292
+ }
293
+
294
+ // Hide interrupt dialog
295
+ function hideInterruptDialog() {
296
+ interruptModal.style.display = 'none';
297
+ }
298
+
299
+ // Continue after interrupt with user input
300
+ async function continueAfterInterrupt() {
301
+ const userInput = interruptInput.value.trim();
302
+ if (!userInput) {
303
+ alert('Please enter a response to continue');
304
+ return;
305
+ }
306
+
307
+ hideInterruptDialog();
308
+ currentInterruptData = null; // 重置 interrupt 数据
309
+
310
+ const newMessageContent = {
311
+ ...currentMessageContent,
312
+ inputs: {
313
+ resume: userInput
314
+ }
315
+ };
316
+
317
+ try {
318
+ statusElement.className = 'status status-connecting';
319
+ statusElement.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i> Resuming...';
320
+
321
+ const response = await fetch(currentApiUrl, {
322
+ method: 'POST',
323
+ headers: {
324
+ 'Accept': 'text/event-stream',
325
+ 'Content-Type': 'application/json'
326
+ },
327
+ body: JSON.stringify(newMessageContent)
328
+ });
329
+
330
+ if (!response.ok) {
331
+ throw new Error(`HTTP error! status: ${response.status}`);
332
+ }
333
+
334
+ const reader = response.body.getReader();
335
+ const decoder = new TextDecoder();
336
+ let buffer = '';
337
+
338
+ const processChunk = ({ done, value }) => {
339
+ if (done) {
340
+ disconnectStream();
341
+ return;
342
+ }
343
+
344
+ buffer += decoder.decode(value, { stream: true });
345
+ const lines = buffer.split('\n');
346
+ buffer = lines.pop();
347
+
348
+ for (const line of lines) {
349
+ if (line.startsWith('data: ')) {
350
+ try {
351
+ const data = JSON.parse(line.substring(6));
352
+ addMessage(data);
353
+ } catch (e) {
354
+ console.error('Error parsing event data:', e);
355
+ }
356
+ }
357
+ }
358
+
359
+ return reader.read().then(processChunk);
360
+ };
361
+
362
+ reader.read().then(processChunk);
363
+
364
+ } catch (error) {
365
+ console.error('Error resuming after interrupt:', error);
366
+ statusElement.className = 'status status-disconnected';
367
+ statusElement.innerHTML = '<i class="fas fa-times-circle"></i> Error Resuming';
368
+
369
+ const errorMessage = {
370
+ node: 'system',
371
+ update_type: 'error',
372
+ timestamp: new Date().toISOString(),
373
+ update_content: {
374
+ content: `Failed to resume after interrupt: ${error.message}`,
375
+ response_metadata: {}
376
+ }
377
+ };
378
+ addMessage(errorMessage);
379
+ }
380
+ }
381
+
382
+ function getOrCreateStreamingMessage(node, updateType, timestamp) {
383
+ if (streamingNodes.has(node) && currentStreamingMessage) {
384
+ return currentStreamingMessage;
385
+ }
386
+
387
+ if (completedStreamingNodes.has(node)) {
388
+ return null;
389
+ }
390
+
391
+ // 否则创建新的消息元素
392
+ const messageElement = document.createElement('div');
393
+ messageElement.className = 'message streaming-message';
394
+
395
+ messageElement.innerHTML = [
396
+ '<button class="copy-btn" title="Copy to clipboard"><i class="far fa-copy"></i></button>',
397
+ '<div class="message-header">',
398
+ `<span class="message-node node-${node}">`,
399
+ node === 'agent' ? '<i class="fas fa-robot"></i>' :
400
+ node === 'tools' ? '<i class="fas fa-tools"></i>' : '<i class="fas fa-server"></i>',
401
+ `${node}</span>`,
402
+ `<span>${updateType}</span>`,
403
+ `<span class="message-timestamp">${timestamp}</span>`,
404
+ '<span class="streaming-indicator"><i class="fas fa-circle"></i> Streaming</span>',
405
+ '</div>',
406
+ '<div class="message-content streaming-content"></div>'
407
+ ].join('');
408
+
409
+ // 添加到DOM
410
+ const emptyContent = streamContainer.querySelector('.empty-content');
411
+ if (emptyContent) {
412
+ emptyContent.remove();
413
+ }
414
+ streamContainer.appendChild(messageElement);
415
+
416
+ // 记录这个节点正在流式输出
417
+ streamingNodes.add(node);
418
+ currentStreamingMessage = messageElement;
419
+ currentStreamingContent = '';
420
+
421
+ // 添加复制按钮功能
422
+ const copyBtn = messageElement.querySelector('.copy-btn');
423
+ copyBtn.addEventListener('click', () => {
424
+ const contentElement = messageElement.querySelector('.message-content');
425
+ const textToCopy = contentElement.textContent || '';
426
+ navigator.clipboard.writeText(textToCopy).then(() => {
427
+ copyBtn.innerHTML = '<i class="fas fa-check"></i>';
428
+ setTimeout(() => {
429
+ copyBtn.innerHTML = '<i class="far fa-copy"></i>';
430
+ }, 2000);
431
+ });
432
+ });
433
+
434
+ return messageElement;
435
+ }
436
+
437
+ // 处理token流式输出
438
+ function handleTokenStream(data) {
439
+ const node = data.node || 'unknown';
440
+ const updateType = data.update_type || 'unknown';
441
+ const timestamp = formatTimestamp(data.timestamp);
442
+ const tokenContent = data.update_content || '';
443
+
444
+ // 如果是complete事件,完成当前流,并检查是否有tool calls需要处理
445
+ if (updateType === 'complete') {
446
+ // 在完成前检查是否有tool calls需要保存
447
+ const content = data.update_content || {};
448
+ if (content.tool_calls && content.tool_calls.length > 0) {
449
+ pendingToolCalls.set(node, {
450
+ tool_calls: content.tool_calls,
451
+ timestamp: data.timestamp,
452
+ update_type: updateType,
453
+ response_metadata: content.response_metadata
454
+ });
455
+ }
456
+ completeStreamingMessage(node);
457
+ return;
458
+ }
459
+
460
+ // 查找或创建当前流式消息的容器
461
+ const messageElement = getOrCreateStreamingMessage(node, updateType, timestamp);
462
+
463
+ // 如果返回null,说明这个节点已经完成流式输出,跳过处理
464
+ if (!messageElement) {
465
+ return;
466
+ }
467
+
468
+ // 更新内容
469
+ if (tokenContent && messageElement) {
470
+ currentStreamingContent += tokenContent;
471
+ const contentElement = messageElement.querySelector('.message-content');
472
+ if (contentElement) {
473
+ contentElement.textContent = currentStreamingContent;
474
+ }
475
+
476
+ // 清除之前的超时
477
+ if (streamingTimeout) {
478
+ clearTimeout(streamingTimeout);
479
+ }
480
+
481
+ // 设置新的超时,如果一段时间没有新token,认为流结束
482
+ streamingTimeout = setTimeout(() => {
483
+ if (streamingNodes.has(node)) {
484
+ completeStreamingMessage(node);
485
+ }
486
+ }, 1000); // 1秒内没有新token认为流结束
487
+ }
488
+
489
+ streamContainer.scrollTop = streamContainer.scrollHeight;
490
+ }
491
+
492
+ // 完成流式消息
493
+ function completeStreamingMessage(node) {
494
+ if (currentStreamingMessage && streamingNodes.has(node)) {
495
+ // 检查是否有待处理的tool calls
496
+ if (pendingToolCalls.has(node)) {
497
+ const toolCallData = pendingToolCalls.get(node);
498
+
499
+ // 为tool calls创建单独的消息卡片
500
+ createToolCallMessage(node, toolCallData);
501
+
502
+ // 清除已处理的tool calls
503
+ pendingToolCalls.delete(node);
504
+ }
505
+
506
+ // 移除流式指示器
507
+ const indicator = currentStreamingMessage.querySelector('.streaming-indicator');
508
+ if (indicator) {
509
+ indicator.remove();
510
+ }
511
+
512
+ // 移除流式样式
513
+ currentStreamingMessage.classList.remove('streaming-message');
514
+ const contentElement = currentStreamingMessage.querySelector('.message-content');
515
+ if (contentElement) {
516
+ contentElement.classList.remove('streaming-content');
517
+ }
518
+
519
+ // 从流式节点集合中移除,并添加到已完成集合
520
+ streamingNodes.delete(node);
521
+ completedStreamingNodes.add(node);
522
+
523
+ // 只有当没有其他节点在流式输出时才重置
524
+ if (streamingNodes.size === 0) {
525
+ currentStreamingMessage = null;
526
+ currentStreamingContent = '';
527
+ }
528
+ }
529
+
530
+ if (streamingTimeout) {
531
+ clearTimeout(streamingTimeout);
532
+ streamingTimeout = null;
533
+ }
534
+ }
535
+
536
+ function createToolCallMessage(node, toolCallData) {
537
+ const timestamp = formatTimestamp(toolCallData.timestamp);
538
+ const toolCallsDisplay = formatToolCalls(toolCallData.tool_calls);
539
+ const tokenUsage = formatTokenUsage(toolCallData.response_metadata);
540
+
541
+ const toolCallElement = document.createElement('div');
542
+ toolCallElement.className = 'message';
543
+
544
+ toolCallElement.innerHTML = [
545
+ '<button class="copy-btn" title="Copy to clipboard"><i class="far fa-copy"></i></button>',
546
+ '<div class="message-header">',
547
+ `<span class="message-node node-${node}">`,
548
+ node === 'agent' ? '<i class="fas fa-robot"></i>' :
549
+ node === 'tools' ? '<i class="fas fa-tools"></i>' : '<i class="fas fa-server"></i>',
550
+ `${node}</span>`,
551
+ `<span>${toolCallData.update_type}</span>`,
552
+ `<span class="message-timestamp">${timestamp}</span>`,
553
+ '</div>',
554
+ '<div class="message-content"><div class="empty-content">[Tool calls initiated]</div></div>',
555
+ toolCallsDisplay,
556
+ tokenUsage
557
+ ].join('');
558
+
559
+ // 添加到DOM,放在当前流式消息之后
560
+ if (currentStreamingMessage && currentStreamingMessage.parentNode) {
561
+ currentStreamingMessage.parentNode.insertBefore(toolCallElement, currentStreamingMessage.nextSibling);
562
+ } else {
563
+ const emptyContent = streamContainer.querySelector('.empty-content');
564
+ if (emptyContent) {
565
+ emptyContent.remove();
566
+ }
567
+ streamContainer.appendChild(toolCallElement);
568
+ }
569
+
570
+ const copyBtn = toolCallElement.querySelector('.copy-btn');
571
+ copyBtn.addEventListener('click', () => {
572
+ const textToCopy = [
573
+ `${node} ${toolCallData.update_type} at ${timestamp}`,
574
+ '',
575
+ 'Tool calls:',
576
+ toolCallsDisplay ? toolCallsDisplay : ''
577
+ ].filter(Boolean).join('\n');
578
+
579
+ navigator.clipboard.writeText(textToCopy).then(() => {
580
+ copyBtn.innerHTML = '<i class="fas fa-check"></i>';
581
+ setTimeout(() => {
582
+ copyBtn.innerHTML = '<i class="far fa-copy"></i>';
583
+ }, 2000);
584
+ });
585
+ });
586
+
587
+ streamContainer.scrollTop = streamContainer.scrollHeight;
588
+ }
589
+
590
+
591
+ // 检查节点是否正在流式输出
592
+ function isNodeStreaming(node) {
593
+ return streamingNodes.has(node);
594
+ }
595
+
596
+ function isNodeCompletedStreaming(node) {
597
+ return completedStreamingNodes.has(node);
598
+ }
599
+
600
+ // Add message to stream
601
+ function addMessage(data) {
602
+ if (!data) return;
603
+
604
+ const node = data.node || 'unknown';
605
+
606
+ // Handle interrupt node
607
+ if (node === '__interrupt__') {
608
+ currentInterruptData = data;
609
+ showInterruptDialog();
610
+ return;
611
+ }
612
+
613
+ // Handle token streaming events
614
+ if (data.event === 'token_stream') {
615
+ handleTokenStream(data);
616
+ return;
617
+ }
618
+
619
+ // 如果这个节点正在流式输出,跳过普通消息(避免重复)
620
+ if (isNodeStreaming(node)) {
621
+ console.log(`Skipping regular message for streaming node: ${node}`);
622
+ return;
623
+ }
624
+
625
+ // 如果这个节点已经完成流式输出,检查是否有tool calls需要特殊处理
626
+ if (isNodeCompletedStreaming(node)) {
627
+ const content = data.update_content || {};
628
+
629
+ // 如果有tool calls,创建单独的工具调用消息
630
+ if (content.tool_calls && content.tool_calls.length > 0) {
631
+ console.log(`Creating tool call message for completed streaming node: ${node}`);
632
+ createToolCallMessage(node, {
633
+ tool_calls: content.tool_calls,
634
+ timestamp: data.timestamp,
635
+ update_type: data.update_type || 'complete',
636
+ response_metadata: content.response_metadata
637
+ });
638
+ } else {
639
+ console.log(`Skipping regular message for completed streaming node: ${node}`);
640
+ }
641
+
642
+ // 从已完成集合中移除,以便后续该节点的新消息可以正常显示
643
+ completedStreamingNodes.delete(node);
644
+ return;
645
+ }
646
+
647
+ // 正常的消息处理逻辑(非流式输出的消息)
648
+ const updateType = data.update_type || 'unknown';
649
+ const timestamp = formatTimestamp(data.timestamp);
650
+ const content = data.update_content || {};
651
+
652
+ // 1. Process references display
653
+ let referencesDisplay = '';
654
+ if (content.references && content.references.length > 0) {
655
+ try {
656
+ referencesDisplay = [
657
+ '<div class="references-section">',
658
+ '<div class="section-title"><i class="fas fa-book"></i> References</div>',
659
+ `<pre class="references-raw">${JSON.stringify(content.references, null, 2)}</pre>`,
660
+ '</div>'
661
+ ].join('');
662
+ } catch (e) {
663
+ referencesDisplay = [
664
+ '<div class="references-section">',
665
+ '<div class="section-title"><i class="fas fa-book"></i> References (原始格式)</div>',
666
+ `<div class="references-raw">${content.references}</div>`,
667
+ '</div>'
668
+ ].join('');
669
+ }
670
+ }
671
+
672
+ // 2. Process main content display
673
+ let displayContent = (content.content || '').trim();
674
+ let toolCallsDisplay = '';
675
+ let chartHtml = '';
676
+ let chartConfig = null;
677
+
678
+ // Handle special case for supervisor_message
679
+ if (data.event === 'node_message' && data.data) {
680
+ displayContent = data.data.trim();
681
+ }
682
+
683
+ // Check for chart data in content
684
+ if (displayContent.includes('```vis-chart')) {
685
+ try {
686
+ const chartConfigMatch = displayContent.match(/```vis-chart\n([\s\S]*?)\n```/);
687
+ if (chartConfigMatch?.[1]) {
688
+ chartConfig = JSON.parse(chartConfigMatch[1]);
689
+ const chartId = `chart-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
690
+ chartHtml = `<div id="${chartId}" class="chart-container"></div>`;
691
+
692
+ displayContent = displayContent.replace(/```vis-chart\n[\s\S]*?\n```(\n)?/g, '').trim();
693
+ }
694
+ } catch (e) {
695
+ console.error('Error parsing chart config:', e);
696
+ }
697
+ }
698
+
699
+ if ((!displayContent || displayContent.trim() === '') && content.tool_calls) {
700
+ toolCallsDisplay = formatToolCalls(content.tool_calls);
701
+ displayContent = '<div class="empty-content">[No content - tool call initiated]</div>';
702
+ } else if (!displayContent) {
703
+ displayContent = '<div class="empty-content">[Empty content]</div>';
704
+ }
705
+
706
+ // 3. Process token usage display
707
+ const tokenUsage = formatTokenUsage(content.response_metadata);
708
+
709
+ // 4. Build complete message element
710
+ const messageElement = document.createElement('div');
711
+ messageElement.className = 'message';
712
+
713
+ messageElement.innerHTML = [
714
+ '<button class="copy-btn" title="Copy to clipboard"><i class="far fa-copy"></i></button>',
715
+ '<div class="message-header">',
716
+ `<span class="message-node node-${node}">`,
717
+ node === 'agent' ? '<i class="fas fa-robot"></i>' :
718
+ node === 'tools' ? '<i class="fas fa-tools"></i>' : '<i class="fas fa-server"></i>',
719
+ `${node}</span>`,
720
+ `<span>${updateType}</span>`,
721
+ `<span class="message-timestamp">${timestamp}</span>`,
722
+ '</div>',
723
+ `<div class="message-content">${displayContent}${chartHtml}</div>`,
724
+ toolCallsDisplay,
725
+ tokenUsage,
726
+ referencesDisplay
727
+ ].join('');
728
+
729
+ // Add to DOM
730
+ const emptyContent = streamContainer.querySelector('.empty-content');
731
+ if (emptyContent) {
732
+ emptyContent.remove();
733
+ }
734
+ streamContainer.appendChild(messageElement);
735
+
736
+ // 5. Render chart after DOM is updated (if chart exists)
737
+ if (chartConfig) {
738
+ setTimeout(() => {
739
+ const chartId = messageElement.querySelector('.chart-container')?.id;
740
+ if (chartId && window.AdvtGptChart?.GPTVis && window.React && window.ReactDOM) {
741
+ try {
742
+ const chartMarkdown = `\`\`\`vis-chart\n${JSON.stringify(chartConfig, null, 2)}\n\`\`\``;
743
+ ReactDOM.createRoot(document.getElementById(chartId)).render(
744
+ React.createElement(
745
+ window.AdvtGptChart.GPTVis,
746
+ null,
747
+ chartMarkdown
748
+ )
749
+ );
750
+ } catch (e) {
751
+ console.error('Chart rendering error:', e);
752
+ const container = document.getElementById(chartId);
753
+ if (container) {
754
+ container.innerHTML = '<div class="chart-error">图表渲染失败</div>';
755
+ }
756
+ }
757
+ }
758
+ }, 0);
759
+ }
760
+
761
+ // 6. Add copy button functionality
762
+ const copyBtn = messageElement.querySelector('.copy-btn');
763
+ copyBtn.addEventListener('click', () => {
764
+ const textToCopy = [
765
+ `${node} ${updateType} at ${timestamp}`,
766
+ '',
767
+ displayContent,
768
+ toolCallsDisplay ? '\nTool calls:\n' + toolCallsDisplay : '',
769
+ referencesDisplay ? '\nReferences:\n' + JSON.stringify(content.references) : ''
770
+ ].filter(Boolean).join('\n');
771
+
772
+ navigator.clipboard.writeText(textToCopy).then(() => {
773
+ copyBtn.innerHTML = '<i class="fas fa-check"></i>';
774
+ setTimeout(() => {
775
+ copyBtn.innerHTML = '<i class="far fa-copy"></i>';
776
+ }, 2000);
777
+ });
778
+ });
779
+
780
+ streamContainer.scrollTop = streamContainer.scrollHeight;
781
+ }
782
+
783
+ // Create a new thread
784
+ async function createThread() {
785
+ const graphName = graphNameInput.value.trim();
786
+ if (!graphName) {
787
+ alert('Please enter a graph name');
788
+ return;
789
+ }
790
+
791
+ try {
792
+ createThreadBtn.disabled = true;
793
+ createThreadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
794
+
795
+ clearStream();
796
+
797
+ const apiUrl = `/api/v1/thread/?graph_name=${encodeURIComponent(graphName)}`;
798
+
799
+ const response = await fetch(apiUrl, {
800
+ method: 'POST',
801
+ headers: {
802
+ 'Accept': 'application/json'
803
+ }
804
+ });
805
+
806
+ if (!response.ok) {
807
+ throw new Error(`HTTP error! status: ${response.status}`);
808
+ }
809
+
810
+ const data = await response.json();
811
+ currentThreadId = data.thread_id || '';
812
+
813
+ threadIdInput.value = currentThreadId;
814
+ threadIdInput.removeAttribute('readonly');
815
+
816
+ apiUrlInput.value = apiUrlTemplate.replace('{thread_id}', currentThreadId);
817
+
818
+ const successMessage = {
819
+ node: 'system',
820
+ update_type: 'thread_created',
821
+ timestamp: new Date().toISOString(),
822
+ update_content: {
823
+ content: `Successfully created new thread with ID: ${currentThreadId}`,
824
+ response_metadata: {}
825
+ }
826
+ };
827
+
828
+ addMessage(successMessage);
829
+
830
+ statusElement.className = 'status status-connected';
831
+ statusElement.innerHTML = '<i class="fas fa-check-circle"></i> Thread Created';
832
+
833
+ } catch (error) {
834
+ console.error('Error creating thread:', error);
835
+ alert(`Failed to create thread: ${error.message}`);
836
+
837
+ const errorMessage = {
838
+ node: 'system',
839
+ update_type: 'error',
840
+ timestamp: new Date().toISOString(),
841
+ update_content: {
842
+ content: `Failed to create thread: ${error.message}`,
843
+ response_metadata: {}
844
+ }
845
+ };
846
+
847
+ addMessage(errorMessage);
848
+
849
+ } finally {
850
+ createThreadBtn.disabled = false;
851
+ createThreadBtn.innerHTML = '<i class="fas fa-plus"></i> Create Thread';
852
+ }
853
+ }
854
+
855
+ // Connect to SSE stream using POST
856
+ async function connectStream() {
857
+ if (isConnected) return;
858
+
859
+ const threadId = threadIdInput.value.trim();
860
+ if (!threadId) {
861
+ alert('Please create or enter a thread ID first');
862
+ return;
863
+ }
864
+
865
+ currentApiUrl = apiUrlInput.value.trim();
866
+ if (!currentApiUrl) {
867
+ alert('Please enter an API endpoint');
868
+ return;
869
+ }
870
+
871
+ currentApiUrl = currentApiUrl.replace('{thread_id}', threadId);
872
+
873
+ try {
874
+ currentMessageContent = generateRequestBody();
875
+
876
+ statusElement.className = 'status status-connecting';
877
+ statusElement.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i> Connecting...';
878
+
879
+ const response = await fetch(currentApiUrl, {
880
+ method: 'POST',
881
+ headers: {
882
+ 'Accept': 'text/event-stream',
883
+ 'Content-Type': 'application/json'
884
+ },
885
+ body: JSON.stringify(currentMessageContent)
886
+ });
887
+
888
+ if (!response.ok) {
889
+ throw new Error(`HTTP error! status: ${response.status}`);
890
+ }
891
+
892
+ const reader = response.body.getReader();
893
+ const decoder = new TextDecoder();
894
+ let buffer = '';
895
+
896
+ const processChunk = ({ done, value }) => {
897
+ if (done) {
898
+ disconnectStream();
899
+ return;
900
+ }
901
+
902
+ buffer += decoder.decode(value, { stream: true });
903
+ const lines = buffer.split('\n');
904
+ buffer = lines.pop();
905
+
906
+ for (const line of lines) {
907
+ if (line.startsWith('data: ')) {
908
+ try {
909
+ const data = JSON.parse(line.substring(6));
910
+ addMessage(data);
911
+ } catch (e) {
912
+ console.error('Error parsing event data:', e);
913
+ }
914
+ }
915
+ }
916
+
917
+ return reader.read().then(processChunk);
918
+ };
919
+
920
+ reader.read().then(processChunk);
921
+
922
+ isConnected = true;
923
+ connectBtn.disabled = true;
924
+ disconnectBtn.disabled = false;
925
+ statusElement.className = 'status status-connected pulse';
926
+ statusElement.innerHTML = '<i class="fas fa-check-circle"></i> Connected - Receiving Data';
927
+
928
+ const connectionMessage = {
929
+ node: 'system',
930
+ update_type: 'connection',
931
+ timestamp: new Date().toISOString(),
932
+ update_content: {
933
+ content: `Successfully connected to SSE stream at ${currentApiUrl}`,
934
+ response_metadata: {}
935
+ }
936
+ };
937
+
938
+ addMessage(connectionMessage);
939
+
940
+ } catch (e) {
941
+ console.error('Error connecting to stream:', e);
942
+ alert('Error connecting to stream: ' + e.message);
943
+ statusElement.className = 'status status-disconnected';
944
+ statusElement.innerHTML = '<i class="fas fa-times-circle"></i> Disconnected';
945
+ }
946
+ }
947
+
948
+ // Disconnect from SSE stream
949
+ function disconnectStream() {
950
+ if (eventSource) {
951
+ eventSource.close();
952
+ eventSource = null;
953
+ }
954
+
955
+ // 完成任何正在进行的流式消息
956
+ streamingNodes.forEach(node => {
957
+ completeStreamingMessage(node);
958
+ });
959
+ streamingNodes.clear();
960
+
961
+ isConnected = false;
962
+ connectBtn.disabled = false;
963
+ disconnectBtn.disabled = true;
964
+ statusElement.className = 'status status-disconnected';
965
+ statusElement.innerHTML = '<i class="fas fa-times-circle"></i> Disconnected';
966
+
967
+ const disconnectionMessage = {
968
+ node: 'system',
969
+ update_type: 'disconnection',
970
+ timestamp: new Date().toISOString(),
971
+ update_content: {
972
+ content: 'Disconnected from SSE stream',
973
+ response_metadata: {}
974
+ }
975
+ };
976
+
977
+ addMessage(disconnectionMessage);
978
+ }
979
+
980
+ // Clear the stream container
981
+ function clearStream() {
982
+ // 重置流式状态
983
+ streamingNodes.forEach(node => {
984
+ completeStreamingMessage(node);
985
+ });
986
+ streamingNodes.clear();
987
+ completedStreamingNodes.clear(); // 清空已完成节点集合
988
+ pendingToolCalls.clear(); // 清空待处理的tool calls
989
+ streamContainer.innerHTML = '<div class="empty-content">No stream data yet. Create a thread and connect to start receiving events.</div>';
990
+ }
991
+
992
+ // Get agent configuration
993
+ async function getAgentConfig() {
994
+ const graphName = graphNameInput.value.trim();
995
+ if (!graphName) {
996
+ alert('Please enter a graph name first');
997
+ return;
998
+ }
999
+
1000
+ try {
1001
+ getConfigBtn.disabled = true;
1002
+ getConfigBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading...';
1003
+
1004
+ const response = await fetch(`/api/v1/config/?graph_name=${encodeURIComponent(graphName)}`, {
1005
+ method: 'GET',
1006
+ headers: {
1007
+ 'Accept': 'application/json'
1008
+ }
1009
+ });
1010
+
1011
+ if (!response.ok) {
1012
+ throw new Error(`HTTP error! status: ${response.status}`);
1013
+ }
1014
+
1015
+ const configData = await response.json();
1016
+
1017
+ // 显示配置信息
1018
+ displayConfig(configData);
1019
+
1020
+ const successMessage = {
1021
+ node: 'system',
1022
+ update_type: 'config_loaded',
1023
+ timestamp: new Date().toISOString(),
1024
+ update_content: {
1025
+ content: `Successfully loaded config for graph: ${graphName}`,
1026
+ response_metadata: {}
1027
+ }
1028
+ };
1029
+ addMessage(successMessage);
1030
+
1031
+ } catch (error) {
1032
+ console.error('Error loading config:', error);
1033
+
1034
+ const errorMessage = {
1035
+ node: 'system',
1036
+ update_type: 'error',
1037
+ timestamp: new Date().toISOString(),
1038
+ update_content: {
1039
+ content: `Failed to load config: ${error.message}`,
1040
+ response_metadata: {}
1041
+ }
1042
+ };
1043
+ addMessage(errorMessage);
1044
+
1045
+ alert(`Failed to load config: ${error.message}`);
1046
+ } finally {
1047
+ getConfigBtn.disabled = false;
1048
+ getConfigBtn.innerHTML = '<i class="fas fa-cog"></i> Get Config';
1049
+ }
1050
+ }
1051
+
1052
+ // Display configuration in a modal
1053
+ function displayConfig(configData) {
1054
+ // 移除现有的配置查看器
1055
+ const existingConfigViewer = document.querySelector('.config-viewer');
1056
+ if (existingConfigViewer) {
1057
+ existingConfigViewer.remove();
1058
+ }
1059
+
1060
+ // 创建配置查看器
1061
+ const configViewer = document.createElement('div');
1062
+ configViewer.className = 'config-viewer';
1063
+ configViewer.style.cssText = `
1064
+ position: fixed;
1065
+ top: 50%;
1066
+ left: 50%;
1067
+ transform: translate(-50%, -50%);
1068
+ width: 80%;
1069
+ max-width: 800px;
1070
+ max-height: 80vh;
1071
+ background: white;
1072
+ border-radius: 8px;
1073
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
1074
+ z-index: 1000;
1075
+ display: flex;
1076
+ flex-direction: column;
1077
+ overflow: hidden;
1078
+ `;
1079
+
1080
+ configViewer.innerHTML = `
1081
+ <div style="
1082
+ padding: 20px;
1083
+ background: #f5f5f5;
1084
+ border-bottom: 1px solid #ddd;
1085
+ display: flex;
1086
+ justify-content: between;
1087
+ align-items: center;
1088
+ ">
1089
+ <h3 style="margin: 0; color: #333;">Agent Configuration</h3>
1090
+ <button class="config-viewer-close" style="
1091
+ background: none;
1092
+ border: none;
1093
+ font-size: 18px;
1094
+ cursor: pointer;
1095
+ color: #666;
1096
+ padding: 5px;
1097
+ " title="Close config viewer">
1098
+ <i class="fas fa-times"></i>
1099
+ </button>
1100
+ </div>
1101
+ <div class="config-content" style="
1102
+ padding: 20px;
1103
+ overflow-y: auto;
1104
+ flex: 1;
1105
+ background: white;
1106
+ "></div>
1107
+ `;
1108
+
1109
+ // 创建遮罩层
1110
+ const overlay = document.createElement('div');
1111
+ overlay.className = 'config-overlay';
1112
+ overlay.style.cssText = `
1113
+ position: fixed;
1114
+ top: 0;
1115
+ left: 0;
1116
+ width: 100%;
1117
+ height: 100%;
1118
+ background: rgba(0,0,0,0.5);
1119
+ z-index: 999;
1120
+ `;
1121
+
1122
+ // 添加到页面
1123
+ document.body.appendChild(overlay);
1124
+ document.body.appendChild(configViewer);
1125
+
1126
+ const configContent = configViewer.querySelector('.config-content');
1127
+
1128
+ try {
1129
+ // 尝试不同的配置数据结构
1130
+ let configToDisplay = configData;
1131
+
1132
+ // 如果配置数据在特定属性中
1133
+ if (configData.config) {
1134
+ configToDisplay = configData.config;
1135
+ }
1136
+
1137
+ // 如果配置数据在 $defs 中
1138
+ if (configData.$defs && configData.$defs.BaseConfiguration) {
1139
+ configToDisplay = configData.$defs.BaseConfiguration;
1140
+ }
1141
+
1142
+ if (typeof configToDisplay === 'object' && configToDisplay !== null) {
1143
+ if (configToDisplay.properties) {
1144
+ // 处理 JSON Schema 格式
1145
+ displayConfigProperties(configContent, configToDisplay.properties, configToDisplay.required || []);
1146
+ } else {
1147
+ // 处理普通对象格式
1148
+ displayConfigObject(configContent, configToDisplay);
1149
+ }
1150
+ } else {
1151
+ configContent.innerHTML = `
1152
+ <div style="color: #666; text-align: center; padding: 40px;">
1153
+ <i class="fas fa-info-circle" style="font-size: 48px; margin-bottom: 20px;"></i>
1154
+ <p>No configuration data available or invalid format</p>
1155
+ <pre style="background: #f5f5f5; padding: 15px; border-radius: 4px; margin-top: 20px; text-align: left; overflow: auto;">${JSON.stringify(configData, null, 2)}</pre>
1156
+ </div>
1157
+ `;
1158
+ }
1159
+ } catch (error) {
1160
+ console.error('Error displaying config:', error);
1161
+ configContent.innerHTML = `
1162
+ <div style="color: #d32f2f; text-align: center; padding: 40px;">
1163
+ <i class="fas fa-exclamation-triangle" style="font-size: 48px; margin-bottom: 20px;"></i>
1164
+ <p>Error displaying configuration</p>
1165
+ <pre style="background: #ffebee; padding: 15px; border-radius: 4px; margin-top: 20px; text-align: left; overflow: auto;">${error.message}</pre>
1166
+ </div>
1167
+ `;
1168
+ }
1169
+
1170
+ // 关闭按钮事件
1171
+ const closeBtn = configViewer.querySelector('.config-viewer-close');
1172
+ const closeViewer = () => {
1173
+ configViewer.remove();
1174
+ overlay.remove();
1175
+ };
1176
+
1177
+ closeBtn.addEventListener('click', closeViewer);
1178
+ overlay.addEventListener('click', closeViewer);
1179
+
1180
+ // ESC 键关闭
1181
+ const handleEscape = (e) => {
1182
+ if (e.key === 'Escape') {
1183
+ closeViewer();
1184
+ document.removeEventListener('keydown', handleEscape);
1185
+ }
1186
+ };
1187
+ document.addEventListener('keydown', handleEscape);
1188
+ }
1189
+
1190
+ // 显示配置属性(JSON Schema 格式)
1191
+ function displayConfigProperties(container, properties, requiredFields = []) {
1192
+ if (!properties || Object.keys(properties).length === 0) {
1193
+ container.innerHTML = '<p style="color: #666; text-align: center;">No configuration properties found</p>';
1194
+ return;
1195
+ }
1196
+
1197
+ const configList = document.createElement('div');
1198
+ configList.className = 'config-list';
1199
+ configList.style.cssText = `
1200
+ display: flex;
1201
+ flex-direction: column;
1202
+ gap: 12px;
1203
+ `;
1204
+
1205
+ Object.entries(properties).forEach(([key, configItem]) => {
1206
+ if (typeof configItem !== 'object') return;
1207
+
1208
+ const configEntry = document.createElement('div');
1209
+ configEntry.className = 'config-entry';
1210
+ configEntry.style.cssText = `
1211
+ padding: 15px;
1212
+ border: 1px solid #e0e0e0;
1213
+ border-radius: 6px;
1214
+ background: #fafafa;
1215
+ `;
1216
+
1217
+ // 配置项标题行
1218
+ const headerRow = document.createElement('div');
1219
+ headerRow.style.cssText = `
1220
+ display: flex;
1221
+ justify-content: between;
1222
+ align-items: flex-start;
1223
+ margin-bottom: 8px;
1224
+ `;
1225
+
1226
+ const keySection = document.createElement('div');
1227
+ keySection.style.flex = '1';
1228
+
1229
+ // 配置项名称
1230
+ const configKey = document.createElement('div');
1231
+ configKey.className = 'config-key';
1232
+ configKey.style.cssText = `
1233
+ font-weight: bold;
1234
+ color: #1976d2;
1235
+ margin-bottom: 4px;
1236
+ font-size: 16px;
1237
+ `;
1238
+ configKey.textContent = configItem.title || key;
1239
+
1240
+ // 配置项类型
1241
+ if (configItem.type) {
1242
+ const typeBadge = document.createElement('span');
1243
+ typeBadge.className = 'config-type';
1244
+ typeBadge.style.cssText = `
1245
+ display: inline-block;
1246
+ background: #e3f2fd;
1247
+ color: #1976d2;
1248
+ padding: 2px 8px;
1249
+ border-radius: 12px;
1250
+ font-size: 12px;
1251
+ font-weight: normal;
1252
+ margin-left: 8px;
1253
+ `;
1254
+ typeBadge.textContent = configItem.type;
1255
+ configKey.appendChild(typeBadge);
1256
+ }
1257
+
1258
+ keySection.appendChild(configKey);
1259
+
1260
+ // 必需标记
1261
+ if (requiredFields.includes(key)) {
1262
+ const requiredBadge = document.createElement('span');
1263
+ requiredBadge.className = 'config-required';
1264
+ requiredBadge.style.cssText = `
1265
+ display: inline-block;
1266
+ background: #ffebee;
1267
+ color: #d32f2f;
1268
+ padding: 2px 8px;
1269
+ border-radius: 12px;
1270
+ font-size: 12px;
1271
+ font-weight: bold;
1272
+ margin-left: 8px;
1273
+ `;
1274
+ requiredBadge.textContent = 'REQUIRED';
1275
+ configKey.appendChild(requiredBadge);
1276
+ }
1277
+
1278
+ headerRow.appendChild(keySection);
1279
+
1280
+ // 配置项描述
1281
+ if (configItem.description) {
1282
+ const description = document.createElement('div');
1283
+ description.className = 'config-description';
1284
+ description.style.cssText = `
1285
+ color: #666;
1286
+ font-size: 14px;
1287
+ line-height: 1.4;
1288
+ margin-bottom: 8px;
1289
+ `;
1290
+ description.textContent = configItem.description;
1291
+ headerRow.appendChild(description);
1292
+ }
1293
+
1294
+ configEntry.appendChild(headerRow);
1295
+
1296
+ // 配置项值
1297
+ const valueSection = document.createElement('div');
1298
+ valueSection.style.cssText = `
1299
+ display: flex;
1300
+ justify-content: between;
1301
+ align-items: center;
1302
+ padding-top: 8px;
1303
+ border-top: 1px solid #e0e0e0;
1304
+ `;
1305
+
1306
+ const valueLabel = document.createElement('span');
1307
+ valueLabel.style.cssText = `
1308
+ font-weight: 500;
1309
+ color: #333;
1310
+ margin-right: 8px;
1311
+ `;
1312
+ valueLabel.textContent = 'Default Value:';
1313
+
1314
+ const configValue = document.createElement('span');
1315
+ configValue.className = 'config-value';
1316
+ configValue.style.cssText = `
1317
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
1318
+ background: #fff;
1319
+ padding: 4px 8px;
1320
+ border-radius: 4px;
1321
+ border: 1px solid #ddd;
1322
+ flex: 1;
1323
+ word-break: break-all;
1324
+ `;
1325
+
1326
+ // 处理敏感信息
1327
+ if (/API_?KEY|SECRET|PASSWORD|TOKEN/i.test(key)) {
1328
+ configValue.textContent = '••••••••••••••••';
1329
+ configValue.style.color = '#d32f2f';
1330
+ configValue.style.fontWeight = 'bold';
1331
+ } else {
1332
+ configValue.textContent = formatConfigValue(configItem.default);
1333
+ }
1334
+
1335
+ valueSection.appendChild(valueLabel);
1336
+ valueSection.appendChild(configValue);
1337
+ configEntry.appendChild(valueSection);
1338
+
1339
+ // 添加枚举信息
1340
+ if (configItem.enum && configItem.enum.length > 0) {
1341
+ const enumSection = document.createElement('div');
1342
+ enumSection.style.cssText = `
1343
+ margin-top: 8px;
1344
+ padding: 8px;
1345
+ background: #f3e5f5;
1346
+ border-radius: 4px;
1347
+ font-size: 12px;
1348
+ `;
1349
+
1350
+ const enumTitle = document.createElement('div');
1351
+ enumTitle.style.cssText = `
1352
+ font-weight: bold;
1353
+ color: #7b1fa2;
1354
+ margin-bottom: 4px;
1355
+ `;
1356
+ enumTitle.textContent = 'Allowed Values:';
1357
+
1358
+ const enumValues = document.createElement('div');
1359
+ enumValues.style.cssText = `
1360
+ display: flex;
1361
+ flex-wrap: wrap;
1362
+ gap: 4px;
1363
+ `;
1364
+
1365
+ configItem.enum.forEach(value => {
1366
+ const valueBadge = document.createElement('span');
1367
+ valueBadge.style.cssText = `
1368
+ background: #fff;
1369
+ color: #7b1fa2;
1370
+ padding: 2px 6px;
1371
+ border-radius: 10px;
1372
+ border: 1px solid #ba68c8;
1373
+ font-size: 11px;
1374
+ `;
1375
+ valueBadge.textContent = formatConfigValue(value);
1376
+ enumValues.appendChild(valueBadge);
1377
+ });
1378
+
1379
+ enumSection.appendChild(enumTitle);
1380
+ enumSection.appendChild(enumValues);
1381
+ configEntry.appendChild(enumSection);
1382
+ }
1383
+
1384
+ configList.appendChild(configEntry);
1385
+ });
1386
+
1387
+ container.appendChild(configList);
1388
+ }
1389
+
1390
+ // 显示配置对象(普通对象格式)
1391
+ function displayConfigObject(container, configObj) {
1392
+ const configList = document.createElement('div');
1393
+ configList.className = 'config-list';
1394
+ configList.style.cssText = `
1395
+ display: flex;
1396
+ flex-direction: column;
1397
+ gap: 8px;
1398
+ `;
1399
+
1400
+ Object.entries(configObj).forEach(([key, value]) => {
1401
+ const configEntry = document.createElement('div');
1402
+ configEntry.className = 'config-entry';
1403
+ configEntry.style.cssText = `
1404
+ display: flex;
1405
+ justify-content: space-between;
1406
+ align-items: center;
1407
+ padding: 12px;
1408
+ border: 1px solid #e0e0e0;
1409
+ border-radius: 4px;
1410
+ background: #fafafa;
1411
+ `;
1412
+
1413
+ const configKey = document.createElement('span');
1414
+ configKey.className = 'config-key';
1415
+ configKey.style.cssText = `
1416
+ font-weight: bold;
1417
+ color: #333;
1418
+ `;
1419
+ configKey.textContent = key;
1420
+
1421
+ const configValue = document.createElement('span');
1422
+ configValue.className = 'config-value';
1423
+ configValue.style.cssText = `
1424
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
1425
+ background: #fff;
1426
+ padding: 4px 8px;
1427
+ border-radius: 4px;
1428
+ border: 1px solid #ddd;
1429
+ `;
1430
+
1431
+ // 处理敏感信息
1432
+ if (/API_?KEY|SECRET|PASSWORD|TOKEN/i.test(key)) {
1433
+ configValue.textContent = '••••••••••••••••';
1434
+ configValue.style.color = '#d32f2f';
1435
+ } else {
1436
+ configValue.textContent = formatConfigValue(value);
1437
+ }
1438
+
1439
+ configEntry.appendChild(configKey);
1440
+ configEntry.appendChild(configValue);
1441
+ configList.appendChild(configEntry);
1442
+ });
1443
+
1444
+ container.appendChild(configList);
1445
+ }
1446
+
1447
+ // 格式化配置值
1448
+ function formatConfigValue(value) {
1449
+ if (value === undefined) return '未设置';
1450
+ if (value === null) return 'null';
1451
+ if (typeof value === 'object') return JSON.stringify(value, null, 2);
1452
+ return String(value);
1453
+ }
1454
+
1455
+ // Initialize
1456
+ function init() {
1457
+ loadGraphs();
1458
+ resetInputForm();
1459
+
1460
+ graphNameInput.addEventListener('change', loadSchema);
1461
+ graphSelect.addEventListener('change', function() {
1462
+ if (this.value) {
1463
+ graphNameInput.value = this.value;
1464
+ loadSchema();
1465
+ }
1466
+ });
1467
+
1468
+ createThreadBtn.addEventListener('click', createThread);
1469
+ connectBtn.addEventListener('click', connectStream);
1470
+ disconnectBtn.addEventListener('click', disconnectStream);
1471
+ clearBtn.addEventListener('click', clearStream);
1472
+ refreshGraphsBtn.addEventListener('click', loadGraphs);
1473
+ getConfigBtn.addEventListener('click', getAgentConfig);
1474
+
1475
+ // Interrupt modal buttons
1476
+ confirmInterruptBtn.addEventListener('click', continueAfterInterrupt);
1477
+ cancelInterruptBtn.addEventListener('click', () => {
1478
+ hideInterruptDialog();
1479
+ currentInterruptData = null;
1480
+ disconnectStream();
1481
+ });
1482
+
1483
+ graphNameInput.addEventListener('keypress', (e) => {
1484
+ if (e.key === 'Enter') createThread();
1485
+ });
1486
+
1487
+ threadIdInput.addEventListener('keypress', (e) => {
1488
+ if (e.key === 'Enter') connectStream();
1489
+ });
1490
+
1491
+ window.addEventListener('beforeunload', () => {
1492
+ if (isConnected) {
1493
+ disconnectStream();
1494
+ }
1495
+ });
1496
+ }
1497
+
1498
+ init();
1499
+ });