More correct working with error nodes + more correct regeneration

This commit is contained in:
dimitrievgs 2025-10-12 22:13:52 +03:00
parent 547c13b0b5
commit 05fbd5a9f4
6 changed files with 549 additions and 186 deletions

237
main.js

File diff suppressed because one or more lines are too long

View File

@ -863,7 +863,8 @@ export class ChatPanel {
const regenerateIconContainer = regenerateButton.createSpan({ cls: 'flex items-center justify-center touch:w-10 h-8 w-8' }); const regenerateIconContainer = regenerateButton.createSpan({ cls: 'flex items-center justify-center touch:w-10 h-8 w-8' });
setIcon(regenerateIconContainer, 'refresh-cw'); setIcon(regenerateIconContainer, 'refresh-cw');
regenerateButton.addEventListener('click', async () => { regenerateButton.addEventListener('click', async () => {
this.props.plugin.eventBus.emit('regenerate-message', { graphId, nodeId }); // Передаем выбранную модель при регенерации
this.props.plugin.eventBus.emit('regenerate-message', { graphId, nodeId, model: this.selectedModel });
}); });
} }

View File

@ -103,7 +103,8 @@ const BaseNodeWrapper: React.FC<BaseNodeWrapperProps> = ({ data, isConnectable,
if (eventBus) { if (eventBus) {
eventBus.emit('regenerate-message', { eventBus.emit('regenerate-message', {
graphId: data.graphId, graphId: data.graphId,
nodeId: data.nodeId nodeId: data.nodeId,
model: (data.app as any).plugins?.plugins?.['llm-agent-plugin']?.settings?.defaultModel || 'gemini-2.5-flash' // НОВОЕ: Добавляем модель
}); });
} }
}; };
@ -122,7 +123,10 @@ const BaseNodeWrapper: React.FC<BaseNodeWrapperProps> = ({ data, isConnectable,
isConnectable: isConnectable isConnectable: isConnectable
}), }),
children, // Здесь будет отображаться специфическое содержимое узла children, // Здесь будет отображаться специфическое содержимое узла
data.isSelected && React.createElement('div', { data.isSelected &&
React.createElement(
'div',
{
style: { style: {
position: 'absolute', position: 'absolute',
bottom: '-30px', bottom: '-30px',
@ -131,18 +135,26 @@ const BaseNodeWrapper: React.FC<BaseNodeWrapperProps> = ({ data, isConnectable,
gap: '5px' gap: '5px'
} }
}, },
React.createElement('button', { React.createElement(
'button',
{
style: deleteButtonStyle, style: deleteButtonStyle,
onClick: handleNodeDelete, onClick: handleNodeDelete,
'aria-label': 'Удалить узел', 'aria-label': 'Удалить узел',
className: 'node-delete-button' className: 'node-delete-button'
}, '🗑️'), },
React.createElement('button', { '🗑️'
),
React.createElement(
'button',
{
style: deleteButtonStyle, style: deleteButtonStyle,
onClick: handleRegenerate, onClick: handleRegenerate,
'aria-label': 'Регенерировать', 'aria-label': 'Регенерировать',
className: 'node-regenerate-button' className: 'node-regenerate-button'
}, '🔄') },
'🔄'
)
) )
); );
}; };
@ -163,12 +175,13 @@ const UserNode: React.FC<any> = ({ data, isConnectable }) => {
// Обновите другие типы узлов (LLMNode, ToolNode, ErrorNode) аналогично // Обновите другие типы узлов (LLMNode, ToolNode, ErrorNode) аналогично
const LLMNode: React.FC<any> = ({ data, isConnectable }) => { const LLMNode: React.FC<any> = ({ data, isConnectable }) => {
const displayLabel = data.title || data.label || 'LLM'; const displayLabel = data.title || data.label || 'LLM';
// ... // Если узел является заглушкой, добавляем класс для стилизации
const nodeClassName = data.is_placeholder ? 'llm-node-placeholder' : '';
return React.createElement( return React.createElement(
BaseNodeWrapper, BaseNodeWrapper,
{ data: data, isConnectable: isConnectable }, { data: data, isConnectable: isConnectable },
React.createElement('div', null, displayLabel) React.createElement('div', { className: nodeClassName }, displayLabel)
); );
}; };
@ -347,12 +360,13 @@ export const GraphPanelComponent: React.FC<GraphPanelProps> = ({
title_generated: node.data?.title_generated || false, title_generated: node.data?.title_generated || false,
onNodeDelete: onNodeDelete, onNodeDelete: onNodeDelete,
isSelected: selectedNodeIds.has(node.id), isSelected: selectedNodeIds.has(node.id),
graphId: (node.data && node.data.message && node.data.message.graph_id) ? node.data.message.graph_id : 'unknown', graphId: node.data && node.data.message && node.data.message.graph_id ? node.data.message.graph_id : 'unknown',
nodeId: node.id, nodeId: node.id,
app: app app: app,
is_placeholder: node.data?.is_placeholder || false // НОВОЕ: Передаем статус placeholder
}, },
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
className: selectedNodeIds.has(node.id) ? 'selected-node' : '', className: selectedNodeIds.has(node.id) ? 'selected-node' : ''
})); }));
const initialEdgesForLayout = graphData.edges.map((edge) => ({ const initialEdgesForLayout = graphData.edges.map((edge) => ({

View File

@ -5,6 +5,10 @@ export class WebSocketService {
private socket: Socket | null = null; private socket: Socket | null = null;
private eventBus: EventBus; private eventBus: EventBus;
private apiBaseUrl: string; private apiBaseUrl: string;
//private receivedEvents: Set<string> = new Set(); // Для отслеживания полученных событий
//private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
private pingIntervalId: ReturnType<typeof setInterval> | null = null;
constructor(eventBus: EventBus, apiBaseUrl: string) { constructor(eventBus: EventBus, apiBaseUrl: string) {
this.eventBus = eventBus; this.eventBus = eventBus;
@ -22,16 +26,25 @@ export class WebSocketService {
// Извлекаем базовый URL без /api // Извлекаем базовый URL без /api
const wsUrl = this.apiBaseUrl.replace('/api', ''); const wsUrl = this.apiBaseUrl.replace('/api', '');
console.log('🔌 Подключение к WebSocket:', wsUrl);
this.socket = io(wsUrl, { this.socket = io(wsUrl, {
transports: ['websocket', 'polling'], transports: ['websocket', 'polling'],
reconnection: true, reconnection: true,
reconnectionDelay: 1000, reconnectionDelay: 1000,
reconnectionDelayMax: 5000, reconnectionDelayMax: 5000,
reconnectionAttempts: 5 reconnectionAttempts: this.maxReconnectAttempts,
timeout: 20000,
forceNew: false,
upgrade: true,
// Добавляем дополнительные опции для стабильности
autoConnect: true,
rememberUpgrade: true,
rejectUnauthorized: false
}); });
this.setupEventHandlers(); this.setupEventHandlers();
this.startProactivePing(); // Запускаем проактивный пинг
} }
/** /**
@ -52,17 +65,45 @@ export class WebSocketService {
console.error('Ошибка подключения WebSocket:', error); console.error('Ошибка подключения WebSocket:', error);
}); });
// ----------------------------------------------- Title Update Events --------------------------------------------------------------- // ----------------------------------------------- Nodes Update Events ---------------------------------------------------------------
this.socket.on('graph_title_updated', (data: { graph_id: string; title: string }) => { this.socket.on(
console.log('📊 Получено обновление заголовка графа:', data); 'node_type_changed',
(data: { graph_id: string; node_id: string; node_type: string }, callback?: (response: { status: string }) => void) => {
console.log('📡 WebSocket получено: node_type_changed', data);
this.eventBus.emit('ws-node-type-changed', {
graphId: data.graph_id,
nodeId: data.node_id,
nodeType: data.node_type
});
if (callback) {
callback({ status: 'ok' }); // Отправляем подтверждение
}
}
);
this.socket.on(
'graph_title_updated',
(data: { graph_id: string; title: string }, callback?: (response: { status: string }) => void) => {
console.log('📡 WebSocket получено: graph_title_updated', data);
this.eventBus.emit('ws-graph-title-updated', data); this.eventBus.emit('ws-graph-title-updated', data);
}); if (callback) {
callback({ status: 'ok' }); // Отправляем подтверждение
}
}
);
this.socket.on(
'node_title_updated',
(data: { graph_id: string; node_id: string; title: string }, callback?: (response: { status: string }) => void) => {
console.log('📡 WebSocket получено: node_title_updated', data);
this.socket.on('node_title_updated', (data: { graph_id: string; node_id: string; title: string }) => {
console.log('📝 Получено обновление заголовка узла:', data);
this.eventBus.emit('ws-node-title-updated', data); this.eventBus.emit('ws-node-title-updated', data);
}); if (callback) {
callback({ status: 'ok' }); // Отправляем подтверждение
}
}
);
} }
/** /**
@ -74,6 +115,7 @@ export class WebSocketService {
this.socket = null; this.socket = null;
console.log('WebSocket отключен'); console.log('WebSocket отключен');
} }
this.stopProactivePing(); // Останавливаем проактивный пинг
} }
/** /**
@ -82,4 +124,51 @@ export class WebSocketService {
isConnected(): boolean { isConnected(): boolean {
return this.socket?.connected ?? false; return this.socket?.connected ?? false;
} }
/**
* Запускает проактивный пинг для проверки соединения.
*/
private startProactivePing(): void {
this.stopProactivePing(); // Убедимся, что предыдущий интервал остановлен
this.pingIntervalId = setInterval(() => {
if (this.socket && this.socket.connected) {
console.log('Client sending proactive ping...');
let ackTimeout = setTimeout(() => {
console.warn('Client: No pong response received for proactive ping within timeout. Forcing reconnect.');
this.disconnect();
this.connect();
}, 3000); // Таймаут ожидания ответа на пинг от сервера (3 секунды)
// Используем callback для получения подтверждения от сервера
this.socket.emit('client_proactive_ping', (response: { status: string }) => {
clearTimeout(ackTimeout);
if (response && response.status === 'pong') {
console.log('Client received proactive pong.');
} else {
console.warn('Client: Invalid pong response or no response for proactive ping. Forcing reconnect.');
this.disconnect();
this.connect();
}
});
} else if (this.socket && !this.socket.connected) { // Здесь убираем '&& !this.socket.connecting'
// Если сокет не подключен, Socket.IO должен сам управлять переподключением,
// так как reconnection: true. Ручной вызов connect() может быть избыточным.
console.log('Client proactive ping: Socket not connected. Relying on Socket.IO\'s auto-reconnect logic.');
// Если вы все же хотите агрессивно вызвать this.connect() при каждом обнаружении отключения,
// оставьте следующую строку:
this.connect();
}
}, 5000); // Проверять каждые 5 секунд
}
/**
* Останавливает проактивный пинг.
*/
private stopProactivePing(): void {
if (this.pingIntervalId) {
clearInterval(this.pingIntervalId);
this.pingIntervalId = null;
console.log('Proactive ping stopped.');
}
}
} }

View File

@ -67,6 +67,7 @@ export class ChatView extends ItemView {
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this)); this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
this.plugin.eventBus.on('graph-selected', this.handleGraphSelected.bind(this)); this.plugin.eventBus.on('graph-selected', this.handleGraphSelected.bind(this));
this.plugin.eventBus.on('regenerate-message', this.handleRegenerateMessage.bind(this)); // Добавлено this.plugin.eventBus.on('regenerate-message', this.handleRegenerateMessage.bind(this)); // Добавлено
this.plugin.eventBus.on('ws-node-type-changed', this.handleNodeTypeChanged.bind(this));
} }
async onClose(): Promise<void> { async onClose(): Promise<void> {
@ -78,6 +79,7 @@ export class ChatView extends ItemView {
this.plugin.eventBus.off('new-chat', this.handleNewChat); this.plugin.eventBus.off('new-chat', this.handleNewChat);
this.plugin.eventBus.off('graph-selected', this.handleGraphSelected); this.plugin.eventBus.off('graph-selected', this.handleGraphSelected);
this.plugin.eventBus.off('regenerate-message', this.handleRegenerateMessage); this.plugin.eventBus.off('regenerate-message', this.handleRegenerateMessage);
this.plugin.eventBus.off('ws-node-type-changed', this.handleNodeTypeChanged);
} }
// ----------------------------------------------- API Methods --------------------------------------------------------------- // ----------------------------------------------- API Methods ---------------------------------------------------------------
@ -173,7 +175,8 @@ export class ChatView extends ItemView {
// Шаг 2: Запускаем стриминг ответа // Шаг 2: Запускаем стриминг ответа
if (this.selectedGraphId) { if (this.selectedGraphId) {
// Проверка на null // Проверка на null
await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel); // Передаем null, так как это не регенерация, а новое сообщение
await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel, null);
} else { } else {
throw new Error('Graph ID не определён после отправки сообщения'); throw new Error('Graph ID не определён после отправки сообщения');
} }
@ -221,23 +224,35 @@ export class ChatView extends ItemView {
await this.fetchMessagesFromRootToNode(data.currentNode); await this.fetchMessagesFromRootToNode(data.currentNode);
} }
async streamLLMResponse(graphId: string, userNodeId: string, selectedModel?: string): Promise<void> { /**
const params = new URLSearchParams({ * Стримит ответ LLM.
graph_id: graphId, * @param {string} graphId - ID графа.
user_node_id: userNodeId, * @param {string} userNodeId - ID узла пользователя.
system_prompt: this.plugin.settings.systemPrompt || '', * @param {string} selectedModel - Выбранная модель.
model: selectedModel || this.plugin.settings.defaultModel * @param {string | null} existingAssistantNodeId - Существующий ID узла ассистента для повторного использования (для регенерации).
}); */
async streamLLMResponse(
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/stream`, { graphId: string,
method: 'POST', userNodeId: string,
headers: { 'Content-Type': 'application/json' }, selectedModel?: string,
body: JSON.stringify({ existingAssistantNodeId: string | null = null
): Promise<void> {
const body = {
graph_id: graphId, graph_id: graphId,
user_node_id: userNodeId, user_node_id: userNodeId,
system_prompt: this.plugin.settings.systemPrompt, system_prompt: this.plugin.settings.systemPrompt,
model: selectedModel || this.plugin.settings.defaultModel model: selectedModel || this.plugin.settings.defaultModel
}) };
// Если есть существующий ID узла ассистента, добавляем его в тело запроса
if (existingAssistantNodeId) {
Object.assign(body, { existing_assistant_node_id: existingAssistantNodeId });
}
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body) // Используем модифицированное тело запроса
}); });
if (!response.ok) { if (!response.ok) {
@ -267,6 +282,10 @@ export class ChatView extends ItemView {
if (data.type === 'node_created') { if (data.type === 'node_created') {
assistantNodeId = data.node_id; assistantNodeId = data.node_id;
// Если узел переиспользуется, нужно очистить его содержимое в UI
if (data.reused) {
this.clearStreamingMessage(assistantNodeId);
}
this.plugin.eventBus.emit('graph-selected', { this.plugin.eventBus.emit('graph-selected', {
graphId: graphId, graphId: graphId,
currentNode: assistantNodeId currentNode: assistantNodeId
@ -275,11 +294,16 @@ export class ChatView extends ItemView {
accumulatedContent += data.content; accumulatedContent += data.content;
this.updateStreamingMessage(assistantNodeId, accumulatedContent); this.updateStreamingMessage(assistantNodeId, accumulatedContent);
} else if (data.type === 'error') { } else if (data.type === 'error') {
console.error('Ошибка стриминга:', data.error); console.error('Ошибка:', data.error);
if (assistantNodeId) { if (assistantNodeId) {
this.markNodeAsError(assistantNodeId, data.error); this.markNodeAsError(assistantNodeId, data.error);
// Эмиттируем событие, чтобы GraphView обновил отображение узла с ошибкой
this.plugin.eventBus.emit('graph-selected', {
graphId: graphId,
currentNode: assistantNodeId
});
} }
break; break; // Прекращаем обработку при ошибке
} else if (data.type === 'done') { } else if (data.type === 'done') {
this.currentNode = assistantNodeId; this.currentNode = assistantNodeId;
this.plugin.eventBus.emit('graph-selected', { this.plugin.eventBus.emit('graph-selected', {
@ -296,6 +320,24 @@ export class ChatView extends ItemView {
} }
} }
/**
* Очищает содержимое сообщения в истории чата, помечая его как заглушку.
* Используется для подготовки к регенерации.
* @param {string} nodeId - ID узла, содержимое которого нужно очистить.
*/
clearStreamingMessage(nodeId: string): void {
const messageIndex = this.chatHistory.findIndex((msg) => msg.node_id === nodeId);
if (messageIndex !== -1) {
this.chatHistory[messageIndex].content = 'Получение ответа...';
this.chatHistory[messageIndex].type = 'llm'; // Убедимся, что тип узла LLM
// Если нужно, сбросьте другие флаги
}
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
}
updateStreamingMessage(nodeId: string, content: string): void { updateStreamingMessage(nodeId: string, content: string): void {
const lastMessage = this.chatHistory[this.chatHistory.length - 1]; const lastMessage = this.chatHistory[this.chatHistory.length - 1];
if (lastMessage && lastMessage.node_id === nodeId) { if (lastMessage && lastMessage.node_id === nodeId) {
@ -319,6 +361,22 @@ export class ChatView extends ItemView {
if (lastMessage && lastMessage.node_id === nodeId) { if (lastMessage && lastMessage.node_id === nodeId) {
lastMessage.type = 'error'; lastMessage.type = 'error';
lastMessage.content = errorMessage; lastMessage.content = errorMessage;
} else {
// Если узел не последний, попробуйте найти его и обновить
const messageIndex = this.chatHistory.findIndex((msg) => msg.node_id === nodeId);
if (messageIndex !== -1) {
this.chatHistory[messageIndex].type = 'error';
this.chatHistory[messageIndex].content = errorMessage;
} else {
// Если узел не найден, добавьте новое сообщение об ошибке
this.chatHistory.push({
role: 'assistant',
content: errorMessage,
node_id: nodeId,
graph_id: this.selectedGraphId || '',
type: 'error'
});
}
} }
if (this.chatPanel) { if (this.chatPanel) {
@ -326,7 +384,8 @@ export class ChatView extends ItemView {
} }
} }
async handleRegenerateMessage(data: { graphId: string; nodeId: string }): Promise<void> { async handleRegenerateMessage(data: { graphId: string; nodeId: string; model: string }): Promise<void> {
// НОВОЕ: Добавляем 'model' в тип данных
try { try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/regenerate`, { const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/regenerate`, {
method: 'POST', method: 'POST',
@ -334,8 +393,7 @@ export class ChatView extends ItemView {
body: JSON.stringify({ body: JSON.stringify({
graph_id: data.graphId, graph_id: data.graphId,
node_id: data.nodeId, node_id: data.nodeId,
system_prompt: this.plugin.settings.systemPrompt, model: data.model // НОВОЕ: Передаем выбранную модель для регенерации
model: this.plugin.settings.defaultModel
}) })
}); });
@ -344,16 +402,31 @@ export class ChatView extends ItemView {
} }
const result = await response.json(); const result = await response.json();
const newNodeId = result.new_node_id; // НОВОЕ: Получаем ID существующего узла ассистента и ID пользовательского узла
const parentNodeId = result.parent_node_id; const existingAssistantNodeId = result.existing_assistant_node_id;
const userNodeId = result.user_node_id;
const modelToUse = result.model;
// Запускаем стриминг для нового узла // Запускаем стриминг, передавая ID существующего узла для повторного использования
if (data.graphId) { if (data.graphId) {
await this.streamLLMResponse(data.graphId, parentNodeId, this.plugin.settings.defaultModel); await this.streamLLMResponse(data.graphId, userNodeId, modelToUse, existingAssistantNodeId);
} }
} catch (error: any) { } catch (error: any) {
console.error('Ошибка регенерации:', error); console.error('Ошибка регенерации:', error);
new Notice(`Ошибка регенерации: ${error?.message || error}`, 5000); new Notice(`Ошибка регенерации: ${error?.message || error}`, 5000);
} }
} }
// ----------------------------------------------- WebSocket Event Handlers ---------------------------------------------------------------
handleNodeTypeChanged(data: { graphId: string; nodeId: string; nodeType: string }): void {
// Обновляем тип узла в истории чата
const messageIndex = this.chatHistory.findIndex((msg) => msg.node_id === data.nodeId);
if (messageIndex !== -1) {
this.chatHistory[messageIndex].type = data.nodeType;
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
}
}
} }

View File

@ -123,6 +123,7 @@ export class GraphView extends ItemView {
// WebSocket Event Handlers // WebSocket Event Handlers
this.plugin.eventBus.on('ws-graph-title-updated', this.handleGraphTitleUpdated.bind(this)); this.plugin.eventBus.on('ws-graph-title-updated', this.handleGraphTitleUpdated.bind(this));
this.plugin.eventBus.on('ws-node-title-updated', this.handleNodeTitleUpdated.bind(this)); this.plugin.eventBus.on('ws-node-title-updated', this.handleNodeTitleUpdated.bind(this));
this.plugin.eventBus.on('ws-node-type-changed', this.handleNodeTypeChanged.bind(this));
// Загружаем список графов // Загружаем список графов
await this.fetchGraphsList(); await this.fetchGraphsList();
@ -151,6 +152,7 @@ export class GraphView extends ItemView {
// WebSocket Event Cleanup // WebSocket Event Cleanup
this.plugin.eventBus.off('ws-graph-title-updated', this.handleGraphTitleUpdated); this.plugin.eventBus.off('ws-graph-title-updated', this.handleGraphTitleUpdated);
this.plugin.eventBus.off('ws-node-title-updated', this.handleNodeTitleUpdated); this.plugin.eventBus.off('ws-node-title-updated', this.handleNodeTitleUpdated);
this.plugin.eventBus.off('ws-node-type-changed', this.handleNodeTypeChanged);
} }
// ----------------------------------------------- Custom Title Update --------------------------------------------------- // ----------------------------------------------- Custom Title Update ---------------------------------------------------
@ -535,6 +537,27 @@ export class GraphView extends ItemView {
// ----------------------------------------------- WebSocket Event Handlers --------------------------------------------------------------- // ----------------------------------------------- WebSocket Event Handlers ---------------------------------------------------------------
/**
* Обработчик изменения типа узла через WebSocket
*/
handleNodeTypeChanged(data: { graphId: string; nodeId: string; nodeType: string }): void {
// Обновляем только если это узел текущего графа
if (this.selectedGraphId !== data.graphId) {
return;
}
// Находим и обновляем тип узла в graphData
const nodeIndex = this.graphData.nodes.findIndex((n) => n.id === data.nodeId);
if (nodeIndex !== -1) {
this.graphData.nodes[nodeIndex].type = data.nodeType;
// Перерисовываем граф с обновленным типом узла
this.renderGraphPanel();
console.log(`✅ Обновлен тип узла ${data.nodeId}: "${data.nodeType}"`);
}
}
/** /**
* Обработчик обновления заголовка графа через WebSocket * Обработчик обновления заголовка графа через WebSocket
*/ */