File size: 5,472 Bytes
5acee3f
79ceb0f
d7c6008
79ceb0f
 
d7c6008
79ceb0f
d7c6008
5acee3f
79ceb0f
 
 
 
 
 
 
 
 
 
 
 
5acee3f
 
79ceb0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7c6008
79ceb0f
 
 
 
 
d7c6008
79ceb0f
 
 
 
 
d7c6008
 
79ceb0f
 
 
 
 
 
d7c6008
79ceb0f
d7c6008
79ceb0f
d7c6008
 
79ceb0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5acee3f
79ceb0f
d7c6008
5acee3f
79ceb0f
5acee3f
 
79ceb0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7c6008
 
79ceb0f
 
 
 
 
 
 
 
 
5acee3f
 
 
79ceb0f
 
 
 
 
 
 
 
 
 
 
 
d7c6008
5acee3f
fcc65c4
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import os, shutil, tempfile, re
from pathlib import Path
import gradio as gr
from git import Repo
import requests

# ---------------- CONFIG ----------------
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
OPENROUTER_MODEL = "nvidia/nemotron-nano-12b-v2-vl:free"
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
HEADERS = {
    "Authorization": f"Bearer {OPENROUTER_API_KEY}",
    "Content-Type": "application/json",
}

ALLOWED_EXT = {
    ".py", ".ipynb", ".md", ".txt", ".js", ".ts", ".tsx", ".jsx", ".java",
    ".kt", ".c", ".cpp", ".cs", ".go", ".rs", ".rb", ".php", ".sql", ".html",
    ".css", ".yml", ".yaml", ".toml", ".ini", ".json"
}
SKIP_DIRS = {
    "node_modules", ".git", "dist", "build", "out", "venv", ".venv",
    "__pycache__", ".next", ".cache", "target", "bin", "obj", ".idea", ".vscode"
}
MAX_FILE_BYTES = 800_000

# ---------------- REPO UTILITIES ----------------
def clone_repo(url: str) -> Path:
    d = Path(tempfile.mkdtemp(prefix=".tmp_repo_")).resolve()
    Repo.clone_from(url, d, depth=1)
    return d

def read_repo_text(repo_dir: Path) -> str:
    buf = []
    for root, dirs, files in os.walk(repo_dir):
        dirs[:] = [x for x in dirs if x not in SKIP_DIRS]
        for f in files:
            p = Path(root) / f
            if p.suffix.lower() in ALLOWED_EXT and p.stat().st_size <= MAX_FILE_BYTES:
                try:
                    txt = p.read_text(encoding="utf-8", errors="ignore")
                    if txt.strip():
                        rel = str(p.relative_to(repo_dir))
                        buf.append(f"\n=== FILE: {rel} ===\n{txt}")
                except Exception:
                    pass
    return "\n".join(buf)

def analyze_repo(url: str):
    if not url or not re.match(r"^https?://", url.strip()):
        return None, "❌ Invalid URL"
    repo_dir = None
    try:
        repo_dir = clone_repo(url.strip())
        text = read_repo_text(repo_dir)
        if not text.strip():
            return None, "⚠️ No readable text files found"
        kb_size = len(text) // 1000
        return text, f"βœ… Repo loaded successfully ({kb_size} KB of text)"
    except Exception as e:
        return None, f"❌ Error: {e}"
    finally:
        if repo_dir and Path(repo_dir).exists():
            shutil.rmtree(repo_dir, ignore_errors=True)

# ---------------- OPENROUTER CLIENT ----------------
def openrouter_chat(system_prompt, user_prompt, context=""):
    messages = [{"role": "system", "content": system_prompt}]
    if context:
        messages.append({"role": "system", "content": f"Repository context:\n{context}"})
    messages.append({"role": "user", "content": user_prompt})

    payload = {"model": OPENROUTER_MODEL, "messages": messages}
    try:
        r = requests.post(OPENROUTER_URL, headers=HEADERS, json=payload, timeout=120)
        r.raise_for_status()
        obj = r.json()
        if "choices" in obj and obj["choices"]:
            msg = obj["choices"][0]["message"]["content"]
            return msg.strip()
        return "[OpenRouter] Unexpected response format."
    except Exception as e:
        return f"[OpenRouter error] {e}"

# ---------------- CHAT LOGIC ----------------
SYSTEM_PROMPT = (
    "You are an expert developer assistant. You help users explore and understand "
    "a GitHub repository. Base every response strictly on the repo's content and structure. "
    "If unsure, say so. Explain clearly and concisely. Avoid hallucinating."
)

def chat_repo(user_msg, chat_history, repo_text):
    if not repo_text:
        chat_history.append({"role": "assistant", "content": "❌ Please analyze a repository first."})
        return chat_history, ""
    
    context = repo_text[:120000]  # truncate for token safety
    response = openrouter_chat(SYSTEM_PROMPT, user_msg, context)
    chat_history.append({"role": "user", "content": user_msg})
    chat_history.append({"role": "assistant", "content": response})
    return chat_history, ""

# ---------------- GRADIO UI ----------------
with gr.Blocks(title="Repo Chatbot Β· OpenRouter") as demo:
    gr.Markdown(
        """
        # πŸ€– Repo Chatbot β€” powered by OpenRouter  
        Chat with your GitHub repository!  
        Upload a repo URL and ask anything about its **code, structure, or design**.  
        _(No embeddings, just pure context reasoning.)_
        """
    )

    repo_state = gr.State()
    chat_history = gr.State([])

    with gr.Row():
        repo_url = gr.Textbox(
            label="GitHub repo URL",
            placeholder="https://github.com/owner/repo",
            scale=4
        )
        analyze_btn = gr.Button("πŸ” Analyze Repo", scale=1)

    status_box = gr.Markdown()

    # βœ… Add type='messages' to match new format
    chatbot = gr.Chatbot(label="Repo Chatbot", height=500, type="messages")
    user_box = gr.Textbox(label="Ask something about the repo...")

    clear_btn = gr.Button("🧹 Clear Chat")

    # ---- CALLBACKS ----
    def analyze_repo_cb(url):
        text, status = analyze_repo(url)
        return text, status

    analyze_btn.click(analyze_repo_cb, inputs=[repo_url], outputs=[repo_state, status_box])
    user_box.submit(chat_repo, inputs=[user_box, chat_history, repo_state],
                    outputs=[chatbot, user_box])
    clear_btn.click(lambda: ([], ""), None, [chatbot, user_box])

    demo.queue()  # βœ… removed unsupported arg
    demo.launch(server_name="0.0.0.0", server_port=7860,mcp_server=True)