"""Gradio entrypoint for the Legislation Explainer app.""" from __future__ import annotations import html import time from pathlib import Path from uuid import uuid4 from typing import Any import gradio as gr from config import APP_DESCRIPTION, APP_TITLE, DEFAULT_PROVIDER from services.example_bills import example_bill_titles, example_bill_urls_by_title from services import ingest from services.providers import get_default_api_key, instantiate_client, list_providers, validate_api_key from services.rag_pipeline import ( AnalysisResult, answer_query_from_full_document, build_cached_document_artifacts, generate_analysis_progress, get_precomputed_example_artifacts, prepare_document_artifacts, warm_embedding_stack, ) def _provider_options() -> list[tuple[str, str, str]]: return [(config.name, config.display_name, config.instructions) for config in list_providers()] PROVIDER_OPTIONS = _provider_options() PROVIDER_LABELS = [display for _, display, _ in PROVIDER_OPTIONS] PROVIDER_BY_LABEL = {display: name for name, display, _ in PROVIDER_OPTIONS} PROVIDER_DETAILS = {name: description for name, _, description in PROVIDER_OPTIONS} PROVIDER_LABEL_BY_NAME = {name: display for name, display, _ in PROVIDER_OPTIONS} ANALYSIS_PLACEHOLDER = "Run an analysis to populate this section." EXAMPLE_BILL_LABELS = example_bill_titles() EXAMPLE_BILL_URLS = example_bill_urls_by_title() CHAT_STREAM_DELAY_SECONDS = 0.004 APP_THEME = gr.themes.Soft( primary_hue="sky", secondary_hue="slate", neutral_hue="stone", ).set( body_background_fill="linear-gradient(180deg, #f7fafc 0%, #edf4f7 100%)", block_background_fill="rgba(255, 255, 255, 0.88)", block_border_color="#d7e3ea", block_shadow="0 18px 48px rgba(15, 23, 42, 0.06)", button_primary_background_fill="linear-gradient(135deg, #7dd3fc 0%, #67e8f9 100%)", button_primary_background_fill_hover="linear-gradient(135deg, #38bdf8 0%, #22d3ee 100%)", button_primary_border_color="#7dd3fc", button_secondary_background_fill="#f8fafc", button_secondary_background_fill_hover="#eef6f8", ) APP_HEAD = """ """ APP_CSS = """ #app-shell { background: transparent; } #layout-shell { gap: 1.25rem; align-items: flex-start; } #topbar-row { justify-content: flex-start; align-items: center; margin-bottom: 0.75rem; } #sidebar-toggle-shell { width: auto; } #sidebar-toggle-shell > div { width: auto !important; } #sidebar-toggle-icon { display: inline-flex; align-items: center; justify-content: center; width: 2.75rem; height: 1.75rem; border: 1px solid var(--button-secondary-border-color); border-radius: 5px; background: var(--button-secondary-background-fill); cursor: pointer; transition: background-color 0.2s ease, border-color 0.2s ease; } #sidebar-toggle-icon:hover { background: var(--button-secondary-background-fill-hover); } #sidebar-toggle-icon svg { width: 1.35rem; height: 1.35rem; stroke: #6b7280; } #sidebar-toggle { display: none; } #sidebar-panel { max-width: 360px; position: sticky; top: 1rem; align-self: flex-start; border-radius: 16px; overflow: hidden; } #main-panel { min-width: 0; } #analysis-panel, #chat-panel { border: 1px solid var(--block-border-color); border-radius: 16px; padding: 1rem; backdrop-filter: blur(12px); } #analysis-header-row { align-items: center; justify-content: space-between; gap: 0.75rem; } #analysis-header-row .prose { margin: 0; } #rerun-summary-shell { flex: 0 0 auto; width: auto !important; min-width: 0 !important; } #analysis-output { min-height: 100px; min-width: 0; overflow-x: auto; } #analysis-output .prose, #analysis-output .md, #analysis-output .markdown { display: block; max-width: 100%; overflow-x: auto; padding-bottom: 0.35rem; } #analysis-output table { width: 100%; min-width: 760px; table-layout: fixed; border-collapse: collapse; } #analysis-output .analysis-table-scroll { display: block; width: 100%; max-width: 100%; overflow-x: auto; margin: 0.75rem 0 1.25rem; padding-bottom: 0.35rem; } #analysis-output .analysis-table-scroll table { margin: 0; } #analysis-output th, #analysis-output td { vertical-align: top; white-space: normal; word-break: normal; overflow-wrap: anywhere; } #rerun-summary-button { width: auto; min-width: 0; min-width: 2rem; width: 2rem; height: 2rem; padding: 0; border-radius: 0 !important; font-size: 0.95rem; line-height: 1; } #clear-analysis-button { width: auto; min-width: 0; min-width: 2rem; width: 2rem; height: 2rem; padding: 0; border-radius: 0 !important; font-size: 1rem; line-height: 1; } #chat-question-row { align-items: flex-start; } #ask-button button { border-radius: 0.6rem !important; } #status-output, #chat-status { min-height: 1.5rem; } #view-source-button { margin-top: 0.5rem; width: 100%; } #view-source-button button { width: 100%; min-width: 0; border-radius: 0.6rem !important; } #reset-button { width: 100%; } #reset-button button { width: 100%; min-width: 0; border-radius: 0.6rem !important; } #example-bills-heading { display: flex; align-items: center; gap: 0.35rem; margin-bottom: 0.35rem; } #example-bills-heading p { margin: 0; } #example-bills-info { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 1rem; height: 1rem; border: 1px solid #94a3b8; border-radius: 999px; color: #64748b; font-size: 0.72rem; font-weight: 700; line-height: 1; cursor: help; } #example-bills-info::after { content: attr(data-tooltip); position: absolute; left: calc(100% + 0.45rem); top: 50%; transform: translateY(-50%); width: 240px; padding: 0.6rem 0.7rem; border-radius: 0.6rem; background: rgba(15, 23, 42, 0.96); color: #f8fafc; font-size: 0.78rem; font-weight: 500; line-height: 1.35; opacity: 0; visibility: hidden; pointer-events: none; box-shadow: 0 12px 28px rgba(15, 23, 42, 0.22); transition: opacity 0.18s ease; z-index: 10; } #example-bills-info:hover::after, #example-bills-info:focus-visible::after { opacity: 1; visibility: visible; } #app-shell .label-wrap, #app-shell .label-wrap > *, #app-shell label, #app-shell legend, #app-shell [data-testid="block-label"] { background: transparent !important; box-shadow: none !important; } @media (max-width: 900px) { #layout-shell { flex-direction: column; } #sidebar-panel { max-width: none; width: 100%; position: static; } #main-panel { width: 100%; } } @media (max-width: 640px) { #analysis-panel, #chat-panel { padding: 0.8rem; } #analysis-output table { min-width: 760px; } #chat-question-row { flex-direction: column; } #chat-question-row > *, #ask-button, #ask-button button { width: 100% !important; min-width: 0 !important; } } """ APP_LAUNCH_KWARGS = { "theme": APP_THEME, "css": APP_CSS, "head": APP_HEAD, } _GRADIO_SESSION_CACHE: dict[str, dict[str, Any]] = {} def _empty_session() -> dict[str, Any]: return {"session_id": uuid4().hex} def _record_defaults() -> dict[str, Any]: return { "api_config": None, "analysis": None, "vector_store": None, "doc_text": None, "chat_history": [], "pending_deeper_question": None, "source_url": None, "message_queue": [], "is_answering": False, "active_message_id": None, "source_generation": 0, "is_analyzing": False, "active_analysis_generation": None, "analysis_cancelled_message": "", } def _session_record(session_state: dict[str, Any] | None) -> dict[str, Any]: session_state = session_state or _empty_session() session_id = session_state.get("session_id") if not session_id: session_id = uuid4().hex session_state["session_id"] = session_id record = _GRADIO_SESSION_CACHE.setdefault(session_id, _record_defaults()) for key, value in _record_defaults().items(): record.setdefault(key, value) return record def _replace_record(record: dict[str, Any], **updates: Any) -> dict[str, Any]: record.clear() record.update(_record_defaults()) record.update(updates) return record def _advance_source_generation(record: dict[str, Any]) -> int: next_generation = int(record.get("source_generation", 0)) + 1 record["source_generation"] = next_generation return next_generation def _analysis_is_current(record: dict[str, Any], generation: int) -> bool: return bool(record.get("is_analyzing")) and record.get("active_analysis_generation") == generation and record.get("source_generation") == generation def _committed_analysis_output(record: dict[str, Any]) -> str: payload = record.get("analysis") if not payload: return ANALYSIS_PLACEHOLDER try: return _format_analysis(AnalysisResult.model_validate(payload)) except Exception: # noqa: BLE001 return ANALYSIS_PLACEHOLDER def _resolve_provider(use_advanced: bool, provider_label: str | None) -> str: return DEFAULT_PROVIDER # Proposed future expansion: restore bring-your-own provider selection. # if not use_advanced: # return DEFAULT_PROVIDER # if not provider_label: # return DEFAULT_PROVIDER # return PROVIDER_BY_LABEL.get(provider_label, DEFAULT_PROVIDER) def _resolve_api_key(provider: str, use_advanced: bool, qwen_key: str | None, custom_key: str | None) -> str | None: return qwen_key or get_default_api_key(provider) # Proposed future expansion: restore bring-your-own provider API keys. # if not use_advanced and provider == DEFAULT_PROVIDER: # return qwen_key or get_default_api_key(provider) # return custom_key or get_default_api_key(provider) def _ingest_sources(file_path: str | None, url: str | None) -> str: chunks = [] if file_path: path = Path(file_path) chunks.append(ingest.ingest_file(path.name, path.read_bytes())) if url and url.strip(): chunks.append(ingest.fetch_url_content(url.strip())) if not chunks: raise ingest.IngestionError("Provide a file or URL to analyze.") return ingest.combine_sources(chunks) def _escape_md_cell(value: str) -> str: return str(value).replace("|", "\\|").replace("\n", " ").strip() def _escape_html_cell(value: str) -> str: return html.escape(str(value).replace("\n", " ").strip()) def _markdown_bullets(items: list[str]) -> str: if not items: return "-" return "\n".join(f"- {_escape_md_cell(item) or '—'}" for item in items) def _analysis_attr(analysis: AnalysisResult, field: str, default: Any) -> Any: return getattr(analysis, field, default) def _swot_attr(analysis: AnalysisResult, field: str) -> list[str]: swot = _analysis_attr(analysis, "swot", None) return getattr(swot, field, []) if swot is not None else [] def _format_html_table(headers: list[str], rows: list[list[str]]) -> str: table_rows = rows or [[""] * len(headers)] header_html = "".join(f"{_escape_html_cell(header) or '—'}" for header in headers) body_html = "".join( "" + "".join(f"{_escape_html_cell(cell) or '—'}" for cell in row) + "" for row in table_rows ) return ( '
' "" f"{header_html}" f"{body_html}" "
" "
" ) def _format_analysis(analysis: AnalysisResult | None) -> str: if analysis is None: return ANALYSIS_PLACEHOLDER executive_summary = _escape_md_cell(_analysis_attr(analysis, "executive_summary", "")) or "No executive summary returned." bill_summary = _markdown_bullets(_analysis_attr(analysis, "bill_summary", [])) implementation_rows = [ [ getattr(item, "stakeholder", ""), getattr(item, "obligation", ""), getattr(item, "implementation_burden", ""), getattr(item, "risk_or_note", ""), ] for item in _analysis_attr(analysis, "implementation", []) ] critique_rows = [ [ getattr(item, "issue", ""), getattr(item, "why_it_matters", ""), getattr(item, "recommendation", ""), ] for item in _analysis_attr(analysis, "critique", []) ] strengths = _swot_attr(analysis, "strengths") weaknesses = _swot_attr(analysis, "weaknesses") opportunities = _swot_attr(analysis, "opportunities") threats = _swot_attr(analysis, "threats") swot_pair_rows = [ [strengths[idx] if idx < len(strengths) else "", weaknesses[idx] if idx < len(weaknesses) else ""] for idx in range(max(len(strengths), len(weaknesses), 1)) ] swot_risk_rows = [ [opportunities[idx] if idx < len(opportunities) else "", threats[idx] if idx < len(threats) else ""] for idx in range(max(len(opportunities), len(threats), 1)) ] return "\n\n".join( [ "## Executive Summary\n" + executive_summary, "## Summary of the Bill\n" + bill_summary, "## Implementation Implications\n" + _format_html_table(["Stakeholder", "Obligation", "Burden", "Risk / Note"], implementation_rows), "## Critique\n" + _format_html_table(["Issue", "Why it matters", "Recommendation"], critique_rows), "## SWOT Analysis\n" + _format_html_table(["Strengths", "Weaknesses"], swot_pair_rows) + "\n\n" + _format_html_table(["Opportunities", "Threats"], swot_risk_rows), ] ) def _render_supporting_snippets(citations: list[dict[str, Any]]) -> str: if not citations: return "" items = [] for item in citations: ref_id = html.escape(str(item["ref_id"])) snippet = html.escape(item["snippet"]) items.append(f"
  • [ref{ref_id}] {snippet}
  • ") count = len(citations) label = "snippet" if count == 1 else "snippets" return ( f"
    Supporting {label} ({count})" f"" "
    " ) def _format_chat_entry(answer_text: str, citations: list[dict[str, Any]], *, provenance: str = "analysis_based") -> str: prefix = "" if provenance == "analysis_based": prefix = "_Based on the summary and analysis._\n\n" elif provenance == "full_document": prefix = "_Full-document answer._\n\n" if not citations: return prefix + answer_text return f"{prefix}{answer_text}\n\n{_render_supporting_snippets(citations)}" def _stream_chat_entry( base_history: list[dict[str, str]], answer_text: str, citations: list[dict[str, Any]], *, provenance: str = "analysis_based", ): prefix = "" if provenance == "analysis_based": prefix = "_Based on the summary and analysis._\n\n" elif provenance == "full_document": prefix = "_Deeper full-document answer._\n\n" formatted_answer = prefix + answer_text streamed_answer = "" for character in formatted_answer: streamed_answer += character yield base_history + [{"role": "assistant", "content": streamed_answer}] if CHAT_STREAM_DELAY_SECONDS: time.sleep(CHAT_STREAM_DELAY_SECONDS) if citations: yield base_history + [{"role": "assistant", "content": _format_chat_entry(answer_text, citations, provenance=provenance)}] def _stream_answer_content(answer_text: str, *, provenance: str = "analysis_based") -> Iterable[str]: prefix = "" if provenance == "analysis_based": prefix = "_Based on the summary and analysis._\n\n" elif provenance == "full_document": prefix = "_Full-document answer._\n\n" streamed_answer = "" for character in prefix + answer_text: streamed_answer += character yield streamed_answer if CHAT_STREAM_DELAY_SECONDS: time.sleep(CHAT_STREAM_DELAY_SECONDS) def _deeper_answer_updates(visible: bool, label: str = "Run deeper full-document answer") -> gr.update: return gr.update(visible=visible, value=label) def _analysis_action_updates(enabled: bool) -> tuple[gr.update, gr.update]: return ( gr.update(interactive=enabled), gr.update(interactive=enabled), ) def _analysis_stop_updates(visible: bool) -> gr.update: return gr.update(visible=visible, interactive=visible) def _answer_stop_updates(visible: bool) -> gr.update: return gr.update(visible=visible, interactive=visible) def _source_control_updates(enabled: bool) -> tuple[gr.update, gr.update, gr.update, gr.update, gr.update]: return ( gr.update(interactive=enabled), gr.update(interactive=enabled), gr.update(interactive=enabled), gr.update(interactive=enabled), gr.update(interactive=enabled), ) def _analysis_available(record: dict[str, Any]) -> bool: return bool(record.get("analysis")) def _question_placeholder_updates(record: dict[str, Any], *, clear_input: bool = False) -> tuple[gr.update, gr.update]: question_input_update: dict[str, Any] = {"interactive": True} if clear_input: question_input_update["value"] = "" return ( gr.update(**question_input_update), gr.update(interactive=True), ) def _combined_control_updates( record: dict[str, Any], *, source_enabled: bool, analysis_actions_enabled: bool, stop_analysis_visible: bool, stop_answer_visible: bool, ) -> tuple[gr.update, ...]: return ( *_source_control_updates(source_enabled), *_analysis_action_updates(analysis_actions_enabled), _analysis_stop_updates(stop_analysis_visible), *_question_placeholder_updates(record), _answer_stop_updates(stop_answer_visible), ) def _idle_control_updates(record: dict[str, Any]) -> tuple[gr.update, ...]: return _combined_control_updates( record, source_enabled=True, analysis_actions_enabled=_analysis_available(record), stop_analysis_visible=False, stop_answer_visible=False, ) def _analysis_busy_control_updates(record: dict[str, Any]) -> tuple[gr.update, ...]: return _combined_control_updates( record, source_enabled=False, analysis_actions_enabled=False, stop_analysis_visible=True, stop_answer_visible=False, ) def _answer_busy_control_updates(record: dict[str, Any]) -> tuple[gr.update, ...]: return _combined_control_updates( record, source_enabled=False, analysis_actions_enabled=False, stop_analysis_visible=bool(record.get("is_analyzing")), stop_answer_visible=True, ) def _summary_control_updates( record: dict[str, Any], *, source_enabled: bool, analysis_actions_enabled: bool, stop_analysis_visible: bool, ) -> tuple[gr.update, ...]: return ( *_source_control_updates(source_enabled), *_analysis_action_updates(analysis_actions_enabled), _analysis_stop_updates(stop_analysis_visible), ) def _summary_idle_control_updates(record: dict[str, Any]) -> tuple[gr.update, ...]: return _summary_control_updates( record, source_enabled=True, analysis_actions_enabled=_analysis_available(record), stop_analysis_visible=False, ) def _summary_busy_control_updates(record: dict[str, Any]) -> tuple[gr.update, ...]: return _summary_control_updates( record, source_enabled=False, analysis_actions_enabled=False, stop_analysis_visible=True, ) def _summary_frame( session_state: dict[str, Any], status: str, analysis_output: str, record: dict[str, Any], controls: tuple[gr.update, ...], *, deeper_visible: bool = False, deeper_hint: str = "", ) -> tuple[Any, ...]: return ( session_state, status, analysis_output, *controls, _deeper_answer_updates(deeper_visible), deeper_hint, ) def _analysis_frame( session_state: dict[str, Any], status: str, analysis_output: str, chatbot: list[dict[str, str]], chat_status: str, record: dict[str, Any], controls: tuple[gr.update, ...], *, deeper_visible: bool = False, deeper_hint: str = "", ) -> tuple[Any, ...]: return ( session_state, status, analysis_output, chatbot, chat_status, *controls, _deeper_answer_updates(deeper_visible), deeper_hint, ) def _chat_frame( chatbot: list[dict[str, str]], session_state: dict[str, Any], chat_status: str, record: dict[str, Any], controls: tuple[gr.update, ...], *, deeper_visible: bool = False, deeper_hint: str = "", ) -> tuple[Any, ...]: return ( chatbot, session_state, chat_status, _deeper_answer_updates(deeper_visible), deeper_hint, *controls, ) def _stage_question( question: str | None, session_state: dict[str, Any] | None, chat_history: list[dict[str, str]] | None, ) -> tuple[str, str, list[dict[str, str]], str, dict[str, Any]]: staged_question = question or "" session_state = session_state or _empty_session() record = _session_record(session_state) question_text = staged_question.strip() if not question_text: return "", "", (chat_history or _displayed_chat_history(record)), _queue_status_text(record), session_state if record.get("is_answering") or record.get("is_analyzing"): _enqueue_question(record, question_text) return "", "", _displayed_chat_history(record), _queue_status_text(record), session_state return question_text, "", (chat_history or _displayed_chat_history(record)), _queue_status_text(record), session_state def _chat_control_updates(record: dict[str, Any], *, is_busy: bool) -> tuple[gr.update, ...]: source_enabled = not is_busy and not bool(record.get("is_analyzing")) return ( *_source_control_updates(source_enabled), *_analysis_action_updates(_analysis_available(record) and source_enabled), *_question_placeholder_updates(record), ) def _queue_placeholder(status: str) -> str: if status == "answering": return "_Answering..._" if status == "failed": return "_Failed._" return "_Queued._" def _displayed_chat_history( record: dict[str, Any], *, answering_content: str | None = None, ) -> list[dict[str, str]]: history = list(record.get("chat_history", [])) queue: list[dict[str, Any]] = list(record.get("message_queue", [])) active_message_id = record.get("active_message_id") for index, item in enumerate(queue): history.append({"role": "user", "content": item["question"]}) placeholder = _queue_placeholder(item.get("status", "queued")) if index == 0 and item.get("id") == active_message_id: placeholder = answering_content or placeholder history.append({"role": "assistant", "content": placeholder}) return history def _queue_status_text(record: dict[str, Any]) -> str: queue: list[dict[str, Any]] = list(record.get("message_queue", [])) if not queue: return "" lines = ["Queued:"] lines.extend(f"{index}. {item['question']}" for index, item in enumerate(queue, start=1)) return "\n".join(lines) def _enqueue_question(record: dict[str, Any], question_text: str) -> None: queue = record.setdefault("message_queue", []) queue.append( { "id": uuid4().hex, "question": question_text, "status": "queued", "source_generation": record.get("source_generation", 0), } ) def _flush_message_queue(record: dict[str, Any], reason: str | None = None) -> str: had_pending = bool(record.get("message_queue")) or bool(record.get("active_message_id")) record["message_queue"] = [] record["is_answering"] = False record["active_message_id"] = None record["is_analyzing"] = False record["active_analysis_generation"] = None record["pending_deeper_question"] = None return reason if had_pending and reason else "" def _can_drain_message_queue(record: dict[str, Any]) -> bool: return ( bool(record.get("message_queue")) and not bool(record.get("is_analyzing")) and bool(record.get("api_config")) and bool(record.get("doc_text") or record.get("vector_store")) ) def _drain_message_queue(record: dict[str, Any]): while _can_drain_message_queue(record): current = record["message_queue"][0] question_generation = current.get("source_generation") if question_generation != record.get("source_generation"): return current["status"] = "answering" record["is_answering"] = True record["active_message_id"] = current["id"] current_message_id = current["id"] yield ( _displayed_chat_history(record), _queue_status_text(record), _answer_busy_control_updates(record), False, "", ) try: provider_client = instantiate_client( record["api_config"]["provider"], record["api_config"]["api_key"], ) answer = answer_query_from_full_document( provider_client, record.get("vector_store"), current["question"], doc_text=record.get("doc_text"), ) except Exception: # noqa: BLE001 if ( question_generation != record.get("source_generation") or record.get("active_message_id") != current_message_id or not record.get("message_queue") or record["message_queue"][0].get("id") != current_message_id ): return message = "We couldn't answer that question right now. Please try again." _show_error(message) record["chat_history"] = list(record.get("chat_history", [])) + [ {"role": "user", "content": current["question"]}, {"role": "assistant", "content": message}, ] record["message_queue"].pop(0) record["active_message_id"] = None record["is_answering"] = False yield ( _displayed_chat_history(record), message if not record.get("message_queue") else _queue_status_text(record), _idle_control_updates(record), False, "", ) continue for partial_answer in _stream_answer_content(answer.answer, provenance=answer.provenance): if ( question_generation != record.get("source_generation") or record.get("active_message_id") != current_message_id or not record.get("message_queue") or record["message_queue"][0].get("id") != current_message_id ): return yield ( _displayed_chat_history(record, answering_content=partial_answer), _queue_status_text(record), _answer_busy_control_updates(record), False, "", ) citations = [citation.model_dump() for citation in answer.citations] final_answer = _format_chat_entry(answer.answer, citations, provenance=answer.provenance) if ( question_generation != record.get("source_generation") or record.get("active_message_id") != current_message_id or not record.get("message_queue") or record["message_queue"][0].get("id") != current_message_id ): return record["chat_history"] = list(record.get("chat_history", [])) + [ {"role": "user", "content": current["question"]}, {"role": "assistant", "content": final_answer}, ] record["message_queue"].pop(0) record["active_message_id"] = None record["is_answering"] = False record["pending_deeper_question"] = None yield ( _displayed_chat_history(record), _queue_status_text(record), _idle_control_updates(record), False, "", ) # Proposed future expansion: restore bring-your-own provider help text and # controls when the app supports non-hackathon model/provider comparison again. # def _provider_help_text(provider_label: str | None, use_advanced: bool) -> str: # provider = _resolve_provider(use_advanced, provider_label) # return PROVIDER_DETAILS[provider] # # # def _toggle_provider_mode(use_advanced: bool) -> tuple[gr.update, gr.update, gr.update, gr.update]: # provider_value = PROVIDER_LABEL_BY_NAME[DEFAULT_PROVIDER] # return ( # gr.update(visible=not use_advanced), # gr.update(visible=use_advanced), # gr.update(visible=use_advanced), # gr.update(value=_provider_help_text(provider_value, use_advanced)), # ) def _set_example_url(example_label: str | None) -> str: if not example_label: return "" return EXAMPLE_BILL_URLS.get(example_label, "") def _handle_uploaded_source_change( uploaded_file: str | None, session_state: dict[str, Any] | None, ) -> tuple[dict[str, Any], gr.update, gr.update, gr.update, str | None, list[dict[str, str]], str, gr.update, str]: session_state = session_state or _empty_session() record = _session_record(session_state) _advance_source_generation(record) status_message = _flush_message_queue(record, "Queued questions were cleared because the source changed.") record["chat_history"] = [] if not uploaded_file: return ( session_state, gr.update(), gr.update(), gr.update(), None, _displayed_chat_history(record), status_message, _deeper_answer_updates(False), "", ) return ( session_state, gr.update(value=""), gr.update(value=None), _view_source_button_update(""), "file", _displayed_chat_history(record), status_message, _deeper_answer_updates(False), "", ) def _handle_url_source_change( url_value: str | None, session_state: dict[str, Any] | None, ) -> tuple[dict[str, Any], gr.update, gr.update, str | None, list[dict[str, str]], str, gr.update, str]: session_state = session_state or _empty_session() record = _session_record(session_state) _advance_source_generation(record) status_message = _flush_message_queue(record, "Queued questions were cleared because the source changed.") record["chat_history"] = [] cleaned_url = (url_value or "").strip() if not cleaned_url: return ( session_state, gr.update(), _view_source_button_update(""), None, _displayed_chat_history(record), status_message, _deeper_answer_updates(False), "", ) return ( session_state, gr.update(value=None), _view_source_button_update(cleaned_url), "url", _displayed_chat_history(record), status_message, _deeper_answer_updates(False), "", ) def _handle_example_source_change( example_label: str | None, session_state: dict[str, Any] | None, ) -> tuple[dict[str, Any], str, gr.update, str | None, list[dict[str, str]], str, gr.update, str]: session_state = session_state or _empty_session() record = _session_record(session_state) _advance_source_generation(record) status_message = _flush_message_queue(record, "Queued questions were cleared because the source changed.") record["chat_history"] = [] url_value = _set_example_url(example_label) return ( session_state, url_value, _view_source_button_update(url_value), ("url" if url_value else None), _displayed_chat_history(record), status_message, _deeper_answer_updates(False), "", ) def _view_source_button_update(url_value: str | None) -> gr.update: has_url = bool(url_value and url_value.strip()) return gr.update(value="View source ↗", interactive=has_url) def _analysis_stage_message(stage: str) -> str: return f"_Processing... {stage}_" def _show_warning(message: str) -> str: gr.Warning(message) return message def _show_error(message: str) -> str: gr.Error(message) return message def analyze_document( uploaded_file: str | None, url_value: str | None, use_advanced: bool, provider_label: str | None, qwen_key: str | None, custom_key: str | None, session_state: dict[str, Any] | None, source_kind: str | None = None, force_refresh: bool = False, ): session_state = session_state or _empty_session() record = _session_record(session_state) analysis_generation = _advance_source_generation(record) _flush_message_queue(record) record["is_analyzing"] = True record["active_analysis_generation"] = analysis_generation record["analysis_cancelled_message"] = "" record_api_config = record.get("api_config") or {} provider = record_api_config.get("provider") or _resolve_provider(use_advanced, provider_label) api_key = record_api_config.get("api_key") or _resolve_api_key(provider, use_advanced, qwen_key, custom_key) is_valid, error = validate_api_key(provider, api_key) if not is_valid: message = "Check the selected provider and API key, then try again." _show_warning(message) record["is_analyzing"] = False record["active_analysis_generation"] = None yield _summary_frame( session_state, message, ANALYSIS_PLACEHOLDER, record, _summary_idle_control_updates(record), ) return active_file = uploaded_file if source_kind != "url" else None active_url = url_value if source_kind != "file" else None precomputed = get_precomputed_example_artifacts(active_url) if precomputed is not None and not force_refresh: _replace_record( record, api_config={"provider": provider, "api_key": api_key}, analysis=precomputed.analysis.model_dump(), vector_store=precomputed.vector_store, doc_text=precomputed.document_text, source_url=active_url, source_generation=record.get("source_generation", 0), is_analyzing=False, active_analysis_generation=None, ) yield _summary_frame( session_state, "Loaded precomputed analysis for this example bill.", _format_analysis(precomputed.analysis), record, _summary_idle_control_updates(record), ) return return yield _summary_frame( session_state, "Generating analysis...", _analysis_stage_message("Loading and parsing document..."), record, _summary_busy_control_updates(record), ) try: document_text = _ingest_sources(active_file, active_url) except ingest.IngestionError as exc: if not _analysis_is_current(record, analysis_generation): return message = str(exc) _show_warning(message) record["is_analyzing"] = False record["active_analysis_generation"] = None yield _summary_frame( session_state, message, ANALYSIS_PLACEHOLDER, record, _summary_idle_control_updates(record), ) return except Exception: # noqa: BLE001 if not _analysis_is_current(record, analysis_generation): return message = "We couldn't load that document. Check the link or upload the file directly and try again." _show_error(message) record["is_analyzing"] = False record["active_analysis_generation"] = None yield _summary_frame( session_state, message, ANALYSIS_PLACEHOLDER, record, _summary_idle_control_updates(record), ) return if not _analysis_is_current(record, analysis_generation): return yield _summary_frame( session_state, "Generating analysis...", _analysis_stage_message("Preparing chunks and provider client..."), record, _summary_busy_control_updates(record), ) try: provider_client = instantiate_client(provider, api_key or "") except Exception: # noqa: BLE001 if not _analysis_is_current(record, analysis_generation): return message = "We couldn't connect to the selected model provider. Check your API key and settings, then try again." _show_error(message) record["is_analyzing"] = False record["active_analysis_generation"] = None yield _summary_frame( session_state, message, ANALYSIS_PLACEHOLDER, record, _summary_idle_control_updates(record), ) return if not _analysis_is_current(record, analysis_generation): return yield _summary_frame( session_state, "Generating analysis...", _analysis_stage_message("Building retrieval index..."), record, _summary_busy_control_updates(record), ) try: _, chunks, vector_store = prepare_document_artifacts(document_text) except Exception: # noqa: BLE001 if not _analysis_is_current(record, analysis_generation): return message = "We couldn't prepare the document for analysis. Please try again." _show_error(message) record["is_analyzing"] = False record["active_analysis_generation"] = None yield _summary_frame( session_state, message, ANALYSIS_PLACEHOLDER, record, _summary_idle_control_updates(record), ) return if not _analysis_is_current(record, analysis_generation): return try: analysis = AnalysisResult() for stage_message, partial in generate_analysis_progress(provider_client, document_text): if not _analysis_is_current(record, analysis_generation): return analysis = partial yield _summary_frame( session_state, stage_message, _format_analysis(partial), record, _summary_busy_control_updates(record), ) except Exception: # noqa: BLE001 if not _analysis_is_current(record, analysis_generation): return message = "We couldn't generate the bill analysis right now. Please try again." _show_error(message) record["is_analyzing"] = False record["active_analysis_generation"] = None yield _summary_frame( session_state, message, ANALYSIS_PLACEHOLDER, record, _summary_idle_control_updates(record), ) return if not _analysis_is_current(record, analysis_generation): return build_cached_document_artifacts( document_text=document_text, chunks=chunks, analysis=analysis, vector_store=vector_store, source_url=active_url, ) queued_questions = list(record.get("message_queue", [])) for item in queued_questions: item["source_generation"] = analysis_generation if item.get("status") == "answering": item["status"] = "queued" _replace_record( record, api_config={"provider": provider, "api_key": api_key}, analysis=analysis.model_dump(), vector_store=vector_store, doc_text=document_text, source_url=active_url, source_generation=record.get("source_generation", 0), message_queue=queued_questions, is_analyzing=False, active_analysis_generation=None, ) analysis_output = _format_analysis(analysis) yield _summary_frame( session_state, "", analysis_output, record, _summary_idle_control_updates(record), ) def _rerun_record_analysis( session_state: dict[str, Any], record: dict[str, Any], *, provider: str, api_key: str | None, ) -> Any: document_text = record.get("doc_text") if not document_text: return analysis_generation = _advance_source_generation(record) _flush_message_queue(record) record["is_analyzing"] = True record["active_analysis_generation"] = analysis_generation record["analysis_cancelled_message"] = "" yield _summary_frame( session_state, "Generating analysis...", _analysis_stage_message("Preparing provider client..."), record, _summary_busy_control_updates(record), ) try: provider_client = instantiate_client(provider, api_key or "") except Exception: # noqa: BLE001 if not _analysis_is_current(record, analysis_generation): return message = "We couldn't connect to the selected model provider. Check your API key and settings, then try again." _show_error(message) record["is_analyzing"] = False record["active_analysis_generation"] = None yield _summary_frame( session_state, message, ANALYSIS_PLACEHOLDER, record, _summary_idle_control_updates(record), ) return try: analysis = AnalysisResult() for stage_message, partial in generate_analysis_progress(provider_client, document_text): if not _analysis_is_current(record, analysis_generation): return analysis = partial yield _summary_frame( session_state, stage_message, _format_analysis(partial), record, _summary_busy_control_updates(record), ) except Exception: # noqa: BLE001 if not _analysis_is_current(record, analysis_generation): return message = "We couldn't generate the bill analysis right now. Please try again." _show_error(message) record["is_analyzing"] = False record["active_analysis_generation"] = None yield _summary_frame( session_state, message, ANALYSIS_PLACEHOLDER, record, _summary_idle_control_updates(record), ) return if not _analysis_is_current(record, analysis_generation): return queued_questions = list(record.get("message_queue", [])) for item in queued_questions: item["source_generation"] = analysis_generation if item.get("status") == "answering": item["status"] = "queued" record.update( { "api_config": {"provider": provider, "api_key": api_key}, "analysis": analysis.model_dump(), "chat_history": [], "pending_deeper_question": None, "message_queue": queued_questions, "is_analyzing": False, "active_analysis_generation": None, } ) analysis_output = _format_analysis(analysis) yield _summary_frame( session_state, "", analysis_output, record, _summary_idle_control_updates(record), ) def rerun_summary( uploaded_file: str | None, url_value: str | None, use_advanced: bool, provider_label: str | None, qwen_key: str | None, custom_key: str | None, session_state: dict[str, Any] | None, source_kind: str | None = None, ): session_state = session_state or _empty_session() record = _session_record(session_state) record_api_config = record.get("api_config") or {} provider = record_api_config.get("provider") or _resolve_provider(use_advanced, provider_label) api_key = record_api_config.get("api_key") or _resolve_api_key(provider, use_advanced, qwen_key, custom_key) is_valid, _error = validate_api_key(provider, api_key) if not is_valid: message = "Check the selected provider and API key, then try again." _show_warning(message) yield _summary_frame( session_state, message, ANALYSIS_PLACEHOLDER, record, _summary_idle_control_updates(record), ) return if record.get("doc_text"): yield from _rerun_record_analysis( session_state, record, provider=provider, api_key=api_key, ) return yield from analyze_document( uploaded_file, url_value, use_advanced, provider_label, qwen_key, custom_key, session_state, source_kind, force_refresh=True, ) def clear_analysis( session_state: dict[str, Any] | None, ): session_state = session_state or _empty_session() record = _session_record(session_state) _advance_source_generation(record) _flush_message_queue(record) record["analysis"] = None record["chat_history"] = [] record["pending_deeper_question"] = None return ( session_state, "", ANALYSIS_PLACEHOLDER, [], "", *_idle_control_updates(record), _deeper_answer_updates(False), "", ) def stop_analysis(session_state: dict[str, Any] | None): session_state = session_state or _empty_session() record = _session_record(session_state) _advance_source_generation(record) record["is_analyzing"] = False record["active_analysis_generation"] = None record["analysis_cancelled_message"] = "Analysis stopped. No new summary was saved." for item in record.get("message_queue", []): item["status"] = "queued" item["source_generation"] = None return _analysis_frame( session_state, record["analysis_cancelled_message"], _committed_analysis_output(record), _displayed_chat_history(record), _queue_status_text(record), record, _idle_control_updates(record), ) def stop_answer( session_state: dict[str, Any] | None, chat_history: list[dict[str, str]] | None, ): session_state = session_state or _empty_session() record = _session_record(session_state) history = list(record.get("chat_history") or []) queue = list(record.get("message_queue", [])) active_message_id = record.get("active_message_id") active_item = next((item for item in queue if item.get("id") == active_message_id), queue[0] if queue else None) if active_item: history.extend( [ {"role": "user", "content": active_item["question"]}, {"role": "assistant", "content": "_Answer cancelled._"}, ] ) record["chat_history"] = history record["message_queue"] = [] record["is_answering"] = False record["active_message_id"] = None record["pending_deeper_question"] = None controls = _analysis_busy_control_updates(record) if record.get("is_analyzing") else _idle_control_updates(record) return _chat_frame( _displayed_chat_history(record), session_state, "", record, controls, ) def _prepare_record_for_question( record: dict[str, Any], *, uploaded_file: str | None, url_value: str | None, use_advanced: bool, provider_label: str | None, qwen_key: str | None, custom_key: str | None, source_kind: str | None = None, ) -> str | None: if (record.get("doc_text") or record.get("vector_store")) and record.get("api_config"): return None provider = _resolve_provider(use_advanced, provider_label) api_key = _resolve_api_key(provider, use_advanced, qwen_key, custom_key) is_valid, _error = validate_api_key(provider, api_key) if not is_valid: return "Check the selected provider and API key, then try again." try: active_file = uploaded_file if source_kind != "url" else None active_url = url_value if source_kind != "file" else None document_text = _ingest_sources(active_file, active_url) except ingest.IngestionError as exc: return str(exc) except Exception: # noqa: BLE001 return "We couldn't load that document. Check the link or upload the file directly and try again." try: _, chunks, vector_store = prepare_document_artifacts(document_text) except Exception: # noqa: BLE001 return "We couldn't prepare the document for question answering. Please try again." _replace_record( record, api_config={"provider": provider, "api_key": api_key}, analysis=None, vector_store=vector_store, doc_text=document_text, source_url=active_url, source_generation=record.get("source_generation", 0), ) return None def ask_question( question: str | None, uploaded_file: str | None, url_value: str | None, use_advanced: bool, provider_label: str | None, qwen_key: str | None, custom_key: str | None, session_state: dict[str, Any] | None, chat_history: list[dict[str, str]] | None, source_kind: str | None = None, ): session_state = session_state or _empty_session() record = _session_record(session_state) chat_history = chat_history or _displayed_chat_history(record) if not question or not question.strip(): if _can_drain_message_queue(record): for chatbot_frame, chat_status, controls, deeper_visible, deeper_hint in _drain_message_queue(record): yield _chat_frame( chatbot_frame, session_state, chat_status, record, controls, deeper_visible=deeper_visible, deeper_hint=deeper_hint, ) return controls = _analysis_busy_control_updates(record) if record.get("is_analyzing") else _idle_control_updates(record) yield _chat_frame(chat_history, session_state, _queue_status_text(record), record, controls) return if record.get("is_analyzing"): _enqueue_question(record, question.strip()) yield _chat_frame( _displayed_chat_history(record), session_state, _queue_status_text(record), record, _analysis_busy_control_updates(record), ) return source_prep_error = _prepare_record_for_question( record, uploaded_file=uploaded_file, url_value=url_value, source_kind=source_kind, use_advanced=use_advanced, provider_label=provider_label, qwen_key=qwen_key, custom_key=custom_key, ) if source_prep_error is not None: yield _chat_frame(chat_history, session_state, source_prep_error, record, _idle_control_updates(record)) return question_text = question.strip() _enqueue_question(record, question_text) if record.get("is_answering"): yield _chat_frame( _displayed_chat_history(record), session_state, _queue_status_text(record), record, _answer_busy_control_updates(record), ) return for chatbot_frame, chat_status, controls, deeper_visible, deeper_hint in _drain_message_queue(record): yield _chat_frame( chatbot_frame, session_state, chat_status, record, controls, deeper_visible=deeper_visible, deeper_hint=deeper_hint, ) def drain_queued_questions_after_analysis( uploaded_file: str | None, url_value: str | None, use_advanced: bool, provider_label: str | None, qwen_key: str | None, custom_key: str | None, session_state: dict[str, Any] | None, chat_history: list[dict[str, str]] | None, source_kind: str | None = None, ): yield from ask_question( "", uploaded_file, url_value, use_advanced, provider_label, qwen_key, custom_key, session_state, chat_history, source_kind, ) def run_deeper_answer( session_state: dict[str, Any] | None, chat_history: list[dict[str, str]] | None, ): session_state = session_state or _empty_session() record = _session_record(session_state) chat_history = chat_history or [] if record.get("is_answering"): yield _chat_frame(chat_history, session_state, _queue_status_text(record), record, _answer_busy_control_updates(record)) return question_text = record.get("pending_deeper_question") if not question_text: yield _chat_frame(chat_history, session_state, "No pending question for deeper analysis.", record, _idle_control_updates(record)) return record["is_answering"] = True yield _chat_frame(chat_history, session_state, "Reading the full document for a deeper answer...", record, _answer_busy_control_updates(record)) try: provider_client = instantiate_client( record["api_config"]["provider"], record["api_config"]["api_key"], ) answer = answer_query_from_full_document( provider_client, record.get("vector_store"), question_text, doc_text=record.get("doc_text"), ) except Exception: # noqa: BLE001 message = "We couldn't generate the deeper full-document answer right now. Please try again." _show_error(message) record["is_answering"] = False yield _chat_frame( chat_history, session_state, message, record, _idle_control_updates(record), deeper_visible=True, deeper_hint="A deeper full-document answer is still available.", ) return citations = [citation.model_dump() for citation in answer.citations] for partial_history in _stream_chat_entry(chat_history, answer.answer, citations, provenance=answer.provenance): yield _chat_frame(partial_history, session_state, "", record, _answer_busy_control_updates(record)) chat_history = chat_history + [ {"role": "assistant", "content": _format_chat_entry(answer.answer, citations, provenance=answer.provenance)} ] record["chat_history"] = chat_history record["pending_deeper_question"] = None record["is_answering"] = False yield _chat_frame(chat_history, session_state, "", record, _idle_control_updates(record)) def reset_session(session_state: dict[str, Any] | None): if session_state and session_state.get("session_id"): _GRADIO_SESSION_CACHE.pop(session_state["session_id"], None) empty = _empty_session() empty_record = _session_record(empty) return ( empty, gr.update(value=None, interactive=True), gr.update(value=None, interactive=True), gr.update(value="", interactive=True), None, _view_source_button_update(""), ANALYSIS_PLACEHOLDER, [], "", "", gr.update(interactive=True), gr.update(interactive=True), *_analysis_action_updates(False), _analysis_stop_updates(False), *_question_placeholder_updates(empty_record), _answer_stop_updates(False), _deeper_answer_updates(False), "", ) def build_app() -> gr.Blocks: with gr.Blocks(title=APP_TITLE, elem_id="app-shell") as demo: session_state = gr.State(_empty_session()) source_kind = gr.State(None) queued_question = gr.State("") gr.Markdown(f"# {APP_TITLE}") gr.Markdown(APP_DESCRIPTION) with gr.Row(elem_id="topbar-row"): gr.HTML( """ """ ) with gr.Row(elem_id="layout-shell"): with gr.Column(scale=1, elem_id="sidebar-panel"): gr.Markdown("## Document Source") gr.Markdown("Upload PDF, DOCX, TXT, or MD") uploaded_file = gr.File(label="Upload PDF, DOCX, TXT, or MD", type="filepath", show_label=False) gr.HTML( """

    Example bills

    i
    """ ) example_selector = gr.Dropdown( label="Example bills", choices=EXAMPLE_BILL_LABELS, value=None, show_label=False, ) gr.Markdown("Document URL") url_value = gr.Textbox(label="Document URL", placeholder="https://example.com/bill", show_label=False) analyze_button = gr.Button("Run analysis", variant="primary") view_source_button = gr.Button("View source ↗", variant="secondary", interactive=False, elem_id="view-source-button") reset_button = gr.Button("Reset session", elem_id="reset-button") status_output = gr.Markdown(elem_id="status-output") use_advanced = gr.State(False) provider_label = gr.State(PROVIDER_LABEL_BY_NAME[DEFAULT_PROVIDER]) qwen_key = gr.State(None) custom_key = gr.State(None) # Proposed future expansion: restore the sidebar Model Settings # section after the hackathon build is no longer pinned to a # single built-in provider. # with gr.Accordion("Model Settings", open=False): # provider_help = gr.Markdown(PROVIDER_DETAILS[DEFAULT_PROVIDER]) # gr.Markdown("Hugging Face Token") # qwen_key = gr.Textbox( # label="Hugging Face Token", # type="password", # placeholder="Leave blank to use HF_TOKEN", # visible=True, # show_label=False, # ) # use_advanced = gr.Checkbox(label="Bring your own provider", value=False) # gr.Markdown("Model Provider") # provider_label = gr.Dropdown( # label="Model Provider", # choices=PROVIDER_LABELS, # value=PROVIDER_LABEL_BY_NAME[DEFAULT_PROVIDER], # visible=False, # show_label=False, # ) # gr.Markdown("API Key") # custom_key = gr.Textbox( # label="API Key", # type="password", # placeholder="Leave blank to use provider env var", # visible=False, # show_label=False, # ) with gr.Column(scale=2, elem_id="main-panel"): with gr.Group(elem_id="analysis-panel"): with gr.Row(elem_id="analysis-header-row"): with gr.Column(scale=1, min_width=0): gr.Markdown("# Summary Highlights") with gr.Column(scale=0, min_width=0, elem_id="rerun-summary-shell"): with gr.Row(): rerun_summary_button = gr.Button("↺", elem_id="rerun-summary-button", variant="secondary", interactive=False) clear_analysis_button = gr.Button("✕", elem_id="clear-analysis-button", variant="secondary", interactive=False) stop_analysis_button = gr.Button("Stop analysis", variant="stop", visible=False) analysis_output = gr.Markdown(ANALYSIS_PLACEHOLDER, elem_id="analysis-output") with gr.Group(elem_id="chat-panel"): gr.Markdown("## Ask Further Questions") chatbot = gr.Chatbot(show_label=False, autoscroll=False) gr.Markdown("Question") with gr.Row(elem_id="chat-question-row"): with gr.Column(scale=6, min_width=0): question_input = gr.Textbox( label="Question", placeholder="What would you like to know?", show_label=False, ) ask_button = gr.Button("Ask", scale=1, variant="primary", elem_id="ask-button") stop_answer_button = gr.Button("Stop answer", variant="stop", visible=False) chat_status = gr.Markdown(elem_id="chat-status") deeper_answer_button = gr.Button("Run deeper full-document answer", visible=False) deeper_answer_hint = gr.Markdown("") # Proposed future expansion: restore bring-your-own provider event wiring # when alternate provider controls are re-enabled. # use_advanced.change( # _toggle_provider_mode, # inputs=[use_advanced], # outputs=[qwen_key, provider_label, custom_key, provider_help], # ) # provider_label.change( # _provider_help_text, # inputs=[provider_label, use_advanced], # outputs=[provider_help], # ) example_selector.change( _handle_example_source_change, inputs=[example_selector, session_state], outputs=[session_state, url_value, view_source_button, source_kind, chatbot, chat_status, deeper_answer_button, deeper_answer_hint], ) uploaded_file.change( _handle_uploaded_source_change, inputs=[uploaded_file, session_state], outputs=[session_state, url_value, example_selector, view_source_button, source_kind, chatbot, chat_status, deeper_answer_button, deeper_answer_hint], ) url_value.input( _handle_url_source_change, inputs=[url_value, session_state], outputs=[session_state, uploaded_file, view_source_button, source_kind, chatbot, chat_status, deeper_answer_button, deeper_answer_hint], ) url_value.change( _handle_url_source_change, inputs=[url_value, session_state], outputs=[session_state, uploaded_file, view_source_button, source_kind, chatbot, chat_status, deeper_answer_button, deeper_answer_hint], ) analysis_event = analyze_button.click( analyze_document, inputs=[uploaded_file, url_value, use_advanced, provider_label, qwen_key, custom_key, session_state, source_kind], outputs=[ session_state, status_output, analysis_output, uploaded_file, example_selector, url_value, analyze_button, reset_button, rerun_summary_button, clear_analysis_button, stop_analysis_button, deeper_answer_button, deeper_answer_hint, ], ) analysis_drain_event = analysis_event.then( drain_queued_questions_after_analysis, inputs=[uploaded_file, url_value, use_advanced, provider_label, qwen_key, custom_key, session_state, chatbot, source_kind], outputs=[chatbot, session_state, chat_status, deeper_answer_button, deeper_answer_hint, uploaded_file, example_selector, url_value, analyze_button, reset_button, rerun_summary_button, clear_analysis_button, stop_analysis_button, question_input, ask_button, stop_answer_button], ) view_source_button.click( None, inputs=[url_value], js=""" (url) => { if (url && url.trim()) { window.open(url.trim(), "_blank", "noopener,noreferrer"); } } """, ) rerun_event = rerun_summary_button.click( rerun_summary, inputs=[uploaded_file, url_value, use_advanced, provider_label, qwen_key, custom_key, session_state, source_kind], outputs=[ session_state, status_output, analysis_output, uploaded_file, example_selector, url_value, analyze_button, reset_button, rerun_summary_button, clear_analysis_button, stop_analysis_button, deeper_answer_button, deeper_answer_hint, ], ) rerun_drain_event = rerun_event.then( drain_queued_questions_after_analysis, inputs=[uploaded_file, url_value, use_advanced, provider_label, qwen_key, custom_key, session_state, chatbot, source_kind], outputs=[chatbot, session_state, chat_status, deeper_answer_button, deeper_answer_hint, uploaded_file, example_selector, url_value, analyze_button, reset_button, rerun_summary_button, clear_analysis_button, stop_analysis_button, question_input, ask_button, stop_answer_button], ) clear_analysis_button.click( clear_analysis, inputs=[session_state], outputs=[ session_state, status_output, analysis_output, chatbot, chat_status, uploaded_file, example_selector, url_value, analyze_button, reset_button, rerun_summary_button, clear_analysis_button, stop_analysis_button, question_input, ask_button, stop_answer_button, deeper_answer_button, deeper_answer_hint, ], ) ask_click_event = ask_button.click( _stage_question, inputs=[question_input, session_state, chatbot], outputs=[queued_question, question_input, chatbot, chat_status, session_state], queue=False, trigger_mode="multiple", concurrency_limit=None, ).then( ask_question, inputs=[queued_question, uploaded_file, url_value, use_advanced, provider_label, qwen_key, custom_key, session_state, chatbot, source_kind], outputs=[chatbot, session_state, chat_status, deeper_answer_button, deeper_answer_hint, uploaded_file, example_selector, url_value, analyze_button, reset_button, rerun_summary_button, clear_analysis_button, stop_analysis_button, question_input, ask_button, stop_answer_button], ) ask_submit_event = question_input.submit( _stage_question, inputs=[question_input, session_state, chatbot], outputs=[queued_question, question_input, chatbot, chat_status, session_state], queue=False, trigger_mode="multiple", concurrency_limit=None, ).then( ask_question, inputs=[queued_question, uploaded_file, url_value, use_advanced, provider_label, qwen_key, custom_key, session_state, chatbot, source_kind], outputs=[chatbot, session_state, chat_status, deeper_answer_button, deeper_answer_hint, uploaded_file, example_selector, url_value, analyze_button, reset_button, rerun_summary_button, clear_analysis_button, stop_analysis_button, question_input, ask_button, stop_answer_button], ) deeper_answer_event = deeper_answer_button.click( run_deeper_answer, inputs=[session_state, chatbot], outputs=[chatbot, session_state, chat_status, deeper_answer_button, deeper_answer_hint, uploaded_file, example_selector, url_value, analyze_button, reset_button, rerun_summary_button, clear_analysis_button, stop_analysis_button, question_input, ask_button, stop_answer_button], ) stop_analysis_button.click( stop_analysis, inputs=[session_state], outputs=[ session_state, status_output, analysis_output, chatbot, chat_status, uploaded_file, example_selector, url_value, analyze_button, reset_button, rerun_summary_button, clear_analysis_button, stop_analysis_button, question_input, ask_button, stop_answer_button, deeper_answer_button, deeper_answer_hint, ], cancels=[analysis_event, rerun_event], queue=False, ) stop_answer_button.click( stop_answer, inputs=[session_state, chatbot], outputs=[chatbot, session_state, chat_status, deeper_answer_button, deeper_answer_hint, uploaded_file, example_selector, url_value, analyze_button, reset_button, rerun_summary_button, clear_analysis_button, stop_analysis_button, question_input, ask_button, stop_answer_button], cancels=[ analysis_event, analysis_drain_event, rerun_event, rerun_drain_event, ask_click_event, ask_submit_event, deeper_answer_event, ], queue=False, ) reset_button.click( reset_session, inputs=[session_state], outputs=[ session_state, uploaded_file, example_selector, url_value, source_kind, view_source_button, analysis_output, chatbot, status_output, chat_status, analyze_button, reset_button, rerun_summary_button, clear_analysis_button, stop_analysis_button, question_input, ask_button, stop_answer_button, deeper_answer_button, deeper_answer_hint, ], ) demo.queue() return demo if gr.NO_RELOAD: warm_embedding_stack() # Expose the default `demo` symbol so Hugging Face Spaces can launch app.py. demo = build_app() app = demo if __name__ == "__main__": app.launch(**APP_LAUNCH_KWARGS)