Skip to content

Commit fcb2c65

Browse files
committed
fix: prevent Agent not initialized crash and duplicate Arraylake buttons
Session resilience: - Add reinitialize() method with auto-retry on process_message - Backup/restore _messages on agent.invoke failure to prevent corruption - Auto-recreate lost sessions in websocket handler Key persistence on reconnect: - Always resend API keys from sessionStorage on ws.onopen - Remove keysConfigured guard from autoSendSessionKeys — keys were lost when WebSocket reconnected because client skipped resend Arraylake UI: - Deduplicate Arraylake Code buttons on multi-variable queries - Merge multiple snippets into single button with combined code block
1 parent acacf1d commit fcb2c65

File tree

3 files changed

+82
-13
lines changed

3 files changed

+82
-13
lines changed

web/agent_wrapper.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ def is_ready(self) -> bool:
134134
"""Check if the agent is ready."""
135135
return self._initialized and self._agent is not None
136136

137+
def reinitialize(self):
138+
"""Retry initialization (e.g., after transient failure)."""
139+
logger.warning("Attempting agent reinitialization...")
140+
self._initialized = False
141+
self._agent = None
142+
self._initialize()
143+
137144
def clear_messages(self):
138145
"""Clear conversation messages."""
139146
self._messages = []
@@ -157,7 +164,11 @@ async def process_message(
157164
Process a user message and stream the response.
158165
"""
159166
if not self.is_ready():
160-
raise RuntimeError("Agent not initialized")
167+
# Try to reinitialize once before giving up
168+
logger.warning("Agent not ready, attempting reinitialization...")
169+
self.reinitialize()
170+
if not self.is_ready():
171+
raise RuntimeError("Agent not initialized")
161172

162173
# Clear any old plots from queue
163174
self.get_pending_plots()
@@ -176,6 +187,9 @@ async def process_message(
176187

177188
# Stream status updates while agent is working
178189
await stream_callback("status", "🤖 Processing with AI...")
190+
191+
# Save message state before invoke (protect against corruption)
192+
messages_backup = list(self._messages)
179193

180194
result = await asyncio.get_event_loop().run_in_executor(
181195
None,
@@ -275,6 +289,8 @@ async def process_message(
275289
return response_text
276290

277291
except Exception as e:
292+
# Restore clean message state to prevent corruption on next call
293+
self._messages = messages_backup
278294
logger.exception(f"Error processing message: {e}")
279295
raise
280296

web/routes/websocket.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,11 @@ async def websocket_chat(websocket: WebSocket):
9595
await manager.send_json(websocket, {"type": "thinking"})
9696

9797
try:
98-
# Get session for this connection
98+
# Get session for this connection (auto-recreate if lost)
9999
session = get_session(connection_id)
100100
if not session:
101-
raise RuntimeError("Session not found")
101+
logger.warning(f"Session lost for {connection_id[:8]}, recreating...")
102+
session = create_session(connection_id)
102103

103104
# Callback for streaming
104105
async def stream_callback(event_type: str, content: str, **kwargs):

web/static/js/chat.js

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,9 @@ class EurusChat {
106106
}
107107

108108
autoSendSessionKeys() {
109-
// After WS connects, if we have session-stored keys and server has none, auto-send them
110-
if (this.serverKeysPresent.openai || this.keysConfigured) return;
109+
// After WS (re)connects, resend saved keys so the new server-side session gets them.
110+
// Only skip if the server has pre-configured env keys (not user-provided).
111+
if (this.serverKeysPresent.openai) return;
111112
const saved = sessionStorage.getItem('eurus-keys');
112113
if (!saved) return;
113114
try {
@@ -192,11 +193,13 @@ class EurusChat {
192193
this.reconnectAttempts = 0;
193194
this.updateConnectionStatus('connected');
194195

196+
// Always resend keys on (re)connect — server session may have been
197+
// destroyed on disconnect, so keys stored in sessionStorage must be
198+
// re-sent even if keysConfigured is true from the previous session.
199+
this.autoSendSessionKeys();
200+
195201
if (this.serverKeysPresent.openai || this.keysConfigured) {
196202
this.sendBtn.disabled = false;
197-
} else {
198-
// Auto-send keys from sessionStorage on reconnect/refresh
199-
this.autoSendSessionKeys();
200203
}
201204
};
202205

@@ -700,17 +703,47 @@ class EurusChat {
700703
const actionsDiv = lastFigure ? lastFigure.querySelector('.plot-actions') : null;
701704

702705
if (actionsDiv) {
706+
// Check if an Arraylake button already exists on this figure
707+
const existingBtn = actionsDiv.querySelector('.arraylake-btn');
708+
if (existingBtn) {
709+
// Append new snippet to the existing code block
710+
const existingCodeDiv = lastFigure.querySelector('.arraylake-code-block');
711+
if (existingCodeDiv) {
712+
const codeEl = existingCodeDiv.querySelector('code');
713+
const prevRaw = existingCodeDiv.getAttribute('data-raw-code') || '';
714+
const combined = prevRaw + '\n\n# ---\n\n' + cleanCode;
715+
existingCodeDiv.setAttribute('data-raw-code', combined);
716+
try {
717+
codeEl.innerHTML = hljs.highlight(combined, { language: 'python' }).value;
718+
} catch (e) {
719+
codeEl.textContent = combined;
720+
}
721+
// Update copy button target
722+
const copyBtn = existingCodeDiv.querySelector('.copy-snippet-btn');
723+
if (copyBtn) {
724+
copyBtn.onclick = () => {
725+
navigator.clipboard.writeText(combined).then(() => {
726+
copyBtn.textContent = '✓ Copied!';
727+
setTimeout(() => copyBtn.textContent = 'Copy Code', 2000);
728+
});
729+
};
730+
}
731+
}
732+
return;
733+
}
734+
703735
// Add button inline with Enlarge/Download/Show Code
704736
const btn = document.createElement('button');
705-
btn.className = 'code-btn';
737+
btn.className = 'code-btn arraylake-btn';
706738
btn.title = 'Arraylake Code';
707739
btn.textContent = '📦 Arraylake Code';
708740
actionsDiv.appendChild(btn);
709741

710742
// Add code block to figure (same pattern as Show Code)
711743
const codeDiv = document.createElement('div');
712-
codeDiv.className = 'plot-code';
744+
codeDiv.className = 'plot-code arraylake-code-block';
713745
codeDiv.style.display = 'none';
746+
codeDiv.setAttribute('data-raw-code', cleanCode);
714747

715748
const pre = document.createElement('pre');
716749
const codeEl = document.createElement('code');
@@ -728,7 +761,8 @@ class EurusChat {
728761
copyBtn.className = 'copy-snippet-btn';
729762
copyBtn.textContent = 'Copy Code';
730763
copyBtn.addEventListener('click', () => {
731-
navigator.clipboard.writeText(cleanCode).then(() => {
764+
const rawCode = codeDiv.getAttribute('data-raw-code') || cleanCode;
765+
navigator.clipboard.writeText(rawCode).then(() => {
732766
copyBtn.textContent = '✓ Copied!';
733767
setTimeout(() => copyBtn.textContent = 'Copy Code', 2000);
734768
});
@@ -747,17 +781,35 @@ class EurusChat {
747781
}
748782
});
749783
} else {
750-
// No plot figure — add as standalone section under the message
784+
// No plot figure — check if standalone section already exists
785+
const existingWrapper = targetMsg.querySelector('.arraylake-snippet-section');
786+
if (existingWrapper) {
787+
// Append to existing standalone snippet
788+
const codeEl = existingWrapper.querySelector('code');
789+
const existingCodeDiv = existingWrapper.querySelector('.plot-code');
790+
const prevRaw = existingCodeDiv.getAttribute('data-raw-code') || '';
791+
const combined = prevRaw + '\n\n# ---\n\n' + cleanCode;
792+
existingCodeDiv.setAttribute('data-raw-code', combined);
793+
try {
794+
codeEl.innerHTML = hljs.highlight(combined, { language: 'python' }).value;
795+
} catch (e) {
796+
codeEl.textContent = combined;
797+
}
798+
return;
799+
}
800+
751801
const wrapper = document.createElement('div');
752802
wrapper.className = 'arraylake-snippet-section';
753803
wrapper.innerHTML = `
754804
<div class="plot-actions">
755-
<button class="code-btn" title="Arraylake Code">📦 Arraylake Code</button>
805+
<button class="code-btn arraylake-btn" title="Arraylake Code">📦 Arraylake Code</button>
756806
</div>
757807
<div class="plot-code" style="display: none;">
758808
<pre><code class="language-python hljs"></code></pre>
759809
</div>
760810
`;
811+
const codeDivEl = wrapper.querySelector('.plot-code');
812+
codeDivEl.setAttribute('data-raw-code', cleanCode);
761813
const codeEl = wrapper.querySelector('code');
762814
try {
763815
codeEl.innerHTML = hljs.highlight(cleanCode, { language: 'python' }).value;

0 commit comments

Comments
 (0)