File size: 4,886 Bytes
6e8672b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import os
os.environ.setdefault("ASTRANEXUS_FINETUNED", "1")  # run the SP3 fine-tuned encoder
import astranexus.config  # noqa: F401 — loads any HF Space secrets from env
import os.path
import gradio as gr
from fastapi import FastAPI
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
from astranexus.ui.app import build_app, DARK

app = FastAPI(title="AstraNexus")

# 'Sign in with Google' web flow (live mode uses the Gmail REST API over 443,
# because the Space firewalls outbound IMAP). No-op if the OAuth client secrets
# aren't set, so the fixture demo still runs.
from astranexus.web import google_oauth
google_oauth.attach(app)

LANDING = """<!doctype html><html><head><meta charset=utf-8>

<meta name=viewport content="width=device-width,initial-scale=1">

<title>AstraNexus</title>

<style>

 *{box-sizing:border-box} body{margin:0;background:#0a0e17;color:#c0caf5;

   font-family:system-ui,Segoe UI,Roboto,sans-serif;line-height:1.6}

 .wrap{max-width:860px;margin:0 auto;padding:64px 24px}

 h1{font-size:44px;margin:0 0 4px;color:#fff;letter-spacing:-.5px}

 .tag{color:#7aa2f7;font-size:18px;margin-bottom:28px}

 .cta{display:inline-block;margin:18px 0 36px;padding:14px 30px;border-radius:10px;

   background:#7aa2f7;color:#0a0e17;font-weight:700;text-decoration:none;font-size:16px}

 .cta:hover{background:#9eb8ff}

 .grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:24px 0}

 .card{background:#0d1320;border:1px solid #29304a;border-radius:12px;padding:16px 18px}

 .card b{color:#fff} .k{color:#7aa2f7}

 .pills span{display:inline-block;background:#0d1320;border:1px solid #29304a;

   border-radius:20px;padding:4px 12px;margin:4px 6px 0 0;font-size:13px}

 a{color:#7aa2f7} .foot{margin-top:40px;opacity:.7;font-size:13px}

</style></head><body><div class=wrap>

 <h1>&#10022; AstraNexus</h1>

 <div class=tag>Your inbox, as a navigable galaxy.</div>

 <p>Unsupervised email organizer. A <b>fine-tuned multimodal encoder</b>

 (MiniLM&nbsp;text&nbsp;+&nbsp;SigLIP&nbsp;vision, LoRA) embeds every email; HDBSCAN groups them into

 <b>constellations</b> that <b>NVIDIA&nbsp;Nemotron-3-Nano-4B</b> names and reasons over;

 threads spanning accounts surface as <b>wormholes</b>. No rules, no folders &mdash;

 the structure is discovered.</p>

 <a class=cta href="app/">Launch the demo &rarr;</a>

 <div class=grid>

  <div class=card><b>Color</b> &mdash; the <span class=k>constellation</span> (topic the

    encoder grouped it into). Each has a Nemotron-named hub star.</div>

  <div class=card><b>Size</b> &mdash; unread (big) vs read. <b>Brightness</b> &prop; urgency.</div>

  <div class=card><b>Gold halo</b> &mdash; the email carries an image (where the

    multimodal model earns its keep).</div>

  <div class=card><b>Pink dashed</b> &mdash; a wormhole: one thread across two accounts.</div>

 </div>

 <p><b>Under the hood:</b> the whole AI pipeline runs on a <b>Modal</b> GPU as a

 <b>LangGraph</b> agentic graph &mdash;

 fetch&rarr;embed&rarr;cluster&rarr;label&rarr;<b>judge</b>&rarr;serialize. An

 <b>LLM-as-judge</b> (Nemotron) scores the labels and loops back to relabel until they

 pass, then it writes an open agent trace &mdash;

 <a href="trace.json">view the latest trace.json &rarr;</a>. An in-app <b>A/B toggle</b>

 pits the fine-tuned encoder against off-the-shelf SigLIP; layered fallbacks

 (Modal&nbsp;&rarr;&nbsp;NIM-hosted&nbsp;Nemotron&nbsp;&rarr;&nbsp;keyword heuristic) keep it alive.</p>

 <p style="opacity:.75;font-size:14px"><b>About the data:</b> the default

 <b>fixture</b> is a <i>synthetic</i> dataset built only for demo/testing &mdash; no real

 email. Google restricts reading people's mail, so live Gmail is invite-only

 (approved test accounts / my own mailboxes); session-only, never stored.</p>

 <div class=pills>

   <span>&#129504; Nemotron-3-Nano-4B</span><span>&#9889; Modal GPU</span>

   <span>&#128376; LangGraph + judge loop</span><span>&#127919; Fine-tuned encoder + A/B</span>

   <span>&#128225; Open agent trace</span>

 </div>

 <p class=foot>Fine-tuned encoder:

   <a href="https://huggingface.co/DriptoBhattacharyya/astranexus-mm-encoder">

   DriptoBhattacharyya/astranexus-mm-encoder</a> &middot; built for the Build Small hackathon.</p>

</div></body></html>"""


@app.get("/", response_class=HTMLResponse)
def home():
    return LANDING


@app.get("/trace.json")
def trace():
    # the LangGraph agent trace from the most recent map (Open Trace)
    if os.path.exists("trace.json"):
        return FileResponse("trace.json", media_type="application/json")
    return JSONResponse({"detail": "Run a map first to generate the agent trace."},
                        status_code=404)


demo = build_app(theme=DARK)            # custom dark Gradio mounted under /app
app = gr.mount_gradio_app(app, demo, path="/app")