npcsh 1.1.17__py3-none-any.whl → 1.1.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- npcsh/_state.py +114 -91
- npcsh/alicanto.py +2 -2
- npcsh/benchmark/__init__.py +8 -2
- npcsh/benchmark/npcsh_agent.py +46 -12
- npcsh/benchmark/runner.py +85 -43
- npcsh/benchmark/templates/install-npcsh.sh.j2 +35 -0
- npcsh/build.py +2 -4
- npcsh/completion.py +2 -6
- npcsh/config.py +1 -3
- npcsh/conversation_viewer.py +389 -0
- npcsh/corca.py +0 -1
- npcsh/execution.py +0 -1
- npcsh/guac.py +0 -1
- npcsh/mcp_helpers.py +2 -3
- npcsh/mcp_server.py +5 -10
- npcsh/npc.py +10 -11
- npcsh/npc_team/jinxs/bin/benchmark.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +321 -17
- npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +312 -67
- npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +366 -44
- npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
- npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +328 -20
- npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +242 -10
- npcsh/npc_team/jinxs/lib/core/sleep.jinx +22 -11
- npcsh/npc_team/jinxs/lib/core/sql.jinx +10 -6
- npcsh/npc_team/jinxs/lib/research/paper_search.jinx +387 -76
- npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +372 -55
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +299 -144
- npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
- npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
- npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
- npcsh/npc_team/jinxs/modes/guac.jinx +544 -0
- npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
- npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
- npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
- npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
- npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
- npcsh/npc_team/jinxs/{bin → modes}/yap.jinx +13 -7
- npcsh/npcsh.py +7 -4
- npcsh/plonk.py +0 -1
- npcsh/pti.py +0 -1
- npcsh/routes.py +1 -3
- npcsh/spool.py +0 -1
- npcsh/ui.py +0 -1
- npcsh/wander.py +0 -1
- npcsh/yap.py +0 -1
- npcsh-1.1.18.data/data/npcsh/npc_team/alicanto.jinx +356 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/arxiv.jinx +720 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
- npcsh-1.1.18.data/data/npcsh/npc_team/corca.jinx +430 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/db_search.jinx +348 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/file_search.jinx +339 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/guac.jinx +544 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +331 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/kg_search.jinx +418 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/mem_review.jinx +73 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/mem_search.jinx +388 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/paper_search.jinx +412 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/plonk.jinx +379 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/pti.jinx +357 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/reattach.jinx +291 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sleep.jinx +22 -11
- npcsh-1.1.18.data/data/npcsh/npc_team/spool.jinx +350 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/sql.jinx +20 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/wander.jinx +455 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/web_search.jinx +283 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.jinx +13 -7
- {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/METADATA +90 -1
- npcsh-1.1.18.dist-info/RECORD +235 -0
- {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/WHEEL +1 -1
- {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/entry_points.txt +0 -3
- npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
- npcsh/npc_team/jinxs/bin/wander.jinx +0 -242
- npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
- npcsh-1.1.17.data/data/npcsh/npc_team/arxiv.jinx +0 -76
- npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +0 -44
- npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +0 -94
- npcsh-1.1.17.data/data/npcsh/npc_team/jinxs.jinx +0 -176
- npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +0 -96
- npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +0 -80
- npcsh-1.1.17.data/data/npcsh/npc_team/paper_search.jinx +0 -101
- npcsh-1.1.17.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
- npcsh-1.1.17.data/data/npcsh/npc_team/spool.jinx +0 -161
- npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +0 -16
- npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +0 -242
- npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +0 -51
- npcsh-1.1.17.dist-info/RECORD +0 -219
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/build.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/confirm.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/convene.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/delegate.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/navigate.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/notify.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/nql.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/search.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/send_message.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sh.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sync.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/write_file.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
- {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
jinx_name: guac
|
|
2
|
+
description: Interactive Python data analysis TUI - live variable inspector, DataFrame viewer, code execution
|
|
3
|
+
inputs:
|
|
4
|
+
- model: null
|
|
5
|
+
- provider: null
|
|
6
|
+
- plots_dir: null
|
|
7
|
+
|
|
8
|
+
steps:
|
|
9
|
+
- name: guac_tui
|
|
10
|
+
engine: python
|
|
11
|
+
code: |
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import io
|
|
15
|
+
import re
|
|
16
|
+
import tty
|
|
17
|
+
import termios
|
|
18
|
+
import traceback
|
|
19
|
+
import textwrap
|
|
20
|
+
import select
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from termcolor import colored
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
import pandas as pd
|
|
27
|
+
import matplotlib
|
|
28
|
+
matplotlib.use('Agg') # Non-interactive backend for TUI
|
|
29
|
+
import matplotlib.pyplot as plt
|
|
30
|
+
|
|
31
|
+
from npcpy.llm_funcs import get_llm_response
|
|
32
|
+
from npcpy.npc_sysenv import render_markdown, get_system_message
|
|
33
|
+
|
|
34
|
+
npc = context.get('npc')
|
|
35
|
+
team = context.get('team')
|
|
36
|
+
messages = context.get('messages', [])
|
|
37
|
+
plots_dir = context.get('plots_dir') or os.path.expanduser("~/.npcsh/plots")
|
|
38
|
+
|
|
39
|
+
# Resolve npc if it's a string (npc name) rather than NPC object
|
|
40
|
+
if isinstance(npc, str) and team:
|
|
41
|
+
npc = team.get(npc) if hasattr(team, 'get') else None
|
|
42
|
+
elif isinstance(npc, str):
|
|
43
|
+
npc = None
|
|
44
|
+
|
|
45
|
+
model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') else None)
|
|
46
|
+
provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') else None)
|
|
47
|
+
|
|
48
|
+
os.makedirs(plots_dir, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
# ========== State ==========
|
|
51
|
+
class GuacState:
|
|
52
|
+
def __init__(self):
|
|
53
|
+
self.locals = {
|
|
54
|
+
'np': np, 'pd': pd, 'plt': plt,
|
|
55
|
+
'Path': Path, 'os': os
|
|
56
|
+
}
|
|
57
|
+
self.history = [] # Code history
|
|
58
|
+
self.output_lines = [] # Current output
|
|
59
|
+
self.plots = [] # Saved plot paths
|
|
60
|
+
self.current_input = ""
|
|
61
|
+
self.cursor_pos = 0
|
|
62
|
+
self.history_idx = -1
|
|
63
|
+
self.scroll_offset = 0
|
|
64
|
+
self.panel = 0 # 0=output, 1=variables, 2=dataframes, 3=plots
|
|
65
|
+
self.selected_var = 0
|
|
66
|
+
self.var_scroll = 0
|
|
67
|
+
self.status = "Ready"
|
|
68
|
+
self.mode = "code" # code, natural, inspect
|
|
69
|
+
self.inspecting = None # Variable being inspected
|
|
70
|
+
self.df_row_offset = 0
|
|
71
|
+
self.df_col_offset = 0
|
|
72
|
+
|
|
73
|
+
state = GuacState()
|
|
74
|
+
|
|
75
|
+
# ========== Helpers ==========
|
|
76
|
+
def get_size():
|
|
77
|
+
try:
|
|
78
|
+
s = os.get_terminal_size()
|
|
79
|
+
return s.columns, s.lines
|
|
80
|
+
except:
|
|
81
|
+
return 80, 24
|
|
82
|
+
|
|
83
|
+
def wrap_text(text, width):
|
|
84
|
+
lines = []
|
|
85
|
+
for line in str(text).split('\n'):
|
|
86
|
+
if len(line) <= width:
|
|
87
|
+
lines.append(line)
|
|
88
|
+
else:
|
|
89
|
+
lines.extend(textwrap.wrap(line, width) or [''])
|
|
90
|
+
return lines
|
|
91
|
+
|
|
92
|
+
def get_user_vars():
|
|
93
|
+
"""Get user-defined variables (not builtins)"""
|
|
94
|
+
skip = {'np', 'pd', 'plt', 'Path', 'os', '__builtins__'}
|
|
95
|
+
return {k: v for k, v in state.locals.items()
|
|
96
|
+
if not k.startswith('_') and k not in skip}
|
|
97
|
+
|
|
98
|
+
def var_info(name, value):
|
|
99
|
+
"""Get info string for a variable"""
|
|
100
|
+
if isinstance(value, pd.DataFrame):
|
|
101
|
+
return f"DataFrame {value.shape}"
|
|
102
|
+
elif isinstance(value, pd.Series):
|
|
103
|
+
return f"Series ({len(value)})"
|
|
104
|
+
elif isinstance(value, np.ndarray):
|
|
105
|
+
return f"ndarray {value.shape} {value.dtype}"
|
|
106
|
+
elif isinstance(value, (list, tuple)):
|
|
107
|
+
return f"{type(value).__name__} ({len(value)})"
|
|
108
|
+
elif isinstance(value, dict):
|
|
109
|
+
return f"dict ({len(value)} keys)"
|
|
110
|
+
elif isinstance(value, str):
|
|
111
|
+
return f"str ({len(value)} chars)"
|
|
112
|
+
elif isinstance(value, (int, float)):
|
|
113
|
+
return f"{type(value).__name__}: {value}"
|
|
114
|
+
else:
|
|
115
|
+
return type(value).__name__
|
|
116
|
+
|
|
117
|
+
def execute_code(code):
|
|
118
|
+
"""Execute Python code and capture output"""
|
|
119
|
+
output = io.StringIO()
|
|
120
|
+
old_stdout, old_stderr = sys.stdout, sys.stderr
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
sys.stdout = output
|
|
124
|
+
sys.stderr = output
|
|
125
|
+
|
|
126
|
+
# Try as expression first
|
|
127
|
+
if '\n' not in code.strip():
|
|
128
|
+
try:
|
|
129
|
+
result = eval(compile(code, "<input>", "eval"), state.locals)
|
|
130
|
+
if result is not None:
|
|
131
|
+
print(repr(result))
|
|
132
|
+
return output.getvalue(), None
|
|
133
|
+
except SyntaxError:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
# Execute as statements
|
|
137
|
+
exec(compile(code, "<input>", "exec"), state.locals)
|
|
138
|
+
return output.getvalue(), None
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
return output.getvalue(), traceback.format_exc()
|
|
142
|
+
finally:
|
|
143
|
+
sys.stdout, sys.stderr = old_stdout, old_stderr
|
|
144
|
+
|
|
145
|
+
def save_plot():
|
|
146
|
+
"""Save current matplotlib figure"""
|
|
147
|
+
if plt.get_fignums():
|
|
148
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
149
|
+
path = os.path.join(plots_dir, f"plot_{timestamp}.png")
|
|
150
|
+
plt.savefig(path, dpi=150, bbox_inches='tight')
|
|
151
|
+
plt.close()
|
|
152
|
+
state.plots.append(path)
|
|
153
|
+
return path
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def load_file(path):
|
|
157
|
+
"""Auto-load file based on extension"""
|
|
158
|
+
path = Path(path).expanduser()
|
|
159
|
+
if not path.exists():
|
|
160
|
+
return None, f"File not found: {path}"
|
|
161
|
+
|
|
162
|
+
ext = path.suffix.lower()
|
|
163
|
+
var_name = path.stem.replace(' ', '_').replace('-', '_')[:20]
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
if ext == '.csv':
|
|
167
|
+
df = pd.read_csv(path)
|
|
168
|
+
state.locals[var_name] = df
|
|
169
|
+
return var_name, f"Loaded CSV as '{var_name}': {df.shape}"
|
|
170
|
+
elif ext in ['.xlsx', '.xls']:
|
|
171
|
+
df = pd.read_excel(path)
|
|
172
|
+
state.locals[var_name] = df
|
|
173
|
+
return var_name, f"Loaded Excel as '{var_name}': {df.shape}"
|
|
174
|
+
elif ext == '.json':
|
|
175
|
+
import json
|
|
176
|
+
with open(path) as f:
|
|
177
|
+
data = json.load(f)
|
|
178
|
+
state.locals[var_name] = data
|
|
179
|
+
return var_name, f"Loaded JSON as '{var_name}'"
|
|
180
|
+
elif ext == '.npy':
|
|
181
|
+
arr = np.load(path)
|
|
182
|
+
state.locals[var_name] = arr
|
|
183
|
+
return var_name, f"Loaded numpy as '{var_name}': {arr.shape}"
|
|
184
|
+
elif ext in ['.txt', '.md']:
|
|
185
|
+
with open(path) as f:
|
|
186
|
+
text = f.read()
|
|
187
|
+
state.locals[var_name] = text
|
|
188
|
+
return var_name, f"Loaded text as '{var_name}': {len(text)} chars"
|
|
189
|
+
else:
|
|
190
|
+
with open(path, 'rb') as f:
|
|
191
|
+
data = f.read()
|
|
192
|
+
state.locals[var_name] = data
|
|
193
|
+
return var_name, f"Loaded binary as '{var_name}': {len(data)} bytes"
|
|
194
|
+
except Exception as e:
|
|
195
|
+
return None, f"Error: {e}"
|
|
196
|
+
|
|
197
|
+
# ========== Rendering ==========
|
|
198
|
+
def render_screen():
|
|
199
|
+
width, height = get_size()
|
|
200
|
+
out = []
|
|
201
|
+
|
|
202
|
+
out.append("\033[2J\033[H")
|
|
203
|
+
|
|
204
|
+
# ===== HEADER =====
|
|
205
|
+
mode_colors = {"code": "\033[32m", "natural": "\033[35m", "inspect": "\033[33m"}
|
|
206
|
+
mode_str = f"{mode_colors[state.mode]}[{state.mode}]\033[0m"
|
|
207
|
+
header = f" GUAC - Python Data Analysis {mode_str} "
|
|
208
|
+
status_color = "\033[33m" if "..." in state.status else "\033[32m"
|
|
209
|
+
out.append(f"\033[1;1H\033[42;30;1m{header.ljust(width)}\033[0m")
|
|
210
|
+
out.append(f"\033[1;{width-len(state.status)-3}H{status_color}[{state.status}]\033[0m")
|
|
211
|
+
|
|
212
|
+
# ===== LAYOUT =====
|
|
213
|
+
# Left: Output (80%)
|
|
214
|
+
# Right: Variables/DataFrames/Plots (20%)
|
|
215
|
+
left_w = int(width * 0.8)
|
|
216
|
+
right_w = width - left_w
|
|
217
|
+
panel_h = height - 6 # Leave room for input
|
|
218
|
+
|
|
219
|
+
user_vars = get_user_vars()
|
|
220
|
+
var_names = list(user_vars.keys())
|
|
221
|
+
|
|
222
|
+
# ===== LEFT PANEL: Output =====
|
|
223
|
+
out.append(f"\033[3;1H\033[32m Output \033[90m{'─' * (left_w-9)}┬\033[0m")
|
|
224
|
+
for i in range(panel_h - 1):
|
|
225
|
+
out.append(f"\033[{4+i};{left_w}H\033[90m│\033[0m")
|
|
226
|
+
out.append(f"\033[{3+panel_h};1H\033[90m{'─' * (left_w-1)}┴\033[0m")
|
|
227
|
+
|
|
228
|
+
output_w = left_w - 3
|
|
229
|
+
output_h = panel_h - 2
|
|
230
|
+
|
|
231
|
+
if state.inspecting and state.inspecting in user_vars:
|
|
232
|
+
# Inspection mode - show in output panel
|
|
233
|
+
val = user_vars[state.inspecting]
|
|
234
|
+
out.append(f"\033[4;2H\033[33mInspecting: {state.inspecting}\033[0m")
|
|
235
|
+
|
|
236
|
+
if isinstance(val, pd.DataFrame):
|
|
237
|
+
cols = list(val.columns)[state.df_col_offset:state.df_col_offset + 5]
|
|
238
|
+
col_w = (output_w - 6) // max(len(cols), 1)
|
|
239
|
+
header = " " + "".join(f"{str(c)[:col_w]:<{col_w}}" for c in cols)
|
|
240
|
+
out.append(f"\033[5;2H\033[1m{header[:output_w]}\033[0m")
|
|
241
|
+
for i, (idx, row) in enumerate(val.iloc[state.df_row_offset:state.df_row_offset + output_h - 3].iterrows()):
|
|
242
|
+
row_str = f"{idx:<4} " + "".join(f"{str(row[c])[:col_w]:<{col_w}}" for c in cols)
|
|
243
|
+
out.append(f"\033[{6+i};2H{row_str[:output_w]}")
|
|
244
|
+
out.append(f"\033[{3+panel_h-1};2H\033[90mArrows:scroll ESC:exit inspect\033[0m")
|
|
245
|
+
elif isinstance(val, np.ndarray):
|
|
246
|
+
out.append(f"\033[5;2H\033[90mShape: {val.shape} dtype: {val.dtype}\033[0m")
|
|
247
|
+
for i, line in enumerate(wrap_text(repr(val), output_w)[:output_h-2]):
|
|
248
|
+
out.append(f"\033[{6+i};2H{line}")
|
|
249
|
+
else:
|
|
250
|
+
for i, line in enumerate(wrap_text(repr(val), output_w)[:output_h]):
|
|
251
|
+
out.append(f"\033[{5+i};2H{line}")
|
|
252
|
+
else:
|
|
253
|
+
# Normal output
|
|
254
|
+
visible = state.output_lines[state.scroll_offset:state.scroll_offset + output_h]
|
|
255
|
+
for i, line in enumerate(visible):
|
|
256
|
+
if 'Error' in line or 'Traceback' in line:
|
|
257
|
+
out.append(f"\033[{4+i};2H\033[31m{line[:output_w]}\033[0m")
|
|
258
|
+
elif line.startswith('>>>'):
|
|
259
|
+
out.append(f"\033[{4+i};2H\033[32m{line[:output_w]}\033[0m")
|
|
260
|
+
else:
|
|
261
|
+
out.append(f"\033[{4+i};2H{line[:output_w]}")
|
|
262
|
+
|
|
263
|
+
if not state.output_lines:
|
|
264
|
+
hints = [
|
|
265
|
+
"\033[90mWelcome to GUAC!\033[0m",
|
|
266
|
+
"", "Type Python code and press Enter",
|
|
267
|
+
"Drop file paths to auto-load", "",
|
|
268
|
+
"\033[33mKeys:\033[0m Tab:panels Arrows:nav",
|
|
269
|
+
"Ctrl+N:NL Ctrl+S:save Ctrl+Q:quit",
|
|
270
|
+
]
|
|
271
|
+
for i, hint in enumerate(hints[:output_h]):
|
|
272
|
+
out.append(f"\033[{4+i};2H{hint}")
|
|
273
|
+
|
|
274
|
+
# ===== RIGHT PANEL: Variables/Plots =====
|
|
275
|
+
right_x = left_w + 1
|
|
276
|
+
panel_names = ["Vars", "DFs", "Plots"]
|
|
277
|
+
tabs = ""
|
|
278
|
+
for i, name in enumerate(panel_names):
|
|
279
|
+
if i == state.panel:
|
|
280
|
+
tabs += f"\033[47;30m {name} \033[0m"
|
|
281
|
+
else:
|
|
282
|
+
tabs += f"\033[90m {name} \033[0m"
|
|
283
|
+
out.append(f"\033[3;{right_x}H{tabs}")
|
|
284
|
+
|
|
285
|
+
# Right panel content
|
|
286
|
+
rpanel_w = right_w - 2
|
|
287
|
+
rpanel_h = panel_h - 2
|
|
288
|
+
|
|
289
|
+
if state.panel == 0: # Variables
|
|
290
|
+
for i, name in enumerate(var_names[state.var_scroll:state.var_scroll + rpanel_h]):
|
|
291
|
+
idx = i + state.var_scroll
|
|
292
|
+
value = user_vars[name]
|
|
293
|
+
info = var_info(name, value)
|
|
294
|
+
display = f"{name[:10]:<10} {info[:rpanel_w-12]}"
|
|
295
|
+
if idx == state.selected_var:
|
|
296
|
+
out.append(f"\033[{4+i};{right_x}H\033[47;30m>{display[:rpanel_w]}\033[0m")
|
|
297
|
+
elif isinstance(value, pd.DataFrame):
|
|
298
|
+
out.append(f"\033[{4+i};{right_x}H\033[34m {display[:rpanel_w]}\033[0m")
|
|
299
|
+
elif isinstance(value, np.ndarray):
|
|
300
|
+
out.append(f"\033[{4+i};{right_x}H\033[35m {display[:rpanel_w]}\033[0m")
|
|
301
|
+
else:
|
|
302
|
+
out.append(f"\033[{4+i};{right_x}H {display[:rpanel_w]}")
|
|
303
|
+
if not var_names:
|
|
304
|
+
out.append(f"\033[5;{right_x}H\033[90mNo vars\033[0m")
|
|
305
|
+
|
|
306
|
+
elif state.panel == 1: # DataFrames
|
|
307
|
+
dfs = {k: v for k, v in user_vars.items() if isinstance(v, pd.DataFrame)}
|
|
308
|
+
df_names = list(dfs.keys())
|
|
309
|
+
for i, name in enumerate(df_names[:rpanel_h]):
|
|
310
|
+
df = dfs[name]
|
|
311
|
+
info = f"{df.shape[0]}x{df.shape[1]}"
|
|
312
|
+
out.append(f"\033[{4+i};{right_x}H\033[34m {name[:10]:<10} {info}\033[0m")
|
|
313
|
+
if not df_names:
|
|
314
|
+
out.append(f"\033[5;{right_x}H\033[90mNo DFs\033[0m")
|
|
315
|
+
|
|
316
|
+
elif state.panel == 2: # Plots
|
|
317
|
+
if state.plots:
|
|
318
|
+
for i, path in enumerate(state.plots[-(rpanel_h):]):
|
|
319
|
+
name = os.path.basename(path)[:rpanel_w]
|
|
320
|
+
out.append(f"\033[{4+i};{right_x}H {name}")
|
|
321
|
+
else:
|
|
322
|
+
out.append(f"\033[5;{right_x}H\033[90mNo plots\033[0m")
|
|
323
|
+
out.append(f"\033[6;{right_x}H\033[90mCtrl+S save\033[0m")
|
|
324
|
+
|
|
325
|
+
# ===== INPUT LINE =====
|
|
326
|
+
input_y = height - 2
|
|
327
|
+
visible_prompt = "NL> " if state.mode == "natural" else ">>> "
|
|
328
|
+
out.append(f"\033[{input_y};1H\033[90m{'─' * width}\033[0m")
|
|
329
|
+
# Clear the input line first, then write prompt and input
|
|
330
|
+
out.append(f"\033[{height-1};1H\033[K") # Clear line
|
|
331
|
+
out.append(f"\033[{height-1};1H\033[32m{visible_prompt}\033[0m{state.current_input}")
|
|
332
|
+
|
|
333
|
+
# Position cursor after prompt + cursor_pos characters
|
|
334
|
+
cursor_col = len(visible_prompt) + state.cursor_pos + 1 # +1 for 1-indexed columns
|
|
335
|
+
out.append(f"\033[{height-1};{cursor_col}H")
|
|
336
|
+
|
|
337
|
+
# ===== FOOTER =====
|
|
338
|
+
hints = "Tab:panels Arrows:nav Ctrl+N:NL Ctrl+S:save Ctrl+D:del Ctrl+Q:quit"
|
|
339
|
+
out.append(f"\033[{height};1H\033[90m{hints[:width]}\033[0m")
|
|
340
|
+
|
|
341
|
+
sys.stdout.write(''.join(out))
|
|
342
|
+
sys.stdout.flush()
|
|
343
|
+
|
|
344
|
+
# ========== Input Handling ==========
|
|
345
|
+
def handle_input(c):
|
|
346
|
+
if c == '\x11': # Ctrl+Q - quit
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
elif c == '\t': # Tab - cycle panels
|
|
350
|
+
state.panel = (state.panel + 1) % 3
|
|
351
|
+
state.selected_var = 0
|
|
352
|
+
state.var_scroll = 0
|
|
353
|
+
|
|
354
|
+
elif c == '\x0e': # Ctrl+N - toggle natural language
|
|
355
|
+
state.mode = "natural" if state.mode == "code" else "code"
|
|
356
|
+
state.status = f"{state.mode} mode"
|
|
357
|
+
|
|
358
|
+
elif c == '\x13': # Ctrl+S - save plot
|
|
359
|
+
path = save_plot()
|
|
360
|
+
if path:
|
|
361
|
+
state.output_lines.append(f"Plot saved: {path}")
|
|
362
|
+
state.status = "Plot saved"
|
|
363
|
+
else:
|
|
364
|
+
state.status = "No plot to save"
|
|
365
|
+
|
|
366
|
+
elif c == '\x04': # Ctrl+D - delete variable
|
|
367
|
+
user_vars = get_user_vars()
|
|
368
|
+
var_names = list(user_vars.keys())
|
|
369
|
+
if var_names and state.selected_var < len(var_names):
|
|
370
|
+
name = var_names[state.selected_var]
|
|
371
|
+
del state.locals[name]
|
|
372
|
+
state.output_lines.append(f"Deleted: {name}")
|
|
373
|
+
state.selected_var = max(0, state.selected_var - 1)
|
|
374
|
+
|
|
375
|
+
elif c == '\x1b': # Escape sequences
|
|
376
|
+
# Check if more input is available (escape sequence) or standalone ESC
|
|
377
|
+
if select.select([sys.stdin], [], [], 0.05)[0]:
|
|
378
|
+
c2 = sys.stdin.read(1)
|
|
379
|
+
if c2 == '[':
|
|
380
|
+
c3 = sys.stdin.read(1)
|
|
381
|
+
if c3 == 'A': # Up
|
|
382
|
+
if state.current_input == "" and state.history:
|
|
383
|
+
state.history_idx = max(0, state.history_idx - 1) if state.history_idx >= 0 else len(state.history) - 1
|
|
384
|
+
state.current_input = state.history[state.history_idx]
|
|
385
|
+
state.cursor_pos = len(state.current_input)
|
|
386
|
+
elif not state.current_input:
|
|
387
|
+
if state.inspecting:
|
|
388
|
+
state.df_row_offset = max(0, state.df_row_offset - 1)
|
|
389
|
+
elif state.panel > 0:
|
|
390
|
+
state.selected_var = max(0, state.selected_var - 1)
|
|
391
|
+
else:
|
|
392
|
+
state.scroll_offset = max(0, state.scroll_offset - 1)
|
|
393
|
+
elif c3 == 'B': # Down
|
|
394
|
+
if state.current_input == "" and state.history and state.history_idx >= 0:
|
|
395
|
+
state.history_idx = min(len(state.history) - 1, state.history_idx + 1)
|
|
396
|
+
state.current_input = state.history[state.history_idx]
|
|
397
|
+
state.cursor_pos = len(state.current_input)
|
|
398
|
+
elif not state.current_input:
|
|
399
|
+
if state.inspecting:
|
|
400
|
+
state.df_row_offset += 1
|
|
401
|
+
elif state.panel > 0:
|
|
402
|
+
user_vars = get_user_vars()
|
|
403
|
+
state.selected_var = min(state.selected_var + 1, len(user_vars) - 1)
|
|
404
|
+
else:
|
|
405
|
+
state.scroll_offset += 1
|
|
406
|
+
elif c3 == 'C': # Right
|
|
407
|
+
state.cursor_pos = min(len(state.current_input), state.cursor_pos + 1)
|
|
408
|
+
elif c3 == 'D': # Left
|
|
409
|
+
state.cursor_pos = max(0, state.cursor_pos - 1)
|
|
410
|
+
else:
|
|
411
|
+
# Standalone ESC - exit inspect mode or clear input
|
|
412
|
+
if state.inspecting:
|
|
413
|
+
state.inspecting = None
|
|
414
|
+
state.mode = "code"
|
|
415
|
+
elif state.current_input:
|
|
416
|
+
state.current_input = ""
|
|
417
|
+
state.cursor_pos = 0
|
|
418
|
+
|
|
419
|
+
elif c == '\r' or c == '\n': # Enter
|
|
420
|
+
if state.current_input.strip():
|
|
421
|
+
code = state.current_input.strip()
|
|
422
|
+
|
|
423
|
+
# Check for file path (drag & drop)
|
|
424
|
+
if os.path.exists(os.path.expanduser(code.strip("'\""))):
|
|
425
|
+
name, msg = load_file(code.strip("'\""))
|
|
426
|
+
state.output_lines.append(msg)
|
|
427
|
+
state.status = "File loaded" if name else "Load failed"
|
|
428
|
+
|
|
429
|
+
elif state.mode == "natural":
|
|
430
|
+
# Natural language -> code generation
|
|
431
|
+
state.status = "Generating code..."
|
|
432
|
+
render_screen()
|
|
433
|
+
|
|
434
|
+
var_context = "Variables: " + ", ".join(f"{k}({var_info(k,v)})" for k,v in get_user_vars().items())
|
|
435
|
+
prompt = f"{var_context}\n\nRequest: {code}\n\nGenerate Python code. Return ONLY code, no explanation."
|
|
436
|
+
|
|
437
|
+
resp = get_llm_response(prompt, model=model, provider=provider, npc=npc)
|
|
438
|
+
gen_code = str(resp.get('response', ''))
|
|
439
|
+
|
|
440
|
+
# Extract code from markdown
|
|
441
|
+
if '```python' in gen_code:
|
|
442
|
+
gen_code = gen_code.split('```python')[1].split('```')[0]
|
|
443
|
+
elif '```' in gen_code:
|
|
444
|
+
gen_code = gen_code.split('```')[1].split('```')[0]
|
|
445
|
+
gen_code = gen_code.strip()
|
|
446
|
+
|
|
447
|
+
state.output_lines.append(f">>> # Generated from: {code[:40]}...")
|
|
448
|
+
state.output_lines.append(gen_code)
|
|
449
|
+
state.output_lines.append("Execute? (y to run)")
|
|
450
|
+
state.history.append(code)
|
|
451
|
+
state.status = "Confirm execution"
|
|
452
|
+
# Store for potential execution
|
|
453
|
+
state.locals['__pending_code__'] = gen_code
|
|
454
|
+
|
|
455
|
+
elif code == 'y' and '__pending_code__' in state.locals:
|
|
456
|
+
# Execute pending generated code
|
|
457
|
+
gen_code = state.locals.pop('__pending_code__')
|
|
458
|
+
output, error = execute_code(gen_code)
|
|
459
|
+
if output:
|
|
460
|
+
state.output_lines.extend(output.split('\n'))
|
|
461
|
+
if error:
|
|
462
|
+
state.output_lines.extend(error.split('\n'))
|
|
463
|
+
state.status = "Executed"
|
|
464
|
+
|
|
465
|
+
else:
|
|
466
|
+
# Direct code execution
|
|
467
|
+
state.output_lines.append(f">>> {code}")
|
|
468
|
+
state.history.append(code)
|
|
469
|
+
|
|
470
|
+
output, error = execute_code(code)
|
|
471
|
+
if output:
|
|
472
|
+
state.output_lines.extend(output.split('\n'))
|
|
473
|
+
if error:
|
|
474
|
+
state.output_lines.extend(error.split('\n'))
|
|
475
|
+
state.status = "Error"
|
|
476
|
+
else:
|
|
477
|
+
state.status = "OK"
|
|
478
|
+
|
|
479
|
+
state.current_input = ""
|
|
480
|
+
state.cursor_pos = 0
|
|
481
|
+
state.history_idx = -1
|
|
482
|
+
state.scroll_offset = max(0, len(state.output_lines) - 10)
|
|
483
|
+
|
|
484
|
+
elif c == '\x7f' or c == '\x08': # Backspace
|
|
485
|
+
if state.cursor_pos > 0:
|
|
486
|
+
state.current_input = state.current_input[:state.cursor_pos-1] + state.current_input[state.cursor_pos:]
|
|
487
|
+
state.cursor_pos -= 1
|
|
488
|
+
|
|
489
|
+
elif c == '\x15': # Ctrl+U - clear line
|
|
490
|
+
state.current_input = ""
|
|
491
|
+
state.cursor_pos = 0
|
|
492
|
+
|
|
493
|
+
elif c == '\x01': # Ctrl+A - start of line
|
|
494
|
+
state.cursor_pos = 0
|
|
495
|
+
|
|
496
|
+
elif c == '\x05': # Ctrl+E - end of line
|
|
497
|
+
state.cursor_pos = len(state.current_input)
|
|
498
|
+
|
|
499
|
+
elif c == '\x0c': # Ctrl+L - clear output
|
|
500
|
+
state.output_lines = []
|
|
501
|
+
state.scroll_offset = 0
|
|
502
|
+
|
|
503
|
+
elif c >= ' ' and c <= '~': # Printable
|
|
504
|
+
state.current_input = state.current_input[:state.cursor_pos] + c + state.current_input[state.cursor_pos:]
|
|
505
|
+
state.cursor_pos += 1
|
|
506
|
+
|
|
507
|
+
return True
|
|
508
|
+
|
|
509
|
+
# ========== Main Loop ==========
|
|
510
|
+
fd = sys.stdin.fileno()
|
|
511
|
+
old_settings = termios.tcgetattr(fd)
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
tty.setcbreak(fd)
|
|
515
|
+
sys.stdout.write('\033[?25h') # Show cursor
|
|
516
|
+
|
|
517
|
+
render_screen()
|
|
518
|
+
|
|
519
|
+
while True:
|
|
520
|
+
c = sys.stdin.read(1)
|
|
521
|
+
if not handle_input(c):
|
|
522
|
+
break
|
|
523
|
+
render_screen()
|
|
524
|
+
|
|
525
|
+
finally:
|
|
526
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
527
|
+
sys.stdout.write('\033[2J\033[H')
|
|
528
|
+
sys.stdout.flush()
|
|
529
|
+
|
|
530
|
+
# Final summary
|
|
531
|
+
user_vars = get_user_vars()
|
|
532
|
+
if user_vars:
|
|
533
|
+
print(colored("=== GUAC SESSION ===\n", "green"))
|
|
534
|
+
print("Variables:")
|
|
535
|
+
for name, val in user_vars.items():
|
|
536
|
+
print(f" {name}: {var_info(name, val)}")
|
|
537
|
+
if state.plots:
|
|
538
|
+
print(f"\nPlots saved: {len(state.plots)}")
|
|
539
|
+
for p in state.plots[-5:]:
|
|
540
|
+
print(f" {p}")
|
|
541
|
+
|
|
542
|
+
context['output'] = "Exited guac mode."
|
|
543
|
+
context['messages'] = messages
|
|
544
|
+
context['guac_locals'] = state.locals
|