Spaces:
Running
Running
Download github_rag/github.py from akadirgok/github-asistani: direct link, hf CLI and curl.
- Browser
- Download file 11.9 kB
-
https://huggingface.co/spaces/akadirgok/github-asistani/resolve/main/github_rag/github.py
- Command line
-
hf download hf://spaces/akadirgok/github-asistani/github_rag/github.py
-
curl -L -o github.py https://huggingface.co/spaces/akadirgok/github-asistani/resolve/main/github_rag/github.py
11.9 kB
| # -*- coding: utf-8 -*- | |
| """GitHub içerik alımı: kullanıcı adı / link -> repo listesi + README'ler. | |
| GitHub REST API'si kullanılır (web scraping yok). Opsiyonel GITHUB_TOKEN ile | |
| saatlik istek limiti 60'tan 5000'e çıkar; token yoksa da temel kullanım çalışır. | |
| """ | |
| import json | |
| import re | |
| from concurrent.futures import ThreadPoolExecutor | |
| from pathlib import Path | |
| import requests | |
| GITHUB_API = "https://api.github.com" | |
| def resolve_target(target: str) -> dict: | |
| """Kullanıcı girdisini normalleştirir; {"kind", "owner", "repo"} döndürür. | |
| Kabul edilen biçimler: | |
| - "kadiirgok" -> kullanıcı | |
| - "https://github.com/kadiirgok" -> kullanıcı | |
| - "github.com/kadiirgok/bilgi-tr" -> tek repo | |
| - "kadiirgok/bilgi-tr" -> tek repo | |
| """ | |
| t = (target or "").strip().strip("/") | |
| t = t.replace("https://", "").replace("http://", "") | |
| t = t.removeprefix("www.") | |
| t = t.removeprefix("github.com/").strip("/") | |
| parts = [p for p in t.split("/") if p] | |
| if not parts or not all(_GECERLI_AD.match(p) for p in parts[:2]): | |
| raise ValueError( | |
| "Geçerli bir GitHub kullanıcı adı veya link girin (örn. kadiirgok). " | |
| "Soru sormak için alttaki sohbet kutusunu kullanın.") | |
| if len(parts) == 1: | |
| return {"kind": "user", "owner": parts[0], "repo": None} | |
| if len(parts) >= 2: | |
| return {"kind": "repo", "owner": parts[0], "repo": parts[1]} | |
| raise ValueError(f"Geçersiz GitHub hedefi: {target!r}") | |
| _GECERLI_AD = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,99})$") | |
| def _headers(token: str) -> dict: | |
| h = {"Accept": "application/vnd.github+json"} | |
| if token: | |
| h["Authorization"] = f"Bearer {token}" | |
| return h | |
| _token_uyarildi = False | |
| def _get(url: str, token: str = "", headers: dict | None = None, **kw) -> requests.Response: | |
| """GET isteği; token süresi dolmuş/geçersizse (401) tokensiz yeniden dener. | |
| Herkese açık veriler token'sız da okunur (60 istek/saat); böylece süresi biten | |
| bir token tüm sistemi çökertmez, yalnızca limiti düşürür. | |
| """ | |
| global _token_uyarildi | |
| h = {**_headers(token), **(headers or {})} | |
| resp = requests.get(url, headers=h, **kw) | |
| if resp.status_code == 401 and token: | |
| if not _token_uyarildi: | |
| print("UYARI: GITHUB_TOKEN geçersiz veya süresi dolmuş (401); " | |
| "tokensiz devam ediliyor. Yeni token oluşturup güncelleyin.") | |
| _token_uyarildi = True | |
| h = {**_headers(""), **(headers or {})} | |
| resp = requests.get(url, headers=h, **kw) | |
| return resp | |
| def list_repos(user: str, token: str = "") -> list[dict]: | |
| """Kullanıcının (fork hariç) repolarını döndürür: [{name, full_name, html_url}].""" | |
| repos = [] | |
| page = 1 | |
| while True: | |
| resp = _get( | |
| f"{GITHUB_API}/users/{user}/repos", token, | |
| params={"per_page": 100, "page": page, "type": "owner", "sort": "updated"}, | |
| timeout=30, | |
| ) | |
| if resp.status_code == 404: | |
| raise ValueError(f"GitHub kullanıcısı bulunamadı: {user}") | |
| if resp.status_code == 403: | |
| raise RuntimeError( | |
| "GitHub API limiti aşıldı (403). Geçerli bir GITHUB_TOKEN ekleyerek " | |
| "limiti 60/saat yerine 5000/saat'e çıkarın." | |
| ) | |
| resp.raise_for_status() | |
| batch = resp.json() | |
| if not batch: | |
| break | |
| repos.extend(batch) | |
| if len(batch) < 100: | |
| break | |
| page += 1 | |
| return repos | |
| def fetch_readme(owner: str, repo: str, token: str = "") -> str | None: | |
| """Repo'nun README metnini döndürür; yoksa None.""" | |
| resp = _get(f"{GITHUB_API}/repos/{owner}/{repo}/readme", token, | |
| headers={"Accept": "application/vnd.github.raw"}, timeout=30) | |
| if resp.status_code in (404, 204): | |
| return None | |
| resp.raise_for_status() | |
| text = (resp.text or "").strip() | |
| return text or None | |
| # Gerçek bilgi içermeyen, araçların otomatik ürettiği README'lerin belirteçleri. | |
| _BOILERPLATE_ISARETLERI = ( | |
| "a new flutter project", | |
| "lab: write your first flutter app", | |
| "getting started with create react app", | |
| "this project was bootstrapped with", | |
| "bootstrapped with create-react-app", | |
| ) | |
| def _readme_bos_metin(text: str) -> bool: | |
| """README'nin şablon (boilerplate) olup olmadığını döndürür. | |
| Flutter/CRA gibi araçların ürettiği hazır README'ler proje hakkında hiçbir | |
| şey söylemez; bunlar indekse alınmazsa soru koda düşer ve asıl yapı okunur. | |
| """ | |
| t = (text or "").lower() | |
| return any(i in t for i in _BOILERPLATE_ISARETLERI) | |
| def _repo_metadata(r: dict) -> dict: | |
| """GitHub repo nesnesinden yalnızca sunum/özet için gereken alanları alır.""" | |
| lic = r.get("license") or {} | |
| return { | |
| "name": r.get("name", ""), | |
| "full_name": r.get("full_name", ""), | |
| "description": (r.get("description") or "").strip(), | |
| "language": r.get("language") or "", | |
| "stars": r.get("stargazers_count", 0), | |
| "forks": r.get("forks_count", 0), | |
| "updated_at": r.get("updated_at", ""), | |
| "created_at": r.get("created_at", ""), | |
| "html_url": r.get("html_url", ""), | |
| "homepage": r.get("homepage") or "", | |
| "license": lic.get("spdx_id") or "", | |
| } | |
| def fetch_repo_metadata(owner: str, repo: str, token: str = "") -> dict | None: | |
| """Tek reponun metadata'sını döndürür; bulunamazsa None.""" | |
| resp = _get(f"{GITHUB_API}/repos/{owner}/{repo}", token, timeout=30) | |
| if resp.status_code != 200: | |
| return None | |
| return _repo_metadata(resp.json()) | |
| def fetch_metadata(target: str, token: str = "") -> list[dict]: | |
| """Yalnızca repo metadata'sını döndürür (README/embedding yok; hafif). | |
| Cache'li koleksiyonda metadata yan dosyası eksikse, index() bunu tek istekle | |
| yeniden doldurmak için kullanır. | |
| """ | |
| parsed = resolve_target(target) | |
| if parsed["kind"] == "repo": | |
| meta = fetch_repo_metadata(parsed["owner"], parsed["repo"], token) | |
| return [meta] if meta else [] | |
| return [_repo_metadata(r) for r in list_repos(parsed["owner"], token)] | |
| def ingest_github(target: str, token: str = "") -> tuple[list[dict], list[dict]]: | |
| """Hedefi çözümleyip (docs, metadata) döndürür. | |
| docs: [{"text","source","url"}] -> embedding için | |
| metadata: [{name, description, language, stars, ...}] -> sunum/özet için | |
| """ | |
| parsed = resolve_target(target) | |
| docs = [] | |
| metadata = [] | |
| if parsed["kind"] == "repo": | |
| owner, repo = parsed["owner"], parsed["repo"] | |
| meta = fetch_repo_metadata(owner, repo, token) | |
| if meta: | |
| metadata.append(meta) | |
| text = fetch_readme(owner, repo, token) | |
| if text and not _readme_bos_metin(text): | |
| docs.append({"text": text, "source": repo, | |
| "url": f"https://github.com/{owner}/{repo}"}) | |
| return docs, metadata | |
| for r in list_repos(parsed["owner"], token): | |
| name = r.get("name", "") | |
| full = r.get("full_name", "") | |
| if not name: | |
| continue | |
| metadata.append(_repo_metadata(r)) | |
| text = fetch_readme(parsed["owner"], name, token) | |
| if text and not _readme_bos_metin(text): | |
| docs.append({"text": text, "source": name, | |
| "url": r.get("html_url", f"https://github.com/{full}")}) | |
| return docs, metadata | |
| # --- Kod dosyası çekme (Katman 2) --- | |
| CODE_EXTENSIONS = { | |
| ".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs", | |
| ".dart", ".java", ".kt", ".kts", ".go", ".rs", ".cs", ".cpp", ".c", ".h", ".hpp", | |
| ".rb", ".php", ".swift", ".sh", ".vue", ".sql", ".html", ".css", ".scss", | |
| ".yaml", ".yml", ".toml", ".md", ".ipynb", | |
| } | |
| # Devasa repolarda gereksiz dizinleri dışarıda bırak (build ürünleri, bağımlılıklar). | |
| EXCLUDED_DIRS = { | |
| "node_modules", "dist", "build", ".git", "__pycache__", ".venv", "venv", | |
| "vendor", "target", ".idea", ".vscode", ".dart_tool", ".next", "coverage", | |
| } | |
| EXCLUDED_FILENAMES = {"package-lock.json", "yarn.lock", "pnpm-lock.yaml", | |
| "poetry.lock", "Cargo.lock", "go.sum"} | |
| def _default_branch(owner: str, repo: str, token: str = "") -> str: | |
| resp = _get(f"{GITHUB_API}/repos/{owner}/{repo}", token, timeout=30) | |
| resp.raise_for_status() | |
| return resp.json().get("default_branch", "main") | |
| def fetch_code_file(owner: str, repo: str, branch: str, path: str) -> str | None: | |
| """raw.githubusercontent.com üzerinden tek dosyayı çeker (API limitine takılmaz).""" | |
| url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}" | |
| resp = requests.get(url, timeout=30) | |
| if resp.status_code != 200: | |
| return None | |
| return resp.text | |
| def _ipynb_to_text(text: str) -> str: | |
| """.ipynb (JSON) dosyasını okunabilir metne çevirir: markdown + kod hücreleri. | |
| Ham JSON hem token açısından verimsizdir hem de LLM için gürültülüdür; bu yüzden | |
| yalnızca hücrelerin kaynak metinleri (markdown açıklama + kod) birleştirilir. | |
| Çıktılar (outputs) ve metadata atılır. | |
| """ | |
| try: | |
| nb = json.loads(text) | |
| except (ValueError, TypeError): | |
| return text | |
| parcalar = [] | |
| for cell in nb.get("cells", []): | |
| kaynak = cell.get("source", []) | |
| if isinstance(kaynak, str): | |
| kaynak = [kaynak] | |
| icerik = "".join(kaynak).strip() | |
| if not icerik: | |
| continue | |
| tip = cell.get("cell_type", "") | |
| etiket = "## (markdown)" if tip == "markdown" else "# (kod)" if tip == "code" else "" | |
| parcalar.append(f"{etiket}\n{icerik}".strip()) | |
| return "\n\n".join(parcalar) | |
| def ingest_code(owner: str, repo: str, token: str = "", | |
| max_files: int = 200, max_size: int = 1_500_000) -> list[dict]: | |
| """Repo'nun kaynak dosyalarını filtreleyip doküman listesi olarak döndürür. | |
| Dosyalar git tree ile listelenir, raw CDN üzerinden paralel çekilir. | |
| `source` alanı dosya yoludur (örn. "github_rag/retrieval.py"). | |
| """ | |
| branch = _default_branch(owner, repo, token) | |
| tree_resp = _get( | |
| f"{GITHUB_API}/repos/{owner}/{repo}/git/trees/{branch}?recursive=1", | |
| token, timeout=60) | |
| tree_resp.raise_for_status() | |
| tree = tree_resp.json().get("tree", []) | |
| secilenler = [] # (path, size) | |
| total = 0 | |
| for item in tree: | |
| if item.get("type") != "blob": | |
| continue | |
| path = item.get("path", "") | |
| size = item.get("size", 0) | |
| if Path(path).suffix.lower() not in CODE_EXTENSIONS: | |
| continue | |
| parcalar = path.split("/") | |
| if any(p in EXCLUDED_DIRS for p in parcalar): | |
| continue | |
| if Path(path).name in EXCLUDED_FILENAMES: | |
| continue | |
| if size > 50_000: # tek dosya üst sınırı | |
| continue | |
| if len(secilenler) >= max_files or total + size > max_size: | |
| break | |
| secilenler.append((path, size)) | |
| total += size | |
| docs = [] | |
| def _fetchet(p): | |
| path, _size = p | |
| text = fetch_code_file(owner, repo, branch, path) | |
| if text and Path(path).suffix.lower() == ".ipynb": | |
| text = _ipynb_to_text(text) | |
| return path, text | |
| with ThreadPoolExecutor(max_workers=8) as ex: | |
| for path, text in ex.map(_fetchet, secilenler): | |
| if text and text.strip(): | |
| # Şablon README'yi kod indeksine de alma (gerçek bilgi içermez). | |
| if Path(path).suffix.lower() == ".md" and _readme_bos_metin(text): | |
| continue | |
| docs.append({"text": text, "source": path, | |
| "url": f"https://github.com/{owner}/{repo}/blob/{branch}/{path}"}) | |
| return docs | |