""" app.py — Gradio entry point for the Feature Finder Space. Flow per request: 1. Parse repo URL, check cache (24h TTL) 2. CPU: fetch GitHub commits + Gemini summarization → team_context 3. CPU: embedding pre-filter against precomputed arXiv pool → top-50 4. GPU (@spaces.GPU): v1.4 scoring of top-50, then base-model PR body 5. Render: paper card, confidence tier, PR body, alternates, CTA The @spaces.GPU function is intentionally narrow: just scoring + generation. Everything else runs on CPU, keeping Zero-GPU quota usage proportional to actual model work. """ from __future__ import annotations import os import time import traceback from typing import Any import gradio as gr import numpy as np import cache import candidate_filter import confidence import model_io import team_context # The @spaces.GPU decorator is for Zero-GPU Spaces only — it spawns a # worker process that requests a GPU from the Zero-GPU pool. On # persistent paid GPU Spaces (h200, a10g-small, etc.), the GPU is # always attached to the main container; the decorator would either be # a no-op or fail at worker-init. # # Set USE_SPACES_GPU=false on persistent GPU Spaces. Default true so # Zero-GPU deployments keep working unchanged. USE_SPACES_GPU = os.environ.get("USE_SPACES_GPU", "true").lower() in ("true", "1", "yes") if USE_SPACES_GPU: import spaces _gpu_decorator = spaces.GPU(duration=60) else: _gpu_decorator = lambda f: f # no-op on persistent GPU # Loaded at cold-start (CPU). The GPU model is loaded inside the # scoring function on first invocation. EMBEDDER_PRELOAD = candidate_filter.load_embedder() POOL_PRELOAD = candidate_filter.load_pool() @_gpu_decorator def gpu_score_and_maybe_generate(team_ctx: str, candidates_records: list[dict], do_local_generation: bool) -> dict: """The GPU-only step. Always scores candidates with v1.4 LoRA enabled. Conditionally generates the PR body locally (LoRA disabled) if do_local_generation=True. When the configured backend is Gemini, pass False — the orchestrator will call Gemini on CPU after this function returns, freeing GPU time for the next request. Each candidate record is expected to include an `embedding_similarity` field (the cosine to the team's domain summary), carried through from the CPU pre-filter. confidence.tier_score uses it as an OOD detector.""" model, tokenizer = model_io.load() # Score every candidate with LoRA active model.enable_adapters() scores = [] for c in candidates_records: paper_text = f"Title: {c['title']}\n\nAbstract: {c['abstract']}" s = model_io.score_pair(team_ctx, paper_text) scores.append(s) scores_arr = np.array(scores, dtype=np.float32) top_idx = int(np.argmax(scores_arr)) top = candidates_records[top_idx] top_score = float(scores_arr[top_idx]) top_sim = float(top.get("embedding_similarity", 0.0)) # Pick alternates: 2nd through 4th by score order = np.argsort(-scores_arr) alternates = [candidates_records[i] for i in order[1:4]] alternate_scores = [float(scores_arr[i]) for i in order[1:4]] alternate_sims = [float(candidates_records[i].get("embedding_similarity", 0.0)) for i in order[1:4]] # Tier the top pick AND each alternate against the same pool # distribution. Pass embedding similarity so OOD vocabulary collisions # (high z but low cosine) are correctly demoted. tier = confidence.tier_score(top_score, scores_arr, similarity=top_sim) alternate_tiers = [ confidence.tier_score(s, scores_arr, similarity=sim) for s, sim in zip(alternate_scores, alternate_sims) ] pr_body: str | None = None if do_local_generation: # Generate PR body with LoRA off (the base model handles generation) model.disable_adapters() paper_text = f"Title: {top['title']}\n\nAbstract: {top['abstract']}" pr_body = model_io._generate_pr_body_local(team_ctx, paper_text) model.enable_adapters() # leave in scoring state for next request return { "top": top, "top_score": top_score, "tier": tier.__dict__, "alternates": [ {"paper": a, "score": s, "tier": t.__dict__} for a, s, t in zip(alternates, alternate_scores, alternate_tiers) ], "pr_body": pr_body, # None when generation is delegated to Gemini "scores_summary": { "n_candidates": len(scores), "max": top_score, "median": float(np.median(scores_arr)), "min": float(np.min(scores_arr)), }, } def _format_result(result: dict, team_ctx: str, elapsed_s: float) -> tuple[str, str, str, str, str]: """Render the per-request result into the 5 Gradio output components.""" top = result["top"] tier = result["tier"] arxiv_id = top.get("arxiv_id", "") url = f"https://arxiv.org/abs/{arxiv_id}" if arxiv_id else "" tier_emoji = {"high": "🟢", "moderate": "🟡", "low": "🟠", "noise": "🔴"} top_emoji = tier_emoji.get(tier["tier"], "⚪️") paper_md = ( f"### {top_emoji} {top['title']}\n\n" f"[arxiv.org/abs/{arxiv_id}]({url}) · " f"score {result['top_score']:+.3f} · " f"margin {tier['margin_z']:+.2f}σ above candidate-pool median\n\n" f"**Abstract**\n\n{top['abstract']}" ) confidence_md = ( f"## {top_emoji} {tier['label']}\n\n" f"**Estimated reliability**: ~{tier['confidence_pct']:.0f}% accurate at this confidence tier\n\n" f"{tier['explainer']}" ) # Button-styled CTA link with explicit colors so it renders # readably in both light and dark Gradio themes (the default # markdown link blue clashes with dark-mode backgrounds). cta_button = ( '' 'Sign up at engine.remyx.ai →' ) if tier["tier"] == "high": # Light footer CTA — user is getting real value from the free # generalist; don't push hard, but keep the upsell visible. confidence_md += ( "\n\n---\n\n" "_Want recommendations tailored to your team's development practices?_\n\n" f"{cta_button}" ) else: # Prominent CTA — user saw the generalist's limits (moderate / # low / noise tier). Personalized scoring directly addresses the # cause: a model adapted to the team's development practices. confidence_md += ( "\n\n---\n\n" "### Get sharper recommendations on this repo\n\n" "A personalized model tailored to your team's development practices " "typically lifts confidence by **10-15pts** on team-specific picks — " "and produces PR drafts that match your team's actual engineering style.\n\n" f"{cta_button}" ) pr_body_md = result["pr_body"] alternates_md = "### Other candidates\n\n" for entry in result["alternates"]: p = entry["paper"] s = entry["score"] t = entry["tier"] emoji = tier_emoji.get(t["tier"], "⚪️") aid = p.get("arxiv_id", "") alternates_md += ( f"- {emoji} **{p['title']}** " f"[arxiv]({f'https://arxiv.org/abs/{aid}' if aid else '#'}) · " f"{t['label']} · score {s:+.3f} ({t['margin_z']:+.2f}σ)\n" ) domain_summary = result.get("domain_summary", "") diagnostics_md = ( f"**Latency**: {elapsed_s:.1f}s\n\n" f"**Candidates scored**: {result['scores_summary']['n_candidates']}\n\n" f"**Score distribution**: " f"median {result['scores_summary']['median']:+.3f}, " f"min {result['scores_summary']['min']:+.3f}, " f"max {result['scores_summary']['max']:+.3f}\n\n" f"
Domain summary (embedding query)\n\n" f"```\n{domain_summary}\n```\n\n
\n\n" f"
Team context (v1.4 scoring prompt)\n\n" f"```\n{team_ctx[:2000]}\n```\n\n
" ) return paper_md, confidence_md, pr_body_md, alternates_md, diagnostics_md def recommend(repo_url: str) -> tuple[str, str, str, str, str]: """End-to-end pipeline. Returns the 5 Markdown outputs.""" start = time.time() if not repo_url or not repo_url.strip(): return ("⚠️ Please enter a GitHub repository URL.", "", "", "", "") cached = cache.get(repo_url) if cached: paper, confidence_md, pr_body_md, alternates, diagnostics = cached diagnostics += "\n\n*Served from cache (24h TTL).*" return paper, confidence_md, pr_body_md, alternates, diagnostics try: tc_result = team_context.build_team_context(repo_url, n_events=4) except ValueError as e: return (f"⚠️ {e}", "", "", "", "") except Exception as e: return (f"⚠️ Couldn't fetch repo history: {e}", "", "", "", "") # `domain_summary` is the embedding query — clean natural-language # description of the team's domain, aligned with arxiv abstract style. # `team_context` is the scoring prompt — corpus format, matches v1.4 # training distribution. try: candidates = candidate_filter.top_k_candidates(tc_result.domain_summary, k=50) except Exception as e: return (f"⚠️ Candidate retrieval failed: {e}", "", "", "", "") # Pass the embedding similarity through to GPU scoring so confidence # tiering can use it as an OOD detector. records = candidates[["arxiv_id", "title", "abstract", "similarity"]].rename( columns={"similarity": "embedding_similarity"} ).to_dict(orient="records") do_local_gen = model_io.generation_runs_on_gpu() try: result = gpu_score_and_maybe_generate(tc_result.team_context, records, do_local_gen) except Exception: return ( f"⚠️ GPU scoring failed:\n```\n{traceback.format_exc()[:2000]}\n```", "", "", "", "", ) # When the configured backend is Gemini, the GPU function returns # pr_body=None and we generate on CPU here. This is the all-Gemini path. if result["pr_body"] is None: try: from llm_api import gemini_generate_pr_body top = result["top"] paper_text = f"Title: {top['title']}\n\nAbstract: {top['abstract']}" result["pr_body"] = gemini_generate_pr_body(tc_result.team_context, paper_text) except Exception as e: result["pr_body"] = ( f"_PR body generation failed (Gemini backend): {e}_\n\n" f"Set MHPD_GENERATION_BACKEND=local to fall back to the " f"self-hosted 2B model." ) # Stash the domain summary in the result so diagnostics can show it result["domain_summary"] = tc_result.domain_summary team_ctx = tc_result.team_context # alias used by _format_result below elapsed = time.time() - start rendered = _format_result(result, team_ctx, elapsed) cache.put(repo_url, rendered) return rendered with gr.Blocks(title="Feature Finder", theme="soft") as demo: gr.Markdown( "# 🔍 Feature Finder\n\n" "Paste a **public** GitHub repository URL → we'll survey the team's " "recent merge history, find a relevant recent arXiv paper, and draft " "a PR-ready spec (summary, motivation, implementation plan, open " "questions) that a coding agent can pick up and run with.\n\n" "Free preview using our open-source generalist scorer (LoRA on " "Qwen3.5-2B). For recommendations tailored to your team's " "development practices: " '' "sign up at engine.remyx.ai.", sanitize_html=False, ) with gr.Row(): repo_input = gr.Textbox( label="GitHub repository URL", placeholder="https://github.com/owner/name", scale=4, ) run_btn = gr.Button("Recommend", variant="primary", scale=1) with gr.Tab("Recommendation"): paper_out = gr.Markdown() # sanitize_html=False so the explicit-color CTA renders with # its inline styles instead of Gradio's theme-blue (invisible # on dark backgrounds — original bug report 2026-05-25). confidence_out = gr.Markdown(sanitize_html=False) with gr.Tab("PR body draft"): pr_body_out = gr.Markdown() with gr.Tab("Alternates"): alternates_out = gr.Markdown() with gr.Tab("Diagnostics"): diagnostics_out = gr.Markdown() gr.Examples( examples=[ # Remyx's own — multimodal data synthesis; demos the moderate # tier on a real internal repo + the CTA value-prop. "https://github.com/remyxai/VQASynth", # Popular multi-agent framework — recognizable to most ML devs, # typically lands high tier with STORM-style multi-agent papers. "https://github.com/microsoft/autogen", # Popular RAG / data-indexing framework — strong high-tier # demo case; relevant retrieval-augmented papers are dense # in the pool. "https://github.com/run-llama/llama_index", # Hyperparameter optimization library — less mainstream, tests # the LLM-training/ML adjacency and demos honest low-tier # behavior on niche subdomains. "https://github.com/hyperactive-project/Hyperactive", ], inputs=[repo_input], ) run_btn.click( recommend, inputs=[repo_input], outputs=[paper_out, confidence_out, pr_body_out, alternates_out, diagnostics_out], ) if __name__ == "__main__": demo.launch()