Muhammad Saad Amin commited on
Commit
2176bf7
·
unverified ·
2 Parent(s): 6e1b1c1e8c581d

Merge pull request #1 from ukzada/main

Browse files

Initial commit: GitHub Profile AI Reviewer

.env.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ GITHUB_TOKEN=
2
+ LANGSMITH_API_KEY=
3
+ APP_HOST=0.0.0.0
4
+ APP_PORT=8000
.gitignore CHANGED
@@ -29,4 +29,4 @@ Thumbs.db
29
  .txtconfig
30
  .txtignore
31
  PolyMentor.txt
32
- venv312/
 
29
  .txtconfig
30
  .txtignore
31
  PolyMentor.txt
32
+ venv312/
Dockerfile ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ FROM pytorch/pytorch:2.7.1-cuda12.6-cudnn9-runtime
2
+ WORKDIR /app
3
+ ENV PYTHONDONTWRITEBYTECODE=1
4
+ ENV PYTHONUNBUFFERED=1
5
+ COPY requirements.txt ./
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+ COPY . .
8
+ EXPOSE 8000
9
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
app/ai/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
app/ai/embeddings.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable
4
+
5
+ from app.core.config import settings
6
+
7
+ try:
8
+ import torch
9
+ from transformers import AutoModel, AutoTokenizer
10
+ except Exception:
11
+ torch = None
12
+ AutoModel = None
13
+ AutoTokenizer = None
14
+
15
+
16
+ class CodeEmbeddingService:
17
+ def __init__(self) -> None:
18
+ self._dim = settings.embedding_dim
19
+ self._ready = False
20
+ self._tokenizer = None
21
+ self._model = None
22
+
23
+ if AutoModel is not None and AutoTokenizer is not None:
24
+ try:
25
+ self._tokenizer = AutoTokenizer.from_pretrained(settings.codebert_model)
26
+ self._model = AutoModel.from_pretrained(settings.codebert_model)
27
+ self._model.eval()
28
+ self._ready = True
29
+ except Exception:
30
+ self._ready = False
31
+
32
+ @property
33
+ def ready(self) -> bool:
34
+ return self._ready
35
+
36
+ @property
37
+ def embedding_dim(self) -> int:
38
+ return self._dim
39
+
40
+ def embed_repository_signals(self, snippets: Iterable[str]) -> list[float]:
41
+ text = "\n".join(snippets).strip()
42
+ if not text:
43
+ return [0.0] * self._dim
44
+
45
+ if self._ready and torch is not None and self._tokenizer is not None and self._model is not None:
46
+ tokens = self._tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
47
+ with torch.no_grad():
48
+ outputs = self._model(**tokens)
49
+ cls = outputs.last_hidden_state[:, 0, :].squeeze(0)
50
+ return cls.tolist()
51
+
52
+ vector = [0.0] * self._dim
53
+ for i, ch in enumerate(text):
54
+ vector[i % self._dim] += (ord(ch) % 31) / 31.0
55
+ norm = sum(abs(v) for v in vector) or 1.0
56
+ return [v / norm for v in vector]
app/ai/scoring.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ try:
6
+ import torch
7
+ import torch.nn as nn
8
+ except Exception:
9
+ torch = None
10
+ nn = None
11
+
12
+
13
+ if nn is not None:
14
+ class DeveloperScoringModel(nn.Module):
15
+ def __init__(self, input_dim: int = 768) -> None:
16
+ super().__init__()
17
+ self.encoder = nn.Sequential(
18
+ nn.Linear(input_dim, 256),
19
+ nn.ReLU(),
20
+ nn.Dropout(0.2),
21
+ nn.Linear(256, 128),
22
+ nn.ReLU(),
23
+ )
24
+ self.level_head = nn.Linear(128, 4)
25
+ self.hiring_head = nn.Linear(128, 1)
26
+
27
+ def forward(self, x):
28
+ feat = self.encoder(x)
29
+ level_logits = self.level_head(feat)
30
+ hiring = torch.sigmoid(self.hiring_head(feat)) * 100
31
+ return level_logits, hiring
32
+ else:
33
+ class DeveloperScoringModel: # type: ignore[no-redef]
34
+ def __init__(self, input_dim: int = 768) -> None:
35
+ self.input_dim = input_dim
36
+
37
+
38
+ @dataclass
39
+ class ScoreOutput:
40
+ level: str
41
+ confidence: float
42
+ hiring_score: int
43
+
44
+
45
+ class ScoringEngine:
46
+ LEVELS = ["Junior", "Mid", "Senior", "Staff / Principal"]
47
+
48
+ def __init__(self, input_dim: int = 768) -> None:
49
+ self._model = DeveloperScoringModel(input_dim=input_dim)
50
+
51
+ def infer(self, embedding: list[float], activity_score: float, consistency_score: float) -> ScoreOutput:
52
+ if torch is None or nn is None or not isinstance(self._model, nn.Module):
53
+ return self._heuristic(activity_score, consistency_score)
54
+
55
+ x = torch.tensor([embedding], dtype=torch.float32)
56
+ self._model.eval()
57
+ with torch.no_grad():
58
+ level_logits, hiring = self._model(x)
59
+
60
+ probs = torch.softmax(level_logits[0], dim=-1)
61
+ level_idx = int(torch.argmax(probs).item())
62
+ base_hiring = int(round(float(hiring[0].item())))
63
+ blended_hiring = int(max(0, min(100, 0.6 * base_hiring + 0.4 * activity_score)))
64
+ confidence = float(probs[level_idx].item())
65
+ return ScoreOutput(level=self.LEVELS[level_idx], confidence=round(confidence, 2), hiring_score=blended_hiring)
66
+
67
+ def _heuristic(self, activity_score: float, consistency_score: float) -> ScoreOutput:
68
+ hiring = int(max(0, min(100, round(0.7 * activity_score + 0.3 * consistency_score))))
69
+ if hiring >= 90:
70
+ return ScoreOutput("Staff / Principal", 0.95, hiring)
71
+ if hiring >= 75:
72
+ return ScoreOutput("Senior", 0.90, hiring)
73
+ if hiring >= 55:
74
+ return ScoreOutput("Mid", 0.86, hiring)
75
+ return ScoreOutput("Junior", 0.82, hiring)
app/clients/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
app/clients/github_graphql.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import httpx
8
+
9
+ from app.clients.queries import ANALYZE_PROFILE_QUERY
10
+ from app.core.config import settings
11
+
12
+
13
+ class GitHubAPIError(RuntimeError):
14
+ pass
15
+
16
+
17
+ class GitHubGraphQLClient:
18
+ async def analyze_user(self, username: str) -> dict[str, Any]:
19
+ # If token missing, use local sample so app can run offline for testing.
20
+ if not settings.github_token:
21
+ sample_path = Path(__file__).with_name("sample_user.json")
22
+ if sample_path.exists():
23
+ with sample_path.open("r", encoding="utf-8") as fh:
24
+ return json.load(fh)
25
+ raise GitHubAPIError("Missing GITHUB_TOKEN in environment and no sample_user.json available.")
26
+
27
+ headers = {"Authorization": f"Bearer {settings.github_token}", "Accept": "application/vnd.github+json"}
28
+ payload = {"query": ANALYZE_PROFILE_QUERY, "variables": {"username": username}}
29
+
30
+ async with httpx.AsyncClient(timeout=settings.request_timeout_seconds) as client:
31
+ response = await client.post(settings.github_api_url, headers=headers, json=payload)
32
+
33
+ if response.status_code >= 400:
34
+ raise GitHubAPIError(f"GitHub API status={response.status_code}: {response.text}")
35
+
36
+ data = response.json()
37
+ if data.get("errors"):
38
+ raise GitHubAPIError(str(data["errors"]))
39
+ if data.get("data", {}).get("user") is None:
40
+ raise GitHubAPIError(f"GitHub user '{username}' not found.")
41
+
42
+ return data
app/clients/queries.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ ANALYZE_PROFILE_QUERY = """
4
+ query AnalyzeProfile($username: String!) {
5
+ user(login: $username) {
6
+ login
7
+ name
8
+ bio
9
+ createdAt
10
+ followers { totalCount }
11
+ contributionsCollection {
12
+ totalCommitContributions
13
+ totalPullRequestContributions
14
+ contributionCalendar {
15
+ totalContributions
16
+ weeks {
17
+ contributionDays {
18
+ contributionCount
19
+ date
20
+ }
21
+ }
22
+ }
23
+ }
24
+ repositories(first: 20, orderBy: { field: STARGAZERS, direction: DESC }) {
25
+ nodes {
26
+ name
27
+ nameWithOwner
28
+ description
29
+ stargazerCount
30
+ forkCount
31
+ primaryLanguage { name }
32
+ languages(first: 5) {
33
+ edges {
34
+ size
35
+ node { name }
36
+ }
37
+ }
38
+ defaultBranchRef {
39
+ target {
40
+ ... on Commit {
41
+ history(first: 1) { totalCount }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ pullRequests(first: 50, states: MERGED) { totalCount }
48
+ }
49
+ }
50
+ """.strip()
app/clients/sample_user.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "data": {
3
+ "user": {
4
+ "repositories": {
5
+ "nodes": [
6
+ {
7
+ "name": "demo-repo",
8
+ "description": "Example repo",
9
+ "primaryLanguage": {"name": "Python"},
10
+ "defaultBranchRef": {"target": {"history": {"totalCount": 42}}},
11
+ "languages": {"edges": [{"node": {"name": "Python"}, "size": 1000}]}
12
+ }
13
+ ]
14
+ },
15
+ "pullRequests": {"totalCount": 5},
16
+ "contributionsCollection": {
17
+ "contributionCalendar": {"totalContributions": 120, "weeks": [
18
+ {"contributionDays": [{"contributionCount": 0}, {"contributionCount": 1}, {"contributionCount": 1}, {"contributionCount": 0}, {"contributionCount": 2}, {"contributionCount": 3}, {"contributionCount": 0}]},
19
+ {"contributionDays": [{"contributionCount": 1}, {"contributionCount": 1}, {"contributionCount": 1}, {"contributionCount": 1}, {"contributionCount": 1}, {"contributionCount": 1}, {"contributionCount": 1}]}
20
+ ]}
21
+ }
22
+ }
23
+ }
24
+ }
app/clients/streak.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from app.schemas import StreakData
4
+
5
+
6
+ def compute_streak_from_calendar(weeks: list[dict]) -> StreakData:
7
+ daily: list[int] = []
8
+ for week in weeks:
9
+ for day in week.get("contributionDays", []):
10
+ daily.append(int(day.get("contributionCount", 0)))
11
+
12
+ current = 0
13
+ for v in reversed(daily):
14
+ if v > 0:
15
+ current += 1
16
+ else:
17
+ break
18
+
19
+ longest = 0
20
+ run = 0
21
+ for v in daily:
22
+ if v > 0:
23
+ run += 1
24
+ longest = max(longest, run)
25
+ else:
26
+ run = 0
27
+
28
+ return StreakData(current_streak=current, longest_streak=longest)
app/core/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
app/core/config.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+
6
+
7
+ class Settings(BaseSettings):
8
+ app_name: str = "GitHub Profile AI Reviewer"
9
+ app_version: str = "1.0.0"
10
+ app_host: str = "0.0.0.0"
11
+ app_port: int = 8000
12
+
13
+ github_token: str = Field(default="", alias="GITHUB_TOKEN")
14
+ github_api_url: str = "https://api.github.com/graphql"
15
+
16
+ codebert_model: str = "microsoft/codebert-base"
17
+ embedding_dim: int = 768
18
+ request_timeout_seconds: float = 30.0
19
+
20
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore", populate_by_name=True)
21
+
22
+
23
+ settings = Settings()
app/core/logging.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+
6
+ def setup_logging() -> None:
7
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s")
app/graph/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
app/graph/state.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, TypedDict
4
+
5
+
6
+ class ProfileState(TypedDict, total=False):
7
+ username: str
8
+ graphql_data: dict[str, Any]
9
+ language_embeddings: list[float]
10
+ feature_vector: list[float]
11
+ strongest_language: str
12
+ language_breakdown: dict[str, int]
13
+ total_commits: int
14
+ merged_prs: int
15
+ total_contributions: int
16
+ consistency_score: int
17
+ current_streak: int
18
+ longest_streak: int
19
+ developer_level: str
20
+ confidence: float
21
+ hiring_score: int
22
+ final_report: dict[str, Any]
app/graph/workflow.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from typing import Any
5
+
6
+ from app.ai.embeddings import CodeEmbeddingService
7
+ from app.ai.scoring import ScoringEngine
8
+ from app.clients.github_graphql import GitHubGraphQLClient
9
+ from app.clients.streak import compute_streak_from_calendar
10
+ from app.graph.state import ProfileState
11
+
12
+
13
+ def _extract_features(user: dict[str, Any]) -> tuple[dict[str, int], int, int, int, list[str]]:
14
+ repositories = user.get("repositories", {}).get("nodes", [])
15
+ language_sizes: dict[str, int] = defaultdict(int)
16
+ repo_snippets: list[str] = []
17
+ total_commits = 0
18
+
19
+ for repo in repositories:
20
+ name = repo.get("name") or ""
21
+ desc = repo.get("description") or ""
22
+ primary = (repo.get("primaryLanguage") or {}).get("name") or ""
23
+ repo_snippets.append(f"repo:{name} lang:{primary} desc:{desc}")
24
+
25
+ default_branch = repo.get("defaultBranchRef") or {}
26
+ history = (default_branch.get("target") or {}).get("history", {})
27
+ total_commits += int(history.get("totalCount", 0))
28
+
29
+ for edge in repo.get("languages", {}).get("edges", []):
30
+ lang_name = edge.get("node", {}).get("name") or "Unknown"
31
+ language_sizes[lang_name] += int(edge.get("size", 0))
32
+
33
+ merged_prs = int(user.get("pullRequests", {}).get("totalCount", 0))
34
+ total_contributions = int(user.get("contributionsCollection", {}).get("contributionCalendar", {}).get("totalContributions", 0))
35
+ return dict(language_sizes), total_commits, merged_prs, total_contributions, repo_snippets
36
+
37
+
38
+ def _normalize_activity(total_contributions: int, total_commits: int, merged_prs: int) -> int:
39
+ a = min(45, total_contributions // 20)
40
+ b = min(30, total_commits // 40)
41
+ c = min(25, merged_prs // 4)
42
+ return int(max(0, min(100, a + b + c)))
43
+
44
+
45
+ def _language_breakdown(language_sizes: dict[str, int]) -> tuple[str, dict[str, int]]:
46
+ if not language_sizes:
47
+ return "Unknown", {}
48
+ strongest = max(language_sizes.items(), key=lambda x: x[1])[0]
49
+ total = sum(language_sizes.values()) or 1
50
+ breakdown = {lang: int((size / total) * 100) for lang, size in sorted(language_sizes.items(), key=lambda x: x[1], reverse=True)}
51
+ return strongest, breakdown
52
+
53
+
54
+ class AnalyzerWorkflow:
55
+ def __init__(self) -> None:
56
+ self._github = GitHubGraphQLClient()
57
+ self._embedder = CodeEmbeddingService()
58
+ self._scorer = ScoringEngine(input_dim=self._embedder.embedding_dim)
59
+
60
+ async def run(self, username: str) -> ProfileState:
61
+ state: ProfileState = {"username": username}
62
+ raw = await self._github.analyze_user(username)
63
+ state["graphql_data"] = raw
64
+
65
+ user = raw["data"]["user"]
66
+ lang_sizes, total_commits, merged_prs, total_contributions, snippets = _extract_features(user)
67
+ strongest_language, breakdown = _language_breakdown(lang_sizes)
68
+
69
+ weeks = user.get("contributionsCollection", {}).get("contributionCalendar", {}).get("weeks", [])
70
+ streak = compute_streak_from_calendar(weeks)
71
+ consistency_score = int((streak.current_streak / streak.longest_streak) * 100) if streak.longest_streak > 0 else 0
72
+
73
+ embedding = self._embedder.embed_repository_signals(snippets)
74
+ activity_score = _normalize_activity(total_contributions, total_commits, merged_prs)
75
+ scored = self._scorer.infer(embedding, activity_score, consistency_score)
76
+
77
+ state["final_report"] = {
78
+ "username": username,
79
+ "developer_level": scored.level,
80
+ "confidence": scored.confidence,
81
+ "strongest_language": strongest_language,
82
+ "language_breakdown": breakdown,
83
+ "hiring_readiness_score": scored.hiring_score,
84
+ "consistency_score": consistency_score,
85
+ "graphql_signals": {
86
+ "total_commits": total_commits,
87
+ "merged_prs": merged_prs,
88
+ "total_contributions": total_contributions,
89
+ },
90
+ "streak_data": {
91
+ "current_streak": streak.current_streak,
92
+ "longest_streak": streak.longest_streak,
93
+ },
94
+ "model_info": {
95
+ "embedding_model": "microsoft/codebert-base",
96
+ "scoring_model": "DeveloperScoringModel",
97
+ "embedding_dim": self._embedder.embedding_dim,
98
+ "embedding_backend": "transformers" if self._embedder.ready else "deterministic-fallback",
99
+ },
100
+ }
101
+ return state
app/main.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from fastapi import FastAPI, HTTPException
4
+
5
+ from app.clients.github_graphql import GitHubAPIError
6
+ from app.core.config import settings
7
+ from app.core.logging import setup_logging
8
+ from app.schemas import AnalyzeRequest, AnalyzeResponse
9
+ from app.service import AnalyzerService
10
+
11
+ setup_logging()
12
+
13
+ app = FastAPI(title=settings.app_name, version=settings.app_version)
14
+ service: AnalyzerService | None = None
15
+
16
+
17
+ def get_service() -> AnalyzerService:
18
+ global service
19
+ if service is None:
20
+ service = AnalyzerService()
21
+ return service
22
+
23
+
24
+ @app.get("/health")
25
+ async def health() -> dict[str, str]:
26
+ return {"status": "ok"}
27
+
28
+
29
+ @app.post("/analyze", response_model=AnalyzeResponse)
30
+ async def analyze(payload: AnalyzeRequest) -> AnalyzeResponse:
31
+ try:
32
+ return await get_service().analyze(payload.username)
33
+ except GitHubAPIError as exc:
34
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
35
+ except Exception as exc:
36
+ raise HTTPException(status_code=500, detail=str(exc)) from exc
app/schemas.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class AnalyzeRequest(BaseModel):
9
+ username: str = Field(min_length=1, max_length=39, pattern=r"^[A-Za-z0-9-]+$")
10
+
11
+
12
+ class GraphQLSignals(BaseModel):
13
+ total_commits: int
14
+ merged_prs: int
15
+ total_contributions: int
16
+
17
+
18
+ class StreakData(BaseModel):
19
+ current_streak: int
20
+ longest_streak: int
21
+
22
+
23
+ class AnalyzeResponse(BaseModel):
24
+ username: str
25
+ developer_level: str
26
+ confidence: float
27
+ strongest_language: str
28
+ language_breakdown: dict[str, int]
29
+ hiring_readiness_score: int
30
+ consistency_score: int
31
+ graphql_signals: GraphQLSignals
32
+ streak_data: StreakData
33
+ model_info: dict[str, Any]
app/service.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from app.graph.workflow import AnalyzerWorkflow
4
+ from app.schemas import AnalyzeResponse
5
+
6
+
7
+ class AnalyzerService:
8
+ def __init__(self) -> None:
9
+ self._workflow = AnalyzerWorkflow()
10
+
11
+ async def analyze(self, username: str) -> AnalyzeResponse:
12
+ state = await self._workflow.run(username)
13
+ return AnalyzeResponse.model_validate(state["final_report"])
docker-compose.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "3.9"
2
+ services:
3
+ api:
4
+ build: .
5
+ ports:
6
+ - "8000:8000"
7
+ environment:
8
+ - GITHUB_TOKEN=${GITHUB_TOKEN}
9
+ - LANGSMITH_API_KEY=${LANGSMITH_API_KEY}
10
+ volumes:
11
+ - hf_cache:/root/.cache/huggingface
12
+ deploy:
13
+ resources:
14
+ reservations:
15
+ devices:
16
+ - capabilities: [gpu]
17
+ redis:
18
+ image: redis:7-alpine
19
+ ports:
20
+ - "6379:6379"
21
+ volumes:
22
+ hf_cache:
main.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import asyncio
5
+ import json
6
+
7
+ from app.service import AnalyzerService
8
+
9
+
10
+ async def run(username: str) -> None:
11
+ service = AnalyzerService()
12
+ result = await service.analyze(username)
13
+ print(json.dumps(result.model_dump(), indent=2))
14
+
15
+
16
+ def main() -> None:
17
+ parser = argparse.ArgumentParser(description="GitHub Profile AI Reviewer CLI")
18
+ parser.add_argument("--username", required=True, help="GitHub username")
19
+ args = parser.parse_args()
20
+ asyncio.run(run(args.username))
21
+
22
+
23
+ if __name__ == "__main__":
24
+ main()
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.12
2
+ uvicorn[standard]==0.34.3
3
+ httpx==0.28.1
4
+ pydantic==2.11.5
5
+ pydantic-settings==2.9.1
6
+ python-dotenv==1.1.0
7
+ langgraph==0.4.8
8
+ transformers==4.52.4
9
+ torch==2.7.1