LazyHuman10 commited on
Commit ·
ff90096
1
Parent(s): 9e39bcc
Add optional remembered assistant settings
Browse files- example.env +5 -1
- pages/Plexi-Assistant.py +191 -0
- requirements.txt +2 -1
example.env
CHANGED
|
@@ -2,4 +2,8 @@
|
|
| 2 |
|
| 3 |
# GitHub repo hosting study materials (owner/repo format)
|
| 4 |
# Defaults to KunalGupta25/plexi-materials if not set
|
| 5 |
-
# MATERIALS_REPO=KunalGupta25/plexi-materials
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
# GitHub repo hosting study materials (owner/repo format)
|
| 4 |
# Defaults to KunalGupta25/plexi-materials if not set
|
| 5 |
+
# MATERIALS_REPO=KunalGupta25/plexi-materials
|
| 6 |
+
|
| 7 |
+
# Secret used to encrypt browser-saved Plexi assistant settings
|
| 8 |
+
# Required for "remember settings on this device"
|
| 9 |
+
# PLEXI_COOKIE_PASSWORD=replace-with-a-long-random-secret
|
pages/Plexi-Assistant.py
CHANGED
|
@@ -10,8 +10,19 @@ Flow per user message:
|
|
| 10 |
3. Build focused prompt with retrieved chunks -> call user's LLM
|
| 11 |
"""
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
import requests
|
| 14 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
from utils import (
|
| 16 |
fetch_rag_index,
|
| 17 |
get_manifest,
|
|
@@ -104,6 +115,17 @@ PROVIDER_NAMES = list(PROVIDERS.keys())
|
|
| 104 |
PLEXI_GPT_URL = "https://chatgpt.com/g/g-69caa671910481919ce71d19952e34e5-plexi"
|
| 105 |
PLEXI_MCP_GUIDE_URL = "https://lazyhuman.notion.site/Setting-Up-Plexi-MCP-for-Claude-and-ChatGPT-336e3502f0918090b69fdbed148e8e55"
|
| 106 |
PLEXI_MCP_ENDPOINT = "https://plexi-mcp.vercel.app/api/mcp"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
|
| 109 |
def _matches_scope(node, semester: str, subject: str) -> bool:
|
|
@@ -118,6 +140,77 @@ def queue_prompt(prompt: str):
|
|
| 118 |
st.rerun()
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
def render_external_access():
|
| 122 |
"""Render low-emphasis outbound access actions."""
|
| 123 |
st.markdown(
|
|
@@ -263,9 +356,33 @@ def render_onboarding(manifest):
|
|
| 263 |
api_key = st.text_input(
|
| 264 |
"API Key",
|
| 265 |
type="password",
|
|
|
|
| 266 |
placeholder="Paste your API key here",
|
| 267 |
)
|
| 268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
semester_names = sorted(manifest.keys())
|
| 270 |
default_semester = st.session_state.get("asst_semester")
|
| 271 |
semester_index = (
|
|
@@ -311,8 +428,24 @@ def render_onboarding(manifest):
|
|
| 311 |
st.session_state.cfg_model = model_name
|
| 312 |
st.session_state.asst_semester = selected_semester
|
| 313 |
st.session_state.asst_subject = selected_subject
|
|
|
|
| 314 |
if api_key:
|
| 315 |
st.session_state.api_key = api_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
st.session_state.pop("messages", None)
|
| 317 |
st.rerun()
|
| 318 |
|
|
@@ -346,6 +479,7 @@ def render_onboarding(manifest):
|
|
| 346 |
)
|
| 347 |
|
| 348 |
|
|
|
|
| 349 |
render_sidebar_intro()
|
| 350 |
|
| 351 |
try:
|
|
@@ -467,24 +601,70 @@ with st.sidebar:
|
|
| 467 |
key="sb_api_key",
|
| 468 |
)
|
| 469 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
changed = (
|
| 471 |
new_provider != provider_name
|
| 472 |
or new_base_url != base_url
|
| 473 |
or new_model != model_name
|
| 474 |
or new_key != api_key
|
|
|
|
| 475 |
)
|
| 476 |
if changed and new_model:
|
| 477 |
if st.button("Apply Changes", use_container_width=True, type="primary"):
|
| 478 |
st.session_state.cfg_provider = new_provider
|
| 479 |
st.session_state.cfg_base_url = new_base_url
|
| 480 |
st.session_state.cfg_model = new_model
|
|
|
|
| 481 |
if new_key:
|
| 482 |
st.session_state.api_key = new_key
|
| 483 |
elif "api_key" in st.session_state:
|
| 484 |
del st.session_state.api_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
st.session_state.pop("messages", None)
|
| 486 |
st.rerun()
|
| 487 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
if st.button("New Chat", use_container_width=True):
|
| 489 |
st.session_state.pop("messages", None)
|
| 490 |
st.rerun()
|
|
@@ -503,6 +683,15 @@ render_page_header(
|
|
| 503 |
|
| 504 |
render_external_access()
|
| 505 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
render_stat_cards(
|
| 507 |
[
|
| 508 |
{
|
|
@@ -636,6 +825,8 @@ if prompt:
|
|
| 636 |
st.stop()
|
| 637 |
if "AUTH_ERROR" in err_text:
|
| 638 |
st.error(err_text.split(": ", 1)[1])
|
|
|
|
|
|
|
| 639 |
if "api_key" in st.session_state:
|
| 640 |
del st.session_state.api_key
|
| 641 |
st.session_state.messages.pop()
|
|
|
|
| 10 |
3. Build focused prompt with retrieved chunks -> call user's LLM
|
| 11 |
"""
|
| 12 |
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
|
| 16 |
import requests
|
| 17 |
import streamlit as st
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from streamlit_cookies_manager_ext import EncryptedCookieManager
|
| 21 |
+
|
| 22 |
+
COOKIES_MANAGER_AVAILABLE = True
|
| 23 |
+
except ImportError:
|
| 24 |
+
EncryptedCookieManager = None
|
| 25 |
+
COOKIES_MANAGER_AVAILABLE = False
|
| 26 |
from utils import (
|
| 27 |
fetch_rag_index,
|
| 28 |
get_manifest,
|
|
|
|
| 115 |
PLEXI_GPT_URL = "https://chatgpt.com/g/g-69caa671910481919ce71d19952e34e5-plexi"
|
| 116 |
PLEXI_MCP_GUIDE_URL = "https://lazyhuman.notion.site/Setting-Up-Plexi-MCP-for-Claude-and-ChatGPT-336e3502f0918090b69fdbed148e8e55"
|
| 117 |
PLEXI_MCP_ENDPOINT = "https://plexi-mcp.vercel.app/api/mcp"
|
| 118 |
+
SAVED_CONFIG_COOKIE = "assistant_config"
|
| 119 |
+
COOKIE_PASSWORD = os.getenv("PLEXI_COOKIE_PASSWORD") or os.getenv("COOKIES_PASSWORD")
|
| 120 |
+
|
| 121 |
+
cookies = None
|
| 122 |
+
if COOKIE_PASSWORD and COOKIES_MANAGER_AVAILABLE:
|
| 123 |
+
cookies = EncryptedCookieManager(
|
| 124 |
+
prefix="plexi/assistant/",
|
| 125 |
+
password=COOKIE_PASSWORD,
|
| 126 |
+
)
|
| 127 |
+
if not cookies.ready():
|
| 128 |
+
st.stop()
|
| 129 |
|
| 130 |
|
| 131 |
def _matches_scope(node, semester: str, subject: str) -> bool:
|
|
|
|
| 140 |
st.rerun()
|
| 141 |
|
| 142 |
|
| 143 |
+
def _saved_config_available():
|
| 144 |
+
return cookies is not None
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _load_saved_config():
|
| 148 |
+
"""Load saved assistant settings from the browser cookie."""
|
| 149 |
+
if not _saved_config_available():
|
| 150 |
+
return None
|
| 151 |
+
|
| 152 |
+
raw_config = cookies.get(SAVED_CONFIG_COOKIE)
|
| 153 |
+
if not raw_config:
|
| 154 |
+
return None
|
| 155 |
+
|
| 156 |
+
try:
|
| 157 |
+
return json.loads(raw_config)
|
| 158 |
+
except (TypeError, json.JSONDecodeError):
|
| 159 |
+
del cookies[SAVED_CONFIG_COOKIE]
|
| 160 |
+
cookies.save()
|
| 161 |
+
return None
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _save_config(config):
|
| 165 |
+
"""Persist assistant settings in the browser cookie."""
|
| 166 |
+
if not _saved_config_available():
|
| 167 |
+
return
|
| 168 |
+
|
| 169 |
+
cookies[SAVED_CONFIG_COOKIE] = json.dumps(config)
|
| 170 |
+
cookies.save()
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _clear_saved_config():
|
| 174 |
+
"""Remove the saved browser-side assistant settings."""
|
| 175 |
+
if not _saved_config_available():
|
| 176 |
+
return
|
| 177 |
+
|
| 178 |
+
if SAVED_CONFIG_COOKIE in cookies:
|
| 179 |
+
del cookies[SAVED_CONFIG_COOKIE]
|
| 180 |
+
cookies.save()
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def _current_config(selected_semester=None, selected_subject=None, api_key=None):
|
| 184 |
+
"""Build the current assistant configuration payload."""
|
| 185 |
+
return {
|
| 186 |
+
"cfg_provider": st.session_state.get("cfg_provider"),
|
| 187 |
+
"cfg_base_url": st.session_state.get("cfg_base_url"),
|
| 188 |
+
"cfg_model": st.session_state.get("cfg_model"),
|
| 189 |
+
"api_key": api_key if api_key is not None else st.session_state.get("api_key"),
|
| 190 |
+
"asst_semester": selected_semester
|
| 191 |
+
if selected_semester is not None
|
| 192 |
+
else st.session_state.get("asst_semester"),
|
| 193 |
+
"asst_subject": selected_subject
|
| 194 |
+
if selected_subject is not None
|
| 195 |
+
else st.session_state.get("asst_subject"),
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def _hydrate_saved_config():
|
| 200 |
+
"""Hydrate session state from a remembered browser config once per load."""
|
| 201 |
+
if st.session_state.get("_saved_config_hydrated"):
|
| 202 |
+
return
|
| 203 |
+
|
| 204 |
+
saved_config = _load_saved_config()
|
| 205 |
+
if saved_config:
|
| 206 |
+
for key, value in saved_config.items():
|
| 207 |
+
if value and key not in st.session_state:
|
| 208 |
+
st.session_state[key] = value
|
| 209 |
+
st.session_state["remember_device"] = True
|
| 210 |
+
|
| 211 |
+
st.session_state["_saved_config_hydrated"] = True
|
| 212 |
+
|
| 213 |
+
|
| 214 |
def render_external_access():
|
| 215 |
"""Render low-emphasis outbound access actions."""
|
| 216 |
st.markdown(
|
|
|
|
| 356 |
api_key = st.text_input(
|
| 357 |
"API Key",
|
| 358 |
type="password",
|
| 359 |
+
value=st.session_state.get("api_key", ""),
|
| 360 |
placeholder="Paste your API key here",
|
| 361 |
)
|
| 362 |
|
| 363 |
+
remember_default = bool(
|
| 364 |
+
st.session_state.get("remember_device") or _load_saved_config()
|
| 365 |
+
)
|
| 366 |
+
remember_device = st.checkbox(
|
| 367 |
+
"Remember these settings on this device",
|
| 368 |
+
value=remember_default,
|
| 369 |
+
disabled=not _saved_config_available(),
|
| 370 |
+
help=(
|
| 371 |
+
"Saves your provider settings and API key in this browser only."
|
| 372 |
+
if _saved_config_available()
|
| 373 |
+
else (
|
| 374 |
+
"Install the optional cookie dependency and set "
|
| 375 |
+
"PLEXI_COOKIE_PASSWORD to enable saved browser settings."
|
| 376 |
+
)
|
| 377 |
+
),
|
| 378 |
+
)
|
| 379 |
+
if not _saved_config_available():
|
| 380 |
+
st.caption(
|
| 381 |
+
"Saved browser settings are disabled until "
|
| 382 |
+
"`streamlit-cookies-manager-ext` is installed and "
|
| 383 |
+
"`PLEXI_COOKIE_PASSWORD` is set."
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
semester_names = sorted(manifest.keys())
|
| 387 |
default_semester = st.session_state.get("asst_semester")
|
| 388 |
semester_index = (
|
|
|
|
| 428 |
st.session_state.cfg_model = model_name
|
| 429 |
st.session_state.asst_semester = selected_semester
|
| 430 |
st.session_state.asst_subject = selected_subject
|
| 431 |
+
st.session_state.remember_device = remember_device
|
| 432 |
if api_key:
|
| 433 |
st.session_state.api_key = api_key
|
| 434 |
+
elif "api_key" in st.session_state:
|
| 435 |
+
del st.session_state.api_key
|
| 436 |
+
if remember_device:
|
| 437 |
+
_save_config(
|
| 438 |
+
{
|
| 439 |
+
"cfg_provider": provider_name,
|
| 440 |
+
"cfg_base_url": base_url,
|
| 441 |
+
"cfg_model": model_name,
|
| 442 |
+
"api_key": api_key,
|
| 443 |
+
"asst_semester": selected_semester,
|
| 444 |
+
"asst_subject": selected_subject,
|
| 445 |
+
}
|
| 446 |
+
)
|
| 447 |
+
else:
|
| 448 |
+
_clear_saved_config()
|
| 449 |
st.session_state.pop("messages", None)
|
| 450 |
st.rerun()
|
| 451 |
|
|
|
|
| 479 |
)
|
| 480 |
|
| 481 |
|
| 482 |
+
_hydrate_saved_config()
|
| 483 |
render_sidebar_intro()
|
| 484 |
|
| 485 |
try:
|
|
|
|
| 601 |
key="sb_api_key",
|
| 602 |
)
|
| 603 |
|
| 604 |
+
remember_sidebar = st.checkbox(
|
| 605 |
+
"Remember these settings on this device",
|
| 606 |
+
value=bool(st.session_state.get("remember_device")),
|
| 607 |
+
disabled=not _saved_config_available(),
|
| 608 |
+
key="sb_remember_device",
|
| 609 |
+
help=(
|
| 610 |
+
"Keeps the selected provider, key, and scope in this browser."
|
| 611 |
+
if _saved_config_available()
|
| 612 |
+
else (
|
| 613 |
+
"Install the optional cookie dependency and set "
|
| 614 |
+
"PLEXI_COOKIE_PASSWORD to enable saved browser settings."
|
| 615 |
+
)
|
| 616 |
+
),
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
changed = (
|
| 620 |
new_provider != provider_name
|
| 621 |
or new_base_url != base_url
|
| 622 |
or new_model != model_name
|
| 623 |
or new_key != api_key
|
| 624 |
+
or remember_sidebar != bool(st.session_state.get("remember_device"))
|
| 625 |
)
|
| 626 |
if changed and new_model:
|
| 627 |
if st.button("Apply Changes", use_container_width=True, type="primary"):
|
| 628 |
st.session_state.cfg_provider = new_provider
|
| 629 |
st.session_state.cfg_base_url = new_base_url
|
| 630 |
st.session_state.cfg_model = new_model
|
| 631 |
+
st.session_state.remember_device = remember_sidebar
|
| 632 |
if new_key:
|
| 633 |
st.session_state.api_key = new_key
|
| 634 |
elif "api_key" in st.session_state:
|
| 635 |
del st.session_state.api_key
|
| 636 |
+
if remember_sidebar:
|
| 637 |
+
_save_config(
|
| 638 |
+
{
|
| 639 |
+
"cfg_provider": new_provider,
|
| 640 |
+
"cfg_base_url": new_base_url,
|
| 641 |
+
"cfg_model": new_model,
|
| 642 |
+
"api_key": new_key,
|
| 643 |
+
"asst_semester": selected_semester,
|
| 644 |
+
"asst_subject": selected_subject,
|
| 645 |
+
}
|
| 646 |
+
)
|
| 647 |
+
else:
|
| 648 |
+
_clear_saved_config()
|
| 649 |
st.session_state.pop("messages", None)
|
| 650 |
st.rerun()
|
| 651 |
|
| 652 |
+
if _load_saved_config():
|
| 653 |
+
if st.button("Forget Saved Settings", use_container_width=True):
|
| 654 |
+
_clear_saved_config()
|
| 655 |
+
st.session_state.remember_device = False
|
| 656 |
+
for key in (
|
| 657 |
+
"cfg_provider",
|
| 658 |
+
"cfg_base_url",
|
| 659 |
+
"cfg_model",
|
| 660 |
+
"api_key",
|
| 661 |
+
"asst_semester",
|
| 662 |
+
"asst_subject",
|
| 663 |
+
):
|
| 664 |
+
st.session_state.pop(key, None)
|
| 665 |
+
st.session_state.pop("messages", None)
|
| 666 |
+
st.rerun()
|
| 667 |
+
|
| 668 |
if st.button("New Chat", use_container_width=True):
|
| 669 |
st.session_state.pop("messages", None)
|
| 670 |
st.rerun()
|
|
|
|
| 683 |
|
| 684 |
render_external_access()
|
| 685 |
|
| 686 |
+
if st.session_state.get("remember_device") and _saved_config_available():
|
| 687 |
+
desired_config = _current_config(
|
| 688 |
+
selected_semester=selected_semester,
|
| 689 |
+
selected_subject=selected_subject,
|
| 690 |
+
api_key=api_key,
|
| 691 |
+
)
|
| 692 |
+
if desired_config != _load_saved_config():
|
| 693 |
+
_save_config(desired_config)
|
| 694 |
+
|
| 695 |
render_stat_cards(
|
| 696 |
[
|
| 697 |
{
|
|
|
|
| 825 |
st.stop()
|
| 826 |
if "AUTH_ERROR" in err_text:
|
| 827 |
st.error(err_text.split(": ", 1)[1])
|
| 828 |
+
_clear_saved_config()
|
| 829 |
+
st.session_state.remember_device = False
|
| 830 |
if "api_key" in st.session_state:
|
| 831 |
del st.session_state.api_key
|
| 832 |
st.session_state.messages.pop()
|
requirements.txt
CHANGED
|
@@ -5,4 +5,5 @@ PyPDF2
|
|
| 5 |
streamlit-pdf-viewer
|
| 6 |
llama-index-core
|
| 7 |
llama-index-embeddings-huggingface
|
| 8 |
-
sentence-transformers
|
|
|
|
|
|
| 5 |
streamlit-pdf-viewer
|
| 6 |
llama-index-core
|
| 7 |
llama-index-embeddings-huggingface
|
| 8 |
+
sentence-transformers
|
| 9 |
+
streamlit-cookies-manager-ext
|