""" Climate Disclosure RAG ================================== Gradio Web Interface for sustainability report QA. Launch: python app.py """ import os import sys import json import time import re import html import inspect from urllib.parse import quote import gradio_client.utils as _gcu _orig_json_schema_fn = _gcu._json_schema_to_python_type def _safe_json_schema_to_python_type(schema, defs=None): if isinstance(schema, bool): return "Any" return _orig_json_schema_fn(schema, defs) _gcu._json_schema_to_python_type = _safe_json_schema_to_python_type import gradio as gr SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) if SCRIPT_DIR not in sys.path: sys.path.insert(0, SCRIPT_DIR) from rag_app_backend import ( build_uploaded_report_chunks, generate, run_rag, run_trustworthy_step1, run_trustworthy_step2, run_trustworthy_step3_claims, run_trustworthy_recluster, API_GEN_MODEL_ALIASES, list_reports, HAS_GPU, OPENAI_EMBED_MODELS, MOCK_MODE, REPORTS_DIR, get_report_chunks, retrieve, retrieve_from_report_chunks, ) # ======================== Constants ======================== PLACEHOLDER_SINGLE = ( "For 2022 Microsoft Environmental Sustainability Report, " "do the environmental/sustainability targets set by the company " "reference external climate change adaptation goals/targets?" ) PLACEHOLDER_MULTI = ( 'For "Does the company encourage downstream partners to carry out climate-related ' 'risk assessments?", is Boeing 2023 Sustainability Report better than ' 'AT&T 2022 Sustainability Summary in disclosure quality?' ) REPORTS_GITHUB_URL = "https://github.com/tobischimanski/ClimRetrieve/tree/main/Reports" CPU_EMBED_MODELS = [ "BM25", "text-embedding-3-large", "text-embedding-3-small", "text-embedding-ada-002", ] GPU_EMBED_MODELS = [ "Qwen3-Embedding-0.6B", "Qwen3-Embedding-4B", ] EMBED_MODELS = (CPU_EMBED_MODELS + GPU_EMBED_MODELS) if HAS_GPU else CPU_EMBED_MODELS GPU_GEN_MODELS = [ "Qwen3-4B-Instruct-2507-FP8", ] API_GEN_MODELS = list(API_GEN_MODEL_ALIASES.keys()) API_GEN_MODEL = API_GEN_MODELS[0] if API_GEN_MODELS else "GPT-5-mini (API)" GEN_MODELS = GPU_GEN_MODELS + API_GEN_MODELS if HAS_GPU else API_GEN_MODELS OPENAI_EMBED_MODELS_SET = set(OPENAI_EMBED_MODELS) DEFAULT_OPENAI_API_KEY = ( os.getenv("OPENAI_API_KEY", "").strip() or os.getenv("OPENAI_API_KEY_88996", "").strip() ) DEFAULT_GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "").strip() SOURCE_LIBRARY = "Sustainability Report Library" SOURCE_UPLOAD = "Upload Your Sustainability Reports" EVIDENCE_CANDIDATE_K = 5 def _is_gemini_generation_model(gen_model: str) -> bool: text = str(gen_model or "").strip().upper() return "GEMINI" in text # ======================== Helpers ======================== _pdf_page_count_cache = {} def _get_pdf_total_pages(report_name: str) -> int: if not report_name: return 1 if report_name in _pdf_page_count_cache: return _pdf_page_count_cache[report_name] pdf_path = os.path.join(REPORTS_DIR, report_name) total = 1 try: try: from pypdf import PdfReader except Exception: from PyPDF2 import PdfReader reader = PdfReader(pdf_path) total = max(1, len(reader.pages)) except Exception: total = 1 _pdf_page_count_cache[report_name] = total return total def _pdf_iframe(report_name: str, page: int = 1) -> str: pdf_path = os.path.abspath(os.path.join(REPORTS_DIR, report_name)).replace("\\", "/") pdf_url = f"/file={quote(pdf_path, safe='/:')}#page={max(1, int(page))}&view=FitH" return ( f'' ) def _format_evidence(contexts, highlight_keys=None, highlight_color: str = "#ff7043"): highlight_set = set(highlight_keys or []) medals = {0: "\U0001F947", 1: "\U0001F948", 2: "\U0001F949"} parts = [] for i, c in enumerate(contexts): badge = medals.get(i, f"#{i+1}") report_short = c["report"].replace(".pdf", "") score = c["score"] page = c.get("page", None) key = (str(c.get("report", "")), str(c.get("chunk_idx", ""))) text_body = str(c.get("text", ""))[:800] if key in highlight_set: safe_text = html.escape(text_body) text_body = ( f"
| Metric | {head_cols}
|---|
{_escape(conclusion.strip())}
" "| Attribute | " f"{header_cells}" "
|---|
{_escape(conclusion)}
| Attribute | {header_cells}
|---|
| Maturity Level | {''.join(maturity_cells)}
| Key Evidence | {''.join(evidence_cells)}
{_escape(conclusion)}
| Attribute | {header}
|---|
| Strength Score | {''.join(strength_cells)}
| Key Evidence | {''.join(evidence_cells)}
| Leaderboard Report | Score | Reason |
|---|
{_escape(conclusion)}
| Attribute | {header}
|---|
| Overall Strength | {''.join(strength_cells)}
| Key Evidence | {''.join(evidence_cells)}
{_escape(conclusion)}
| Checklist Item | {header}
|---|
| Summary | {''.join(summary_cells)}
| Key Evidence | {''.join(evidence_cells)}
{_escape(conclusion)}
| Bucket | {header}
|---|
| Coverage Level | {''.join(coverage_cells)}
| Key Evidence | {''.join(evidence_cells)}
| Key Evidence |
|---|
|
{_escape(conclusion)}
| Rule | Result | Note |
|---|
| Score Item | Value |
|---|---|
| consistent | {_escape(scores.get('consistent', 0))} |
| inconsistent | {_escape(scores.get('inconsistent', 0))} |
| insufficient | {_escape(scores.get('insufficient', 0))} |
| consistency_rate | {_escape(scores.get('consistency_rate', 'N/A'))} |
{_escape(conclusion)}
| Count Item | Value |
|---|---|
| explicit | {_escape(counts.get('explicit', 0))} ({_escape(percentages.get('explicit', 'N/A'))}%) |
| partial | {_escape(counts.get('partial', 0))} ({_escape(percentages.get('partial', 'N/A'))}%) |
| missing | {_escape(counts.get('missing', 0))} ({_escape(percentages.get('missing', 'N/A'))}%) |
| total | {_escape(counts.get('total', len(per_report)))} |
| Report | Label | Key Evidence |
|---|
' '\U0001f4a1 Tip: We recommend prefixing your question with the report name, ' 'e.g. "For [Report Name], does the company ...?"' '
' ), visible=True, ), ) return ( gr.update(placeholder=PLACEHOLDER_MULTI, value=""), gr.update( value=( '' '\U0001f4a1 Tip: We recommend prefixing your question with the report name, ' 'e.g. "For [Report 1 Name] and [Report 2 Name], does ...?"' '
' ), visible=True, ), ) def on_model_selection_change(gen_model, embed_model): use_api_gen = "(API)" in str(gen_model) use_gemini_gen = use_api_gen and _is_gemini_generation_model(gen_model) needs_openai_key = (str(embed_model) in OPENAI_EMBED_MODELS_SET) or (use_api_gen and not use_gemini_gen) needs_gemini_key = use_gemini_gen return ( gr.update(visible=needs_openai_key), gr.update(visible=needs_gemini_key), ) def on_source_mode_change(source_mode): is_upload = source_mode == SOURCE_UPLOAD if is_upload: return ( gr.update( value=( '' 'Upload one or more sustainability reports and ask questions over only those uploaded documents.' '
' ) ), gr.update(visible=True), gr.update(value="**No uploaded reports yet.**", visible=True), ) return ( gr.update( value=( '' 'Ask questions over the curated sustainability report collection used by ClimateRAG.' '
' ) ), gr.update(visible=False), gr.update(value="**No uploaded reports yet.**", visible=False), ) def _uploaded_file_name(file_obj) -> str: if isinstance(file_obj, str): return os.path.basename(file_obj) if isinstance(file_obj, dict): for key in ("orig_name", "name", "path"): value = file_obj.get(key) if isinstance(value, str) and value.strip(): return os.path.basename(value) for attr in ("orig_name", "name", "path"): value = getattr(file_obj, attr, None) if isinstance(value, str) and value.strip(): return os.path.basename(value) return "uploaded_report" def render_uploaded_files_summary(files): if not files: return gr.update(value="**No uploaded reports yet.**", visible=True) if not isinstance(files, list): files = [files] names = [] for file_obj in files: name = _uploaded_file_name(file_obj).strip() if name and name not in names: names.append(name) if not names: return gr.update(value="**No uploaded reports yet.**", visible=True) lines = [f"**Uploaded reports ({len(names)}):**", ""] lines.extend([f"- `{name}`" for name in names]) return gr.update(value="\n".join(lines), visible=True) def on_report_select(report_name): if not report_name: return ( "No report selected.
", 1, 1, "Page: 1 / 1", gr.update(interactive=False), gr.update(interactive=False), ) total = _get_pdf_total_pages(report_name) return ( _pdf_iframe(report_name, page=1), 1, total, f"Page: 1 / {total}", gr.update(interactive=False), gr.update(interactive=total > 1), ) def on_prev_page(report_name, current_page, total_pages): if not report_name: return ( "No report selected.
", 1, 1, "Page: 1 / 1", gr.update(interactive=False), gr.update(interactive=False), ) total = max(1, int(total_pages or 1)) page = max(1, int(current_page or 1) - 1) return ( _pdf_iframe(report_name, page=page), page, total, f"Page: {page} / {total}", gr.update(interactive=page > 1), gr.update(interactive=page < total), ) def on_next_page(report_name, current_page, total_pages): if not report_name: return ( "No report selected.
", 1, 1, "Page: 1 / 1", gr.update(interactive=False), gr.update(interactive=False), ) total = max(1, int(total_pages or 1)) page = min(total, max(1, int(current_page or 1) + 1)) return ( _pdf_iframe(report_name, page=page), page, total, f"Page: {page} / {total}", gr.update(interactive=page > 1), gr.update(interactive=page < total), ) def on_run_start(): return "## Retrieving evidence...", "", _render_waiting("Retrieving evidence...") def _has_openai_api_key(local_api_key: str) -> bool: if str(local_api_key or "").strip(): return True if os.getenv("OPENAI_API_KEY", "").strip(): return True if os.getenv("OPENAI_API_KEY_88996", "").strip(): return True return False def _has_gemini_api_key(local_api_key: str) -> bool: if str(local_api_key or "").strip(): return True if os.getenv("GEMINI_API_KEY", "").strip(): return True return False def _set_runtime_api_keys(openai_api_key: str, gemini_api_key: str): openai_key = str(openai_api_key or "").strip() gemini_key = str(gemini_api_key or "").strip() if openai_key: os.environ["OPENAI_API_KEY"] = openai_key if gemini_key: os.environ["GEMINI_API_KEY"] = gemini_key return openai_key, gemini_key def _backend_api_key_for_model(gen_model: str, openai_key: str, gemini_key: str) -> str: if _is_gemini_generation_model(gen_model): return gemini_key or openai_key return openai_key def _validate_retrieval_inputs(question, source_mode, uploaded_files, embed_model, openai_key): if not question or not str(question).strip(): return "\u26a0\ufe0f Please enter a question." if source_mode == SOURCE_UPLOAD and not uploaded_files: return "\u26a0\ufe0f Please upload at least one PDF or TXT sustainability report." if (str(embed_model) in OPENAI_EMBED_MODELS_SET) and (not _has_openai_api_key(openai_key)): return ( "\u26a0\ufe0f OpenAI embedding model selected but API key is missing. " "Please input API key or set OPENAI_API_KEY." ) return "" def _validate_generation_inputs(question, contexts, gen_model, openai_key, gemini_key): if not question or not str(question).strip(): return "\u26a0\ufe0f Please enter a question." if not isinstance(contexts, list) or not contexts: return "\u26a0\ufe0f Please retrieve evidence before generating an answer." if (not HAS_GPU) and ("(API)" not in str(gen_model)): return "\u26a0\ufe0f No GPU detected. Please use an API generation model." if "(API)" in str(gen_model): if _is_gemini_generation_model(gen_model): if not _has_gemini_api_key(gemini_key): return ( "\u26a0\ufe0f Gemini API generation model selected but API key is missing. " "Please input API key or set GEMINI_API_KEY." ) elif not _has_openai_api_key(openai_key): return ( "\u26a0\ufe0f OpenAI API generation model selected but API key is missing. " "Please input API key or set OPENAI_API_KEY." ) return "" def _evidence_choice_labels(contexts): labels = [] for i, c in enumerate(contexts if isinstance(contexts, list) else []): report = str(c.get("report", "")).replace(".pdf", "").strip() or "Unknown report" page = c.get("page", None) page_text = f", page {page}" if page not in (None, "", "NA") else "" chunk = c.get("chunk_idx", "") score = float(c.get("score", 0.0) or 0.0) labels.append(f"E{i + 1}: {report}{page_text}, chunk {chunk}, score {score:.4f}") return labels def _selected_evidence_ids(selected_labels): if not isinstance(selected_labels, list): selected_labels = [selected_labels] if selected_labels else [] ids = [] for label in selected_labels: m = re.match(r"\s*E(\d+)\s*:", str(label or "")) if not m: continue ids.append(int(m.group(1)) - 1) return ids def _contexts_from_selected_labels(contexts, selected_labels, selected_only: bool): contexts = contexts if isinstance(contexts, list) else [] if not selected_only: return contexts picked = [] for idx in _selected_evidence_ids(selected_labels): if 0 <= idx < len(contexts): picked.append(contexts[idx]) return picked def _feedback_query_suffix(feedback_text) -> str: feedback = str(feedback_text or "").strip() if not feedback: return "" return "\n\nUser feedback for evidence retrieval:\n" + feedback def _normalize_top_k(top_k) -> int: try: return max(1, int(top_k)) except Exception: return EVIDENCE_CANDIDATE_K def _retrieve_contexts_for_source( question, source_mode, uploaded_files, uploaded_chunks_state, rag_mode, embed_model, api_key, top_k=EVIDENCE_CANDIDATE_K, feedback_suffix="", ): query = str(question or "").strip() + str(feedback_suffix or "") base_top_k = _normalize_top_k(top_k) if source_mode == SOURCE_UPLOAD: report_chunks = uploaded_chunks_state if isinstance(uploaded_chunks_state, dict) and uploaded_chunks_state else None if report_chunks is None: report_chunks = build_uploaded_report_chunks(uploaded_files) contexts = retrieve_from_report_chunks( question=query, report_chunks=report_chunks, top_k=base_top_k, embed_name=embed_model, api_key=api_key, cache_namespace="uploaded_reports", ) return contexts, report_chunks chunk_mode = "structure" if str(rag_mode or "ClimateRAG") == "ClimateRAG" else "length" contexts = retrieve( question=query, chunk_mode=chunk_mode, doc_mode="multi", top_k=base_top_k, embed_name=embed_model, api_key=api_key, ) return contexts, {} def _render_retrieval_pipeline(contexts, source_mode, feedback_used=False): label = "uploaded sustainability reports" if source_mode == SOURCE_UPLOAD else "the sustainability report library" lines = [ "## STEP 1 - RETRIEVE EVIDENCE", f"- Source: **{label}**", f"- Retrieved **{len(contexts if isinstance(contexts, list) else [])}** evidence candidates", ] if feedback_used: lines.append("- User feedback was included in the retrieval query.") lines.append("") lines.append("Use the controls below to regenerate evidence, use all evidence, or select key evidence.") return "\n".join(lines) def do_retrieve_evidence( question, source_mode, uploaded_files, doc_mode_label, rag_mode, embed_model, gen_model, openai_api_key, gemini_api_key, top_k, ): empty_btns = _default_claim_button_updates() openai_key, gemini_key = _set_runtime_api_keys(openai_api_key, gemini_api_key) err = _validate_retrieval_inputs(question, source_mode, uploaded_files, embed_model, openai_key) if err: return err, "", err, "", "", [], [], *empty_btns, gr.update(choices=[], value=[]), {}, str(question or "") t0 = time.perf_counter() try: contexts, report_chunks = _retrieve_contexts_for_source( question=question, source_mode=source_mode, uploaded_files=uploaded_files, uploaded_chunks_state={}, rag_mode=rag_mode, embed_model=embed_model, api_key=openai_key, top_k=top_k, ) elapsed = time.perf_counter() - t0 evidence_md = _format_evidence(contexts) status = f"\u2705 Retrieved {len(contexts)} evidence candidates. Choose how to generate the answer." timing_md = f"\u23f1\ufe0f **Elapsed:** `{elapsed:.2f}s`" pipeline_md = _render_retrieval_pipeline(contexts, source_mode) return ( "*Evidence is ready. Choose an evidence action to generate the answer.*", evidence_md, status, timing_md, pipeline_md, contexts, [], *empty_btns, gr.update(choices=_evidence_choice_labels(contexts), value=_evidence_choice_labels(contexts)), report_chunks, str(question or "").strip(), ) except Exception as e: elapsed = time.perf_counter() - t0 err = f"\u26a0\ufe0f Evidence retrieval failed: {e}" return "*Evidence retrieval failed.*", "", err, f"\u23f1\ufe0f **Elapsed before failure:** `{elapsed:.2f}s`", err, [], [], *empty_btns, gr.update(choices=[], value=[]), {}, str(question or "") def do_regenerate_evidence( question, source_mode, uploaded_files, uploaded_chunks_state, rag_mode, embed_model, openai_api_key, evidence_feedback, top_k, ): empty_btns = _default_claim_button_updates() openai_key, _ = _set_runtime_api_keys(openai_api_key, "") err = _validate_retrieval_inputs(question, source_mode, uploaded_files or uploaded_chunks_state, embed_model, openai_key) if err: return "*Evidence regeneration failed.*", "", err, "", err, [], [], *empty_btns, gr.update(choices=[], value=[]), uploaded_chunks_state or {}, str(question or "") feedback_suffix = _feedback_query_suffix(evidence_feedback) t0 = time.perf_counter() try: contexts, report_chunks = _retrieve_contexts_for_source( question=question, source_mode=source_mode, uploaded_files=uploaded_files, uploaded_chunks_state=uploaded_chunks_state, rag_mode=rag_mode, embed_model=embed_model, api_key=openai_key, top_k=top_k, feedback_suffix=feedback_suffix, ) elapsed = time.perf_counter() - t0 evidence_md = _format_evidence(contexts) status = f"\u2705 Regenerated {len(contexts)} evidence candidates from feedback." timing_md = f"\u23f1\ufe0f **Elapsed:** `{elapsed:.2f}s`" pipeline_md = _render_retrieval_pipeline(contexts, source_mode, feedback_used=bool(feedback_suffix)) return ( "*Evidence was regenerated. Choose an evidence action to generate the answer.*", evidence_md, status, timing_md, pipeline_md, contexts, [], *empty_btns, gr.update(choices=_evidence_choice_labels(contexts), value=_evidence_choice_labels(contexts)), report_chunks, str(question or "").strip(), ) except Exception as e: elapsed = time.perf_counter() - t0 err = f"\u26a0\ufe0f Evidence regeneration failed: {e}" return "*Evidence regeneration failed.*", "", err, f"\u23f1\ufe0f **Elapsed before failure:** `{elapsed:.2f}s`", err, [], [], *empty_btns, gr.update(choices=[], value=[]), uploaded_chunks_state or {}, str(question or "") def do_generate_answer_from_evidence( question, doc_mode_label, rag_mode, gen_model, openai_api_key, gemini_api_key, contexts_state, selected_evidence, selected_only, ): empty_btns = _default_claim_button_updates() openai_key, gemini_key = _set_runtime_api_keys(openai_api_key, gemini_api_key) contexts = _contexts_from_selected_labels(contexts_state, selected_evidence, selected_only=bool(selected_only)) if bool(selected_only) and not contexts: msg = "\u26a0\ufe0f Please check at least one evidence item before generating from selected evidence." yield msg, _format_evidence(contexts_state or []), msg, "", msg, contexts_state or [], [], *empty_btns return err = _validate_generation_inputs(question, contexts, gen_model, openai_key, gemini_key) if err: yield err, _format_evidence(contexts_state or []), err, "", err, contexts_state or [], [], *empty_btns return backend_api_key = _backend_api_key_for_model(gen_model, openai_key, gemini_key) doc_mode = "single" if doc_mode_label == "Single-document" else "multi" q = str(question or "").strip() t0 = time.perf_counter() if str(rag_mode or "ClimateRAG") != "ClimateRAG": answer = generate( question=q, contexts=contexts, doc_mode=doc_mode, gen_model=gen_model, api_key=backend_api_key, ) elapsed = time.perf_counter() - t0 pipeline_md = ( "## Baseline RAG\n" f"- Generated from **{len(contexts)}** reviewed evidence item(s).\n" "- Single-step generation completed." ) yield ( _format_answer(answer), _format_evidence(contexts), f"\u2705 Baseline answer generated from {len(contexts)} evidence item(s).", f"\u23f1\ufe0f **Elapsed:** `{elapsed:.2f}s`", pipeline_md, contexts, [], *empty_btns, ) return answer_md = "*Generating answer from reviewed evidence...*" evidence_md = _format_evidence(contexts) pipeline_md = _render_waiting("STEP 1 Evidence clustering...") yield answer_md, evidence_md, "ClimateRAG generation started.", "", pipeline_md, contexts, [], *empty_btns try: reclustered = run_trustworthy_recluster( question=q, contexts=contexts, gen_model=gen_model, api_key=backend_api_key, ) step1 = { "contexts": contexts, "average_similarity": sum(float(c.get("score", 0.0) or 0.0) for c in contexts) / max(1, len(contexts)), "clusters": reclustered.get("clusters", []), "cluster_raw_output": reclustered.get("cluster_raw_output", ""), } step1_md = _render_step1_clusters_md(step1) pipeline_md = step1_md + "\n\n---\n\n" + _render_waiting("STEP 2 Generating answer...") yield answer_md, evidence_md, "STEP 1 completed. Running STEP 2...", "", pipeline_md, contexts, [], *empty_btns step2 = run_trustworthy_step2( question=q, doc_mode=doc_mode, contexts=contexts, clusters=step1.get("clusters", []), gen_model=gen_model, api_key=backend_api_key, ) answer_md = _format_answer(step2.get("answer", "")) step2_md = _render_step2_claims_md(step2) pipeline_md = step1_md + "\n\n---\n\n" + step2_md + "\n\n---\n\n" + _render_waiting("STEP 3 Extracting claims...") yield answer_md, evidence_md, "STEP 2 completed. Running STEP 3...", "", pipeline_md, contexts, [], *empty_btns step3 = run_trustworthy_step3_claims( question=q, answer=step2.get("answer", ""), contexts=contexts, doc_mode=doc_mode, gen_model=gen_model, api_key=backend_api_key, ) step2_summary_md = _render_step2_summary_md(step2) step3_md = _render_step3_md(step3) final_trace = _prepare_claim_trace(step3) btn_updates = _claim_button_updates(final_trace) elapsed = time.perf_counter() - t0 pipeline_md = step1_md + "\n\n---\n\n" + step2_summary_md + "\n\n---\n\n" + step3_md yield ( answer_md, evidence_md, f"\u2705 ClimateRAG answer generated from {len(contexts)} reviewed evidence item(s).", f"\u23f1\ufe0f **Elapsed:** `{elapsed:.2f}s`", pipeline_md, contexts, final_trace, *btn_updates, ) return except Exception as e: elapsed = time.perf_counter() - t0 err = f"\u26a0\ufe0f ClimateRAG generation failed: {e}" yield "*ClimateRAG generation failed.*", evidence_md, err, f"\u23f1\ufe0f **Elapsed before failure:** `{elapsed:.2f}s`", err, contexts, [], *empty_btns return def do_generate_answer_current( question, doc_mode_label, rag_mode, gen_model, openai_api_key, gemini_api_key, contexts_state, selected_evidence, ): yield from do_generate_answer_from_evidence( question, doc_mode_label, rag_mode, gen_model, openai_api_key, gemini_api_key, contexts_state, selected_evidence, selected_only=False, ) def do_generate_answer_selected( question, doc_mode_label, rag_mode, gen_model, openai_api_key, gemini_api_key, contexts_state, selected_evidence, ): yield from do_generate_answer_from_evidence( question, doc_mode_label, rag_mode, gen_model, openai_api_key, gemini_api_key, contexts_state, selected_evidence, selected_only=True, ) def do_query(question, doc_mode_label, rag_mode, embed_model, gen_model, openai_api_key, gemini_api_key, top_k): empty_btns = _default_claim_button_updates() empty_state_contexts = [] empty_state_trace = [] openai_key = str(openai_api_key or "").strip() gemini_key = str(gemini_api_key or "").strip() if not question or not question.strip(): yield "\u26a0\ufe0f Please enter a question.", "", "", "", "", empty_state_contexts, empty_state_trace, *empty_btns return if (not HAS_GPU) and ("(API)" not in str(gen_model)): msg = "\u26a0\ufe0f No GPU detected. Please use an API generation model." yield msg, "", msg, "", "", empty_state_contexts, empty_state_trace, *empty_btns return if (str(embed_model) in OPENAI_EMBED_MODELS_SET) and (not _has_openai_api_key(openai_key)): msg = ( "\u26a0\ufe0f OpenAI embedding model selected but API key is missing. " "Please input API key or set OPENAI_API_KEY." ) yield msg, "", msg, "", "", empty_state_contexts, empty_state_trace, *empty_btns return if "(API)" in str(gen_model): if _is_gemini_generation_model(gen_model): if not _has_gemini_api_key(gemini_key): msg = ( "\u26a0\ufe0f Gemini API generation model selected but API key is missing. " "Please input API key or set GEMINI_API_KEY." ) yield msg, "", msg, "", "", empty_state_contexts, empty_state_trace, *empty_btns return elif not _has_openai_api_key(openai_key): msg = ( "\u26a0\ufe0f OpenAI API generation model selected but API key is missing. " "Please input API key or set OPENAI_API_KEY." ) yield msg, "", msg, "", "", empty_state_contexts, empty_state_trace, *empty_btns return if openai_key: os.environ["OPENAI_API_KEY"] = openai_key if gemini_key: os.environ["GEMINI_API_KEY"] = gemini_key backend_api_key = openai_key if (not backend_api_key) and _is_gemini_generation_model(gen_model): backend_api_key = gemini_key doc_mode = "single" if doc_mode_label == "Single-document" else "multi" rag_mode = str(rag_mode or "ClimateRAG") q = question.strip() try: base_top_k = max(1, int(top_k)) except Exception: base_top_k = 5 t0 = time.perf_counter() if rag_mode != "ClimateRAG": answer, contexts = run_rag( question=q, chunk_mode="length", doc_mode=doc_mode, top_k=base_top_k, embed_name=embed_model, gen_model=gen_model, api_key=backend_api_key, ) elapsed = time.perf_counter() - t0 answer_md = _format_answer(answer) evidence_md = _format_evidence(contexts) status = f"\u2705 Baseline RAG complete: retrieved {len(contexts)} passages." timing_md = f"\u23f1\ufe0f **Elapsed:** `{elapsed:.2f}s`" pipeline_md = ( "## Baseline RAG\n" f"- Retrieved **{len(contexts)}** passages\n" "- Single-step retrieval + generation completed." ) yield answer_md, evidence_md, status, timing_md, pipeline_md, contexts, [], *empty_btns return answer_md = "*Waiting for STEP 2 answer...*" evidence_md = "*Waiting for retrieval...*" status = "⏳ ClimateRAG pipeline started." timing_md = "" pipeline_md = _render_waiting("STEP 1 Waiting......") yield answer_md, evidence_md, status, timing_md, pipeline_md, empty_state_contexts, empty_state_trace, *empty_btns try: # ---------- Step 1: retrieval + clustering ---------- step1 = run_trustworthy_step1( question=q, doc_mode=doc_mode, top_k=base_top_k, embed_name=embed_model, gen_model=gen_model, api_key=backend_api_key, ) step1_md = _render_step1_clusters_md(step1) evidence_md = _format_evidence(step1.get("contexts", [])) status = "⏳ STEP 1 completed. Running STEP 2..." pipeline_md = step1_md + "\n\n---\n\n" + _render_waiting("STEP 2 Waiting......") yield answer_md, evidence_md, status, "", pipeline_md, step1.get("contexts", []), empty_state_trace, *empty_btns # ---------- Step 2: answer generation ---------- step2 = run_trustworthy_step2( question=q, doc_mode=doc_mode, contexts=step1.get("contexts", []), clusters=step1.get("clusters", []), gen_model=gen_model, api_key=backend_api_key, ) answer_md = _format_answer(step2.get("answer", "")) step2_md = _render_step2_claims_md(step2) status = "⏳ STEP 2 completed. Running STEP 3..." pipeline_md = step1_md + "\n\n---\n\n" + step2_md + "\n\n---\n\n" + _render_waiting("STEP 3 Waiting......") yield answer_md, evidence_md, status, "", pipeline_md, step1.get("contexts", []), empty_state_trace, *empty_btns # ---------- Step 3: claim extractor ---------- step3 = run_trustworthy_step3_claims( question=q, answer=step2.get("answer", ""), contexts=step1.get("contexts", []), doc_mode=doc_mode, gen_model=gen_model, api_key=backend_api_key, ) step3_md = _render_step3_md(step3) step2_summary_md = _render_step2_summary_md(step2) final_trace = _prepare_claim_trace(step3) btn_updates = _claim_button_updates(final_trace) elapsed = time.perf_counter() - t0 status = ( "\u2705 ClimateRAG pipeline completed: " f"{len(step1.get('contexts', []))} passages, {len(step3.get('claims', []))} claims extracted." ) timing_md = f"\u23f1\ufe0f **Elapsed:** `{elapsed:.2f}s`" pipeline_md = step1_md + "\n\n---\n\n" + step2_summary_md + "\n\n---\n\n" + step3_md yield answer_md, evidence_md, status, timing_md, pipeline_md, step1.get("contexts", []), final_trace, *btn_updates return except Exception as e: elapsed = time.perf_counter() - t0 err = f"\u26a0\ufe0f ClimateRAG pipeline failed: {e}" timing_md = f"\u23f1\ufe0f **Elapsed before failure:** `{elapsed:.2f}s`" yield "*ClimateRAG pipeline failed.*", "", err, timing_md, f"{_render_waiting('Waiting......')}\n\n{err}", empty_state_contexts, empty_state_trace, *empty_btns return def build_report_name_list(): """Build report name list without requiring local PDF files.""" reports = list_reports() names = sorted({str(r.get("name", "")).strip() for r in reports if isinstance(r, dict) and str(r.get("name", "")).strip()}) if names: return names # Fallback to chunk JSON source names when Reports/ is removed. try: chunks = get_report_chunks("structure") names = sorted([str(x).strip() for x in chunks.keys() if str(x).strip()]) if names: return names except Exception: pass try: chunks = get_report_chunks("length") names = sorted([str(x).strip() for x in chunks.keys() if str(x).strip()]) if names: return names except Exception: pass return [] def render_report_names_md(names): if not names: return "_No report names found from local PDFs or chunk JSON sources._" lines = [f"### Report Names ({len(names)})", ""] lines.extend([f"- `{n}`" for n in names]) return "\n".join(lines) # ======================== CSS ======================== CUSTOM_CSS = """ :root { --font: "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important; } html, body, button, input, textarea, select { font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* Hide image toolbar buttons */ .gradio-image button, .gradio-image .absolute { display: none !important; } footer { display: none !important; } .built-with { display: none !important; } .hint-text { color: #666; font-size: 0.95em; margin-top: 2px; margin-bottom: 8px; width: 100%; max-width: none; line-height: 1.45; white-space: normal; } .logo-header { display: flex; align-items: center; justify-content: center; padding: 20px 16px 12px 16px; background: linear-gradient(180deg, #f0f7ff 0%, #ffffff 100%); border-radius: 12px; margin-bottom: 8px; min-height: 124px; } .logo-header-text { text-align: center; width: 100%; max-width: 900px; } .logo-header h2 { margin: 0 0 2px 0; color: #1a5276; font-size: 1.45em; letter-spacing: 0.02em; } .logo-header p { color: #666; font-size: 0.95em; margin: 0; } .mock-banner { background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; padding: 12px 20px; margin: 0 0 12px 0; color: #856404; font-size: 0.92em; } .custom-footer { text-align: center; padding: 14px 0; color: #bbb; font-size: 0.83em; border-top: 1px solid #eee; margin-top: 20px; } .waiting-banner { font-size: 2rem; font-weight: 700; color: #d35400; text-align: center; } .maturity-card { border: 1px solid #dbe7f3; border-radius: 12px; padding: 14px; background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%); } .maturity-card h3 { margin: 0 0 12px 0; color: #154360; } .maturity-card h4 { margin: 10px 0 6px 0; color: #1b4f72; } .maturity-table-wrap { overflow-x: auto; } .maturity-table { width: 100%; border-collapse: collapse; font-size: 0.95rem; } .maturity-table th, .maturity-table td { border: 1px solid #d6e4f0; padding: 10px; vertical-align: top; text-align: left; line-height: 1.45; } .maturity-table thead th { background: #eaf3ff; } .maturity-table tbody tr:nth-child(even) { background: #fbfdff; } .maturity-badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-weight: 600; font-size: 0.86rem; } .maturity-badge.level-high { background: #e8f8f0; color: #117864; } .maturity-badge.level-moderate { background: #fff4e5; color: #9c640c; } .maturity-badge.level-low { background: #fdecea; color: #922b21; } .maturity-badge.level-insufficient { background: #fdecea; color: #922b21; } .maturity-badge.level-unknown { background: #eef2f7; color: #34495e; } .maturity-list { margin: 0; padding-left: 18px; } .maturity-list li { margin: 0 0 6px 0; } .muted { color: #9aa5b1; } .maturity-conclusion { margin-top: 12px; border-top: 1px dashed #cad9e8; padding-top: 10px; } .maturity-conclusion h4 { margin: 0 0 6px 0; color: #1b4f72; } .maturity-conclusion p { margin: 0; } .maturity-note { margin-top: 10px; padding: 8px 10px; border: 1px dashed #cad9e8; border-radius: 8px; color: #516274; background: #f8fbff; } .metric-chip-wrap { margin-top: 10px; } .metric-chip { display: inline-block; margin: 4px 6px 0 0; padding: 4px 10px; border-radius: 999px; border: 1px solid #c8ddf2; background: #edf5ff; color: #1b4f72; font-size: 0.84rem; } .metric-chip.trend-up { background: #e8f8f0; border-color: #bfe8d3; color: #117864; } .metric-chip.trend-down { background: #fdecea; border-color: #f3c6c2; color: #922b21; } .metric-chip.trend-flat { background: #fff4e5; border-color: #f2ddba; color: #9c640c; } .metric-chip.trend-unknown { background: #eef2f7; border-color: #d6dce3; color: #5d6d7e; } .maturity-split { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 10px; } .confidence-card { margin-top: 12px; border: 1px solid #dbe7f3; border-radius: 10px; padding: 10px 12px; background: #f8fbff; } .confidence-card h4 { margin: 0 0 8px 0; color: #1b4f72; } .conf-row { display: grid; grid-template-columns: 70px 1fr 60px; gap: 8px; align-items: center; margin: 6px 0; } .conf-label { font-weight: 600; color: #34495e; } .conf-bar { height: 10px; border-radius: 999px; background: #e9eff6; overflow: hidden; } .conf-fill { height: 100%; border-radius: 999px; } .conf-fill.conf-high { background: #27ae60; } .conf-fill.conf-medium { background: #f39c12; } .conf-fill.conf-low { background: #e74c3c; } .conf-value { text-align: right; color: #566573; font-variant-numeric: tabular-nums; } #upload_reports_box { position: relative !important; min-height: 230px; } #upload_reports_box::after { content: "Drop files here\\00000A- or -\\00000AClick to upload"; white-space: pre-line; position: absolute; z-index: 9999; left: 12px; right: 12px; top: 38px; bottom: 12px; min-height: 170px; display: flex; align-items: center; justify-content: center; text-align: center; border: 1px dashed #9ca3af; border-radius: 8px; background: #ffffff; color: #374151; font-size: 0.95rem; line-height: 1.45; font-weight: 600; pointer-events: none; } #upload_reports_box:hover::after { border-color: #2563eb; background: #f8fbff; } #upload_reports_box[data-has-files="true"]::after { display: none !important; } @media (max-width: 900px) { .logo-header { min-height: auto; padding-top: 12px; padding-bottom: 12px; flex-direction: column; } .maturity-split { grid-template-columns: 1fr; } } """ CUSTOM_JS = r""" () => { const replacements = [ ["\u5c06\u6587\u4ef6\u62d6\u62fd\u5230\u6b64\u5904", "Drop files here"], ["\u5c06\u6587\u4ef6\u62d6\u653e\u5230\u6b64\u5904", "Drop files here"], ["\u5c06\u6587\u4ef6\u62d6\u62fd\u5230\u8fd9\u91cc", "Drop files here"], ["\u5c06\u6587\u4ef6\u62d6\u653e\u5230\u8fd9\u91cc", "Drop files here"], ["\u62d6\u62fd\u6587\u4ef6\u81f3\u6b64\u5904", "Drop files here"], ["\u62d6\u653e\u6587\u4ef6\u81f3\u6b64\u5904", "Drop files here"], ["\u62d6\u653e\u6587\u4ef6\u5230\u6b64\u5904", "Drop files here"], ["\u62d6\u62fd\u6587\u4ef6\u5230\u6b64\u5904", "Drop files here"], ["\u62d6\u62fd\u6587\u4ef6\u5230\u8fd9\u91cc", "Drop files here"], ["\u62d6\u653e\u6587\u4ef6\u5230\u8fd9\u91cc", "Drop files here"], ["\u70b9\u51fb\u4e0a\u4f20", "Click to upload"], ["\u5355\u51fb\u4e0a\u4f20", "Click to upload"], ["\u4e0a\u4f20\u6587\u4ef6", "Upload files"], ["- \u6216 -", "- or -"], ["\u6216", "or"], ["\u6e05\u9664", "Clear"], ]; const replaceText = (value) => { let text = value || ""; for (const [source, target] of replacements) { text = text.split(source).join(target); } return text; }; const normalizeUploadText = () => { const roots = Array.from(document.querySelectorAll("#upload_reports_box, [id='upload_reports_box']")); for (const root of roots) { const hasInputFiles = Array.from(root.querySelectorAll("input[type='file']")) .some((input) => input.files && input.files.length > 0); const hasRenderedFiles = /\.(pdf|txt)\b/i.test(root.textContent || ""); root.setAttribute("data-has-files", (hasInputFiles || hasRenderedFiles) ? "true" : "false"); const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode); for (const node of nodes) { const nextValue = replaceText(node.nodeValue || ""); if (nextValue !== node.nodeValue) node.nodeValue = nextValue; } const attrs = ["aria-label", "title", "placeholder", "data-testid", "data-label"]; for (const el of root.querySelectorAll("*")) { for (const attr of attrs) { if (!el.hasAttribute(attr)) continue; const oldValue = el.getAttribute(attr) || ""; const newValue = replaceText(oldValue); if (newValue !== oldValue) el.setAttribute(attr, newValue); } } } const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); const globalNodes = []; while (walker.nextNode()) globalNodes.push(walker.currentNode); for (const node of globalNodes) { const raw = node.nodeValue || ""; let nextValue = raw; for (const [source, target] of replacements) { if (nextValue.includes(source)) { nextValue = nextValue.split(source).join(target); } } if (nextValue !== raw) node.nodeValue = nextValue; } }; normalizeUploadText(); const observer = new MutationObserver(normalizeUploadText); observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true }); window.setInterval(normalizeUploadText, 500); } """ # ======================== Gradio UI ======================== def _launch_supports(name: str) -> bool: try: return name in inspect.signature(gr.Blocks.launch).parameters except Exception: return False def _blocks_kwargs(): kwargs = { "title": "Climate Disclosure RAG", "analytics_enabled": False, } # Gradio 6 moved css/js/theme to launch(); Gradio 4 expects them here. if not (_launch_supports("css") and _launch_supports("js") and _launch_supports("theme")): kwargs.update( css=CUSTOM_CSS, js=CUSTOM_JS, theme=gr.themes.Soft(primary_hue="blue", secondary_hue="slate"), ) return kwargs with gr.Blocks(**_blocks_kwargs()) as demo: # ---------- Header ---------- gr.HTML( f"""AI-powered analysis of corporate sustainability & climate disclosures
' 'Ask questions over the curated sustainability report collection used by ClimateRAG.' '
' ) upload_files = gr.File( label="Upload PDF/TXT Reports", file_count="multiple", file_types=[".pdf", ".txt"], visible=False, elem_id="upload_reports_box", ) uploaded_files_md = gr.Markdown("**No uploaded reports yet.**", visible=False) with gr.Row(): doc_mode_radio = gr.Radio( choices=["Multi-document", "Single-document"], value="Multi-document", label="Question Type", info="Single: ask about one report | Multi: compare across reports", ) single_hint = gr.Markdown( '' '\U0001f4a1 Tip: We recommend prefixing your question with the report name, ' 'e.g. "For [Report 1 Name] and [Report 2 Name], does ...?"' '
', visible=True, ) question_box = gr.Textbox( label="Your Question", placeholder=PLACEHOLDER_MULTI, lines=3, max_lines=6, info='Please click "Use Example Question" to use the recommended question.', ) use_example_btn = gr.Button("Use Example Question", variant="primary") gr.Markdown("#### \u2699\ufe0f Model Configuration") with gr.Row(): with gr.Column(scale=1): rag_mode_dd = gr.Dropdown( choices=["ClimateRAG", "Baseline RAG"], value="ClimateRAG", label="RAG Mode", ) with gr.Column(scale=1): embed_model_dd = gr.Dropdown( choices=EMBED_MODELS, value=EMBED_MODELS[0], label="\U0001f9e0 Embedding Model", ) with gr.Column(scale=1): gen_model_dd = gr.Dropdown( choices=GEN_MODELS, value=(GEN_MODELS[0] if HAS_GPU else API_GEN_MODEL), label="\U0001f916 Generation Model", ) if not HAS_GPU: gr.Markdown( "GPU not detected: local generation models are disabled. " "Only API generation models are available." ) gr.Markdown( "Disabled (GPU-only): " + ", ".join(GPU_GEN_MODELS) + "" ) default_gen_model = GEN_MODELS[0] if HAS_GPU else API_GEN_MODEL default_embed_model = EMBED_MODELS[0] default_need_openai_key = ( (default_embed_model in OPENAI_EMBED_MODELS_SET) or (("(API)" in str(default_gen_model)) and (not _is_gemini_generation_model(default_gen_model))) ) default_need_gemini_key = ("(API)" in str(default_gen_model)) and _is_gemini_generation_model(default_gen_model) openai_api_key_box = gr.Textbox( label="\U0001f511 OpenAI API Key", type="password", placeholder="sk-...", value=DEFAULT_OPENAI_API_KEY, visible=default_need_openai_key, info="Required for OpenAI embedding models and OpenAI API generation models.", ) gemini_api_key_box = gr.Textbox( label="\U0001f511 Gemini API Key", type="password", placeholder="AIza...", value=DEFAULT_GEMINI_API_KEY, visible=default_need_gemini_key, info="Required for Gemini API generation models.", ) top_k_slider = gr.Slider( minimum=1, maximum=20, value=5, step=1, label="\U0001f3af Top-K Retrieved Passages", ) submit_btn = gr.Button("\U0001f50e Retrieve Evidence", variant="primary", size="lg") status_md = gr.Markdown("") timing_md = gr.Markdown("") with gr.Row(): with gr.Column(scale=1): gr.Markdown("#### ClimateRAG Pipeline") pipeline_md = gr.Markdown( value="*Three-step ClimateRAG pipeline output will appear here after Run.*", sanitize_html=False, ) gr.Markdown( "#### Evidence Actions\n" "After Step 1 retrieves the selected number of evidence candidates, either give feedback to retrieve better evidence, " "generate from all retrieved evidence, or check only the key evidence items below." ) evidence_feedback_box = gr.Textbox( label="Evidence Feedback", placeholder=( "Feedback is appended to the retrieval query. Example: Prefer evidence with explicit " "targets, years, metrics, or page-specific climate risk disclosures." ), lines=2, max_lines=4, ) regenerate_evidence_btn = gr.Button("Regenerate Evidence from Feedback") selected_evidence_cbg = gr.CheckboxGroup( choices=[], value=[], label="Select evidence for answer", info="Check evidence items here, then click Generate Answer from Selected Evidence.", ) with gr.Row(): generate_current_btn = gr.Button("Generate Answer from Current Evidence", variant="primary") generate_selected_btn = gr.Button("Generate Answer from Selected Evidence", variant="primary") gr.Markdown("#### Generated Answer") answer_box = gr.Markdown( value="*Answer will appear here after you retrieve evidence and choose an evidence action.*", sanitize_html=False, ) gr.Markdown("#### Claim Trace (Click to Highlight Evidence)") with gr.Row(): claim_btn_1 = gr.Button("Claim 1", visible=False) claim_btn_2 = gr.Button("Claim 2", visible=False) claim_btn_3 = gr.Button("Claim 3", visible=False) clear_highlight_btn = gr.Button("Clear Highlight", visible=True) with gr.Column(scale=1): gr.Markdown("#### Retrieved Evidence") evidence_box = gr.Markdown( value="*Evidence will appear here after you click Retrieve Evidence.*", sanitize_html=False, ) contexts_state = gr.State([]) claim_trace_state = gr.State([]) uploaded_chunks_state = gr.State({}) retrieved_question_state = gr.State("") # ---- Wiring ---- source_mode_radio.change( fn=on_source_mode_change, inputs=[source_mode_radio], outputs=[source_mode_hint, upload_files, uploaded_files_md], queue=False, ) upload_files.change( fn=render_uploaded_files_summary, inputs=[upload_files], outputs=[uploaded_files_md], queue=False, ) doc_mode_radio.change( fn=on_doc_mode_change, inputs=[doc_mode_radio], outputs=[question_box, single_hint], ) use_example_btn.click( fn=lambda mode: PLACEHOLDER_SINGLE if mode == "Single-document" else PLACEHOLDER_MULTI, inputs=[doc_mode_radio], outputs=[question_box], ) gen_model_dd.change( fn=on_model_selection_change, inputs=[gen_model_dd, embed_model_dd], outputs=[openai_api_key_box, gemini_api_key_box], queue=False, ) embed_model_dd.change( fn=on_model_selection_change, inputs=[gen_model_dd, embed_model_dd], outputs=[openai_api_key_box, gemini_api_key_box], queue=False, ) demo.load( fn=on_model_selection_change, inputs=[gen_model_dd, embed_model_dd], outputs=[openai_api_key_box, gemini_api_key_box], queue=False, ) submit_btn.click( fn=on_run_start, outputs=[status_md, timing_md, pipeline_md], queue=False, ).then( fn=do_retrieve_evidence, inputs=[ question_box, source_mode_radio, upload_files, doc_mode_radio, rag_mode_dd, embed_model_dd, gen_model_dd, openai_api_key_box, gemini_api_key_box, top_k_slider, ], outputs=[ answer_box, evidence_box, status_md, timing_md, pipeline_md, contexts_state, claim_trace_state, claim_btn_1, claim_btn_2, claim_btn_3, selected_evidence_cbg, uploaded_chunks_state, retrieved_question_state, ], ) regenerate_evidence_btn.click( fn=do_regenerate_evidence, inputs=[ question_box, source_mode_radio, upload_files, uploaded_chunks_state, rag_mode_dd, embed_model_dd, openai_api_key_box, evidence_feedback_box, top_k_slider, ], outputs=[ answer_box, evidence_box, status_md, timing_md, pipeline_md, contexts_state, claim_trace_state, claim_btn_1, claim_btn_2, claim_btn_3, selected_evidence_cbg, uploaded_chunks_state, retrieved_question_state, ], ) generate_current_btn.click( fn=do_generate_answer_current, inputs=[ retrieved_question_state, doc_mode_radio, rag_mode_dd, gen_model_dd, openai_api_key_box, gemini_api_key_box, contexts_state, selected_evidence_cbg, ], outputs=[ answer_box, evidence_box, status_md, timing_md, pipeline_md, contexts_state, claim_trace_state, claim_btn_1, claim_btn_2, claim_btn_3, ], ) generate_selected_btn.click( fn=do_generate_answer_selected, inputs=[ retrieved_question_state, doc_mode_radio, rag_mode_dd, gen_model_dd, openai_api_key_box, gemini_api_key_box, contexts_state, selected_evidence_cbg, ], outputs=[ answer_box, evidence_box, status_md, timing_md, pipeline_md, contexts_state, claim_trace_state, claim_btn_1, claim_btn_2, claim_btn_3, ], ) claim_btn_1.click( fn=lambda ctx, trace: on_claim_click(0, ctx, trace), inputs=[contexts_state, claim_trace_state], outputs=[evidence_box], queue=False, ) claim_btn_2.click( fn=lambda ctx, trace: on_claim_click(1, ctx, trace), inputs=[contexts_state, claim_trace_state], outputs=[evidence_box], queue=False, ) claim_btn_3.click( fn=lambda ctx, trace: on_claim_click(2, ctx, trace), inputs=[contexts_state, claim_trace_state], outputs=[evidence_box], queue=False, ) clear_highlight_btn.click( fn=clear_claim_highlight, inputs=[contexts_state], outputs=[evidence_box], queue=False, ) # ---- Tab 2: Document Library ---- with gr.Tab("\U0001f4da Document Library"): gr.Markdown( "### Sustainability Report Collection\n" "Direct PDF download is disabled in this Space. " "Use the official GitHub link to access report files." ) gr.Markdown(f"Report download link: [ClimRetrieve Reports]({REPORTS_GITHUB_URL})") report_names = build_report_name_list() gr.Markdown(render_report_names_md(report_names)) # ==================== Tab 3: About ==================== with gr.Tab("ℹ️ About"): gr.Markdown(""" ### ClimateRAG — Climate Disclosure Retrieval-Augmented Generation for Evidence-based Question-Answering Increasingly stringent global regulations require companies to provide detailed and auditable climate-related disclosures. These reports are often lengthy and visually complex, making manual analysis challenging for regulators and auditors who require precise evidence grounding rather than free-form answers. ClimateRAG is a structured processing and reasoning framework designed for automated climate disclosure analysis. The system integrates hierarchical document chunking, an agent-based reasoning pipeline, and a claim extractor module to produce traceable, evidence-linked, and auditable outputs. It supports both single-document and multi-document analysis scenarios. We additionally introduce a dataset of 367 expert-annotated question–answer pairs covering realistic regulatory and audit workflows. Experimental evaluation demonstrates the effectiveness and efficiency of the proposed framework for climate disclosure analysis. The goal of ClimateRAG is to bridge Large Language Models with the rigorous standards required in regulatory auditing and sustainability reporting. --- ### Key Contributions 1. We develop ClimateRAG, the first system specifically designed for auditable and evidence-linked climate disclosure analysis with multi-document reasoning capability. 2. We construct a dataset of 367 annotated QA pairs spanning single-document and cross-document settings, aligned with real-world regulatory and auditing scenarios. 3. We conduct systematic evaluation to assess both retrieval and generation performance, validating the robustness and practical utility of the system. --- ### Project Website https://cheng-tf.github.io/ClimateRAG/ """) # ---------- Custom Footer ---------- gr.HTML( '' ) # ======================== Launch ======================== if __name__ == "__main__": server_name = os.getenv("APP_HOST", "0.0.0.0") server_port = int(os.getenv("APP_PORT", "7860")) root_path = os.getenv("APP_ROOT_PATH", "") share = os.getenv("APP_SHARE", "false").lower() in {"1", "true", "yes", "y"} allowed_paths = [ p for p in [REPORTS_DIR, SCRIPT_DIR] if isinstance(p, str) and os.path.exists(p) ] launch_kwargs = dict( server_name=server_name, server_port=server_port, share=share, show_error=True, root_path=root_path if root_path else None, ) if _launch_supports("css"): launch_kwargs["css"] = CUSTOM_CSS if _launch_supports("js"): launch_kwargs["js"] = CUSTOM_JS if _launch_supports("theme"): launch_kwargs["theme"] = gr.themes.Soft(primary_hue="blue", secondary_hue="slate") if _launch_supports("ssr_mode"): launch_kwargs["ssr_mode"] = False if allowed_paths: launch_kwargs["allowed_paths"] = allowed_paths demo.launch(**launch_kwargs)