Spaces:
Sleeping
Sleeping
| 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'<div style="background:{colors[label]};padding:10px;border-radius:6px;font-weight:bold">Confidence: {label}</div>' | |
| 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' | |
| 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"}, | |
| ) | |
| async def health_api(): | |
| return {"status": "ok"} | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860, share=False) | |