archon-backend / github_client.py
davex-ai's picture
Update github_client.py
2e99494 verified
Raw
History Blame Contribute Delete
8.26 kB
import base64
import traceback
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
import requests
import os
from dotenv import load_dotenv
import json
import re
from ai import call_llm
from fake_ai import generate_mock_questions
load_dotenv()
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
HEADERS = {
"Accept": "application/vnd.github+json"
}
if GITHUB_TOKEN:
HEADERS["Authorization"] = f"Bearer {GITHUB_TOKEN}"
def get_repo_tree(owner, repo, branch):
url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{branch}?recursive=1"
res = requests.get(url, headers=HEADERS)
if res.status_code != 200:
raise Exception(f"GitHub API error: {res.json()}")
return res.json()["tree"]
ALLOWED_EXTENSIONS = (
".py", ".js", ".ts", ".jsx", ".tsx", ".md", ".json",
".html", ".htm", ".css", ".scss", ".sass", ".less",
".csv", ".sql", ".xml", ".sh", ".bash", ".bat", ".ps1", ".ipynb",
".php", ".rb", ".java", ".go", ".cs", ".scala", ".kt", ".kts", ".ex", ".exs",
".swift", ".m", ".mm", ".dart", ".rst", ".gitattributes"
)
IMPORTANT_FILES = [
"server/",
"src/",
"app/",
"index",
"main",
"api",
]
def filter_files(tree):
return [
file for file in tree
if file["type"] == "blob" and file["path"].endswith(ALLOWED_EXTENSIONS)
]
def get_file_content(owner, repo, path):
url = f"https://api.github.com/repos/{owner}/{repo}/contents/{path}"
res = requests.get(url, headers=HEADERS)
if res.status_code != 200:
return None
data = res.json()
content = base64.b64decode(data["content"]).decode("utf-8", errors="ignore")
return content
from urllib.parse import urlparse
def parse_github_url(url: str):
path = urlparse(url).path.strip("/")
parts = path.split("/")
if len(parts) < 2:
raise ValueError("Invalid GitHub URL")
owner, repo = parts[0], parts[1]
return owner, repo
def get_default_branch(owner, repo):
url = f"https://api.github.com/repos/{owner}/{repo}"
res = requests.get(url, headers=HEADERS)
if res.status_code != 200:
raise Exception(f"GitHub API error: {res.json()}")
return res.json()["default_branch"]
def fetch_repo_contents(repo_url):
owner, repo = parse_github_url(repo_url)
branch = get_default_branch(owner, repo)
tree = get_repo_tree(owner, repo, branch)
files = filter_files(tree)
results = []
for file in prioritize_files(files)[:20]:
content = get_file_content(owner, repo, file["path"])
if content:
results.append({
"path": file["path"],
"content": content
})
return results
def score_file(path):
score = 0
if "README" in path:
score += 3
if any(key in path.lower() for key in IMPORTANT_FILES):
score += 5
if path.endswith((".js", ".ts", ".py", ".java", ".jsx", ".c")):
score += 4
return score
def prioritize_files(files):
return sorted(files, key=lambda f: score_file(f["path"]), reverse=True)
def chunk_text(text, size=1000):
lines = text.split("\n")
chunks = []
current = []
current_len = 0
for line in lines:
current.append(line)
current_len += len(line)
if current_len >= size:
chunks.append("\n".join(current))
current = []
current_len = 0
if current:
chunks.append("\n".join(current))
return chunks
def process_files(files):
processed = []
for file in files:
chunks = chunk_text(file["content"], size=1000)
processed.append({
"path": file["path"],
"chunks": chunks
})
return processed
def classify_file(path):
if "server" in path or path.endswith(".py") or path.endswith(".js"):
return "backend"
if "client" in path or path.endswith(".jsx"):
return "frontend"
if "config" in path or path.endswith(".json"):
return "config"
return "other"
def filter_chunks(chunks):
return [
c for c in chunks
if len(c.strip()) > 50 and (
"import" in c or "function" in c or "class" in c
)
]
def build_retrieval_query():
return """
core architecture system design scalability performance
state management data flow backend logic frontend interaction
real-time communication concurrency bottlenecks tradeoffs
"""
embeddings = [
{
"chunk": "...",
"vector": [...]
}
]
def retrieve(query_vector, embeddings, top_k=5):
scored = []
for item in embeddings:
score = cosine_similarity(
[query_vector],
[item["vector"]]
)[0][0]
scored.append((score, item))
scored = sorted(scored, key=lambda x: x[0], reverse=True)
return [item for _, item in scored[:top_k]]
def build_embeddings(processed_files):
index = []
for file in processed_files:
filtered = filter_chunks(file["chunks"])
for chunk in filtered:
vector = embed_text(chunk)
index.append({
"chunk": chunk,
"vector": vector,
"path": file["path"]
})
return index
def format_context(chunks):
return "\n\n".join([
f"[FILE: {c['path']}]\n{c['chunk'][:800]}"
for c in chunks
])
def generate_questions_from_repo(repo_url, num_questions=5):
# 1. Fetch
files = fetch_repo_contents(repo_url)
# 2. Process
processed = process_files(files)
# 3. Build embeddings
embedding_index = build_embeddings(processed)
# 4. Build query
query = build_retrieval_query()
query_vector = embed_text(query)
# 5. Retrieve relevant chunks
top_chunks = retrieve(query_vector, embedding_index, top_k=8)
# 6. Build context
context = format_context(top_chunks)
# 7. Build final prompt
prompt = f"""
You are a senior software engineer conducting a deep technical interview.
Analyze this code context:
{context}
Generate {num_questions} deep technical interview questions and answers.
Focus on:
- architecture
- scalability
- tradeoffs
- real-world engineering challenges
Format the response as a JSON array of objects with the keys "question" and "answer".
Example: [{{"question": "...", "answer": "..."}}]
Do not include any other text, markdown, or commentary.
STRICT RULES:
1. Output MUST be a valid JSON array.
2. The array MUST contain objects with ONLY keys "question" and "answer".
3. NO conversational text.
4. NO markdown formatting.
5. Example: [{{"question": "Q1", "answer": "A1"}}]
"""
last_err = None
for attempt in range(3):
try:
result = call_llm(prompt)
parsed = extract_json_array(result)
if isinstance(parsed, list) and len(parsed) > 0:
return {
"mode": "llm",
"data": parsed
}
last_error = ValueError("Empty or invalid array")
except Exception as e:
last_error = e
traceback.print_exc()
print("LLM failed after retries, falling back to mock:", str(last_error))
mock = generate_mock_questions(top_chunks, num_questions)
return {"mode": "mock", "data": mock}
model = SentenceTransformer("all-MiniLM-L6-v2")
def embed_text(text):
return model.encode(text)
repo_cache = {}
def get_or_create_embeddings(repo_url, processed):
if repo_url in repo_cache:
return repo_cache[repo_url]
embeddings = build_embeddings(processed)
repo_cache[repo_url] = embeddings
return embeddings
def extract_json_array(raw_text: str):
cleaned = re.sub(r"```json|```", "", raw_text).strip()
start = cleaned.find("[")
if start == -1:
raise ValueError(f"No JSON array found. LLM output was: {raw_text[:200]}...")
depth = 0
for i in range(start, len(cleaned)):
if cleaned[i] == "[":
depth += 1
elif cleaned[i] == "]":
depth -= 1
if depth == 0:
return json.loads(cleaned[start:i+1])
raise ValueError(f"Unbalanced JSON array. LLM output was: {raw_text[:200]}...")