Upload folder using huggingface_hub
Browse files- web/agent_wrapper.py +17 -1
- web/routes/websocket.py +3 -2
- web/static/js/chat.js +62 -10
web/agent_wrapper.py
CHANGED
|
@@ -134,6 +134,13 @@ class AgentSession:
|
|
| 134 |
"""Check if the agent is ready."""
|
| 135 |
return self._initialized and self._agent is not None
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
def clear_messages(self):
|
| 138 |
"""Clear conversation messages."""
|
| 139 |
self._messages = []
|
|
@@ -157,7 +164,11 @@ class AgentSession:
|
|
| 157 |
Process a user message and stream the response.
|
| 158 |
"""
|
| 159 |
if not self.is_ready():
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
# Clear any old plots from queue
|
| 163 |
self.get_pending_plots()
|
|
@@ -176,6 +187,9 @@ class AgentSession:
|
|
| 176 |
|
| 177 |
# Stream status updates while agent is working
|
| 178 |
await stream_callback("status", "π€ Processing with AI...")
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
result = await asyncio.get_event_loop().run_in_executor(
|
| 181 |
None,
|
|
@@ -275,6 +289,8 @@ class AgentSession:
|
|
| 275 |
return response_text
|
| 276 |
|
| 277 |
except Exception as e:
|
|
|
|
|
|
|
| 278 |
logger.exception(f"Error processing message: {e}")
|
| 279 |
raise
|
| 280 |
|
|
|
|
| 134 |
"""Check if the agent is ready."""
|
| 135 |
return self._initialized and self._agent is not None
|
| 136 |
|
| 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 |
+
|
| 144 |
def clear_messages(self):
|
| 145 |
"""Clear conversation messages."""
|
| 146 |
self._messages = []
|
|
|
|
| 164 |
Process a user message and stream the response.
|
| 165 |
"""
|
| 166 |
if not self.is_ready():
|
| 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")
|
| 172 |
|
| 173 |
# Clear any old plots from queue
|
| 174 |
self.get_pending_plots()
|
|
|
|
| 187 |
|
| 188 |
# Stream status updates while agent is working
|
| 189 |
await stream_callback("status", "π€ Processing with AI...")
|
| 190 |
+
|
| 191 |
+
# Save message state before invoke (protect against corruption)
|
| 192 |
+
messages_backup = list(self._messages)
|
| 193 |
|
| 194 |
result = await asyncio.get_event_loop().run_in_executor(
|
| 195 |
None,
|
|
|
|
| 289 |
return response_text
|
| 290 |
|
| 291 |
except Exception as e:
|
| 292 |
+
# Restore clean message state to prevent corruption on next call
|
| 293 |
+
self._messages = messages_backup
|
| 294 |
logger.exception(f"Error processing message: {e}")
|
| 295 |
raise
|
| 296 |
|
web/routes/websocket.py
CHANGED
|
@@ -95,10 +95,11 @@ async def websocket_chat(websocket: WebSocket):
|
|
| 95 |
await manager.send_json(websocket, {"type": "thinking"})
|
| 96 |
|
| 97 |
try:
|
| 98 |
-
# Get session for this connection
|
| 99 |
session = get_session(connection_id)
|
| 100 |
if not session:
|
| 101 |
-
|
|
|
|
| 102 |
|
| 103 |
# Callback for streaming
|
| 104 |
async def stream_callback(event_type: str, content: str, **kwargs):
|
|
|
|
| 95 |
await manager.send_json(websocket, {"type": "thinking"})
|
| 96 |
|
| 97 |
try:
|
| 98 |
+
# Get session for this connection (auto-recreate if lost)
|
| 99 |
session = get_session(connection_id)
|
| 100 |
if not session:
|
| 101 |
+
logger.warning(f"Session lost for {connection_id[:8]}, recreating...")
|
| 102 |
+
session = create_session(connection_id)
|
| 103 |
|
| 104 |
# Callback for streaming
|
| 105 |
async def stream_callback(event_type: str, content: str, **kwargs):
|
web/static/js/chat.js
CHANGED
|
@@ -106,8 +106,9 @@ class EurusChat {
|
|
| 106 |
}
|
| 107 |
|
| 108 |
autoSendSessionKeys() {
|
| 109 |
-
// After WS connects,
|
| 110 |
-
if
|
|
|
|
| 111 |
const saved = sessionStorage.getItem('eurus-keys');
|
| 112 |
if (!saved) return;
|
| 113 |
try {
|
|
@@ -192,11 +193,13 @@ class EurusChat {
|
|
| 192 |
this.reconnectAttempts = 0;
|
| 193 |
this.updateConnectionStatus('connected');
|
| 194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
if (this.serverKeysPresent.openai || this.keysConfigured) {
|
| 196 |
this.sendBtn.disabled = false;
|
| 197 |
-
} else {
|
| 198 |
-
// Auto-send keys from sessionStorage on reconnect/refresh
|
| 199 |
-
this.autoSendSessionKeys();
|
| 200 |
}
|
| 201 |
};
|
| 202 |
|
|
@@ -700,17 +703,47 @@ class EurusChat {
|
|
| 700 |
const actionsDiv = lastFigure ? lastFigure.querySelector('.plot-actions') : null;
|
| 701 |
|
| 702 |
if (actionsDiv) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
// Add button inline with Enlarge/Download/Show Code
|
| 704 |
const btn = document.createElement('button');
|
| 705 |
-
btn.className = 'code-btn';
|
| 706 |
btn.title = 'Arraylake Code';
|
| 707 |
btn.textContent = 'π¦ Arraylake Code';
|
| 708 |
actionsDiv.appendChild(btn);
|
| 709 |
|
| 710 |
// Add code block to figure (same pattern as Show Code)
|
| 711 |
const codeDiv = document.createElement('div');
|
| 712 |
-
codeDiv.className = 'plot-code';
|
| 713 |
codeDiv.style.display = 'none';
|
|
|
|
| 714 |
|
| 715 |
const pre = document.createElement('pre');
|
| 716 |
const codeEl = document.createElement('code');
|
|
@@ -728,7 +761,8 @@ class EurusChat {
|
|
| 728 |
copyBtn.className = 'copy-snippet-btn';
|
| 729 |
copyBtn.textContent = 'Copy Code';
|
| 730 |
copyBtn.addEventListener('click', () => {
|
| 731 |
-
|
|
|
|
| 732 |
copyBtn.textContent = 'β Copied!';
|
| 733 |
setTimeout(() => copyBtn.textContent = 'Copy Code', 2000);
|
| 734 |
});
|
|
@@ -747,17 +781,35 @@ class EurusChat {
|
|
| 747 |
}
|
| 748 |
});
|
| 749 |
} else {
|
| 750 |
-
// No plot figure β
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
const wrapper = document.createElement('div');
|
| 752 |
wrapper.className = 'arraylake-snippet-section';
|
| 753 |
wrapper.innerHTML = `
|
| 754 |
<div class="plot-actions">
|
| 755 |
-
<button class="code-btn" title="Arraylake Code">π¦ Arraylake Code</button>
|
| 756 |
</div>
|
| 757 |
<div class="plot-code" style="display: none;">
|
| 758 |
<pre><code class="language-python hljs"></code></pre>
|
| 759 |
</div>
|
| 760 |
`;
|
|
|
|
|
|
|
| 761 |
const codeEl = wrapper.querySelector('code');
|
| 762 |
try {
|
| 763 |
codeEl.innerHTML = hljs.highlight(cleanCode, { language: 'python' }).value;
|
|
|
|
| 106 |
}
|
| 107 |
|
| 108 |
autoSendSessionKeys() {
|
| 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;
|
| 112 |
const saved = sessionStorage.getItem('eurus-keys');
|
| 113 |
if (!saved) return;
|
| 114 |
try {
|
|
|
|
| 193 |
this.reconnectAttempts = 0;
|
| 194 |
this.updateConnectionStatus('connected');
|
| 195 |
|
| 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 |
+
|
| 201 |
if (this.serverKeysPresent.openai || this.keysConfigured) {
|
| 202 |
this.sendBtn.disabled = false;
|
|
|
|
|
|
|
|
|
|
| 203 |
}
|
| 204 |
};
|
| 205 |
|
|
|
|
| 703 |
const actionsDiv = lastFigure ? lastFigure.querySelector('.plot-actions') : null;
|
| 704 |
|
| 705 |
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 |
+
|
| 735 |
// Add button inline with Enlarge/Download/Show Code
|
| 736 |
const btn = document.createElement('button');
|
| 737 |
+
btn.className = 'code-btn arraylake-btn';
|
| 738 |
btn.title = 'Arraylake Code';
|
| 739 |
btn.textContent = 'π¦ Arraylake Code';
|
| 740 |
actionsDiv.appendChild(btn);
|
| 741 |
|
| 742 |
// Add code block to figure (same pattern as Show Code)
|
| 743 |
const codeDiv = document.createElement('div');
|
| 744 |
+
codeDiv.className = 'plot-code arraylake-code-block';
|
| 745 |
codeDiv.style.display = 'none';
|
| 746 |
+
codeDiv.setAttribute('data-raw-code', cleanCode);
|
| 747 |
|
| 748 |
const pre = document.createElement('pre');
|
| 749 |
const codeEl = document.createElement('code');
|
|
|
|
| 761 |
copyBtn.className = 'copy-snippet-btn';
|
| 762 |
copyBtn.textContent = 'Copy Code';
|
| 763 |
copyBtn.addEventListener('click', () => {
|
| 764 |
+
const rawCode = codeDiv.getAttribute('data-raw-code') || cleanCode;
|
| 765 |
+
navigator.clipboard.writeText(rawCode).then(() => {
|
| 766 |
copyBtn.textContent = 'β Copied!';
|
| 767 |
setTimeout(() => copyBtn.textContent = 'Copy Code', 2000);
|
| 768 |
});
|
|
|
|
| 781 |
}
|
| 782 |
});
|
| 783 |
} else {
|
| 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 |
+
|
| 801 |
const wrapper = document.createElement('div');
|
| 802 |
wrapper.className = 'arraylake-snippet-section';
|
| 803 |
wrapper.innerHTML = `
|
| 804 |
<div class="plot-actions">
|
| 805 |
+
<button class="code-btn arraylake-btn" title="Arraylake Code">π¦ Arraylake Code</button>
|
| 806 |
</div>
|
| 807 |
<div class="plot-code" style="display: none;">
|
| 808 |
<pre><code class="language-python hljs"></code></pre>
|
| 809 |
</div>
|
| 810 |
`;
|
| 811 |
+
const codeDivEl = wrapper.querySelector('.plot-code');
|
| 812 |
+
codeDivEl.setAttribute('data-raw-code', cleanCode);
|
| 813 |
const codeEl = wrapper.querySelector('code');
|
| 814 |
try {
|
| 815 |
codeEl.innerHTML = hljs.highlight(cleanCode, { language: 'python' }).value;
|