| |
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| """ |
| # REASONING-ENFORCED AGENT |
| ... (full policy – keep as before) |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| import json |
| import re |
| import time |
| import asyncio |
| import traceback |
|
|
| import gradio as gr |
| import spaces |
| from gradio_client import Client |
|
|
|
|
| |
| |
| |
|
|
| APP_NAME = "X-RUDRA" |
| VERSION = "3.8.3" |
|
|
| M1_REPO = os.getenv("M1_REPO", "Shrijanagain/M1") |
| M2_REPO = os.getenv("M2_REPO", "Shrijanagain/M2") |
| PORT = int(os.getenv("PORT", "7860")) |
|
|
| HF_TOKEN = os.getenv("HF_TOKEN") |
| if HF_TOKEN: |
| os.environ["HF_TOKEN"] = HF_TOKEN |
|
|
|
|
| |
| |
| |
|
|
| _M1_CLIENT = None |
| _M2_CLIENT = None |
|
|
| def get_m1_client(): |
| global _M1_CLIENT |
| if _M1_CLIENT is None: |
| try: |
| url = f"https://{M1_REPO.replace('/', '-')}.hf.space" |
| _M1_CLIENT = Client(url) |
| except Exception as e: |
| print(f"Could not connect to M1: {e}") |
| _M1_CLIENT = None |
| return _M1_CLIENT |
|
|
| def get_m2_client(): |
| global _M2_CLIENT |
| if _M2_CLIENT is None: |
| try: |
| url = f"https://{M2_REPO.replace('/', '-')}.hf.space" |
| _M2_CLIENT = Client(url) |
| except Exception as e: |
| print(f"Could not connect to M2: {e}") |
| _M2_CLIENT = None |
| return _M2_CLIENT |
|
|
|
|
| |
| |
| |
|
|
| CLASSIFIER_SYSTEM_PROMPT = """ |
| You are an intelligent assistant that classifies user messages into two categories: |
| - ACTION: The user asks for information that requires research, fact‑checking, retrieval of current data, or external knowledge. This includes questions about news, comparisons, statistics, history, technology, science, politics, etc. |
| - CASUAL: The user is just chatting, greeting, making small talk, joking, or asking a simple question that can be answered from general knowledge without a search. |
| |
| Respond with ONLY ONE WORD: ACTION or CASUAL. |
| Do NOT add any extra text, punctuation, or explanation. |
| """ |
|
|
| def classify_intent(question: str) -> str: |
| prompt = f"{CLASSIFIER_SYSTEM_PROMPT}\n\nUser message: \"{question}\"\n\nClassification:" |
| |
| |
| m1_client = get_m1_client() |
| if m1_client is not None: |
| try: |
| result = m1_client.predict( |
| prompt=prompt, |
| max_tokens=64, |
| temperature=0.1, |
| api_name="/generate" |
| ) |
| if result: |
| result = result.strip().upper() |
| if "ACTION" in result: |
| print(f"[Classifier] M1 → ACTION") |
| return "ACTION" |
| elif "CASUAL" in result: |
| print(f"[Classifier] M1 → CASUAL") |
| return "CASUAL" |
| else: |
| print(f"[Classifier] M1 ambiguous: {result}") |
| except Exception as e: |
| print(f"[Classifier] M1 call failed: {e}") |
|
|
| |
| m2_client = get_m2_client() |
| if m2_client is not None: |
| try: |
| result = m2_client.predict( |
| prompt=prompt, |
| max_tokens=64, |
| temperature=0.1, |
| api_name="/generate" |
| ) |
| if result: |
| result = result.strip().upper() |
| if "ACTION" in result: |
| print(f"[Classifier] M2 → ACTION") |
| return "ACTION" |
| elif "CASUAL" in result: |
| print(f"[Classifier] M2 → CASUAL") |
| return "CASUAL" |
| else: |
| print(f"[Classifier] M2 ambiguous: {result}") |
| except Exception as e: |
| print(f"[Classifier] M2 call failed: {e}") |
|
|
| |
| |
| word_count = len(question.split()) |
| if word_count <= 3: |
| print("[Classifier] Fallback → CASUAL (short message)") |
| return "CASUAL" |
| else: |
| print("[Classifier] Fallback → ACTION (longer message)") |
| return "ACTION" |
|
|
|
|
| |
| |
| |
|
|
| _ENGINE = None |
|
|
| def get_engine(): |
| global _ENGINE |
| if _ENGINE is None: |
| from web_search import XrudraWebSearch |
| _ENGINE = XrudraWebSearch() |
| return _ENGINE |
|
|
|
|
| |
| |
| |
|
|
| def call_model(client, prompt, max_tokens=512, temperature=0.7): |
| if client is None: |
| return None |
| try: |
| print(f"Calling model with max_tokens={max_tokens}, prompt length={len(prompt)}") |
| result = client.predict( |
| prompt=prompt, |
| max_tokens=max_tokens, |
| temperature=temperature, |
| api_name="/generate" |
| ) |
| if result and isinstance(result, str) and result.strip(): |
| print(f"Response length: {len(result)} chars") |
| return result.strip() |
| else: |
| print("Empty response") |
| return None |
| except Exception as e: |
| print(f"Model call failed: {e}") |
| return None |
|
|
|
|
| |
| |
| |
|
|
| def get_combined_model_answer(question, sources, max_tokens=512, temperature=0.7): |
| top_sources = sources[:5] if sources else [] |
| sources_text = "" |
| if top_sources: |
| for i, src in enumerate(top_sources, 1): |
| title = src.get("title", "Untitled") |
| snippet = src.get("snippet", src.get("description", "")) |
| sources_text += f"{i}. {title}: {snippet[:300]}\n" |
| else: |
| sources_text = "No specific information available." |
|
|
| base_prompt = f"""Question: {question} |
| |
| Information: |
| {sources_text} |
| |
| Based on the information above and your knowledge, provide a comprehensive, accurate, and well‑structured answer to the question. Be direct and natural – write as if you are an expert answering a user. |
| |
| Answer:""" |
|
|
| m1_client = get_m1_client() |
| m2_client = get_m2_client() |
|
|
| |
| draft_m1 = call_model(m1_client, base_prompt, max_tokens, temperature) |
| draft_m2 = call_model(m2_client, base_prompt, max_tokens, temperature) |
|
|
| |
| if not draft_m1 and not draft_m2: |
| if sources: |
| parts = ["Based on available information:"] |
| for i, src in enumerate(sources[:5], 1): |
| title = src.get("title", "Untitled") |
| snippet = src.get("snippet", src.get("description", "")) |
| parts.append(f"{i}. {title}: {snippet[:200]}..." if snippet else f"{i}. {title}") |
| return "\n\n".join(parts), "" |
| else: |
| return "I couldn't find specific information on that topic. Could you rephrase?", "" |
|
|
| |
| if not draft_m1: |
| thinking = "" |
| clean = draft_m2 |
| think_match = re.search(r"<think>(.*?)</think>", draft_m2, re.DOTALL) |
| if think_match: |
| thinking = think_match.group(1).strip() |
| clean = re.sub(r"<think>.*?</think>", "", draft_m2, flags=re.DOTALL).strip() |
| return clean, thinking |
|
|
| if not draft_m2: |
| thinking = "" |
| clean = draft_m1 |
| think_match = re.search(r"<think>(.*?)</think>", draft_m1, re.DOTALL) |
| if think_match: |
| thinking = think_match.group(1).strip() |
| clean = re.sub(r"<think>.*?</think>", "", draft_m1, flags=re.DOTALL).strip() |
| return clean, thinking |
|
|
| |
| merge_prompt = f"""Question: {question} |
| |
| Draft from Model A: |
| {draft_m1} |
| |
| Draft from Model B: |
| {draft_m2} |
| |
| Combine these two drafts into a single, comprehensive, accurate, and natural answer. Keep the best parts from each. Ensure the final answer directly addresses the question, is well‑structured, and reads as a single coherent response. Do NOT mention that you are combining drafts or that you used multiple models. Just provide the final answer. |
| |
| Final answer:""" |
|
|
| merge_max_tokens = max(1024, max_tokens * 2) |
| merged = call_model(m2_client, merge_prompt, merge_max_tokens, temperature) |
|
|
| if merged and len(merged) < 100: |
| print(f"Merged answer too short ({len(merged)} chars), retrying with 2048 tokens...") |
| merged = call_model(m2_client, merge_prompt, 2048, temperature) |
|
|
| if not merged: |
| print("Merge failed, falling back to draft_m1") |
| merged = draft_m1 |
|
|
| |
| thinking_content = "" |
| clean_answer = merged |
| think_match = re.search(r"<think>(.*?)</think>", merged, re.DOTALL) |
| if think_match: |
| thinking_content = think_match.group(1).strip() |
| clean_answer = re.sub(r"<think>.*?</think>", "", merged, flags=re.DOTALL).strip() |
|
|
| return clean_answer, thinking_content |
|
|
|
|
| |
| |
| |
|
|
| def get_casual_model_response(query: str) -> str: |
| client = get_m1_client() |
| if client is not None: |
| try: |
| result = client.predict( |
| prompt=f"User: {query}\nAssistant:", |
| max_tokens=128, |
| temperature=0.7, |
| api_name="/generate" |
| ) |
| if result and isinstance(result, str) and result.strip(): |
| clean = re.sub(r"<think>.*?</think>", "", result, flags=re.DOTALL).strip() |
| return clean |
| except Exception as e: |
| print(f"M1 casual failed: {e}") |
|
|
| client = get_m2_client() |
| if client is not None: |
| try: |
| result = client.predict( |
| prompt=f"User: {query}\nAssistant:", |
| max_tokens=128, |
| temperature=0.7, |
| api_name="/generate" |
| ) |
| if result and isinstance(result, str) and result.strip(): |
| clean = re.sub(r"<think>.*?</think>", "", result, flags=re.DOTALL).strip() |
| return clean |
| except Exception as e: |
| print(f"M2 casual failed: {e}") |
|
|
| return ( |
| f"👋 Hi there! I'm X‑RUDRA, your research assistant. " |
| f"How can I help you today? (Your message `{query}` was casual, so I kept it light.)" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def format_sources(sources): |
| if not sources: |
| return "## 📚 Sources\n\nNo sources were returned." |
| output = ["## 📚 Sources", ""] |
| for idx, src in enumerate(sources, 1): |
| if not isinstance(src, dict): |
| continue |
| title = src.get("title", "Untitled") |
| url = src.get("url", "") |
| method = src.get("fetch_method", "web") |
| score = src.get("source_score", src.get("score", "N/A")) |
| snippet = src.get("snippet", src.get("description", "")) |
| if url: |
| output.append(f"### {idx}. [{title}]({url})") |
| else: |
| output.append(f"### {idx}. {title}") |
| output.append(f"**Fetcher:** `{method}`") |
| output.append(f"**Source score:** `{score}`") |
| if snippet: |
| output.append(f"\n> {snippet}") |
| output.append("") |
| return "\n".join(output) |
|
|
|
|
| def format_evidence(claims): |
| if not claims: |
| return "## 🧠 Evidence\n\nNo structured evidence was returned." |
| output = ["## 🧠 Evidence", ""] |
| for idx, claim in enumerate(claims, 1): |
| if not isinstance(claim, dict): |
| continue |
| text = claim.get("claim", claim.get("text", "")) |
| score = claim.get("support_score", claim.get("score", "N/A")) |
| source = claim.get("source_url", claim.get("url", "")) |
| output.append(f"### Evidence {idx}") |
| output.append(str(text)) |
| output.append(f"**Support:** `{score}`") |
| if source: |
| output.append(f"**Source:** {source}") |
| output.append("---") |
| return "\n\n".join(output) |
|
|
|
|
| def format_verification(contradictions): |
| if not contradictions: |
| return "## ⚖️ Verification\n\n✅ No major contradictions detected." |
| output = ["## ⚖️ Verification", "", "⚠️ Potential contradictions detected:", ""] |
| for idx, item in enumerate(contradictions, 1): |
| if not isinstance(item, dict): |
| continue |
| claim_a = item.get("claim_a", "") |
| claim_b = item.get("claim_b", "") |
| source_a = item.get("source_a", "") |
| source_b = item.get("source_b", "") |
| output.append(f"### Contradiction {idx}") |
| output.append(f"**A:** {claim_a}") |
| if source_a: |
| output.append(f"Source A: `{source_a}`") |
| output.append("") |
| output.append(f"**B:** {claim_b}") |
| if source_b: |
| output.append(f"Source B: `{source_b}`") |
| output.append("---") |
| return "\n\n".join(output) |
|
|
|
|
| def build_activity(data, elapsed_ms): |
| sources = data.get("sources", []) or data.get("results", []) |
| claims = data.get("claims", []) |
| contradictions = data.get("contradictions", []) |
| rounds = data.get("rounds", data.get("research_rounds", "N/A")) |
| return f""" |
| ## ⚡ X-RUDRA Research |
| |
| | Stage | Status | |
| |---|---| |
| | Task analysis | ✅ Complete | |
| | M1 research | ✅ Draft generated | |
| | M2 research | ✅ Draft generated + merged | |
| | Web discovery | ✅ Complete | |
| | Evidence extraction | {"✅" if claims else "⚙️"} | |
| | Source verification | ✅ Complete | |
| | Contradiction check | {"⚠️ Found" if contradictions else "✅ Clear"} | |
| | Final synthesis | ✅ Complete | |
| |
| **Sources:** `{len(sources)}` |
| **Claims:** `{len(claims)}` |
| **Rounds:** `{rounds}` |
| **Time:** `{elapsed_ms} ms` |
| |
| ### Engine |
| |
| `M1` → `{M1_REPO}` |
| |
| `M2` → `{M2_REPO}` |
| |
| `Web` → `DuckDuckGo` |
| |
| `Fetcher` → `Scrapling` |
| |
| `Browser` → `Playwright` |
| """ |
|
|
|
|
| |
| |
| |
|
|
| def safe_dict(value): |
| if isinstance(value, dict): |
| return value |
| if hasattr(value, "model_dump"): |
| try: |
| return value.model_dump() |
| except Exception: |
| pass |
| if hasattr(value, "dict"): |
| try: |
| return value.dict() |
| except Exception: |
| pass |
| return {"result": str(value)} |
|
|
|
|
| |
| |
| |
|
|
| async def do_research(question, max_results, max_rounds, use_models, freshness): |
| empty_history = [] |
| empty_activity = "⚪ Enter a question to start." |
| empty_sources = "" |
| empty_evidence = "" |
| empty_verification = "" |
| empty_thinking = "" |
|
|
| if not question or not str(question).strip(): |
| return empty_history, empty_activity, empty_sources, empty_evidence, empty_verification, empty_thinking |
|
|
| question = str(question).strip() |
|
|
| |
| intent = classify_intent(question) |
| print(f"[Intent] {intent} for: {question}") |
|
|
| |
| if intent == "CASUAL": |
| answer = get_casual_model_response(question) |
| history = [ |
| {"role": "user", "content": question}, |
| {"role": "assistant", "content": answer} |
| ] |
| return history, "⚡ Casual chat (model reply, no search).", "", "", "", "" |
|
|
| |
| started = time.perf_counter() |
| try: |
| engine = get_engine() |
| report = await engine.search( |
| question=question, |
| max_results=int(max_results), |
| max_rounds=int(max_rounds), |
| use_models=bool(use_models), |
| freshness_mode=str(freshness), |
| ) |
| data = safe_dict(report) |
| elapsed_ms = int((time.perf_counter() - started) * 1000) |
|
|
| |
| sources = data.get("sources", []) |
| if not sources: |
| results = data.get("results", []) |
| for res in results: |
| if isinstance(res, dict): |
| sources.append({ |
| "title": res.get("title", ""), |
| "url": res.get("url", ""), |
| "snippet": res.get("snippet", ""), |
| "fetch_method": "web", |
| "source_score": res.get("rank", "N/A"), |
| "description": res.get("snippet", ""), |
| }) |
| data["sources"] = sources |
|
|
| |
| final_answer, thinking_content = get_combined_model_answer(question, sources) |
|
|
| sources_md = format_sources(sources) |
| evidence_md = format_evidence(data.get("claims", [])) |
| verification_md = format_verification(data.get("contradictions", [])) |
| activity_md = build_activity(data, elapsed_ms) |
|
|
| thinking_md = f"### 🧠 Reasoning\n\n{thinking_content}" if thinking_content else "" |
|
|
| history = [ |
| {"role": "user", "content": question}, |
| {"role": "assistant", "content": final_answer} |
| ] |
|
|
| return history, activity_md, sources_md, evidence_md, verification_md, thinking_md |
|
|
| except Exception as exc: |
| error = f"❌ **X-RUDRA Error**\n\n`{type(exc).__name__}: {exc}`" |
| print("\n" + "="*70) |
| print("X-RUDRA ERROR") |
| print(traceback.format_exc()) |
| print("="*70 + "\n") |
| history = [ |
| {"role": "user", "content": question}, |
| {"role": "assistant", "content": error} |
| ] |
| return history, "❌ Research failed.", "", "", "", "" |
|
|
|
|
| |
| |
| |
|
|
| @spaces.GPU |
| def run_research(question, max_results, max_rounds, use_models, freshness): |
| return asyncio.run(do_research(question, max_results, max_rounds, use_models, freshness)) |
|
|
|
|
| |
| |
| |
|
|
| def health_check(): |
| return f""" |
| ## 🟢 X-RUDRA Online |
| **Version:** `{VERSION}` |
| **M1:** `{M1_REPO}` |
| **M2:** `{M2_REPO}` |
| **Engine:** Lazy initialized |
| """ |
|
|
|
|
| |
| |
| |
|
|
| CSS = """ |
| body { background: #f7f7f8; } |
| .gradio-container { max-width: 1500px !important; } |
| #header { text-align: center; padding: 20px 0 10px 0; } |
| #logo { font-size: 38px; font-weight: 800; } |
| #tagline { opacity: 0.65; font-size: 15px; } |
| #chat { border-radius: 18px; } |
| #send { min-height: 52px; font-size: 18px; font-weight: 700; } |
| footer { display: none !important; } |
| |
| @keyframes think-pulse { |
| 0% { opacity: 0.3; transform: scale(0.95); } |
| 50% { opacity: 1; transform: scale(1.05); } |
| 100% { opacity: 0.3; transform: scale(0.95); } |
| } |
| .thinking-spinner { |
| display: inline-block; |
| width: 12px; |
| height: 12px; |
| border-radius: 50%; |
| background: #6b7280; |
| margin-right: 8px; |
| animation: think-pulse 1.2s ease-in-out infinite; |
| } |
| .thinking-container { |
| background: #f3f4f6; |
| border-left: 4px solid #6366f1; |
| padding: 12px 16px; |
| border-radius: 8px; |
| margin: 12px 0; |
| font-family: monospace; |
| white-space: pre-wrap; |
| word-wrap: break-word; |
| } |
| """ |
|
|
|
|
| |
| |
| |
|
|
| with gr.Blocks(title=APP_NAME) as demo: |
| gr.HTML(""" |
| <div id="header"> |
| <div id="logo">⚡ X-RUDRA</div> |
| <div id="tagline">Dual‑Model AI · Live Web Research · Evidence</div> |
| </div> |
| """) |
|
|
| with gr.Row(): |
| with gr.Column(scale=7): |
| chatbot = gr.Chatbot(label="X-RUDRA", height=600, elem_id="chat") |
| with gr.Row(): |
| question = gr.Textbox(placeholder="Ask X-RUDRA anything...", lines=2, show_label=False, scale=8) |
| send = gr.Button("➤", variant="primary", elem_id="send", scale=1) |
|
|
| with gr.Column(scale=4): |
| gr.Markdown("## 🔬 Live Research") |
| activity = gr.Markdown("⚪ Waiting for your question.") |
| thinking = gr.Markdown("", visible=True) |
| gr.Markdown("---") |
| gr.Markdown(f""" |
| ### Model Spaces |
| **M1** `{M1_REPO}` |
| **M2** `{M2_REPO}` |
| ### Web Stack |
| `DuckDuckGo` · `Scrapling` · `Playwright` |
| """) |
|
|
| with gr.Accordion("⚙️ Research Controls", open=False): |
| with gr.Row(): |
| max_results = gr.Slider(1, 30, value=10, step=1, label="Max Sources") |
| max_rounds = gr.Slider(1, 5, value=3, step=1, label="Research Rounds") |
| with gr.Row(): |
| use_models = gr.Checkbox(value=True, label="Use M1 + M2") |
| freshness = gr.Dropdown(["auto","latest","recent","current"], value="auto", label="Freshness") |
|
|
| with gr.Tabs(): |
| with gr.Tab("📚 Sources"): |
| sources = gr.Markdown("Sources will appear here.") |
| with gr.Tab("🧠 Evidence"): |
| evidence = gr.Markdown("Evidence will appear here.") |
| with gr.Tab("⚖️ Verification"): |
| verification = gr.Markdown("Verification will appear here.") |
|
|
| with gr.Accordion("🩺 System Health", open=False): |
| health_button = gr.Button("Check X-RUDRA") |
| health_output = gr.Markdown() |
|
|
| gr.Markdown("### Try X-RUDRA") |
| gr.Examples( |
| examples=[ |
| ["What are the latest UNESCO AI education initiatives?"], |
| ["What are the latest developments in open source AI?"], |
| ["Compare the latest major AI models."], |
| ["Research India's current AI ecosystem."] |
| ], |
| inputs=question |
| ) |
|
|
| inputs = [question, max_results, max_rounds, use_models, freshness] |
| outputs = [chatbot, activity, sources, evidence, verification, thinking] |
|
|
| send.click(fn=run_research, inputs=inputs, outputs=outputs) |
| question.submit(fn=run_research, inputs=inputs, outputs=outputs) |
| health_button.click(fn=health_check, inputs=[], outputs=[health_output]) |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| print(f"Starting {APP_NAME} {VERSION}") |
| print("M1:", M1_REPO) |
| print("M2:", M2_REPO) |
| print("Lazy engine initialization: ON") |
| if HF_TOKEN: |
| print("HF_TOKEN set – rate limits reduced.") |
| else: |
| print("HF_TOKEN not set – you may experience rate limits. Set it as a Secret in your Space.") |
| demo.launch(server_name="0.0.0.0", server_port=PORT, css=CSS, show_error=True) |