""" AI Research Assistant β Gradio Interface (9 tabs) Updated: Paper type classification, citation velocity, Why This Paper Matters, bullet-point insights. """ from dotenv import load_dotenv load_dotenv() import logging import sys import time import uuid import json import re from pathlib import Path import gradio as gr sys.path.insert(0, str(Path(__file__).parent)) from workflows.langgraph_workflow import run_research_pipeline, run_with_refinement from database.memory_store import MemoryStore from agents.llm_helper import check_ollama_available, list_available_models from vectorstore.faiss_store import FAISSStore from knowledge_graph.graph_builder import get_graph_stats from analysis.research_trends import format_timeline_display logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", datefmt="%H:%M:%S" ) logger = logging.getLogger(__name__) memory = MemoryStore() _states = {} # ------------------------------------------------------------------ # # Cache management # # ------------------------------------------------------------------ # def clear_search_cache() -> str: """Clear FAISS index and embed cache.""" try: store = FAISSStore() count = store.index.ntotal store.clear() cache_path = Path("data/embed_cache.json") if cache_path.exists(): with open(cache_path, "w") as f: json.dump({"embedded_ids": []}, f) logger.info(f"[App] Cache cleared. Removed {count} vectors.") return ( f"β Search cache cleared successfully!\n" f"Removed {count} stored paper embeddings.\n" f"Your next search will start fresh." ) except Exception as e: logger.error(f"[App] Cache clear failed: {e}") return f"β Cache clear failed: {str(e)}" # ------------------------------------------------------------------ # # Global helper # # ------------------------------------------------------------------ # def _clean(text: str) -> str: """Strip ** markers and clean text for display.""" if not text: return "Not available." text = re.sub(r'\*\*', '', str(text)) text = re.sub(r'(? list: """Papers table with paper type indicator in Theme column.""" return [ [ i + 1, p.get("title", "")[:80], ", ".join(p.get("authors", [])[:2]) + (" et al." if len(p.get("authors", [])) > 2 else ""), p.get("published", "")[:4], p.get("citation_count", 0), p.get("venue", "arXiv")[:30], round(p.get("final_score", 0), 3), # Paper type badge + theme ( f"{'π΅' if p.get('paper_type') == 'Core Systems' else 'π’'} " f"{p.get('cluster_theme', '')[:25]}" ), p.get("arxiv_url", "") ] for i, p in enumerate(ranked_papers[:20]) ] def _score_breakdown_md(paper: dict) -> str: """ Display detailed paper insights: - Paper type badge (Core Systems / Application) - Why This Paper Matters - Citation velocity - Bullet-point insights - Paper comparison engine """ sb = paper.get("score_breakdown", {}) insights = paper.get("insights", {}) comparison = paper.get("comparison_data", {}) paper_type = paper.get("paper_type", "Unknown") significance = paper.get("significance", "") # Paper type badge and description type_badge = ( "π΅ **Core Systems Paper**" if paper_type == "Core Systems" else "π’ **Application Paper**" ) type_desc = ( "Focuses on multi-agent framework design, architecture, " "coordination methods, or benchmarks." if paper_type == "Core Systems" else "Applies multi-agent LLM methods to a specific domain problem." ) # ββ Title + Type + Significance ββββββββββββββββββββββββββββββ header_section = ( f"### {_clean(paper.get('title', ''))[:80]}\n\n" f"{type_badge} β {type_desc}\n\n" ) if significance: header_section += ( f"> π‘ **Why This Paper Matters:** " f"{_clean(significance)}\n\n" ) # ββ Score table ββββββββββββββββββββββββββββββββββββββββββββββ score_section = ( f"| Component | Weight | Score |\n" f"|-----------|--------|-------|\n" f"| Citation (velocity-adjusted) | 35% | " f"{sb.get('citation_score', 0):.3f} |\n" f"| Recency | 20% | {sb.get('recency_score', 0):.3f} |\n" f"| Venue | 10% | {sb.get('venue_score', 0):.3f} |\n" f"| Relevance | 35% | {sb.get('relevance_score', 0):.3f} |\n" f"| **Final** | **100%** | " f"**{sb.get('final_score', 0):.3f}** |\n\n" f"**Citations:** {sb.get('citation_count', 0)} | " f"**Velocity:** {sb.get('citation_velocity', 0):.1f}/year | " f"**Venue:** {_clean(sb.get('venue', 'Unknown'))} " f"({sb.get('venue_tier', 'unknown')} tier)\n\n" f"**Theme:** {_clean(paper.get('cluster_theme', 'N/A'))}\n\n" f"**Keywords:** " f"{', '.join(paper.get('cluster_keywords', []) or ['N/A'])}\n\n" f"**Strengths:** " f"{' | '.join(_clean(s) for s in paper.get('strengths', ['N/A']))}\n\n" f"**Weaknesses:** " f"{' | '.join(_clean(w) for w in paper.get('weaknesses', ['N/A']))}\n\n" ) # ββ Extracted Insights (bullet points) βββββββββββββββββββββββ insights_section = ( f"---\n\n" f"### Extracted Insights\n\n" f"**Problem Statement**\n\n" f"{_clean(insights.get('problem_statement', insights.get('problem', '')))}\n\n" f"**Methodology**\n\n" f"{_clean(insights.get('methodology', ''))}\n\n" f"**Datasets Used**\n\n" f"{_clean(insights.get('datasets', ''))}\n\n" f"**Evaluation Metrics**\n\n" f"{_clean(insights.get('evaluation_metrics', insights.get('metrics', '')))}\n\n" f"**Key Contributions**\n\n" f"{_clean(insights.get('key_contributions', ''))}\n\n" f"**Limitations**\n\n" f"{_clean(insights.get('limitations', ''))}\n\n" f"**Future Work**\n\n" f"{_clean(insights.get('future_work', ''))}\n\n" ) # ββ Paper Comparison Engine βββββββββββββββββββββββββββββββββββ comparison_section = "" if comparison and any(comparison.values()): comparison_section = ( f"---\n\n" f"### Paper Comparison Engine\n\n" f"> *How this paper relates to others in the result set*\n\n" f"**Shared Methods Across Papers**\n\n" f"{_clean(comparison.get('shared_methods', ''))}\n\n" f"**Key Differences**\n\n" f"{_clean(comparison.get('key_differences', ''))}\n\n" f"**Contradictions Found**\n\n" f"{_clean(comparison.get('contradictions', ''))}\n\n" f"**Complementary Findings**\n\n" f"{_clean(comparison.get('complementary_findings', ''))}\n\n" f"**Strongest Paper Assessment**\n\n" f"{_clean(comparison.get('strongest_paper', ''))}\n\n" ) return header_section + score_section + insights_section + comparison_section def _report_md(insights: dict) -> str: """Display research report β clean all ** markers.""" if not insights or not insights.get("background"): return "*Run a search to generate the report.*" return ( f"# Research Report: {insights.get('topic', '')}\n" f"*Papers analyzed: {insights.get('total_papers_analyzed', 0)}*\n\n" f"---\n## Background\n" f"{_clean(insights.get('background', ''))}\n\n" f"---\n## Key Methods\n" f"{_clean(insights.get('key_methods', ''))}\n\n" f"---\n## Datasets\n" f"{_clean(insights.get('common_datasets', ''))}\n\n" f"---\n## Evaluation Metrics\n" f"{_clean(insights.get('evaluation_metrics', ''))}\n\n" f"---\n## Limitations\n" f"{_clean(insights.get('limitations', ''))}\n\n" f"---\n## Research Gaps\n" f"{_clean(insights.get('research_gaps', ''))}\n\n" f"---\n## Future Directions\n" f"{_clean(insights.get('future_directions', ''))}" ) def _kg_stats_md(entities: list, edges: list) -> str: """Display knowledge graph statistics.""" stats = get_graph_stats(entities, edges) tc = stats.get("entity_types", {}) rc = stats.get("relation_types", {}) relations = " | ".join(f"`{r}`: {c}" for r, c in rc.items()) return ( f"### Knowledge Graph\n" f"| Metric | Value |\n|--------|-------|\n" f"| Total Entities | {stats['total_entities']} |\n" f"| Total Edges | {stats['total_edges']} |\n" f"| Papers | {tc.get('paper', 0)} |\n" f"| Methods | {tc.get('method', 0)} |\n" f"| Datasets | {tc.get('dataset', 0)} |\n" f"| Tasks | {tc.get('task', 0)} |\n\n" f"**Relations:** {relations}" ) def _clusters_md(cluster_info: list) -> str: """Display research themes with keywords and top paper.""" if not cluster_info: return "*Run a search to detect research themes.*" colors = ["π΅", "π ", "π’", "π£", "π΄", "π‘"] lines = [f"## Research Themes ({len(cluster_info)} clusters)\n"] for c in cluster_info: cid = c.get("cluster_id", 0) theme = c.get("theme", "Unknown") count = c.get("paper_count", 0) titles = c.get("paper_titles", []) keywords = c.get("keywords", []) top = c.get("top_paper", "") icon = colors[cid % len(colors)] lines.append(f"### {icon} {theme}") lines.append(f"*{count} paper{'s' if count != 1 else ''}*\n") if keywords: lines.append(f"**Keywords:** {', '.join(keywords)}\n") if top: lines.append(f"**Top paper:** {top}\n") for t in titles[:4]: lines.append(f"- {t}") lines.append("") return "\n".join(lines) def _trends_md(research_trends: dict) -> str: """Display research timeline.""" if not research_trends or not research_trends.get("timeline"): return "*Run a search to generate the timeline.*" timeline = research_trends.get("timeline", {}) trend_summary = research_trends.get("trend_summary", "") peak_year = research_trends.get("peak_year", "N/A") growth_rate = research_trends.get("growth_rate") maturity = research_trends.get("maturity_status", "unknown") year_range = research_trends.get("year_range", (0, 0)) data_warning = research_trends.get("data_warning", False) timeline_disp = format_timeline_display(timeline) icons = { "emerging": "π±", "growing": "π", "mature": "ποΈ", "unknown": "β", "insufficient_data": "β οΈ" } result = "## Research Timeline\n\n" if data_warning: result += ( "> β οΈ **Limited data** β " "statistics may not be reliable.\n\n" ) result += ( f"| Metric | Value |\n|--------|-------|\n" f"| Year Range | {year_range[0]} β {year_range[1]} |\n" f"| Peak Year | {peak_year} |\n" ) if growth_rate is not None: result += f"| Growth Rate | {growth_rate}% |\n" else: result += "| Growth Rate | β οΈ Insufficient data |\n" result += ( f"| Status | {icons.get(maturity, 'β')} " f"{maturity.replace('_', ' ').capitalize()} |\n\n" f"### Publication Timeline\n\n" f"```\n{timeline_disp}\n```\n\n" f"### Analysis\n\n{_clean(trend_summary)}" ) return result def _render_kg_html(kg_html_path: str) -> str: """Render knowledge graph as base64 iframe.""" if not kg_html_path: return ( "
" "No graph yet.
" ) path = Path(kg_html_path) if not path.exists(): return ( "" "Graph file not found.
" ) try: import base64 html_content = path.read_text(encoding="utf-8") encoded = base64.b64encode( html_content.encode("utf-8") ).decode("utf-8") return ( f'' ) except Exception as e: return f"Graph error: {e}
" def save_file(content: str, filename: str): """Save content to file and return path.""" if not content: return None p = Path(f"data/artifacts/{filename}") p.parent.mkdir(parents=True, exist_ok=True) p.write_text(content, encoding="utf-8") return str(p) # ------------------------------------------------------------------ # # Pipeline runners # # ------------------------------------------------------------------ # def run_pipeline( query, year_filter, exclude_surveys, max_papers, generate_rw_flag, progress=gr.Progress() ): if not query.strip(): return ( "β οΈ Please enter a query.", [], "", "", "", "", "", "", "", "", None, "" ) session_id = str(uuid.uuid4()) filters = { "max_papers": int(max_papers), "generate_related_work": generate_rw_flag } if year_filter > 2018: filters["year_after"] = int(year_filter) if exclude_surveys: filters["exclude_surveys"] = True progress(0.05, desc="π§ Planning subtopics...") state = run_research_pipeline( query=query, filters=filters, session_id=session_id ) _states[session_id] = state progress(0.90, desc="π Formatting results...") ranked = state.get("ranked_papers", []) insights = state.get("insights", {}) arts = state.get("artifacts", {}) entities = state.get("knowledge_graph_entities", []) edges = state.get("knowledge_graph_edges", []) metrics = state.get("metrics", {}) errors = state.get("errors", []) cluster_info = state.get("cluster_info", []) research_trends = state.get("research_trends", {}) # Cluster summary cluster_summary = "" if cluster_info: themes = [c.get("theme", "?") for c in cluster_info[:3]] cluster_summary = f"\n- Themes: **{', '.join(themes)}**" # Trend note trend_note = "" if research_trends.get("data_warning"): trend_note = "\n- β οΈ Trend data insufficient for reliable stats" elif research_trends.get("maturity_status"): trend_note = ( f"\n- Field: " f"**{research_trends.get('maturity_status','').capitalize()}**" f" | Peak: **{research_trends.get('peak_year','N/A')}**" ) # Paper type breakdown core_count = metrics.get("core_papers", 0) app_count = metrics.get("app_papers", 0) type_note = "" if core_count or app_count: type_note = ( f"\n- Types: π΅ **{core_count} Core Systems** | " f"π’ **{app_count} Applications**" ) status = ( f"β **Research Complete**\n" f"- Session: `{session_id[:8]}`\n" f"- Papers: **{metrics.get('papers_retrieved', 0)}** retrieved | " f"**{len(ranked)}** ranked\n" f"- Clusters: **{len(cluster_info)}** themes" f"{cluster_summary}" f"{type_note}\n" f"- Score mean: **{metrics.get('score_mean', 0):.3f}** | " f"Time: **{metrics.get('query_time_sec', 0):.1f}s**" f"{trend_note}\n" + ( f"- β οΈ {len(errors)} error(s): {errors[0][:50]}" if errors else "" ) ) kg_display = _render_kg_html(arts.get("knowledge_graph_html", "")) progress(1.0, desc="β Done!") return ( status, _papers_table(ranked), _score_breakdown_md(ranked[0]) if ranked else "No papers found.", _report_md(insights), _clusters_md(cluster_info), _trends_md(research_trends), arts.get("report", "{}"), arts.get("bibtex", "% No BibTeX"), arts.get("related_work", "# No related work"), _kg_stats_md(entities, edges), kg_display, session_id ) def apply_refinement( refinement_cmd: str, session_id: str, progress=gr.Progress() ): if not session_id or not refinement_cmd.strip(): return ( "β οΈ Run a search first.", [], "", "", "", "", "", "", "", "", None, session_id ) prev = _states.get(session_id) if not prev: return ( "β οΈ Session not found. Run a new search.", [], "", "", "", "", "", "", "", "", None, session_id ) progress(0.1, desc="π Applying refinement...") state = run_with_refinement(session_id, refinement_cmd, prev) _states[session_id] = state ranked = state.get("ranked_papers", []) insights = state.get("insights", {}) arts = state.get("artifacts", {}) entities = state.get("knowledge_graph_entities", []) edges = state.get("knowledge_graph_edges", []) cluster_info = state.get("cluster_info", []) research_trends = state.get("research_trends", {}) kg_display = _render_kg_html( arts.get("knowledge_graph_html", "") ) progress(1.0) return ( f"β **Refinement applied:** *{refinement_cmd}* | " f"Papers: **{len(ranked)}**", _papers_table(ranked), _score_breakdown_md(ranked[0]) if ranked else "", _report_md(insights), _clusters_md(cluster_info), _trends_md(research_trends), arts.get("report", "{}"), arts.get("bibtex", ""), arts.get("related_work", ""), _kg_stats_md(entities, edges), kg_display, session_id ) def get_system_status() -> str: """Display system status including LLM provider and sources.""" llm_ok = check_ollama_available() models = list_available_models() if llm_ok else [] try: faiss_stats = FAISSStore().stats() faiss_ok = True except Exception: faiss_stats = {} faiss_ok = False sessions = memory.list_sessions() try: from agents.llm_helper import GROQ_API_KEY provider = ( f"Groq ({models[0] if models else 'llama-3.1-8b-instant'})" if GROQ_API_KEY else "Ollama (local)" ) except Exception: provider = "Unknown" return ( "## System Status\n\n" "| Component | Status |\n|-----------|--------|\n" f"| LLM Provider | " f"{'β ' + provider if llm_ok else 'β Not configured'} |\n" f"| FAISS Index | {'β Ready' if faiss_ok else 'β Error'} |\n" f"| SQLite DB | β Ready |\n" f"| ArXiv | β Free (no key needed) |\n" f"| Semantic Scholar | β Connected |\n" f"| PubMed | β Free (no key needed) |\n" f"| CORE | β Connected |\n\n" f"**FAISS:** " f"{faiss_stats.get('total_documents', 0)} / " f"{faiss_stats.get('max_documents', 500)} papers stored\n\n" f"**Sessions:** {len(sessions)}\n\n" "### Paper Classification\n" "- π΅ **Core Systems** β framework, architecture, coordination\n" "- π’ **Application** β domain-specific implementations\n\n" "### Active Sources\n" "- ArXiv β latest preprints\n" "- Semantic Scholar β 200M papers + citations\n" "- PubMed β 36M peer-reviewed medical papers\n" "- CORE β 200M open access papers\n\n" "### Tips\n" "- Clear cache when switching to a different topic\n" "- Use 7-10 max papers for best quality\n" "- Enable Related Work only when needed\n" "- π΅ Core Systems papers = foundational research\n" "- π’ Application papers = domain implementations\n" ) # ------------------------------------------------------------------ # # Gradio UI # # ------------------------------------------------------------------ # def build_ui(): with gr.Blocks(title="AI Research Assistant") as demo: gr.HTML( "" "Hierarchical Multi-Agent System β’ LangGraph β’ " "4-Source Search β’ Paper Classification β’ " "Comparison Engine" "
" "" "Knowledge graph will appear here after search." "
" ) # ββ Tab 7: Refine Search ββββββββββββββββββββββββββββββ with gr.Tab("π Refine Search"): gr.Markdown( "### Conversational Refinement\n\n" "Refine your current results without a full " "new search.\n\n" "**Examples:**\n" "- `Focus on papers after 2022`\n" "- `Exclude survey papers`\n" "- `Drill deeper into clinical applications`" ) refine_input = gr.Textbox( label="Refinement Command", lines=1 ) refine_btn = gr.Button( "π Apply Refinement", variant="secondary" ) # ββ Tab 8: Session History ββββββββββββββββββββββββββββ with gr.Tab("ποΈ Session History"): refresh_btn = gr.Button("π Refresh") history_tbl = gr.Dataframe( headers=["Session ID", "Query", "Timestamp"], datatype=["str", "str", "str"], column_count=(3, "fixed") ) # ββ Tab 9: System Status ββββββββββββββββββββββββββββββ with gr.Tab("βοΈ System Status"): refresh_status_btn = gr.Button("π Refresh Status") status_out_sys = gr.Markdown(get_system_status()) # ββ All outputs (12) ββββββββββββββββββββββββββββββββββββββ all_outputs = [ status_out, # 1 papers_tbl, # 2 score_detail, # 3 report_md_out, # 4 clusters_out, # 5 trends_out, # 6 report_json_out, # 7 bibtex_out, # 8 rw_out, # 9 kg_stats_out, # 10 kg_viz_out, # 11 session_state # 12 ] # ββ Event bindings ββββββββββββββββββββββββββββββββββββββββ run_btn.click( fn=run_pipeline, inputs=[ query_input, year_filter, exclude_surveys, max_papers, generate_rw_flag ], outputs=all_outputs, show_progress=True ) query_input.submit( fn=run_pipeline, inputs=[ query_input, year_filter, exclude_surveys, max_papers, generate_rw_flag ], outputs=all_outputs, show_progress=True ) refine_btn.click( fn=apply_refinement, inputs=[refine_input, session_state], outputs=all_outputs, show_progress=True ) clear_btn.click( fn=clear_search_cache, outputs=[clear_status] ) dl_report_btn.click( fn=lambda c: save_file(c, "dl_report.json"), inputs=[report_json_out], outputs=[dl_report_btn] ) dl_bib_btn.click( fn=lambda c: save_file(c, "dl_references.bib"), inputs=[bibtex_out], outputs=[dl_bib_btn] ) dl_rw_btn.click( fn=lambda c: save_file(c, "dl_related_work.md"), inputs=[rw_out], outputs=[dl_rw_btn] ) refresh_btn.click( fn=lambda: [ [ s["session_id"][:8], s["query"][:60], s["created_at"][:19] ] for s in memory.list_sessions()[:20] ], outputs=[history_tbl] ) refresh_status_btn.click( fn=get_system_status, outputs=[status_out_sys] ) demo.load( fn=lambda: [ [ s["session_id"][:8], s["query"][:60], s["created_at"][:19] ] for s in memory.list_sessions()[:20] ], outputs=[history_tbl] ) return demo # ------------------------------------------------------------------ # # Entry point # # ------------------------------------------------------------------ # if __name__ == "__main__": Path("data/artifacts").mkdir(parents=True, exist_ok=True) logger.info("=" * 50) logger.info(" AI Research Assistant") logger.info("=" * 50) if not check_ollama_available(): logger.warning( "β οΈ No LLM configured. Set GROQ_API_KEY in secrets." ) else: try: from agents.llm_helper import get_active_provider logger.info(f"β LLM: {get_active_provider()}") except Exception: logger.info("β LLM configured.") # Hugging Face compatible launch build_ui().launch()