Corso
I modelli con contesto lungo sbloccano un modo diverso di analizzare i codebase. Invece di suddividere un repository in chunk, crearne gli embedding e recuperare i pezzi rilevanti, puoi impacchettare un intero repo piccolo-medio in un unico prompt e lasciare che il modello ragioni end-to-end.
In questo tutorial, ti mostrerò come costruire un analizzatore di repository GitHub usando NVIDIA Nemotron 3 Super con un'interfaccia Gradio. L'app prende un URL di un repo pubblico, lo clona, filtra i file non essenziali, stima l'uso di token e impacchetta il contenuto restante in un unico prompt strutturato. Questo prompt viene poi inviato a Nemotron 3 Super tramite l'endpoint ospitato di OpenRouter, che restituisce output JSON strutturati per attività come analisi dell'architettura, guida all'onboarding, lacune nei test, opportunità di miglioramento e altro.
Non c'è nessun passaggio di embedding, nessun database vettoriale e nessun loop di retrieval. Al contrario, ci affidiamo interamente al prompt packing, che funziona bene finché il repository rientra nella finestra di contesto pratica del modello. Alla fine di questo tutorial, avrai un'app Gradio che può:
- Clonare e preprocessare un repository GitHub pubblico
- Stimare se rientra in un budget di token
- Eseguire una o più attività di analisi del repo
- Presentare risultati strutturati tramite una UI consapevole del task
Il video qui sotto mostra il progetto in azione (con tempi di caricamento abbreviati per fini dimostrativi):
Puoi accedere al codice completo di questa demo qui.
Che cos'è NVIDIA Nemotron 3 Super?
Nemotron 3 Super è il modello open di NVIDIA per il reasoning in workflow agentici e con contesti lunghi. Usa un'architettura ibrida Mamba-Transformer Latent MoE con 120B parametri totali e 12B attivi, include la multi-token prediction (MTP) ed è progettato per task con forte componente di ragionamento come coding, uso di tool e analisi di contesti lunghi.
NVIDIA lo posiziona specificamente per applicazioni multi-agente complesse e reasoning sull'intero codebase.

Figura 1: Architettura di Nemotron 3 Super (NVIDIA Blog)
Alcune idee chiave dietro Nemotron 3 Super includono:
- Uno scheletro ibrido Mamba-Transformer (Figura 1) per efficienza su sequenze lunghe e recupero preciso,
- Latent MoE per instradare i token attraverso più specialisti allo stesso costo effettivo,
- MTP per migliorare velocità di generazione e qualità del ragionamento,
- Una finestra di contesto nativa da 1M di token nello stack di deployment supportato da NVIDIA.
Per questo tutorial useremo il free endpoint ospitato su OpenRouter, dato che sia la variante completa sia quella quantizzata del modello richiedono tipicamente almeno una GPU H100 per inferenza pratica. L'endpoint ospitato supporta attualmente una finestra di contesto da 262K token (invece dell'intero 1M), comunque sufficiente per analizzare repository di medie dimensioni in questa demo.
Famiglia Nemotron 3
La famiglia Nemotron 3 di NVIDIA è stata introdotta come una famiglia di modelli open per sistemi di AI agentica, con diverse dimensioni mirate a differenti esigenze di deployment e reasoning, denominate livelli Nano, Super e Ultra.
A livello pratico, puoi pensare alla famiglia così:
- Nemotron 3 Nano: la linea di modelli più leggera, mirata a task mirati ed efficienti e inferenza a costo ridotto, focalizzata su workload agentici e passaggi specifici all'interno di workflow più ampi.
- Nemotron 3 Super: il livello intermedio di "reasoning serio", noto per il ragionamento tecnico più denso, task di coding complessi, analisi di contesti lunghi e sistemi agentici di volume più alto.
- Nemotron 3 Ultra: il livello più grande per scenari di reasoning/deployment ancora più pesanti.
Quindi, Nemotron 3 Super è la scelta giusta qui perché vogliamo un modello che sappia ragionare su un intero repository in una volta sola, non solo risolvere sotto-task di coding isolati.
Tutorial Nemotron 3 Super: crea un analizzatore di repository con Gradio
In questa sezione costruiremo un analizzatore di repo usando Nemotron 3 Super con interfaccia Gradio. A grandi linee, ecco cosa fa l'app finale:
- Accetta un URL di repository GitHub pubblico e un nome di branch opzionale.
- Clona il repository in locale, filtra i file non essenziali e stima se il codice rimanente rientra nel budget di contesto disponibile.
- Esclude opzionalmente test, documentazione o notebook per rendere il repository impacchettato più piccolo e più semplice da analizzare.
- Impacchetta il repository filtrato in un unico prompt a contesto lungo e lo invia a Nemotron 3 Super tramite OpenRouter.
- Infine, restituisce risultati strutturati relativi al repo.
Costruiamola passo dopo passo.
Passo 1: Installa le dipendenze
Prima di costruire l'analizzatore di repo, ci serve un ambiente minimale che gestisca tre responsabilità chiave: rendering della UI, elaborazione del repository e interazione con il modello. Dato che non eseguiamo Nemotron in locale, il setup resta leggero e mirato.
Installa tutto eseguendo:
!pip -q install gradio openai tiktoken gitpython orjson pandas
In questo progetto useremo:
openaiper interagire con l'API di OpenRouter (endpoint Nemotron)gradioper costruire la UI interattivatiktokenper stimare l'uso di token prima di inviare i promptorjsonper il parsing JSON veloce degli output del modellopandasper rendere i risultati strutturati come tabellegitpythonper clonare e lavorare con i repository
A differenza dei setup LLM locali (che richiedono CUDA, vLLM o quantizzazione), questo approccio evita del tutto dipendenze GPU e mantiene l'ambiente adatto a Colab.
Passo 2: Inizializza il client OpenRouter
In questo passo configuriamo Nemotron 3 Super via OpenRouter e definiamo alcune costanti chiave che controllano come vengono processati i repository. Poiché non eseguiamo il modello in locale, questo client diventa l'interfaccia centrale per inviare prompt di grandi dimensioni impacchettati e ricevere output di analisi strutturati.
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
MODEL = "nvidia/nemotron-3-super-120b-a12b:free"
WORKDIR = "/content/repo_ui"
if not OPENROUTER_API_KEY:
raise ValueError("Set OPENROUTER_API_KEY in environment before running.")
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=OPENROUTER_API_KEY,
)
enc = tiktoken.get_encoding("cl100k_base")
A questo stadio, l'applicazione comunica con Nemotron 3 Super tramite OpenRouter usando il client Python openai configurato con una base_url personalizzata e una chiave API memorizzata in OPENROUTER_API_KEY.
Il modello selezionato punta all'endpoint ospitato su OpenRouter, che ci consente di eseguire un modello molto grande senza necessitare di infrastruttura GPU locale.
Per gestire la dimensione del contesto, inizializziamo un tokenizer (tiktoken con cl100k_base) così da stimare quanti token consumerà il repository impacchettato prima di inviarlo al modello. Questo perché il modello ha una finestra di contesto finita.
Alcune considerazioni pratiche:
- Il free endpoint di OpenRouter registra sia i prompt sia gli output, quindi questo setup andrebbe usato solo con repository pubblici.
- La finestra di contesto disponibile (~262K token) è più piccola del massimo teorico del modello, quindi i repository grandi possono richiedere filtraggio (ad esempio escludendo test, documenti o notebook). Evita inoltre di inviare codice proprietario o sensibile per motivi di sicurezza.
- Poiché tutto è instradato tramite un'API remota, il setup rimane leggero ed evita la dipendenza da GPU con molta memoria come le H100.
Passo 3: Filtri del repo e regole del prompt
Ora ci servono due salvaguardie: filtraggio dei file e ancoraggio del prompt.
La prima impedisce di sprecare contesto su artefatti irrilevanti come output di build, notebook, immagini o file binari.
La seconda assicura che, una volta impacchettato il repository, il modello rimanga ancorato al codice reale invece di scivolare in consigli software generici.
Insieme, queste regole rendono l'analisi del repo più efficiente.
BASE_IGNORE_DIRS = {
".git", ".github", "__pycache__", "node_modules", ".venv", "venv",
"dist", "build", ".mypy_cache", ".pytest_cache", ".idea", ".vscode"
}
IGNORE_SUFFIXES = {
".png", ".jpg", ".jpeg", ".gif", ".webp", ".pdf", ".zip", ".gz",
".tar", ".mp4", ".mov", ".onnx", ".pt", ".bin", ".parquet", ".feather",
".ico", ".svg", ".lock"
}
MAX_FILE_BYTES = 250_000
SYSTEM_PROMPT = """You are a codebase analysis model.
You are given the complete repository contents in one prompt.
Rules:
1. Use only the provided repository content.
2. Do not invent files, modules, libraries, or dependencies.
3. Every substantial claim must cite one or more file paths.
4. Clearly separate direct observations from engineering inferences or recommendations.
5. For modernization or improvement suggestions, explicitly distinguish:
- repo-grounded findings
- general engineering recommendations
6. Return valid JSON only.
7. If uncertain, say so explicitly.
8. For anything involving security, dead code, dependency replacement, or testing gaps,
present findings as likely candidates unless directly proven by repository content.
"""
Le tre variabili di filtraggio seguenti controllano il preprocessing del repository:
BASE_IGNORE_DIRSesclude directory irrilevanti per l'analisi del codice o troppo rumorose per essere utili, come.git,node_modules, cartelle di build e cache specifiche dell'editor.IGNORE_SUFFIXESfiltra tipi di file generalmente poco utili per il reasoning sul repo, incluse immagini, archivi, video, checkpoint di modelli e altri artefatti.MAX_FILE_BYTESaggiunge un ulteriore controllo di sicurezza saltando i file troppo grandi, aiutando a mantenere il repository impacchettato entro un budget di contesto pratico.
Il SYSTEM_PROMPT è importante quanto i filtri dei file.
Poiché il modello vede il repository come un unico lungo prompt, dobbiamo fornire vincoli comportamentali chiari per evitare di inventare moduli, fraintendere astrazioni o dare raccomandazioni troppo generiche.
Queste istruzioni impongono esplicitamente al modello di restare ancorato al repository fornito, citare i percorsi dei file per le affermazioni importanti e separare le evidenze dirette dal giudizio generale.
Passo 4: Schemi dei task e salvaguardie
Ora che abbiamo il filtraggio dei file e un system prompt, il passo successivo è definire un set di task a livello di repository, ciascuno associato a uno schema JSON strutturato.
Questo ci dà due vantaggi principali: il modello sa esattamente che tipo di output produrre e l'app può fare parsing e renderizzare in modo affidabile i risultati in tabelle specifiche per task.
TASK_DEFS: Dict[str, Dict[str, Any]] = {
"Architecture Overview": {
"description": "Explain the architecture, major modules, likely entrypoints, dependencies, and architectural issues.",
"schema": {
"repo_overview": {
"summary": "",
"primary_languages": [],
"likely_entrypoints": []
},
"modules": [
{
"name": "",
"responsibility": "",
"key_files": [],
"evidence_paths": []
}
],
"dependencies": [
{
"from_module": "",
"to_module": "",
"relationship": "",
"evidence_paths": []
}
],
"issues": [
{
"type": "",
"description": "",
"severity": "low|medium|high",
"evidence_paths": []
}
],
"unknowns_and_uncertainties": []
},
},
...
}
Il dizionario TASK_DEFS funge da registro centrale per tutte le modalità di analisi supportate dall'app.
Ogni task contiene una descrizione in linguaggio naturale che dice al modello cosa fare e uno schema rigoroso che definisce l'output JSON atteso.
È questo che permette allo stesso repository impacchettato di supportare forme di reasoning molto diverse, come mappatura dell'architettura, analisi delle duplicazioni, guida all'onboarding o rilevamento di lacune nei test.
Per esempio, Architecture Overview chiede al modello di identificare:
- un sommario di alto livello del repository,
- i moduli principali,
- le relazioni di dipendenza,
- e i problemi architetturali.
Altri task nel codice completo includono:
Code Duplication, incentrato sulla logica ripetuta e le opportunità di refactor correlate,Improvement Opportunitiesche identifica guadagni di manutenibilità e modularità,- Refactor Roadmap che trasforma i risultati in un piano di ingegneria prioritizzato,
Testing Gapsche evidenzia aree sotto-testate,Onboarding Guideche spiega come un nuovo ingegnere dovrebbe approcciare il codebase,Library Modernizationche fa emergere librerie obsolete o sovrapposte,Code Quality Hotspotsche indica aree fragili o inclini ai bug,- e
SecurityoConfig Hygieneche segnala impostazioni rischiose o problemi operativi.
FULL_REVIEW_TASKS = [
"Architecture Overview",
"Improvement Opportunities",
"Code Quality Hotspots",
"Testing Gaps",
"Refactor Roadmap",
]
TASK_CHOICES = [
"Architecture Overview",
"Code Duplication",
"Improvement Opportunities",
"Refactor Roadmap",
"Testing Gaps",
"Onboarding Guide",
"Library Modernization",
"Code Quality Hotspots",
"Security / Config Hygiene",
"Full Review",
]
def safe_str(value: Any) -> str:
if value is None:
return ""
if isinstance(value, str):
return value.strip()
return str(value).strip()
La lista FULL_REVIEW_TASKS definisce una revisione multi-step in bundle.
Invece di eseguire ogni possibile task, avvia un sottoinsieme curato che dà una visione ingegneristica ampia del repository. TASK_CHOICES è poi usato direttamente nel menu a discesa di Gradio così che l'utente possa scegliere un singolo task o la revisione in bundle.
L'ultima parte, safe_str(), risolve un problema di stabilità sorprendentemente importante. Durante lo sviluppo, una modalità di errore comune era:
'NoneType' object has no attribute 'strip'
Questo può accadere quando:
- un campo di input di Gradio restituisce
None, - manca un valore di task,
- una risposta del modello ha contenuto vuoto,
- o un parser riceve un valore null inatteso.
Normalizzando tutti questi valori tramite la funzione safe_str(), rendiamo l'app molto più robusta.
Passo 5: Clonazione e impacchettamento del repo
Prima di poter inviare qualcosa al modello, dobbiamo trasformare un repository GitHub in un unico prompt strutturato.
Questo passo gestisce l'intera pipeline di ingestione: clonazione del repo, filtraggio dei file irrilevanti, estrazione del solo testo utilizzabile e impacchettamento di tutto in un formato che il modello possa consumare in un'unica passata.
A differenza delle pipeline RAG tradizionali, qui non facciamo chunking o embedding. Stiamo invece comprimendo l'intero repository in un unico input a contesto lungo, quindi essere selettivi e strutturati in questa fase è fondamentale sia per le prestazioni sia per la qualità.
def clone_repo(repo_url: str, branch: str = "main") -> str:
repo_url = safe_str(repo_url)
branch = safe_str(branch)
if not repo_url:
raise ValueError("Repository URL is empty.")
if os.path.exists(WORKDIR):
shutil.rmtree(WORKDIR)
cmd = ["git", "clone", "--depth", "1"]
if branch:
cmd += ["--branch", branch]
cmd += [repo_url, WORKDIR]
subprocess.run(cmd, check=True, capture_output=True, text=True)
return WORKDIR
def is_probably_text(path: Path) -> bool:
try:
with open(path, "rb") as f:
chunk = f.read(2048)
if b"\x00" in chunk:
return False
chunk.decode("utf-8")
return True
except Exception:
return False
def iter_repo_files(root: str, exclude_tests: bool, exclude_docs: bool, exclude_notebooks: bool):
root = Path(root)
for path in root.rglob("*"):
if not path.is_file():
continue
rel = path.relative_to(root)
parts = set(rel.parts)
rel_str = str(rel).lower()
if any(part in BASE_IGNORE_DIRS for part in rel.parts):
continue
if exclude_tests and ("test" in parts or "tests" in parts or "/tests/" in f"/{rel_str}/"):
continue
if exclude_docs and ("docs" in parts or rel_str.endswith(".md") or rel_str.endswith(".rst")):
continue
if exclude_notebooks and rel_str.endswith(".ipynb"):
continue
if path.suffix.lower() in IGNORE_SUFFIXES:
continue
try:
if path.stat().st_size > MAX_FILE_BYTES:
continue
except Exception:
continue
if not is_probably_text(path):
continue
yield path
def repo_tree_string(root: str, exclude_tests: bool, exclude_docs: bool, exclude_notebooks: bool) -> str:
root = Path(root)
lines: List[str] = []
for path in sorted(root.rglob("*")):
rel = path.relative_to(root)
rel_str = str(rel).lower()
parts = set(rel.parts)
if any(part in BASE_IGNORE_DIRS for part in rel.parts):
continue
if path.is_file() and path.suffix.lower() in IGNORE_SUFFIXES:
continue
if exclude_tests and ("test" in parts or "tests" in parts or "/tests/" in f"/{rel_str}/"):
continue
if exclude_docs and ("docs" in parts or rel_str.endswith(".md") or rel_str.endswith(".rst")):
continue
if exclude_notebooks and rel_str.endswith(".ipynb"):
continue
lines.append(f"{rel}/" if path.is_dir() else str(rel))
return "\n".join(lines)
def pack_repo(root: str, exclude_tests: bool, exclude_docs: bool, exclude_notebooks: bool):
root = Path(root)
tree = repo_tree_string(root, exclude_tests, exclude_docs, exclude_notebooks)
files: List[Tuple[str, int, str]] = []
total_tokens = len(enc.encode(tree))
for path in iter_repo_files(root, exclude_tests, exclude_docs, exclude_notebooks):
rel = path.relative_to(root)
try:
content = path.read_text(encoding="utf-8", errors="ignore")
except Exception:
continue
block = f"\n=== FILE START: {rel} ===\n{content}\n=== FILE END: {rel} ===\n"
tok = len(enc.encode(block))
files.append((str(rel), tok, block))
total_tokens += tok
return tree, files, total_tokens
def build_repo_context(tree: str, files: List[Tuple[str, int, str]]) -> str:
joined_files = "".join(block for _, _, block in files)
return f"""=== REPO TREE START ===
{tree}
=== REPO TREE END ===
=== REPOSITORY FILES START ===
{joined_files}
=== REPOSITORY FILES END ===
"""
Questo passo definisce l'intera pipeline di ingestione e impacchettamento del repository, ma ogni funzione ha una responsabilità specifica:
funzione clone_repo(): clona un repository GitHub in una directory di lavoro temporanea usando un clone superficiale (--depth 1) per evitare di scaricare l'intera cronologia. Input come URL del repo e branch sono normalizzati consafe_str()per prevenire errori a runtime. L'output è un percorso locale (WORKDIR) su cui opera il resto della pipeline.funzione iter_repo_files(): agisce come livello principale di filtraggio. Scansiona ricorsivamente il repository e restituisce solo file ad alto segnale. Esclude directory di sistema, tipi di file non supportati, file troppo grandi e, opzionalmente, filtra test, documentazione e notebook.funzione is_probably_text(): è un controllo di sicurezza leggero usato durante il filtraggio. Legge un piccolo chunk di ogni file e verifica l'assenza di byte null (che di solito indicano contenuto binario) e la decodifica UTF-8 riuscita. Solo i file che superano questo controllo sono considerati testo valido e inclusi nel dataset finale.funzione repo_tree_string(): costruisce una rappresentazione in stringa della struttura del repository. Crea un albero di directory (applicando le stesse regole di filtraggio) e produce un elenco di directory e file usato nel prompt così che il modello ottenga una comprensione di alto livello del layout del codebase prima di leggere i contenuti dei file.funzione pack_repo(): è la funzione di impacchettamento principale. Itera su tutti i file filtrati, ne legge i contenuti e avvolge ciascun file tra marcatori espliciti come=== FILE START ===e=== FILE END ===. Stima anche l'uso dei token contiktokene accumula un conteggio totale.funzione build_repo_context(): combina tutto in un unico prompt strutturato. Inserisce prima l'albero del repo, seguito da tutti i contenuti dei file. Il modello comprende prima la struttura, poi legge i dettagli di implementazione.
L'effetto complessivo è un repository GitHub grezzo trasformato in un unico input strutturato a contesto lungo.
Passo 6: Prompt dei task e parsing JSON
Ora che il repository è stato filtrato e impacchettato in un unico input a contesto lungo, il passo successivo è dire al modello che tipo di analisi eseguire e come formattare il risultato.
Invece di inviare la stessa istruzione generica per ogni caso d'uso, generiamo un prompt specifico per il task che include il nome del task scelto, la sua descrizione e l'esatto schema JSON atteso nella risposta.
def build_task_prompt(task_name: str, repo_context: str) -> str:
task_name = safe_str(task_name)
if not task_name:
raise ValueError("Task name is empty.")
if task_name not in TASK_DEFS:
raise ValueError(f"Unknown task: {task_name}")
task_def = TASK_DEFS[task_name]
return f"""Analyze this full repository.
Task Name:
{task_name}
Task Description:
{task_def["description"]}
Return JSON matching this schema exactly:
{json.dumps(task_def["schema"], indent=2)}
{repo_context}
"""
def try_parse_json(text: str) -> Dict[str, Any]:
text = safe_str(text)
if not text:
raise ValueError("Model returned empty text.")
try:
return orjson.loads(text)
except Exception:
start = text.find("{")
end = text.rfind("}")
if start != -1 and end != -1 and end > start:
return orjson.loads(text[start:end+1])
raise ValueError("Could not parse model output as JSON.")
Il frammento di codice sopra definisce due funzioni strettamente correlate che fanno da ponte tra il repository impacchettato e l'output di analisi specifico per task.
funzione build_task_prompt(): crea il prompt finale inviato al modello. Normalizza innanzitutto il nome del task consafe_str()così che un input vuoto non rompa la pipeline. Valida poi che il task esista inTASK_DEFS, prevenendo l'invio di task non supportati o scritti male. Una volta validato, recupera la descrizione e lo schema JSON del task e li inserisce nel prompt finale insieme al contesto completo del repository impacchettato.
funzione try_parse_json(): gestisce l'output del modello dopo l'inferenza. In pratica, i modelli a contesto lungo a volte avvolgono il JSON con testo extra, artefatti di formattazione o contenuto esplicativo parziale. Per rendere l'app più robusta, la funzione tenta prima il parsing diretto conorjson.loads(). Combinando prompting rigoroso con un parsing tollerante, l'app risulta molto più stabile.
Nel prossimo passo, collegheremo questa logica di prompting alla chiamata al modello e inizieremo a generare risultati strutturati di analisi del repository.
Passo 7: Inferenza del modello
A questo punto eseguiamo l'analisi inviando il prompt a Nemotron 3 Super tramite OpenRouter.
Invece di spezzettare o recuperare parti del repo, inviamo l'intero codebase filtrato in un'unica richiesta e lasciamo che il modello ragioni globalmente tra file, moduli e dipendenze.
def call_model(prompt: str, max_tokens: int = 5000) -> str:
prompt = safe_str(prompt)
if not prompt:
raise ValueError("Prompt is empty.")
resp = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt},
],
temperature=0.2,
max_tokens=max_tokens,
extra_headers={
"HTTP-Referer": "https://colab.research.google.com",
"X-Title": "Whole Repo Analysis Demo"
}
)
if not getattr(resp, "choices", None):
raise ValueError("Model response has no choices.")
message = getattr(resp.choices[0], "message", None)
if message is None:
raise ValueError("Model response has no message.")
content = getattr(message, "content", None)
content = safe_str(content)
if not content:
raise ValueError("Model returned empty content.")
return content
La funzione call_model() è responsabile dell'esecuzione effettiva della chiamata al modello ed estrazione della risposta. Ecco la pipeline completa:
- Validazione input: la funzione inizia normalizzando il prompt, evitando l'invio di richieste non valide all'API.
- Costruzione della richiesta al modello: la richiesta usa il client compatibile con OpenAI con OpenRouter come backend. Il payload include un system prompt che impone grounding, output JSON e ragionamento basato su evidenze, e un messaggio utente contenente il prompt del task e il contesto del repository. La temperatura è impostata a
0.2per mantenere output deterministici e il parametromax_tokenslimita la dimensione della risposta, assicurando che il modello si concentri su output strutturato.
- Validazione della risposta: controlla che nella risposta esistano delle scelte e verifica la presenza di un oggetto message. Estrae inoltre il contenuto e lo normalizza. Se manca qualcosa, la funzione solleva un errore.
- Gestione di risposte vuote: l'ultimo guardrail assicura che, anche se la chiamata all'API ha successo, una stringa vuota non si propaghi a valle. Ciò previene direttamente problemi come "Model returned empty content" e fallimenti nel parsing JSON successivo.
Infine, possiamo passare alla parte di inferenza del modello e capire come eseguire l'analisi tramite il modello ospitato su OpenRouter.
Passo 8: Funzioni di supporto
Ora servono alcune piccole funzioni di supporto che rendono facile la gestione della UI e degli output.
def to_df(items, columns=None):
if items is None:
return pd.DataFrame(columns=columns or [])
if isinstance(items, dict):
return pd.DataFrame([items])
if not items:
return pd.DataFrame(columns=columns or [])
return pd.DataFrame(items)
def get_tasks_to_run(task: str) -> List[str]:
task = safe_str(task)
if not task:
raise ValueError("Task is empty.")
return FULL_REVIEW_TASKS if task == "Full Review" else [task]
def visible_for_task(task: str) -> Dict[str, bool]:
task = safe_str(task)
if task == "Full Review":
return {
"modules": True,
"deps": True,
"issues": True,
"dups": False,
"refactor": False,
"improvements": True,
"roadmap": True,
"testing": True,
"onboarding": False,
"libs": False,
"hotspots": True,
"security": False,
}
return {
"modules": task == "Architecture Overview",
"deps": task == "Architecture Overview",
"issues": task == "Architecture Overview",
"dups": task == "Code Duplication",
"refactor": task == "Code Duplication",
"improvements": task == "Improvement Opportunities",
"roadmap": task == "Refactor Roadmap",
"testing": task == "Testing Gaps",
"onboarding": task == "Onboarding Guide",
"libs": task == "Library Modernization",
"hotspots": task == "Code Quality Hotspots",
"security": task == "Security / Config Hygiene",
}
def render_markdown_summary(repo_url: str, total_tokens: int, files, results: Dict[str, Dict[str, Any]]) -> str:
repo_url = safe_str(repo_url)
lines = [
"# Whole-Repo Analysis Report",
f"**Repository:** {repo_url}",
f"**Files included:** {len(files)}",
f"**Estimated input tokens:** {total_tokens}",
"",
]
if "Architecture Overview" in results:
overview = results["Architecture Overview"].get("repo_overview", {})
lines += [
"## Architecture",
overview.get("summary", "No summary returned."),
"",
f"**Primary languages:** {', '.join(overview.get('primary_languages', [])) or 'N/A'}",
f"**Likely entrypoints:** {', '.join(overview.get('likely_entrypoints', [])) or 'N/A'}",
"",
]
if "Improvement Opportunities" in results:
lines += [
"## Improvement Opportunities",
f"- Identified: {len(results['Improvement Opportunities'].get('improvement_opportunities', []))}",
"",
]
if "Code Quality Hotspots" in results:
lines += [
"## Code Quality Hotspots",
f"- Identified: {len(results['Code Quality Hotspots'].get('quality_hotspots', []))}",
"",
]
if "Testing Gaps" in results:
lines += [
"## Testing Gaps",
f"- Identified: {len(results['Testing Gaps'].get('testing_gaps', []))}",
"",
]
if "Code Duplication" in results:
dup = results["Code Duplication"]
lines += [
"## Duplication & Refactor Summary",
f"- Duplication candidates: {len(dup.get('duplication_candidates', []))}",
f"- Design smells: {len(dup.get('design_smells', []))}",
f"- High risk files: {len(dup.get('high_risk_files', []))}",
f"- Refactor recommendations: {len(dup.get('refactor_recommendations', []))}",
"",
]
if "Library Modernization" in results:
lines += [
"## Library Modernization",
f"- Recommendations: {len(results['Library Modernization'].get('library_recommendations', []))}",
"",
]
if "Security / Config Hygiene" in results:
lines += [
"## Security / Config Hygiene",
f"- Findings: {len(results['Security / Config Hygiene'].get('security_and_config_findings', []))}",
"",
]
if "Refactor Roadmap" in results:
lines += [
"## Refactor Roadmap",
f"- Planned items: {len(results['Refactor Roadmap'].get('refactor_roadmap', []))}",
"",
]
if "Onboarding Guide" in results:
onboarding = results["Onboarding Guide"].get("onboarding_guide", {})
lines += [
"## Onboarding Guide",
onboarding.get("summary", "No summary returned."),
"",
f"**Recommended reading order:** {', '.join(onboarding.get('recommended_reading_order', [])) or 'N/A'}",
f"**Entrypoints:** {', '.join(onboarding.get('entrypoints', [])) or 'N/A'}",
"",
]
return "\n".join(lines)
def hidden_update():
return gr.update(value=pd.DataFrame(), visible=False)
def visible_update(df):
return gr.update(value=df, visible=True)
def empty_outputs(stats, status_message):
return (
status_message,
"",
*all_hidden(),
"{}",
json.dumps(stats, indent=2),
)
Questo passo definisce un piccolo set di funzioni helper che, insieme, gestiscono gran parte della logica di presentazione dell'app.
funzione to_df(): converte gli output del modello in un DataFramepandascosì da renderli pulitamente nelle tabelle di Gradio.funzione get_tasks_to_run(): decide se l'app deve eseguire un singolo task o il bundleFull Review. Se il task selezionato è "Full Review", lo espande nella lista predefinita di task di revisione. Altrimenti, restituisce semplicemente una lista a un elemento con il task selezionato.funzione visible_for_task(): controlla quali tabelle del report mostrare effettivamente nella UI. Ad esempio, se l'utente selezionaOnboarding Guide, non ha senso mostrare tabelle di duplicazione o sicurezza.funzione render_markdown_summary(): invece di scaricare direttamente il JSON grezzo, genera un riepilogo leggibile con metadati del repository, conteggio token ed evidenze per task.funzioni hidden_update()evisible_update(): piccoli helper di Gradio usati per controllare lo stato dei componenti.hidden_update()pulisce una tabella e la nasconde, mentrevisible_update()riempie una tabella con dati e la rende visibile. Questo pattern è importante perché Gradio si aspetta un valore di ritorno per ogni componente di output e, senza reset espliciti, la UI può mantenere stati obsoleti.funzione empty_outputs(): gestisce uscite anticipate e stati di errore. Se il repo è troppo grande, il task non è valido o qualcos'altro fallisce, l'app deve comunque restituire output validi per ogni componente della UI.
Insieme, queste funzioni helper non cambiano la logica core del modello, ma compongono un'app usabile.
Passo 9: Analisi end-to-end del repository
Questa funzione orchestra l'intero workflow. Valida gli input, prepara il repository, esegue uno o più task di analisi e restituisce output strutturati alla UI di Gradio.
def analyze_repo(repo_url, branch, task, token_budget, exclude_tests, exclude_docs, exclude_notebooks):
repo_url = safe_str(repo_url)
branch = safe_str(branch)
task = safe_str(task)
try:
if not repo_url:
stats = {"repo_url": "", "branch": branch}
return empty_outputs(stats, "Error: Repository URL is empty.")
if not task:
stats = {"repo_url": repo_url, "branch": branch}
return empty_outputs(stats, "Error: Task is empty.")
repo_path = clone_repo(repo_url, branch)
tree, files, total_tokens = pack_repo(repo_path, exclude_tests, exclude_docs, exclude_notebooks)
stats = {
"repo_url": repo_url,
"branch": branch,
"files_included": len(files),
"estimated_input_tokens": total_tokens,
"token_budget": token_budget,
"fits_budget": total_tokens <= token_budget,
"sample_files": [f[0] for f in files[:30]],
"tasks_requested": get_tasks_to_run(task),
}
if total_tokens > token_budget:
msg = (
f"Repo too large for current budget.\n\n"
f"Estimated tokens: {total_tokens}\n"
f"Budget: {token_budget}\n\n"
f"Try excluding tests/docs/notebooks or use a smaller repo."
)
return empty_outputs(stats, msg)
repo_context = build_repo_context(tree, files)
tasks_to_run = get_tasks_to_run(task)
results: Dict[str, Dict[str, Any]] = {}
for task_name in tasks_to_run:
prompt = build_task_prompt(task_name, repo_context)
raw_text = call_model(prompt)
results[task_name] = try_parse_json(raw_text)
report_md = render_markdown_summary(repo_url, total_tokens, files, results)
vis = visible_for_task(task)
arch_json = results.get("Architecture Overview", {})
dup_json = results.get("Code Duplication", {})
imp_json = results.get("Improvement Opportunities", {})
roadmap_json = results.get("Refactor Roadmap", {})
testing_json = results.get("Testing Gaps", {})
onboarding_json = results.get("Onboarding Guide", {})
lib_json = results.get("Library Modernization", {})
hotspot_json = results.get("Code Quality Hotspots", {})
security_json = results.get("Security / Config Hygiene", {})
modules_df = to_df(arch_json.get("modules", []), ["name", "responsibility", "key_files", "evidence_paths"])
deps_df = to_df(arch_json.get("dependencies", []), ["from_module", "to_module", "relationship", "evidence_paths"])
issues_df = to_df(arch_json.get("issues", []), ["type", "description", "severity", "evidence_paths"])
dups_df = to_df(dup_json.get("duplication_candidates", []), ["summary", "files_involved", "why_it_looks_duplicate", "confidence"])
refactor_df = to_df(dup_json.get("refactor_recommendations", []), ["title", "description", "target_files", "expected_benefit"])
improvements_df = to_df(imp_json.get("improvement_opportunities", []), ["title", "description", "why_it_matters", "affected_files", "suggested_change", "expected_impact", "effort", "evidence_paths"])
roadmap_df = to_df(roadmap_json.get("refactor_roadmap", []), ["priority", "title", "description", "target_files", "impact", "effort", "why_now"])
testing_df = to_df(testing_json.get("testing_gaps", []), ["module_or_file", "why_it_needs_tests", "likely_missing_cases", "recommended_test_type", "evidence_paths"])
onboarding_df = to_df(onboarding_json.get("onboarding_guide", {}), ["summary", "recommended_reading_order", "entrypoints", "important_directories", "debugging_start_points", "common_confusion_points"])
libs_df = to_df(lib_json.get("library_recommendations", []), ["current_library_or_pattern", "where_used", "suggested_alternative", "why_better", "migration_difficulty", "confidence", "evidence_paths", "recommendation_type"])
hotspots_df = to_df(hotspot_json.get("quality_hotspots", []), ["file_or_module", "risk_level", "why_it_is_a_hotspot", "evidence_paths", "recommended_cleanup"])
security_df = to_df(security_json.get("security_and_config_findings", []), ["title", "severity", "description", "affected_files", "evidence_paths", "recommended_fix", "confidence"])
status = (
f"Done.\n\n"
f"Files included: {len(files)}\n"
f"Estimated input tokens: {total_tokens}\n"
f"Tasks run: {', '.join(tasks_to_run)}"
)
return (
status,
report_md,
visible_update(modules_df) if vis["modules"] else hidden_update(),
visible_update(deps_df) if vis["deps"] else hidden_update(),
visible_update(issues_df) if vis["issues"] else hidden_update(),
visible_update(dups_df) if vis["dups"] else hidden_update(),
visible_update(refactor_df) if vis["refactor"] else hidden_update(),
visible_update(improvements_df) if vis["improvements"] else hidden_update(),
visible_update(roadmap_df) if vis["roadmap"] else hidden_update(),
visible_update(testing_df) if vis["testing"] else hidden_update(),
visible_update(onboarding_df) if vis["onboarding"] else hidden_update(),
visible_update(libs_df) if vis["libs"] else hidden_update(),
visible_update(hotspots_df) if vis["hotspots"] else hidden_update(),
visible_update(security_df) if vis["security"] else hidden_update(),
json.dumps(results, indent=2),
json.dumps(stats, indent=2),
)
except subprocess.CalledProcessError as e:
stats = {"repo_url": repo_url, "branch": branch}
stderr = safe_str(getattr(e, "stderr", "")) or safe_str(e)
return empty_outputs(stats, f"Git clone failed:\n{stderr}")
except Exception as e:
stats = {"repo_url": repo_url, "branch": branch}
return empty_outputs(stats, f"Error: {safe_str(e)}")
Questa funzione orchestra l'intera pipeline, dal repo grezzo all'output strutturato.
- Gestione input e preparazione del repo: il repository viene clonato e impacchettato in un prompt strutturato, con stima dei token. Se il repo supera il budget di token, la funzione esce anticipatamente.
- Loop di esecuzione dei task (logica core): per ogni task richiesto, la funzione costruisce un prompt specifico, poi chiama il modello ed esegue il parsing dell'output JSON strutturato. Tutti i risultati vengono poi conservati in un unico dizionario
results. - Strutturazione degli output per la UI: gli output JSON grezzi vengono convertiti in DataFrame con
to_df()e mappati ai componenti della UI. La visibilità è controllata tramitevisible_for_task(), così da mostrare solo le tabelle rilevanti per il task selezionato. - Reportistica e gestione errori: un report in markdown riassume i punti chiave, mentre gli output JSON completi restano disponibili per debug. Tutti i fallimenti (problemi di clone, risposte vuote, errori di parsing) sono gestiti in sicurezza tramite
empty_outputs()per mantenere stabile la UI.
Passo 10: Costruisci la UI con Gradio
Infine, avvolgiamo tutto in una semplice interfaccia Gradio.
with gr.Blocks(title="Nemotron Repo Analyzer") as demo:
gr.Markdown("# Nemotron Repo Analyzer")
# gr.Markdown("Analyze a public GitHub repository without RAG using a single packed prompt.")
with gr.Tab("Analyze"):
with gr.Row():
repo_url = gr.Textbox(
label="GitHub Repo URL",
value="https://github.com/AashiDutt/Nemotron-3-Nano.git"
)
branch = gr.Textbox(label="Branch", value="main")
with gr.Row():
task = gr.Dropdown(
choices=TASK_CHOICES,
value="Full Review",
label="Task"
)
token_budget = gr.Slider(
minimum=40000,
maximum=180000,
step=5000,
value=120000,
label="Token Budget"
)
with gr.Row():
exclude_tests = gr.Checkbox(value=False, label="Exclude tests")
exclude_docs = gr.Checkbox(value=False, label="Exclude docs / markdown")
exclude_notebooks = gr.Checkbox(value=True, label="Exclude notebooks")
analyze_btn = gr.Button("Analyze Repo", variant="primary")
status = gr.Textbox(label="Status", lines=7)
with gr.Tab("Report"):
report_md = gr.Markdown()
modules_df = gr.Dataframe(label="Modules", visible=False)
deps_df = gr.Dataframe(label="Dependencies", visible=False)
issues_df = gr.Dataframe(label="Architecture Issues", visible=False)
dups_df = gr.Dataframe(label="Duplication Candidates", visible=False)
refactor_df = gr.Dataframe(label="Refactor Recommendations", visible=False)
improvements_df = gr.Dataframe(label="Improvement Opportunities", visible=False)
roadmap_df = gr.Dataframe(label="Refactor Roadmap", visible=False)
testing_df = gr.Dataframe(label="Testing Gaps", visible=False)
onboarding_df = gr.Dataframe(label="Onboarding Guide", visible=False)
libs_df = gr.Dataframe(label="Library Modernization", visible=False)
hotspots_df = gr.Dataframe(label="Code Quality Hotspots", visible=False)
security_df = gr.Dataframe(label="Security / Config Hygiene", visible=False)
with gr.Tab("Raw JSON"):
all_results_json_box = gr.Code(label="All Task Results JSON", language="json")
stats_box = gr.Code(label="Run Stats", language="json")
analyze_btn.click(
analyze_repo,
inputs=[repo_url, branch, task, token_budget, exclude_tests, exclude_docs, exclude_notebooks],
outputs=[
status,
report_md,
modules_df,
deps_df,
issues_df,
dups_df,
refactor_df,
improvements_df,
roadmap_df,
testing_df,
onboarding_df,
libs_df,
hotspots_df,
security_df,
all_results_json_box,
stats_box,
]
)
demo.launch(debug=True, share=True)
Il contenitore gr.Blocks() definisce il layout complessivo dell'app, mentre il Markdown di alto livello imposta titolo e contesto. L'interfaccia è suddivisa in tre tab per mantenere il workflow strutturato.
- La tab Analyze funge da livello di input. Raccoglie URL e branch del repository, permette di selezionare un task e configura il budget di token. Fornisce anche interruttori per escludere test, documenti o notebook, aiutando a controllare dimensioni e rumore del prompt. Il pulsante “Analyze Repo” attiva l'intera pipeline e la casella di stato mostra avanzamento, errori o dettagli di completamento.
- La tab Report è il livello di output principale. Renderizza un sommario in Markdown per una panoramica veloce, seguito da tabelle DataFrame strutturate per l'ispezione.
- La tab Raw JSON funge da livello di debug e trasparenza. Espone gli output strutturati completi restituiti dal modello insieme ai metadati di esecuzione. È utile per validare gli output, fare debug dei fallimenti o esportare risultati in sistemi a valle.
Questo collega gli input dell'utente direttamente alla funzione analyze_repo() e mappa i suoi output ai componenti della UI. L'ordinamento degli output è cruciale qui, poiché ogni valore di ritorno è instradato a uno specifico elemento della UI.
Infine, l'app viene avviata usando:
demo.launch(debug=True, share=True)
Il flag debug=True aiuta a far emergere errori a runtime durante lo sviluppo, mentre share=True crea un link pubblico, rendendo semplice condividere l'app senza deployment.
Conclusione
In questo tutorial ho mostrato come costruire un analizzatore di codebase di repo usando NVIDIA Nemotron 3 Super, OpenRouter e Gradio.
Invece di fare affidamento su chunking, embedding e retrieval, l'app clona un repository GitHub pubblico, filtra e impacchetta il codebase in un unico prompt e chiede al modello di ragionare sull'intero repository in una volta sola.
Questo approccio a contesto completo è utile per task che richiedono una visione globale del codebase, come spiegazione dell'architettura, guida all'onboarding, analisi delle lacune nei test, opportunità di miglioramento e pianificazione del refactor.
Da qui, puoi estendere il progetto in vari modi:
- Aggiungere un pulsante “preview fit” che stima il conteggio dei token prima dell'analisi,
- Aggiungere la gestione dei fallimenti parziali per Full Review,
- Supportare report in markdown scaricabili,
- Oppure confrontare il prompt packing del repo con una baseline RAG sullo stesso repository.
FAQ
Questo tutorial usa l'intera finestra di contesto da 1M di token di Nemotron 3 Super?
No. NVIDIA documenta fino a 1M di token per i deployment ufficiali di Nemotron 3 Super, ma il free endpoint ospitato su OpenRouter attualmente espone un contesto da 262.144, che è sufficiente per questo tutorial.
Perché non eseguire Nemotron 3 Super in locale?
Nemotron 3 Super è un grande modello di reasoning (circa 85Gb via Ollama) ed è molto più semplice da usare tramite un endpoint ospitato per un tutorial come questo.
Quale task di Nemotron è più affidabile per questa app?
In pratica, i task più solidi sono:
- Architecture Overview
- Improvement Opportunities
- Testing Gaps
- Onboarding Guide
- Refactor Roadmap
Questi beneficiano maggiormente dalla visione del repository completo in una volta sola.
Dovrei usare Full Review per impostazione predefinita?
Per i repo pubblici più piccoli, sì. Per i repo più grandi, l'analisi di un singolo task è di solito più stabile.
Sono una Google Developers Expert in ML (Gen AI), una Kaggle 3x Expert e una Women Techmakers Ambassador con oltre 3 anni di esperienza nel tech. Ho co-fondato una startup health-tech nel 2020 e sto conseguendo un master in informatica al Georgia Tech, con specializzazione in machine learning.


