import gradio as gr import threading import json import asyncio import sys import io import threading as _threading from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse from pydantic import BaseModel as _BaseModel from agent.crew import run_research from output.exporter import export_markdown, export_pdf _current_result: dict = {} _lock = threading.Lock() DEPTH_DESCRIPTIONS = { "quick": "Quick (5 sources, ~30s) — surface overview", "standard": "Standard (10 sources, ~90s) — balanced depth", "deep": "Deep (15 sources, ~3min) — exhaustive, academic-heavy", } CSS = """ #report-box { font-family: 'Georgia', serif; font-size: 14px; } #confidence-badge { font-weight: bold; font-size: 16px; padding: 8px; border-radius: 6px; } """ def _confidence_color(report: str) -> str: import re match = re.search(r"Confidence Assessment.*?(HIGH|MEDIUM|LOW|INSUFFICIENT)", report, re.DOTALL | re.IGNORECASE) label = match.group(1).upper() if match else "UNKNOWN" colors = {"HIGH": "#d4edda", "MEDIUM": "#fff3cd", "LOW": "#f8d7da", "INSUFFICIENT": "#e2e3e5", "UNKNOWN": "#e2e3e5"} return f'
Confidence: {label}
' def run_research_ui(query: str, depth_label: str, progress=gr.Progress()): global _current_result depth = [k for k, v in DEPTH_DESCRIPTIONS.items() if v == depth_label] depth = depth[0] if depth else "standard" if not query.strip(): return "Please enter a research topic.", "", None, None progress(0.05, desc="Starting research crew...") try: progress(0.15, desc="Searching sources...") result = run_research(query=query.strip(), depth=depth) progress(0.90, desc="Formatting report...") with _lock: _current_result = result report = result.get("report", "No report generated.") badge = _confidence_color(report) sources = result.get("sources", []) source_summary = "" if sources: source_summary = f"**{len(sources)} accepted sources** (sorted by credibility)\n\n" for s in sources[:8]: score = s.get("credibility_score", 0) conf = s.get("confidence", "low") title = s.get("title", "Untitled")[:60] url = s.get("url", "") source_summary += f"- [{title}]({url}) — score: **{score:.2f}** ({conf})\n" progress(1.0, desc="Done.") return report, badge + "\n\n" + source_summary, None, None except Exception as e: return f"Error during research: {e}", "", None, None def export_md(): with _lock: result = dict(_current_result) if not result.get("report"): return None return export_markdown(result["report"], result.get("query", "research")) def export_pdf_fn(): with _lock: result = dict(_current_result) if not result.get("report"): return None return export_pdf(result["report"], result.get("query", "research")) with gr.Blocks(css=CSS, title="AI Research Agent") as demo: gr.Markdown( """ # AI Research Agent **Powered by CrewAI · Tavily · arXiv · Groq** Searches multiple authoritative sources, scores credibility, and synthesizes a fully cited report. Only sources scoring ≥ 0.35/1.0 on the credibility scale are included. """ ) with gr.Row(): with gr.Column(scale=3): query_input = gr.Textbox( label="Research Topic", placeholder="e.g. What are the latest advances in mRNA vaccine technology?", lines=2, ) with gr.Column(scale=1): depth_input = gr.Radio( choices=list(DEPTH_DESCRIPTIONS.values()), value=DEPTH_DESCRIPTIONS["standard"], label="Research Depth", ) run_btn = gr.Button("Start Research", variant="primary", size="lg") with gr.Tabs(): with gr.TabItem("Report"): report_output = gr.Markdown(label="Research Report", elem_id="report-box") with gr.TabItem("Sources & Confidence"): meta_output = gr.Markdown(label="Source Summary") with gr.Row(): md_btn = gr.Button("Export Markdown") pdf_btn = gr.Button("Export PDF") md_file = gr.File(label="Markdown Download", visible=True) pdf_file = gr.File(label="PDF Download", visible=True) run_btn.click( fn=run_research_ui, inputs=[query_input, depth_input], outputs=[report_output, meta_output, md_file, pdf_file], ) md_btn.click(fn=export_md, inputs=[], outputs=[md_file]) pdf_btn.click(fn=export_pdf_fn, inputs=[], outputs=[pdf_file]) gr.Examples( examples=[ ["What are the health risks of microplastics in drinking water?", DEPTH_DESCRIPTIONS["standard"]], ["Latest advances in large language model reasoning capabilities 2024-2025", DEPTH_DESCRIPTIONS["deep"]], ["Economic impact of remote work on urban commercial real estate", DEPTH_DESCRIPTIONS["standard"]], ["CRISPR gene editing applications in treating genetic diseases", DEPTH_DESCRIPTIONS["quick"]], ], inputs=[query_input, depth_input], ) # ── Attach REST API to Gradio's underlying FastAPI app ──────────── demo.app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) _thread_local_app = _threading.local() _original_stdout = sys.stdout class _RoutedStream(io.TextIOBase): def write(self, text: str) -> int: q = getattr(_thread_local_app, "log_queue", None) if q is not None: for line in text.splitlines(): line = line.strip() if line and not line.startswith("\x1b"): try: q.put_nowait(line) except Exception: pass else: try: _original_stdout.write(text) except Exception: pass return len(text) def flush(self): try: _original_stdout.flush() except Exception: pass def isatty(self): return False sys.stdout = _RoutedStream(_original_stdout) class _ResearchReq(_BaseModel): query: str depth: str = "standard" async def _sse_stream(query: str, depth: str): loop = asyncio.get_event_loop() log_queue: asyncio.Queue = asyncio.Queue(maxsize=500) done_event = asyncio.Event() result_holder: dict = {} def _worker(): _thread_local_app.log_queue = log_queue try: result = run_research(query, depth) result_holder.update(result) except Exception as e: result_holder["error"] = str(e) finally: _thread_local_app.log_queue = None loop.call_soon_threadsafe(done_event.set) _threading.Thread(target=_worker, daemon=True).start() while not done_event.is_set(): try: line = log_queue.get_nowait() yield f"data: {json.dumps(str(line))}\n\n" except Exception: await asyncio.sleep(0.05) while not log_queue.empty(): try: line = log_queue.get_nowait() yield f"data: {json.dumps(str(line))}\n\n" except Exception: break if "error" in result_holder: yield f"data: {json.dumps('[ERROR] ' + result_holder['error'])}\n\n" else: yield f"data: {json.dumps({'type': 'report', 'content': result_holder.get('report', '')})}\n\n" yield f"data: {json.dumps({'type': 'sources', 'content': result_holder.get('sources', [])})}\n\n" yield 'data: "__DONE__"\n\n' @demo.app.post("/research") async def research_api(req: _ResearchReq): if not req.query.strip(): return {"error": "Query cannot be empty"} return StreamingResponse( _sse_stream(req.query, req.depth), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, ) @demo.app.get("/api/health") async def health_api(): return {"status": "ok"} if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860, share=False)