github-asistani / app /api.py
kadiirgok
Açılışta otomatik hesap yükleme
90da4f6
Raw History Blame Contribute Delete
7.84 kB
# -*- coding: utf-8 -*-
"""FastAPI katmanı: /health, /ingest, /chat + statik arayüz.
İş mantığı github_rag paketindedir; bu dosya yalnızca HTTP tarafını içerir.
"""
import os
import sys
import threading
import time
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
PROJECT_ROOT = Path(__file__).resolve().parent.parent
# `uvicorn app.api:app` ile çalıştırılırken github_rag paketinin bulunabilmesi için
# proje kökünü sys.path'in başına ekle.
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from github_rag import GithubRag # noqa: E402
from github_rag.config import load_config # noqa: E402
STATIC_DIR = PROJECT_ROOT / "static"
config = load_config()
active_rag: GithubRag | None = None
def _last_target_path() -> Path:
return Path(config.chroma_dir) / "last_target.txt"
def _save_last_target(hedef: str) -> None:
try:
_last_target_path().write_text(hedef.strip(), encoding="utf-8")
except Exception: # noqa: BLE001 — işaret dosyası yazılamasa da ingest başarılı sayılır
pass
def _load_last_target() -> str | None:
p = _last_target_path()
if p.is_file():
t = p.read_text(encoding="utf-8").strip()
return t or None
return None
def _varsayilan_hedef() -> str:
"""Açılışta otomatik yüklenecek hesap (HF diski geçici olduğundan yeniden başlatmada gerekir)."""
return os.environ.get("GITHUB_RAG_DEFAULT_TARGET", "kadiirgok").strip()
def _otomatik_yukle() -> None:
"""Son hedefi cache'ten, yoksa varsayılan hedefi GitHub'dan yükler (arka planda)."""
global active_rag
for hedef in filter(None, [_load_last_target(), _varsayilan_hedef()]):
try:
rag = GithubRag(config=config)
rag.index(hedef) # cache doluysa GitHub'a gitmez
active_rag = rag
_save_last_target(hedef)
print(f"Hedef yüklendi: {hedef}")
return
except Exception as exc: # noqa: BLE001 — bir hedef başarısızsa diğerini dene
print(f"Hedef yüklenemedi ({hedef}): {exc}")
@asynccontextmanager
async def lifespan(app: FastAPI):
# Sunucu hemen açılır; yükleme arka planda sürer (sağlık kontrolü zaman aşımına uğramasın).
threading.Thread(target=_otomatik_yukle, daemon=True).start()
yield
app = FastAPI(title="GitHub RAG API", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class IngestRequest(BaseModel):
hedef: str
force: bool = False
class RefreshRequest(BaseModel):
hedef: str = ""
class SoruRequest(BaseModel):
soru: str
class CevapResponse(BaseModel):
cevap: str
kaynak: str
sure_saniye: float
dogrulandi: bool
def _github_durumu() -> dict:
"""GITHUB_TOKEN'ın geçerli olup olmadığını ve kalan API limitini bildirir (token değeri açılmaz)."""
import requests
tok = config.github_token
try:
r = requests.get("https://api.github.com/rate_limit", timeout=10,
headers={"Authorization": f"Bearer {tok}"} if tok else {})
core = r.json().get("resources", {}).get("core", {}) if r.ok else {}
return {"token_var": bool(tok), "token_uzunluk": len(tok), "http": r.status_code,
"gecerli": bool(tok) and r.status_code == 200 and core.get("limit", 0) > 60,
"limit": core.get("limit"), "kalan": core.get("remaining")}
except Exception as exc: # noqa: BLE001
return {"token_var": bool(tok), "hata": str(exc)[:100]}
@app.get("/health")
def health():
return {
"github": _github_durumu(),
"durum": "hazir",
"llm": {
"deepseek": bool(config.deepseek_api_key),
"groq": bool(config.groq_api_key),
},
"hedef": _load_last_target(),
}
@app.post("/ingest")
def ingest(req: IngestRequest):
"""GitHub kullanıcı adı / linki alır, repo+README'leri indeksler ve aktif yapar."""
global active_rag
t0 = time.time()
try:
rag = GithubRag(config=config)
repo_sayisi = rag.index(req.hedef, force=req.force)
active_rag = rag
_save_last_target(req.hedef)
except Exception as exc: # noqa: BLE001 — kullanıcıya anlaşılır hata döndür
return JSONResponse(status_code=400, content={"durum": "hata", "mesaj": str(exc)})
return {"durum": "tamam", "hedef": req.hedef, "repo_sayisi": repo_sayisi,
"repolar": sorted(active_rag.repo_names),
"sure_saniye": round(time.time() - t0, 2)}
@app.post("/chat", response_model=CevapResponse)
def chat(req: SoruRequest):
"""Aktif indeks üzerinden soruyu RAG ile yanıtlar."""
global active_rag
if active_rag is None:
return CevapResponse(cevap="Önce bir GitHub kullanıcı adı veya linki yükleyin.",
kaynak="none", sure_saniye=0.0, dogrulandi=False)
res = active_rag.ask(req.soru)
return CevapResponse(**res)
@app.get("/repos")
def repos():
"""Aktif hedefin repo listesini metadata (dil/açıklama/yıldız) ile döndürür."""
global active_rag
if active_rag is None:
return JSONResponse(status_code=400, content={"durum": "hata",
"mesaj": "Önce bir GitHub hedefi yükleyin."})
return {"durum": "tamam", "hedef": active_rag.target,
"repolar": sorted(active_rag.repo_metadata, key=lambda m: -(m.get("stars", 0) or 0))}
@app.get("/profile")
def profile():
"""Aktif hedefin toplu özetini döndürür: dil dağılımı, yıldız, repo listesi."""
global active_rag
if active_rag is None:
return JSONResponse(status_code=400, content={"durum": "hata",
"mesaj": "Önce bir GitHub hedefi yükleyin."})
return {"durum": "tamam", "hedef": active_rag.target, **active_rag.profile()}
@app.get("/check-updates")
def check_updates(hedef: str = ""):
"""Cache'li hedef ile GitHub'daki güncel durum arasındaki farkı döndürür (kuru koşu)."""
global active_rag
if active_rag is None:
return JSONResponse(status_code=400, content={"durum": "hata",
"mesaj": "Önce bir GitHub hedefi yükleyin."})
return {"durum": "tamam", **active_rag.detect_changes(hedef or None)}
@app.post("/refresh")
def refresh(req: RefreshRequest):
"""Değişiklik varsa hedefi temiz şekilde yeniden indeksler; yoksa 'guncel' döner."""
global active_rag
if active_rag is None:
return JSONResponse(status_code=400, content={"durum": "hata",
"mesaj": "Önce bir GitHub hedefi yükleyin."})
hedef = req.hedef or active_rag.target
diff = active_rag.detect_changes(hedef)
if not diff["guncel"]:
active_rag.index(hedef, force=True)
return {"durum": "tamam", **diff, "repo_sayisi": active_rag.repo_sayisi}
@app.exception_handler(Exception)
async def genel_hata_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=500,
content={
"cevap": "Sunucu beklenmeyen bir hata oluştu. Lütfen tekrar deneyin.",
"kaynak": "none",
"sure_saniye": 0.0,
"dogrulandi": False,
},
)
# Statik arayüz en sona mount edilir ki API rotaları önce eşleşsin.
app.mount("/", StaticFiles(directory=str(STATIC_DIR), html=True), name="static")