dmpantiu commited on
Commit
63c1568
·
verified ·
1 Parent(s): b31b816

Upload folder using huggingface_hub

Browse files
web/agent_wrapper.py CHANGED
@@ -37,12 +37,13 @@ class AgentSession:
37
  Manages a single agent session with streaming support.
38
  """
39
 
40
- def __init__(self):
41
  self._agent = None
42
  self._repl_tool: Optional[PythonREPLTool] = None
43
  self._messages: List[Dict] = []
44
  self._initialized = False
45
-
 
46
  # Global singleton keeps the dataset cache (shared across sessions)
47
  self._memory = get_memory()
48
  # Per-session conversation memory — never touches other sessions
@@ -57,10 +58,17 @@ class AgentSession:
57
  """Initialize the agent and tools."""
58
  logger.info("Initializing agent session...")
59
 
60
- if not os.environ.get("ARRAYLAKE_API_KEY"):
 
 
 
 
61
  logger.warning("ARRAYLAKE_API_KEY not found")
 
 
 
62
 
63
- if not os.environ.get("OPENAI_API_KEY"):
64
  logger.error("OPENAI_API_KEY not found")
65
  return
66
 
@@ -82,11 +90,12 @@ class AgentSession:
82
  # Replace the default REPL with our configured one
83
  tools = [t for t in tools if t.name != "python_repl"] + [self._repl_tool]
84
 
85
- # Initialize LLM
86
  logger.info("Connecting to LLM...")
87
  llm = ChatOpenAI(
88
  model=CONFIG.model_name,
89
- temperature=CONFIG.temperature
 
90
  )
91
 
92
  # Use session-local memory for datasets (NOT global!)
@@ -254,12 +263,12 @@ class AgentSession:
254
  _sessions: Dict[str, AgentSession] = {}
255
 
256
 
257
- def create_session(connection_id: str) -> AgentSession:
258
  """Create a new session for a connection."""
259
  if connection_id in _sessions:
260
  # Close existing session first
261
  _sessions[connection_id].close()
262
- session = AgentSession()
263
  _sessions[connection_id] = session
264
  logger.info(f"Created session for connection: {connection_id}")
265
  return session
 
37
  Manages a single agent session with streaming support.
38
  """
39
 
40
+ def __init__(self, api_keys: Optional[Dict[str, str]] = None):
41
  self._agent = None
42
  self._repl_tool: Optional[PythonREPLTool] = None
43
  self._messages: List[Dict] = []
44
  self._initialized = False
45
+ self._api_keys = api_keys or {}
46
+
47
  # Global singleton keeps the dataset cache (shared across sessions)
48
  self._memory = get_memory()
49
  # Per-session conversation memory — never touches other sessions
 
58
  """Initialize the agent and tools."""
59
  logger.info("Initializing agent session...")
60
 
61
+ # Resolve API keys: user-provided take priority over env vars
62
+ openai_key = self._api_keys.get("openai_api_key") or os.environ.get("OPENAI_API_KEY")
63
+ arraylake_key = self._api_keys.get("arraylake_api_key") or os.environ.get("ARRAYLAKE_API_KEY")
64
+
65
+ if not arraylake_key:
66
  logger.warning("ARRAYLAKE_API_KEY not found")
67
+ else:
68
+ # Set in env so retrieval tools can pick it up
69
+ os.environ["ARRAYLAKE_API_KEY"] = arraylake_key
70
 
71
+ if not openai_key:
72
  logger.error("OPENAI_API_KEY not found")
73
  return
74
 
 
90
  # Replace the default REPL with our configured one
91
  tools = [t for t in tools if t.name != "python_repl"] + [self._repl_tool]
92
 
93
+ # Initialize LLM with resolved key
94
  logger.info("Connecting to LLM...")
95
  llm = ChatOpenAI(
96
  model=CONFIG.model_name,
97
+ temperature=CONFIG.temperature,
98
+ api_key=openai_key,
99
  )
100
 
101
  # Use session-local memory for datasets (NOT global!)
 
263
  _sessions: Dict[str, AgentSession] = {}
264
 
265
 
266
+ def create_session(connection_id: str, api_keys: Optional[Dict[str, str]] = None) -> AgentSession:
267
  """Create a new session for a connection."""
268
  if connection_id in _sessions:
269
  # Close existing session first
270
  _sessions[connection_id].close()
271
+ session = AgentSession(api_keys=api_keys)
272
  _sessions[connection_id] = session
273
  logger.info(f"Created session for connection: {connection_id}")
274
  return session
web/routes/api.py CHANGED
@@ -51,6 +51,15 @@ class ConfigResponse(BaseModel):
51
  model: str
52
 
53
 
 
 
 
 
 
 
 
 
 
54
  @router.get("/health", response_model=HealthResponse)
55
  async def health_check():
56
  """Check if the server and agent are healthy."""
 
51
  model: str
52
 
53
 
54
+ @router.get("/keys-status")
55
+ async def keys_status():
56
+ """Check which API keys are configured via environment variables."""
57
+ return {
58
+ "openai": bool(os.environ.get("OPENAI_API_KEY")),
59
+ "arraylake": bool(os.environ.get("ARRAYLAKE_API_KEY")),
60
+ }
61
+
62
+
63
  @router.get("/health", response_model=HealthResponse)
64
  async def health_check():
65
  """Check if the server and agent are healthy."""
web/routes/websocket.py CHANGED
@@ -51,14 +51,32 @@ async def websocket_chat(websocket: WebSocket):
51
  logger.info(f"New connection: {connection_id}")
52
 
53
  try:
54
- # Create session for this connection
55
  from web.agent_wrapper import create_session, get_session, close_session
56
- session = create_session(connection_id)
57
-
58
  while True:
59
  data = await websocket.receive_json()
60
  message = data.get("message", "").strip()
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  if not message:
63
  continue
64
 
 
51
  logger.info(f"New connection: {connection_id}")
52
 
53
  try:
54
+ # Session created lazily after we receive API keys
55
  from web.agent_wrapper import create_session, get_session, close_session
56
+ session = None
57
+
58
  while True:
59
  data = await websocket.receive_json()
60
  message = data.get("message", "").strip()
61
 
62
+ # Handle API key configuration from client
63
+ if data.get("type") == "configure_keys":
64
+ api_keys = {
65
+ "openai_api_key": data.get("openai_api_key", ""),
66
+ "arraylake_api_key": data.get("arraylake_api_key", ""),
67
+ }
68
+ session = create_session(connection_id, api_keys=api_keys)
69
+ ready = session.is_ready()
70
+ await manager.send_json(websocket, {
71
+ "type": "keys_configured",
72
+ "ready": ready,
73
+ })
74
+ continue
75
+
76
+ # Create default session if not yet created (keys from env)
77
+ if session is None:
78
+ session = create_session(connection_id)
79
+
80
  if not message:
81
  continue
82
 
web/static/css/style.css CHANGED
@@ -766,4 +766,89 @@ dialog .close-modal:hover {
766
 
767
  [data-theme="light"] ::selection {
768
  background: rgba(26, 115, 232, 0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
769
  }
 
766
 
767
  [data-theme="light"] ::selection {
768
  background: rgba(26, 115, 232, 0.2);
769
+ }
770
+
771
+ /* ===== API KEYS PANEL ===== */
772
+ .api-keys-panel {
773
+ margin: 0 auto 16px;
774
+ max-width: 480px;
775
+ background: var(--bg-tertiary);
776
+ border: 1px solid var(--glass-border);
777
+ border-radius: 12px;
778
+ overflow: hidden;
779
+ }
780
+
781
+ .api-keys-header {
782
+ padding: 12px 16px;
783
+ font-weight: 600;
784
+ color: var(--text-primary);
785
+ background: var(--glass-bg);
786
+ border-bottom: 1px solid var(--glass-border);
787
+ }
788
+
789
+ .api-keys-body {
790
+ padding: 16px;
791
+ }
792
+
793
+ .api-keys-note {
794
+ font-size: 13px;
795
+ color: var(--text-secondary);
796
+ margin-bottom: 12px;
797
+ line-height: 1.4;
798
+ }
799
+
800
+ .api-key-field {
801
+ margin-bottom: 12px;
802
+ }
803
+
804
+ .api-key-field label {
805
+ display: block;
806
+ font-size: 13px;
807
+ font-weight: 500;
808
+ color: var(--text-secondary);
809
+ margin-bottom: 4px;
810
+ }
811
+
812
+ .api-key-field .required {
813
+ color: #ef4444;
814
+ }
815
+
816
+ .api-key-field input {
817
+ width: 100%;
818
+ padding: 8px 12px;
819
+ background: var(--bg-primary);
820
+ border: 1px solid var(--glass-border);
821
+ border-radius: 6px;
822
+ color: var(--text-primary);
823
+ font-family: monospace;
824
+ font-size: 13px;
825
+ box-sizing: border-box;
826
+ }
827
+
828
+ .api-key-field input:focus {
829
+ outline: none;
830
+ border-color: var(--accent-primary);
831
+ box-shadow: var(--focus-ring);
832
+ }
833
+
834
+ .save-keys-btn {
835
+ width: 100%;
836
+ padding: 10px;
837
+ background: var(--accent-primary);
838
+ color: #fff;
839
+ border: none;
840
+ border-radius: 6px;
841
+ font-size: 14px;
842
+ font-weight: 600;
843
+ cursor: pointer;
844
+ margin-top: 4px;
845
+ }
846
+
847
+ .save-keys-btn:hover {
848
+ opacity: 0.9;
849
+ }
850
+
851
+ .save-keys-btn:disabled {
852
+ opacity: 0.5;
853
+ cursor: not-allowed;
854
  }
web/static/js/chat.js CHANGED
@@ -8,6 +8,8 @@ class EurusChat {
8
  this.messageId = 0;
9
  this.currentAssistantMessage = null;
10
  this.isConnected = false;
 
 
11
  this.reconnectAttempts = 0;
12
  this.maxReconnectAttempts = 5;
13
  this.reconnectDelay = 1000;
@@ -20,6 +22,10 @@ class EurusChat {
20
  this.clearBtn = document.getElementById('clear-btn');
21
  this.cacheBtn = document.getElementById('cache-btn');
22
  this.cacheModal = document.getElementById('cache-modal');
 
 
 
 
23
 
24
  marked.setOptions({
25
  highlight: (code, lang) => {
@@ -37,10 +43,82 @@ class EurusChat {
37
  }
38
 
39
  init() {
 
40
  this.connect();
41
  this.setupEventListeners();
42
  this.setupImageModal();
43
  this.setupTheme();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
  setupTheme() {
@@ -83,7 +161,21 @@ class EurusChat {
83
  this.isConnected = true;
84
  this.reconnectAttempts = 0;
85
  this.updateConnectionStatus('connected');
86
- this.sendBtn.disabled = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  };
88
 
89
  this.ws.onclose = () => {
@@ -277,6 +369,18 @@ class EurusChat {
277
 
278
  handleMessage(data) {
279
  switch (data.type) {
 
 
 
 
 
 
 
 
 
 
 
 
280
  case 'thinking':
281
  this.showThinkingIndicator();
282
  break;
 
8
  this.messageId = 0;
9
  this.currentAssistantMessage = null;
10
  this.isConnected = false;
11
+ this.keysConfigured = false;
12
+ this.serverKeysPresent = { openai: false, arraylake: false };
13
  this.reconnectAttempts = 0;
14
  this.maxReconnectAttempts = 5;
15
  this.reconnectDelay = 1000;
 
22
  this.clearBtn = document.getElementById('clear-btn');
23
  this.cacheBtn = document.getElementById('cache-btn');
24
  this.cacheModal = document.getElementById('cache-modal');
25
+ this.apiKeysPanel = document.getElementById('api-keys-panel');
26
+ this.saveKeysBtn = document.getElementById('save-keys-btn');
27
+ this.openaiKeyInput = document.getElementById('openai-key');
28
+ this.arraylakeKeyInput = document.getElementById('arraylake-key');
29
 
30
  marked.setOptions({
31
  highlight: (code, lang) => {
 
43
  }
44
 
45
  init() {
46
+ this.checkKeysStatus();
47
  this.connect();
48
  this.setupEventListeners();
49
  this.setupImageModal();
50
  this.setupTheme();
51
+ this.setupKeysPanel();
52
+ }
53
+
54
+ async checkKeysStatus() {
55
+ try {
56
+ const resp = await fetch('/api/keys-status');
57
+ const data = await resp.json();
58
+ this.serverKeysPresent = data;
59
+
60
+ if (data.openai) {
61
+ // Keys pre-configured on server — hide the panel
62
+ this.apiKeysPanel.style.display = 'none';
63
+ this.keysConfigured = true;
64
+ } else {
65
+ // No server keys — check localStorage for saved keys
66
+ const savedOpenai = localStorage.getItem('eurus-openai-key');
67
+ const savedArraylake = localStorage.getItem('eurus-arraylake-key');
68
+ if (savedOpenai) {
69
+ this.openaiKeyInput.value = savedOpenai;
70
+ }
71
+ if (savedArraylake) {
72
+ this.arraylakeKeyInput.value = savedArraylake;
73
+ }
74
+ this.apiKeysPanel.style.display = 'block';
75
+ this.keysConfigured = false;
76
+ }
77
+ } catch (e) {
78
+ // Can't reach server yet, show panel
79
+ this.apiKeysPanel.style.display = 'block';
80
+ }
81
+ }
82
+
83
+ setupKeysPanel() {
84
+ this.saveKeysBtn.addEventListener('click', () => this.saveAndSendKeys());
85
+
86
+ // Allow Enter in key fields to submit
87
+ [this.openaiKeyInput, this.arraylakeKeyInput].forEach(input => {
88
+ input.addEventListener('keydown', (e) => {
89
+ if (e.key === 'Enter') {
90
+ e.preventDefault();
91
+ this.saveAndSendKeys();
92
+ }
93
+ });
94
+ });
95
+ }
96
+
97
+ saveAndSendKeys() {
98
+ const openaiKey = this.openaiKeyInput.value.trim();
99
+ const arraylakeKey = this.arraylakeKeyInput.value.trim();
100
+
101
+ if (!openaiKey) {
102
+ this.openaiKeyInput.focus();
103
+ return;
104
+ }
105
+
106
+ // Save to localStorage (client-side only)
107
+ localStorage.setItem('eurus-openai-key', openaiKey);
108
+ if (arraylakeKey) {
109
+ localStorage.setItem('eurus-arraylake-key', arraylakeKey);
110
+ }
111
+
112
+ // Send keys via WebSocket
113
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
114
+ this.saveKeysBtn.disabled = true;
115
+ this.saveKeysBtn.textContent = 'Connecting...';
116
+ this.ws.send(JSON.stringify({
117
+ type: 'configure_keys',
118
+ openai_api_key: openaiKey,
119
+ arraylake_api_key: arraylakeKey,
120
+ }));
121
+ }
122
  }
123
 
124
  setupTheme() {
 
161
  this.isConnected = true;
162
  this.reconnectAttempts = 0;
163
  this.updateConnectionStatus('connected');
164
+
165
+ // If server has no keys, auto-send saved keys from localStorage
166
+ if (!this.serverKeysPresent.openai) {
167
+ const savedOpenai = localStorage.getItem('eurus-openai-key');
168
+ if (savedOpenai) {
169
+ const savedArraylake = localStorage.getItem('eurus-arraylake-key') || '';
170
+ this.ws.send(JSON.stringify({
171
+ type: 'configure_keys',
172
+ openai_api_key: savedOpenai,
173
+ arraylake_api_key: savedArraylake,
174
+ }));
175
+ }
176
+ } else {
177
+ this.sendBtn.disabled = false;
178
+ }
179
  };
180
 
181
  this.ws.onclose = () => {
 
369
 
370
  handleMessage(data) {
371
  switch (data.type) {
372
+ case 'keys_configured':
373
+ this.keysConfigured = data.ready;
374
+ if (data.ready) {
375
+ this.apiKeysPanel.style.display = 'none';
376
+ this.sendBtn.disabled = false;
377
+ } else {
378
+ this.saveKeysBtn.disabled = false;
379
+ this.saveKeysBtn.textContent = 'Connect';
380
+ this.showError('Failed to initialize agent. Check your API keys.');
381
+ }
382
+ break;
383
+
384
  case 'thinking':
385
  this.showThinkingIndicator();
386
  break;
web/templates/index.html CHANGED
@@ -4,6 +4,25 @@
4
 
5
  {% block content %}
6
  <div class="chat-container">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  <div id="chat-messages" class="chat-messages">
8
  <div class="message system-message">
9
  <h3>Welcome to Eurus</h3>
 
4
 
5
  {% block content %}
6
  <div class="chat-container">
7
+ <!-- API Keys panel — hidden when keys are pre-configured via env -->
8
+ <div id="api-keys-panel" class="api-keys-panel" style="display: none;">
9
+ <div class="api-keys-header">
10
+ <span>API Keys Required</span>
11
+ </div>
12
+ <div class="api-keys-body">
13
+ <p class="api-keys-note">Enter your API keys to use Eurus. Keys are stored in your browser only and never saved on the server.</p>
14
+ <div class="api-key-field">
15
+ <label for="openai-key">OpenAI API Key <span class="required">*</span></label>
16
+ <input type="password" id="openai-key" placeholder="sk-..." autocomplete="off">
17
+ </div>
18
+ <div class="api-key-field">
19
+ <label for="arraylake-key">Arraylake API Key</label>
20
+ <input type="password" id="arraylake-key" placeholder="ema_..." autocomplete="off">
21
+ </div>
22
+ <button id="save-keys-btn" class="save-keys-btn">Connect</button>
23
+ </div>
24
+ </div>
25
+
26
  <div id="chat-messages" class="chat-messages">
27
  <div class="message system-message">
28
  <h3>Welcome to Eurus</h3>