| """AgAdvisor β Gradio frontend for Hugging Face Spaces. |
| |
| HF retired the Streamlit SDK, and Gradio/Docker Spaces on cpu-basic now require PRO, |
| so the free path is a Gradio Space on ZeroGPU hardware. This module is a view layer |
| only: it drives the SAME pipeline as src/streamlit_app_conversational.py |
| (parse_query -> ToolMatcher -> ToolExecutor) and the SAME AccountsService (bcrypt |
| auth, per-user daily quota, history synced to a private HF Dataset), so abstention |
| and page-level citations behave identically. The Streamlit app remains the local / |
| EC2 entrypoint; keep the two in sync at the pipeline boundary, not the UI. |
| """ |
|
|
| |
| |
| |
| |
| |
| |
| |
| try: |
| import spaces |
| except Exception: |
| import sys as _sys |
| import types as _types |
|
|
| spaces = _types.ModuleType("spaces") |
| spaces.GPU = lambda fn=None, **_kw: (fn if callable(fn) else (lambda f: f)) |
| _sys.modules["spaces"] = spaces |
|
|
|
|
| @spaces.GPU |
| def _zero_gpu_probe(): |
| """Exists solely so ZeroGPU detects a @spaces.GPU function at startup; unused.""" |
| return None |
|
|
|
|
| import logging |
|
|
| import gradio as gr |
|
|
| from src.accounts.service import AccountsService |
| from src.cdms.product_catalog import get_catalog |
| from src.parser import parse_query |
| from src.tools.tool_executor import ToolExecutor |
| from src.tools.tool_matcher import ToolMatcher |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| |
| accounts = AccountsService() |
| tool_matcher = ToolMatcher() |
| tool_executor = ToolExecutor() |
|
|
| try: |
| PRODUCTS = sorted(get_catalog().available_products()) |
| except Exception: |
| logger.exception("Could not load the product catalog") |
| PRODUCTS = [] |
|
|
| CONTEXT_TURNS = 5 |
|
|
| _TOOL_LABELS = { |
| "cdms_label": "CDMS label", "cdms": "CDMS label", "pesticide_label": "CDMS label", |
| "rag": "CDMS label", "documentation": "CDMS label", |
| "weather": "weather", "soil": "soil", "agriculture_web": "agriculture web", "ag_web": "agriculture web", |
| } |
|
|
|
|
| def _confidence_badge(tool: str, confidence: float) -> str: |
| """Answer confidence/star badge (ISA feedback #3, ported from the Streamlit UI). |
| |
| Plain-markdown so it renders cleanly on mobile β the Streamlit version's badge |
| was CSS-clipped on phones. `confidence` is the tool-router's match score (0-1). |
| """ |
| stars = max(1, min(5, round((confidence or 0.0) * 5))) |
| bar = "β
" * stars + "β" * (5 - stars) |
| label = _TOOL_LABELS.get(tool, tool) |
| return f"\n\n---\n`{bar}` Β· **{confidence:.0%} confidence** Β· answered via *{label}*" |
|
|
|
|
| |
| |
| |
| |
| |
| |
| import hashlib as _hashlib |
| import time as _time |
|
|
| _RESP_CACHE: dict[str, tuple[str, float]] = {} |
| _RESP_CACHE_TTL = 6 * 3600 |
| _RESP_CACHE_MAX = 500 |
|
|
|
|
| def _cache_key(question: str) -> str: |
| norm = " ".join(question.lower().split()) |
| return _hashlib.sha256(norm.encode()).hexdigest() |
|
|
|
|
| def _cache_put(key: str, reply: str) -> None: |
| if len(_RESP_CACHE) >= _RESP_CACHE_MAX: |
| _RESP_CACHE.pop(min(_RESP_CACHE, key=lambda k: _RESP_CACHE[k][1]), None) |
| _RESP_CACHE[key] = (reply, _time.time()) |
|
|
|
|
| def answer(question: str, history: list[dict], on_step=None) -> tuple[str, dict]: |
| """Run one question through the tool pipeline. Mirrors the Streamlit flow. |
| |
| Retrieval is auto-mode (decided in the CDMS tool): serve from the committed |
| index when the label is present, else live-fetch + index + cache it. |
| `on_step(str)` is called at each stage so the UI can show the real process |
| (and make first-time label fetches legible as the slow step). |
| |
| Returns (reply, debug) where debug holds routing/source details for the |
| optional Debug panel. |
| """ |
| def _step(msg: str) -> None: |
| if on_step: |
| try: |
| on_step(msg) |
| except Exception: |
| pass |
|
|
| |
| cacheable = not history |
| if cacheable: |
| _ck = _cache_key(question) |
| _hit = _RESP_CACHE.get(_ck) |
| if _hit and (_time.time() - _hit[1]) < _RESP_CACHE_TTL: |
| _step("β‘ Served from a recent cached answer.") |
| return _hit[0], {"cached": True} |
| context = [ |
| {"role": m["role"], "content": m["content"]} |
| for m in (history or [])[-CONTEXT_TURNS * 2:] |
| ] |
|
|
| _step("Understanding your questionβ¦") |
| try: |
| keywords = parse_query(question).get("extracted_keywords", []) |
| except Exception: |
| keywords = [] |
|
|
| confidence = 0.3 |
| _step("Choosing the right toolβ¦") |
| try: |
| match = tool_matcher.match_tool(keywords, question, conversation_context=context) |
| tool = match["tool_name"] |
| confidence = float(match.get("confidence", 0.3) or 0.3) |
| except Exception: |
| logger.exception("Tool matching failed; falling back to the label tool") |
| tool = "cdms_label" |
|
|
| debug = {"tool": tool, "confidence": confidence, "keywords": keywords} |
| try: |
| result = tool_executor.execute( |
| tool_name=tool, user_question=question, conversation_context=context, |
| on_step=on_step, |
| ) |
| except Exception: |
| |
| logger.exception("Tool execution failed") |
| return "I ran into an unexpected problem answering that. Please try rephrasing.", debug |
|
|
| raw = result.get("raw_data", {}) if isinstance(result, dict) else {} |
| debug.update({ |
| "tool_used": result.get("tool_used", tool), |
| "source": raw.get("source", "index"), |
| "chunks": raw.get("total_chunks_found", raw.get("pdfs_indexed")), |
| "success": result.get("success", False), |
| }) |
|
|
| if not result.get("success", False): |
| return result.get( |
| "llm_response", "I couldn't find that in my label set." |
| ), debug |
| reply = result.get("llm_response", "I couldn't process that request.") |
| |
| |
| if "don't have the label" not in reply: |
| reply += _confidence_badge(tool, confidence) |
| |
| |
| if cacheable: |
| _cache_put(_ck, reply) |
| return reply, debug |
|
|
|
|
| import queue as _queue |
| import threading as _threading |
|
|
|
|
| def _format_steps(steps: list[str]) -> str: |
| """Render the live pipeline steps as a small checklist for the accordion.""" |
| if not steps: |
| return "_workingβ¦_" |
| return "\n".join(f"- {s}" for s in steps) |
|
|
|
|
| def _format_debug(debug: dict | None) -> str: |
| """Render the last answer's routing details for the Debug panel.""" |
| if not debug: |
| return "_No debug info yet β ask a question._" |
| if debug.get("cached"): |
| return "**Debug:** served from the in-process response cache (no pipeline run)." |
| rows = [ |
| f"- **Tool:** `{debug.get('tool_used', debug.get('tool', '?'))}`", |
| f"- **Confidence:** {float(debug.get('confidence', 0) or 0):.0%}", |
| f"- **Retrieval source:** `{debug.get('source', '?')}` " |
| f"({'live-fetched + cached' if debug.get('source') == 'live' else 'served from index'})", |
| f"- **Chunks used:** {debug.get('chunks', '?')}", |
| f"- **Keywords:** {', '.join(debug.get('keywords') or []) or 'β'}", |
| ] |
| return "**Debug β last answer**\n" + "\n".join(rows) |
|
|
|
|
| def on_submit(question: str, history: list[dict], user: dict | None, last_mid): |
| """Quota-gate, persist, answer, persist β as a streaming generator. |
| |
| Yields progressive updates so the UI shows the ACTUAL pipeline steps live |
| (retrieval / live-fetch / indexing / writing), then collapses them into the |
| "Process steps" accordion once the answer is in. The synchronous pipeline |
| runs in a worker thread that pushes step strings onto a queue; this generator |
| drains the queue and re-yields. Outputs: |
| (chatbot, question, quota, steps_md, steps_accordion, last_msg_id, debug_md) |
| """ |
| history = history or [] |
| if not user: |
| yield history, "", "Please sign in first.", "", gr.update(), last_mid, gr.update() |
| return |
| if not question or not question.strip(): |
| yield history, "", "", "", gr.update(), last_mid, gr.update() |
| return |
|
|
| uid, chat_id = user["id"], user["chat_id"] |
|
|
| if not accounts.check_quota(uid): |
| history = history + [ |
| {"role": "user", "content": question}, |
| {"role": "assistant", |
| "content": "You've reached today's question limit. Please come back tomorrow."}, |
| ] |
| yield history, "", _quota_label(uid), "", gr.update(open=False), last_mid, gr.update() |
| return |
|
|
| accounts.add_message(chat_id, uid, "user", question) |
| accounts.record_query(uid) |
|
|
| |
| working_history = history + [{"role": "user", "content": question}] |
| steps: list[str] = [] |
| step_q: _queue.Queue = _queue.Queue() |
| box: dict = {} |
|
|
| def _run(): |
| try: |
| box["reply"], box["debug"] = answer(question, history, on_step=lambda m: step_q.put(m)) |
| except Exception as e: |
| logger.exception("answer() failed in worker thread") |
| box["error"] = e |
| finally: |
| step_q.put(None) |
|
|
| worker = _threading.Thread(target=_run, daemon=True) |
| worker.start() |
|
|
| yield working_history, "", _quota_label(uid), _format_steps(steps), gr.update(open=True), last_mid, gr.update() |
|
|
| while True: |
| item = step_q.get() |
| if item is None: |
| break |
| steps.append(item) |
| yield working_history, "", _quota_label(uid), _format_steps(steps), gr.update(open=True), last_mid, gr.update() |
|
|
| worker.join() |
|
|
| if box.get("error") is not None: |
| reply = "I ran into an unexpected problem answering that. Please try rephrasing." |
| else: |
| reply = box.get("reply", "I couldn't process that request.") |
|
|
| |
| mid = accounts.add_message(chat_id, uid, "assistant", reply, metadata={"tool": "cdms_label"}) |
|
|
| final_history = working_history + [{"role": "assistant", "content": reply}] |
| steps.append("Done.") |
| yield (final_history, "", _quota_label(uid), _format_steps(steps), |
| gr.update(open=False), mid, gr.update(value=_format_debug(box.get("debug")))) |
|
|
|
|
| def _quota_label(uid: int) -> str: |
| try: |
| return f"{accounts.remaining_quota(uid)} questions left today" |
| except Exception: |
| return "" |
|
|
|
|
| def _load_user(user_row: dict): |
| """Attach a chat (resuming the most recent) and hydrate its history.""" |
| uid = user_row["id"] |
| chats = accounts.list_chats(uid) |
| chat_id = chats[0]["id"] if chats else accounts.create_chat(uid, "Chat 1") |
| messages = accounts.get_messages(chat_id, uid) |
| history = [{"role": m["role"], "content": m["content"]} for m in messages] |
| user = {"id": uid, "username": user_row["username"], "chat_id": chat_id} |
| return user, history |
|
|
|
|
| def do_login(username: str, password: str): |
| try: |
| row = accounts.login(username, password) |
| except Exception as e: |
| return None, [], gr.update(visible=True), gr.update(visible=False), str(e), "", "", None |
| user, history = _load_user(row) |
| return ( |
| user, history, |
| gr.update(visible=False), gr.update(visible=True), |
| "", f"Signed in as **{user['username']}**", _quota_label(user["id"]), None, |
| ) |
|
|
|
|
| def do_signup(username: str, password: str): |
| try: |
| row = accounts.signup(username, password) |
| except Exception as e: |
| return None, [], gr.update(visible=True), gr.update(visible=False), str(e), "", "", None |
| user, history = _load_user(row) |
| return ( |
| user, history, |
| gr.update(visible=False), gr.update(visible=True), |
| "", f"Signed in as **{user['username']}**", _quota_label(user["id"]), None, |
| ) |
|
|
|
|
| def do_logout(): |
| return ( |
| None, [], |
| gr.update(visible=True), gr.update(visible=False), |
| "", "", "", None, |
| ) |
|
|
|
|
| def do_new_chat(user: dict | None): |
| if not user: |
| return [], user, None |
| n = len(accounts.list_chats(user["id"])) + 1 |
| chat_id = accounts.create_chat(user["id"], f"Chat {n}") |
| return [], {**user, "chat_id": chat_id}, None |
|
|
|
|
| |
| def on_like(user: dict | None, last_mid, evt: gr.LikeData) -> str: |
| """Record a thumbs up/down for the most recent assistant answer.""" |
| if not user or last_mid is None: |
| return "" |
| rating = 1 if evt.liked else -1 |
| try: |
| ok = accounts.add_feedback(last_mid, user["id"], rating=rating) |
| except Exception: |
| logger.exception("add_feedback (rating) failed") |
| return "Couldn't record that rating." |
| if ok is None: |
| return "" |
| return "Thanks β π noted." if rating > 0 else "Thanks β π noted." |
|
|
|
|
| def submit_comment(user: dict | None, last_mid, comment: str): |
| """Persist an optional free-text comment against the latest answer.""" |
| if not user or last_mid is None: |
| return comment, "Ask a question first, then comment on its answer." |
| if not comment or not comment.strip(): |
| return comment, "" |
| try: |
| ok = accounts.add_feedback(last_mid, user["id"], comment=comment.strip()) |
| except Exception: |
| logger.exception("add_feedback (comment) failed") |
| return comment, "Couldn't save your comment." |
| if ok is None: |
| return comment, "Couldn't save your comment." |
| return "", "Thanks for the feedback! π" |
|
|
|
|
| |
| def _chat_choices(user: dict | None): |
| """(radio choices, current value) for the chat-session switcher.""" |
| if not user: |
| return [], None |
| try: |
| chats = accounts.list_chats_with_counts(user["id"]) |
| except Exception: |
| logger.exception("list_chats_with_counts failed") |
| return [], user.get("chat_id") |
| choices = [(f"{c['name']} Β· {c['msg_count']} msgs", c["id"]) for c in chats] |
| return choices, user.get("chat_id") |
|
|
|
|
| def load_chat(user: dict | None, chat_id: str | None): |
| """Switch the active chat: load its messages, repoint the user's chat_id.""" |
| if not user or not chat_id: |
| return gr.update(), user, None |
| try: |
| messages = accounts.get_messages(chat_id, user["id"]) |
| except Exception: |
| logger.exception("get_messages failed") |
| return gr.update(), user, None |
| history = [{"role": m["role"], "content": m["content"]} for m in messages] |
| return history, {**user, "chat_id": chat_id}, None |
|
|
|
|
| def delete_selected_chat(user: dict | None, chat_id: str | None): |
| """Delete the selected chat, then fall back to the most recent (or a fresh one).""" |
| if not user or not chat_id: |
| choices, cur = _chat_choices(user) |
| return gr.update(choices=choices, value=cur), gr.update(), user, None |
| try: |
| accounts.delete_chat(chat_id, user["id"]) |
| except Exception: |
| logger.exception("delete_chat failed") |
| chats = accounts.list_chats(user["id"]) |
| new_id = chats[0]["id"] if chats else accounts.create_chat(user["id"], "Chat 1") |
| messages = accounts.get_messages(new_id, user["id"]) |
| history = [{"role": m["role"], "content": m["content"]} for m in messages] |
| user2 = {**user, "chat_id": new_id} |
| choices, _ = _chat_choices(user2) |
| return gr.update(choices=choices, value=new_id), history, user2, None |
|
|
|
|
| |
| import csv as _csv |
| import os as _os |
| import tempfile as _tempfile |
|
|
|
|
| def _is_admin(user: dict | None) -> bool: |
| """True if the user's name is in AGADVISOR_ADMINS (comma-separated env).""" |
| if not user: |
| return False |
| admins = {a.strip().lower() for a in _os.getenv("AGADVISOR_ADMINS", "").split(",") if a.strip()} |
| return user.get("username", "").lower() in admins |
|
|
|
|
| def _stats_md(user: dict | None) -> str: |
| """Counts panel: all-users for admins, own activity otherwise.""" |
| if not user: |
| return "" |
| try: |
| if _is_admin(user): |
| s = accounts.stats(None) |
| return (f"**π All users:** {s['feedback']} feedback Β· {s['queries']} queries Β· " |
| f"{s['users']} users \n_(admin view β the CSV export covers everyone)_") |
| s = accounts.stats(user["id"]) |
| return f"**π Your activity:** {s['feedback']} feedback Β· {s['queries']} queries" |
| except Exception: |
| logger.exception("stats failed") |
| return "" |
|
|
|
|
| def export_csv(user: dict | None): |
| """Write a feedback CSV (prompt/response/rating/comment) and return its path. |
| Admins export all users; everyone else exports only their own.""" |
| if not user: |
| return None |
| scope_all = _is_admin(user) |
| try: |
| rows = accounts.export_feedback(None if scope_all else user["id"]) |
| except Exception: |
| logger.exception("export_feedback failed") |
| return None |
| path = _tempfile.mktemp(prefix="agadvisor_feedback_", suffix=".csv") |
| with open(path, "w", newline="", encoding="utf-8") as f: |
| w = _csv.writer(f) |
| w.writerow(["username", "timestamp", "rating", "comment", "prompt", "response"]) |
| for r in rows: |
| ts = _time.strftime("%Y-%m-%d %H:%M:%S", _time.localtime(r.get("created_at") or 0)) |
| w.writerow([ |
| r.get("username", ""), ts, r.get("rating", ""), r.get("comment") or "", |
| r.get("prompt") or "", r.get("response") or "", |
| ]) |
| return path |
|
|
|
|
| def _refresh_sidebar(user: dict | None): |
| """Refresh the chat switcher + stats panel (chained after auth/answer/new-chat).""" |
| choices, cur = _chat_choices(user) |
| return gr.update(choices=choices, value=cur), _stats_md(user) |
|
|
|
|
| import base64 as _base64 |
| from pathlib import Path as _Path |
|
|
|
|
| def _logo_uri(name: str) -> str: |
| """Inline a logo as a base64 data URI (robust on Spaces β no static-path serving).""" |
| try: |
| b = (_Path(__file__).parent / "assets" / "logos" / name).read_bytes() |
| return "data:image/png;base64," + _base64.b64encode(b).decode() |
| except Exception: |
| logger.exception("Could not load logo %s", name) |
| return "" |
|
|
|
|
| |
| |
| |
| _LOGO_CARD = ("flex:1 1 0;display:flex;justify-content:center;align-items:center;" |
| "background:#fff;border-radius:12px;padding:12px 10px;box-shadow:0 1px 5px rgba(0,0,0,.14);") |
| _LOGO_IMG = "max-height:82px;max-width:100%;height:auto;width:auto;display:block;" |
|
|
| _LOGO_BANNER = f""" |
| <div class="agadvisor-logos" style="display:flex;gap:12px;align-items:stretch;padding:10px 0 6px;"> |
| <div style="{_LOGO_CARD}"> |
| <img src="__TRAC__" alt="Translational AI Center (TrAC)" style="{_LOGO_IMG}"/> |
| </div> |
| <div style="{_LOGO_CARD}"> |
| <img src="__COALESCE__" alt="COALESCE" style="{_LOGO_IMG}"/> |
| </div> |
| <div style="{_LOGO_CARD}"> |
| <img src="__AIIRA__" alt="AI Institute for Resilient Agriculture (AIIRA)" style="{_LOGO_IMG}"/> |
| </div> |
| </div> |
| """.replace("__TRAC__", _logo_uri("trac.png")) \ |
| .replace("__COALESCE__", _logo_uri("coalesce.png")) \ |
| .replace("__AIIRA__", _logo_uri("aiira.png")) |
|
|
|
|
| |
| |
| |
| |
| _MOBILE_CSS = """ |
| .gradio-container {max-width: 940px !important; margin: 0 auto !important;} |
| #agadvisor-chat {height: 460px !important;} |
| .theme-btn {min-width: 120px;} |
| .dark .agadvisor-logos > div {box-shadow: 0 0 0 1px rgba(255,255,255,.12) !important;} |
| @media (max-width: 700px) { |
| .gradio-container {padding: 8px !important;} |
| #agadvisor-chat {height: 60vh !important; min-height: 300px !important;} |
| .agadvisor-btns {flex-wrap: wrap !important; gap: 8px !important;} |
| .agadvisor-btns button {flex: 1 1 46% !important; min-height: 44px !important;} |
| } |
| """ |
|
|
|
|
| with gr.Blocks(title="AgAdvisor", theme=gr.themes.Soft(primary_hue="green"), css=_MOBILE_CSS) as demo: |
| user_state = gr.State(None) |
| last_msg_state = gr.State(None) |
|
|
| gr.HTML(_LOGO_BANNER) |
|
|
| gr.Markdown( |
| "# πΏ AgAdvisor\n" |
| "A CDMS pesticide-label assistant with weather, soil, and agronomic tools. " |
| "Answers include page-level citations.\n\n" |
| "> Pesticide labels are legally binding. This is a research prototype and is " |
| "**not** a substitute for reading the label. Always verify against the label of record." |
| ) |
|
|
| |
| with gr.Column(visible=True) as login_view: |
| gr.Markdown("### Sign in or create an account") |
| username = gr.Textbox(label="Username", autofocus=True) |
| password = gr.Textbox(label="Password", type="password") |
| with gr.Row(): |
| login_btn = gr.Button("Sign in", variant="primary") |
| signup_btn = gr.Button("Create account") |
| auth_error = gr.Markdown("") |
|
|
| |
| with gr.Column(visible=False) as chat_view: |
| with gr.Row(): |
| who = gr.Markdown("") |
| quota = gr.Markdown("") |
| |
| |
| theme_btn = gr.Button( |
| "π Light / Dark", scale=0, min_width=120, elem_classes=["theme-btn"] |
| ) |
| |
| with gr.Accordion("π¬ Chat sessions", open=False): |
| chat_selector = gr.Radio(choices=[], label="Your chats", value=None) |
| delete_chat_btn = gr.Button("ποΈ Delete selected chat", size="sm") |
| |
| |
| chatbot = gr.Chatbot(type="messages", height=460, label="AgAdvisor", |
| elem_id="agadvisor-chat") |
| |
| |
| with gr.Accordion("π Process steps", open=False) as steps_accordion: |
| steps_md = gr.Markdown("") |
| question = gr.Textbox( |
| placeholder="e.g. What is the application rate for Roundup on soybeans?", |
| label="Your question", |
| autofocus=True, |
| ) |
| with gr.Row(elem_classes=["agadvisor-btns"]): |
| send_btn = gr.Button("Ask", variant="primary") |
| new_chat_btn = gr.Button("New chat") |
| logout_btn = gr.Button("Log out") |
|
|
| |
| |
| with gr.Row(): |
| comment_box = gr.Textbox( |
| placeholder="Optional: tell us what was wrong or missing in the last answer", |
| label="Feedback comment", scale=4, |
| ) |
| feedback_btn = gr.Button("Submit feedback", scale=1) |
| feedback_status = gr.Markdown("") |
|
|
| |
| |
| with gr.Accordion("π Data export", open=False): |
| export_stats = gr.Markdown("") |
| with gr.Row(): |
| refresh_stats_btn = gr.Button("Refresh stats", size="sm") |
| download_btn = gr.Button("Download feedback CSV", size="sm") |
| export_file = gr.File(label="Feedback export (CSV)") |
|
|
| |
| with gr.Accordion("βοΈ Debug", open=False): |
| debug_toggle = gr.Checkbox(label="Show debug info for the last answer", value=False) |
| debug_md = gr.Markdown("", visible=False) |
|
|
| if PRODUCTS: |
| gr.Examples( |
| examples=[ |
| [f"What is the application rate for {p}?"] for p in PRODUCTS[:6] |
| ], |
| inputs=question, |
| label=f"Labels in the index ({len(PRODUCTS)} products)", |
| ) |
|
|
| login_btn.click( |
| do_login, [username, password], |
| [user_state, chatbot, login_view, chat_view, auth_error, who, quota, last_msg_state], |
| ).then(_refresh_sidebar, user_state, [chat_selector, export_stats]) |
| signup_btn.click( |
| do_signup, [username, password], |
| [user_state, chatbot, login_view, chat_view, auth_error, who, quota, last_msg_state], |
| ).then(_refresh_sidebar, user_state, [chat_selector, export_stats]) |
| logout_btn.click( |
| do_logout, None, |
| [user_state, chatbot, login_view, chat_view, auth_error, who, quota, last_msg_state], |
| ).then(_refresh_sidebar, user_state, [chat_selector, export_stats]) |
| new_chat_btn.click( |
| do_new_chat, user_state, [chatbot, user_state, last_msg_state] |
| ).then(_refresh_sidebar, user_state, [chat_selector, export_stats]) |
|
|
| |
| theme_btn.click(None, None, None, js="() => { document.body.classList.toggle('dark'); }") |
|
|
| |
| chat_selector.change(load_chat, [user_state, chat_selector], [chatbot, user_state, last_msg_state]) |
| delete_chat_btn.click( |
| delete_selected_chat, [user_state, chat_selector], |
| [chat_selector, chatbot, user_state, last_msg_state], |
| ) |
|
|
| |
| refresh_stats_btn.click(_stats_md, user_state, export_stats) |
| download_btn.click(export_csv, user_state, export_file) |
| debug_toggle.change(lambda on: gr.update(visible=on), debug_toggle, debug_md) |
|
|
| |
| chatbot.like(on_like, [user_state, last_msg_state], feedback_status) |
| feedback_btn.click( |
| submit_comment, [user_state, last_msg_state, comment_box], |
| [comment_box, feedback_status], |
| ) |
|
|
| for trigger in (send_btn.click, question.submit): |
| trigger( |
| on_submit, |
| [question, chatbot, user_state, last_msg_state], |
| [chatbot, question, quota, steps_md, steps_accordion, last_msg_state, debug_md], |
| ).then(_refresh_sidebar, user_state, [chat_selector, export_stats]) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch(server_name="0.0.0.0", server_port=7860) |
|
|