dmpantiu commited on
Commit
7d5ca8f
Β·
verified Β·
1 Parent(s): 09f0030

Upload folder using huggingface_hub

Browse files
src/eurus/retrieval.py CHANGED
@@ -31,6 +31,45 @@ from eurus.memory import get_memory
31
  logger = logging.getLogger(__name__)
32
 
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  def _format_coord(value: float) -> str:
35
  """Format coordinates for stable, filename-safe identifiers."""
36
  if abs(value) < 0.005:
@@ -448,10 +487,18 @@ def retrieve_era5_data(
448
  # Size guard β€” prevent downloading datasets larger than the configured limit
449
  estimated_gb = ds_out.nbytes / (1024 ** 3)
450
  if estimated_gb > CONFIG.max_download_size_gb:
 
 
 
 
 
 
451
  return (
452
  f"Error: Estimated download size ({estimated_gb:.1f} GB) exceeds the "
453
  f"{CONFIG.max_download_size_gb} GB limit.\n"
454
- f"Try narrowing the time range or spatial area."
 
 
455
  )
456
  if estimated_gb > 1.0:
457
  logger.info(
@@ -527,6 +574,12 @@ def retrieve_era5_data(
527
  logger.info(f"Retrying in {wait_time:.1f}s...")
528
  time.sleep(wait_time)
529
  else:
 
 
 
 
 
 
530
  return (
531
  f"Error: Failed after {CONFIG.max_retries} attempts.\n"
532
  f"Last error: {error_msg}\n\n"
@@ -534,7 +587,9 @@ def retrieve_era5_data(
534
  f"1. Check your ARRAYLAKE_API_KEY\n"
535
  f"2. Verify internet connection\n"
536
  f"3. Try a smaller date range or region\n"
537
- f"4. Check if variable '{short_var}' is available"
 
 
538
  )
539
 
540
  return "Error: Unexpected failure in retrieval logic."
 
31
  logger = logging.getLogger(__name__)
32
 
33
 
34
+ def _arraylake_snippet(
35
+ variable: str,
36
+ query_type: str,
37
+ start_date: str,
38
+ end_date: str,
39
+ min_lat: float,
40
+ max_lat: float,
41
+ min_lon: float,
42
+ max_lon: float,
43
+ ) -> str:
44
+ """Generate a ready-to-paste Python snippet for direct Arraylake access."""
45
+ # Convert negative lons to 0-360 for ERA5
46
+ era5_min = min_lon % 360 if min_lon < 0 else min_lon
47
+ era5_max = max_lon % 360 if max_lon < 0 else max_lon
48
+ return (
49
+ f"\nπŸ“¦ Reproduce this download yourself (copy-paste into Jupyter):\n"
50
+ f"```python\n"
51
+ f"import os, xarray as xr\n"
52
+ f"from arraylake import Client\n"
53
+ f"\n"
54
+ f"client = Client(token=os.environ['ARRAYLAKE_API_KEY'])\n"
55
+ f"repo = client.get_repo('{CONFIG.data_source}')\n"
56
+ f"session = repo.readonly_session('main')\n"
57
+ f"\n"
58
+ f"ds = xr.open_dataset(session.store, engine='zarr',\n"
59
+ f" consolidated=False, zarr_format=3,\n"
60
+ f" chunks=None, group='{query_type}')\n"
61
+ f"\n"
62
+ f"subset = ds['{variable}'].sel(\n"
63
+ f" time=slice('{start_date}', '{end_date}'),\n"
64
+ f" latitude=slice({max_lat}, {min_lat}), # ERA5: descending\n"
65
+ f" longitude=slice({era5_min}, {era5_max}),\n"
66
+ f")\n"
67
+ f"\n"
68
+ f"subset.load().to_dataset(name='{variable}').to_zarr('my_data.zarr', mode='w')\n"
69
+ f"```"
70
+ )
71
+
72
+
73
  def _format_coord(value: float) -> str:
74
  """Format coordinates for stable, filename-safe identifiers."""
75
  if abs(value) < 0.005:
 
487
  # Size guard β€” prevent downloading datasets larger than the configured limit
488
  estimated_gb = ds_out.nbytes / (1024 ** 3)
489
  if estimated_gb > CONFIG.max_download_size_gb:
490
+ snippet = _arraylake_snippet(
491
+ short_var, query_type, start_date, end_date,
492
+ min_latitude, max_latitude,
493
+ min_longitude if min_longitude >= 0 else min_longitude % 360,
494
+ max_longitude if max_longitude >= 0 else max_longitude % 360,
495
+ )
496
  return (
497
  f"Error: Estimated download size ({estimated_gb:.1f} GB) exceeds the "
498
  f"{CONFIG.max_download_size_gb} GB limit.\n"
499
+ f"Try narrowing the time range or spatial area.\n\n"
500
+ f"Alternatively, fetch it yourself with this snippet:\n\n"
501
+ f"{snippet}"
502
  )
503
  if estimated_gb > 1.0:
504
  logger.info(
 
574
  logger.info(f"Retrying in {wait_time:.1f}s...")
575
  time.sleep(wait_time)
576
  else:
577
+ snippet = _arraylake_snippet(
578
+ short_var, query_type, start_date, end_date,
579
+ min_latitude, max_latitude,
580
+ min_longitude if min_longitude >= 0 else min_longitude % 360,
581
+ max_longitude if max_longitude >= 0 else max_longitude % 360,
582
+ )
583
  return (
584
  f"Error: Failed after {CONFIG.max_retries} attempts.\n"
585
  f"Last error: {error_msg}\n\n"
 
587
  f"1. Check your ARRAYLAKE_API_KEY\n"
588
  f"2. Verify internet connection\n"
589
  f"3. Try a smaller date range or region\n"
590
+ f"4. Check if variable '{short_var}' is available\n\n"
591
+ f"Manual retrieval snippet:\n\n"
592
+ f"{snippet}"
593
  )
594
 
595
  return "Error: Unexpected failure in retrieval logic."
web/agent_wrapper.py CHANGED
@@ -25,6 +25,7 @@ from langchain.agents import create_agent
25
 
26
  # IMPORT FROM EURUS PACKAGE - SINGLE SOURCE OF TRUTH
27
  from eurus.config import CONFIG, AGENT_SYSTEM_PROMPT
 
28
  from eurus.memory import get_memory, SmartConversationMemory # Singleton for datasets, per-session for chat
29
  from eurus.tools import get_all_tools
30
  from eurus.tools.repl import PythonREPLTool
@@ -181,12 +182,14 @@ class AgentSession:
181
  lambda: self._agent.invoke({"messages": self._messages}, config=config)
182
  )
183
 
184
- # Update messages
 
185
  self._messages = result["messages"]
 
186
 
187
- # Parse messages to show tool calls made
188
  tool_calls_made = []
189
- for msg in self._messages:
190
  if hasattr(msg, 'tool_calls') and msg.tool_calls:
191
  for tc in msg.tool_calls:
192
  tool_name = tc.get('name', 'unknown')
@@ -198,6 +201,24 @@ class AgentSession:
198
  await stream_callback("status", f"πŸ› οΈ Used tools: {tools_str}")
199
  await asyncio.sleep(0.5)
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  # Extract response
202
  last_message = self._messages[-1]
203
 
@@ -244,6 +265,10 @@ class AgentSession:
244
  # Default to plot (png, jpg, etc.)
245
  await stream_callback("plot", "", data=base64_data, path=filepath, code=code)
246
 
 
 
 
 
247
  # Save to memory
248
  self._conversation.add_message("assistant", response_text)
249
 
 
25
 
26
  # IMPORT FROM EURUS PACKAGE - SINGLE SOURCE OF TRUTH
27
  from eurus.config import CONFIG, AGENT_SYSTEM_PROMPT
28
+ from eurus.retrieval import _arraylake_snippet
29
  from eurus.memory import get_memory, SmartConversationMemory # Singleton for datasets, per-session for chat
30
  from eurus.tools import get_all_tools
31
  from eurus.tools.repl import PythonREPLTool
 
182
  lambda: self._agent.invoke({"messages": self._messages}, config=config)
183
  )
184
 
185
+ # Only scan NEW messages from this turn
186
+ prev_count = len(self._messages)
187
  self._messages = result["messages"]
188
+ new_messages = self._messages[prev_count:]
189
 
190
+ # Parse NEW messages to show tool calls made
191
  tool_calls_made = []
192
+ for msg in new_messages:
193
  if hasattr(msg, 'tool_calls') and msg.tool_calls:
194
  for tc in msg.tool_calls:
195
  tool_name = tc.get('name', 'unknown')
 
201
  await stream_callback("status", f"πŸ› οΈ Used tools: {tools_str}")
202
  await asyncio.sleep(0.5)
203
 
204
+ # Collect Arraylake snippet from NEW messages only
205
+ arraylake_snippets = []
206
+ for msg in new_messages:
207
+ if hasattr(msg, 'tool_calls') and msg.tool_calls:
208
+ for tc in msg.tool_calls:
209
+ if tc.get('name') == 'retrieve_era5_data':
210
+ args = tc.get('args', {})
211
+ arraylake_snippets.append(_arraylake_snippet(
212
+ variable=args.get('variable_id', 'sst'),
213
+ query_type=args.get('query_type', 'spatial'),
214
+ start_date=args.get('start_date', ''),
215
+ end_date=args.get('end_date', ''),
216
+ min_lat=args.get('min_latitude', -90),
217
+ max_lat=args.get('max_latitude', 90),
218
+ min_lon=args.get('min_longitude', 0),
219
+ max_lon=args.get('max_longitude', 360),
220
+ ))
221
+
222
  # Extract response
223
  last_message = self._messages[-1]
224
 
 
265
  # Default to plot (png, jpg, etc.)
266
  await stream_callback("plot", "", data=base64_data, path=filepath, code=code)
267
 
268
+ # Send Arraylake snippets AFTER response + plots exist in DOM
269
+ for snippet in arraylake_snippets:
270
+ await stream_callback("arraylake_snippet", snippet)
271
+
272
  # Save to memory
273
  self._conversation.add_message("assistant", response_text)
274
 
web/static/css/style.css CHANGED
@@ -880,4 +880,72 @@ dialog .close-modal:hover {
880
  .save-keys-btn:disabled {
881
  opacity: 0.5;
882
  cursor: not-allowed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
883
  }
 
880
  .save-keys-btn:disabled {
881
  opacity: 0.5;
882
  cursor: not-allowed;
883
+ }
884
+
885
+ /* ===== ARRAYLAKE SNIPPET (in-message) ===== */
886
+ .arraylake-snippet-section {
887
+ margin-top: 0.75rem;
888
+ }
889
+
890
+ .arraylake-actions {
891
+ display: flex;
892
+ gap: 0.5rem;
893
+ }
894
+
895
+ .arraylake-btn {
896
+ padding: 0.5rem 0.875rem;
897
+ font-size: 0.75rem;
898
+ font-weight: 500;
899
+ border: 1px solid var(--glass-border);
900
+ border-radius: 0.375rem;
901
+ background: var(--bg-tertiary);
902
+ color: var(--text-secondary);
903
+ cursor: pointer;
904
+ transition: all 0.15s ease;
905
+ }
906
+
907
+ .arraylake-btn:hover {
908
+ border-color: var(--accent-primary);
909
+ color: var(--accent-primary);
910
+ }
911
+
912
+ .arraylake-code {
913
+ margin-top: 0.5rem;
914
+ border-radius: 0.5rem;
915
+ overflow: hidden;
916
+ border: 1px solid var(--glass-border);
917
+ }
918
+
919
+ .arraylake-code pre {
920
+ margin: 0;
921
+ padding: 0.875rem;
922
+ background: var(--code-bg);
923
+ overflow-x: auto;
924
+ font-size: 0.8125rem;
925
+ line-height: 1.5;
926
+ }
927
+
928
+ .arraylake-code code {
929
+ font-family: 'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace;
930
+ }
931
+
932
+ .copy-snippet-btn {
933
+ display: block;
934
+ margin: 0;
935
+ padding: 0.5rem 0.875rem;
936
+ width: 100%;
937
+ font-size: 0.75rem;
938
+ font-weight: 500;
939
+ border: none;
940
+ border-top: 1px solid var(--glass-border);
941
+ background: var(--bg-tertiary);
942
+ color: var(--text-secondary);
943
+ cursor: pointer;
944
+ transition: all 0.15s ease;
945
+ text-align: center;
946
+ }
947
+
948
+ .copy-snippet-btn:hover {
949
+ color: var(--accent-primary);
950
+ background: var(--bg-secondary);
951
  }
web/static/js/chat.js CHANGED
@@ -430,6 +430,10 @@ class EurusChat {
430
  this.sendBtn.disabled = false;
431
  break;
432
 
 
 
 
 
433
  case 'error':
434
  this.showError(data.content);
435
  this.sendBtn.disabled = false;
@@ -677,6 +681,105 @@ class EurusChat {
677
  this.currentAssistantMessage = null;
678
  }
679
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
  showError(message) {
681
  this.removeThinkingIndicator();
682
 
 
430
  this.sendBtn.disabled = false;
431
  break;
432
 
433
+ case 'arraylake_snippet':
434
+ this.addArraylakeSnippet(data.content);
435
+ break;
436
+
437
  case 'error':
438
  this.showError(data.content);
439
  this.sendBtn.disabled = false;
 
681
  this.currentAssistantMessage = null;
682
  }
683
 
684
+ addArraylakeSnippet(snippetText) {
685
+ // Find the latest assistant message
686
+ const messages = this.messagesContainer.querySelectorAll('.assistant-message');
687
+ const targetMsg = messages.length > 0 ? messages[messages.length - 1] : null;
688
+ if (!targetMsg) return;
689
+
690
+ // Strip markdown fences for raw code display
691
+ let cleanCode = snippetText
692
+ .replace(/^\n?πŸ“¦[^\n]*\n/, '')
693
+ .replace(/^```python\n?/, '')
694
+ .replace(/\n?```$/, '')
695
+ .trim();
696
+
697
+ // Find the last plot figure's action bar to add button inline
698
+ const figures = targetMsg.querySelectorAll('.plot-figure');
699
+ const lastFigure = figures.length > 0 ? figures[figures.length - 1] : null;
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');
717
+ codeEl.className = 'language-python hljs';
718
+ try {
719
+ codeEl.innerHTML = hljs.highlight(cleanCode, { language: 'python' }).value;
720
+ } catch (e) {
721
+ codeEl.textContent = cleanCode;
722
+ }
723
+ pre.appendChild(codeEl);
724
+ codeDiv.appendChild(pre);
725
+
726
+ // Copy button inside code block
727
+ const copyBtn = document.createElement('button');
728
+ copyBtn.className = 'copy-snippet-btn';
729
+ copyBtn.textContent = 'Copy Code';
730
+ copyBtn.addEventListener('click', () => {
731
+ navigator.clipboard.writeText(cleanCode).then(() => {
732
+ copyBtn.textContent = 'βœ“ Copied!';
733
+ setTimeout(() => copyBtn.textContent = 'Copy Code', 2000);
734
+ });
735
+ });
736
+ codeDiv.appendChild(copyBtn);
737
+ lastFigure.appendChild(codeDiv);
738
+
739
+ // Toggle
740
+ btn.addEventListener('click', () => {
741
+ if (codeDiv.style.display === 'none') {
742
+ codeDiv.style.display = 'block';
743
+ btn.textContent = 'πŸ“¦ Hide Arraylake';
744
+ } else {
745
+ codeDiv.style.display = 'none';
746
+ btn.textContent = 'πŸ“¦ Arraylake Code';
747
+ }
748
+ });
749
+ } else {
750
+ // No plot figure β€” add as standalone section under the message
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;
764
+ } catch (e) {
765
+ codeEl.textContent = cleanCode;
766
+ }
767
+ const btn = wrapper.querySelector('.code-btn');
768
+ const codeDiv = wrapper.querySelector('.plot-code');
769
+ btn.addEventListener('click', () => {
770
+ if (codeDiv.style.display === 'none') {
771
+ codeDiv.style.display = 'block';
772
+ btn.textContent = 'πŸ“¦ Hide Arraylake';
773
+ } else {
774
+ codeDiv.style.display = 'none';
775
+ btn.textContent = 'πŸ“¦ Arraylake Code';
776
+ }
777
+ });
778
+ targetMsg.appendChild(wrapper);
779
+ }
780
+ this.scrollToBottom();
781
+ }
782
+
783
  showError(message) {
784
  this.removeThinkingIndicator();
785
 
web/templates/index.html CHANGED
@@ -60,5 +60,5 @@
60
  {% endblock %}
61
 
62
  {% block scripts %}
63
- <script src="/static/js/chat.js?v=20260218c"></script>
64
  {% endblock %}
 
60
  {% endblock %}
61
 
62
  {% block scripts %}
63
+ <script src="/static/js/chat.js?v=20260219b"></script>
64
  {% endblock %}