Feature-Finder / team_context.py
salma-remyx's picture
ux: accept github.com/repo + owner/repo + SSH + with-subpath URL variants
ed58412 verified
Raw
History Blame Contribute Delete
15 kB
"""
team_context.py β€” Turn a GitHub repo URL into TWO artifacts in a single
Gemini call:
team_context β€” Corpus-format prompt (bullets, [Category] tags,
iteration_chain_keys) that matches the distribution
v1.4 was trained on. Used at scoring time.
domain_summary β€” 2-4 natural-language sentences describing the
team's domain and technical focus. Used as the
embedding query for candidate pre-filtering.
The two artifacts serve different downstream tasks:
- Scoring wants the exact training distribution.
- Embedding retrieval wants a clean topical summary that aligns with
arxiv abstracts (which are also natural-language descriptions of
research topics, not bullet-formatted commit logs).
Using the corpus-format prompt for embedding retrieval was an early
architectural shortcut. The "Recently shipped by the team", dates,
brackets, and trailing "Generate a paper suggestion" line are pure
noise for cosine similarity β€” they pull the query vector toward generic
"experiment-history" similarity instead of the team's actual domain.
"""
from __future__ import annotations
import json
import os
import re
import urllib.parse
import urllib.request
from dataclasses import dataclass
from datetime import datetime
from typing import Any
import llm_api
# Separator that splits the two sections in Gemini's response. Chosen to
# be unique and unlikely to appear in any reasonable model output.
_SECTION_SEP = "---DOMAIN-SUMMARY---"
@dataclass
class TeamContextResult:
team_context: str # corpus-format prompt β€” fed to v1.4 LoRA at scoring
domain_summary: str # natural-language topical summary β€” embedded for retrieval
def is_stub(self) -> bool:
"""True when we fell back to description-only (no real commits)."""
return "limited history available" in self.team_context
GH_API = "https://api.github.com"
# Few-shot example in the training distribution β€” used to coach Gemini
# into the exact prompt format the model was trained on. Without this,
# Gemini's free-form summary may drift in style and hurt scoring
# calibration.
FEW_SHOT_EXAMPLE = """\
Recently shipped by amrit110/oli (sample of their experiment history):
- 2025-04-15 β€” Initial implementation with multi-model support [Research; iteration_chain_key: anthropic_model_version, iter #1]
Initial version of 'oli', a terminal-based AI coding assistant with a Rust backend and React/Ink frontend. Establishes support for multiple model providers including Anthropic (Claude 3.7 Sonnet), OpenAI (GPT-4o), Google (Gemini), and local models via Ollama, as described in the README.
- 2025-05-13 β€” Refactor agent executor [General]
Refactors the core agent executor for improved logic, clarity, and testability. This change enhances the maintainability of the agent's decision-making loop."""
def parse_repo_url(url: str) -> tuple[str, str] | None:
"""Return (owner, name) from a GitHub URL or repo slug.
Accepts (whitespace tolerated):
https://github.com/owner/repo
http://github.com/owner/repo
github.com/owner/repo
www.github.com/owner/repo
git@github.com:owner/repo.git (SSH URL)
owner/repo (bare slug)
https://github.com/owner/repo/tree/main (any subpath gets ignored)
Any of the above with .git suffix or trailing /
"""
s = (url or "").strip()
if not s:
return None
# SSH URL: git@github.com:owner/repo.git
m = re.match(r"^git@github\.com:([\w.-]+)/([\w.-]+?)(?:\.git)?/?$", s)
if m:
return m.group(1), m.group(2)
# Strip optional scheme + www + github.com prefix
s = re.sub(r"^(https?://)?(www\.)?github\.com/", "", s)
# Take the first two path segments (so /tree/main, /pull/123, etc.
# at the end of a URL paste don't break us). Strip .git suffix.
m = re.match(r"^([\w.-]+)/([\w.-]+?)(?:\.git)?(?:/.*)?$", s)
return (m.group(1), m.group(2)) if m else None
def _gh_get(path: str) -> Any:
"""Unauthenticated GitHub API GET. 60 req/hr/IP β€” caching at request
level keeps us safely within that for demo traffic."""
req = urllib.request.Request(
f"{GH_API}{path}",
headers={"User-Agent": "mhpd-paper-recommender", "Accept": "application/vnd.github+json"},
)
with urllib.request.urlopen(req, timeout=10) as r:
return json.loads(r.read())
def fetch_github_commits(owner: str, name: str, n: int = 100) -> list[dict]:
"""Pull the latest N commits. Each result includes title, body, date,
and author. Pre-filters out obvious dependabot/renovate/CI noise
before handing to Gemini (cheaper than letting Gemini filter the
full list).
Default n=100 (up from 30) because some repos have long runs of
dependabot/renovate commits β€” fetching too few leaves us with zero
meaningful commits after filtering. 100 is a reasonable upper bound
that still fits in one paginated API call."""
raw = _gh_get(f"/repos/{owner}/{name}/commits?per_page={n}")
if not isinstance(raw, list):
return []
out: list[dict] = []
skip_authors = {"dependabot[bot]", "renovate[bot]", "github-actions[bot]"}
skip_title_prefixes = ("Bump ", "Update ", "chore(deps)", "chore: bump")
for c in raw:
commit = c.get("commit") or {}
msg = commit.get("message") or ""
title = msg.split("\n", 1)[0]
body = msg.split("\n", 1)[1].strip() if "\n" in msg else ""
# Drop bot commits
author_user = (c.get("author") or {}).get("login") or ""
if author_user in skip_authors:
continue
# Drop obvious dependency-bump titles
if any(title.startswith(p) for p in skip_title_prefixes):
continue
# Drop merge commits (they aggregate; the actual feature commits
# contain the real signal)
if title.lower().startswith("merge "):
continue
out.append({
"date": (commit.get("author") or {}).get("date", "")[:10],
"title": title,
"body": body[:600], # cap to keep Gemini prompt small
"author": author_user or (commit.get("author") or {}).get("name", "unknown"),
})
return out
def fetch_repo_meta(owner: str, name: str) -> dict:
"""Pull description + topics + stars β€” useful for Gemini context."""
try:
d = _gh_get(f"/repos/{owner}/{name}")
return {
"description": d.get("description") or "",
"topics": d.get("topics") or [],
"stars": d.get("stargazers_count", 0),
"language": d.get("language") or "",
}
except Exception:
return {"description": "", "topics": [], "stars": 0, "language": ""}
def extract_team_context(
owner: str, name: str, commits: list[dict], meta: dict, n_events: int = 4
) -> TeamContextResult:
"""Hand the filtered commits to Gemini Flash and ask for BOTH the
corpus-style team-context AND a clean domain summary in one call.
Returns TeamContextResult with both artifacts populated, even in
the stub/heuristic fallback paths (with degraded quality)."""
if not commits:
return _stub_result(owner, name, meta)
if not os.environ.get("GEMINI_API_KEY"):
return _heuristic_result(owner, name, commits[:n_events], meta)
sample_lines = []
for c in commits[:12]: # give Gemini more context than it needs
sample_lines.append(f"- {c['date']} β€” {c['title']}")
if c["body"]:
sample_lines.append(f" {c['body'][:400]}")
raw_history = "\n".join(sample_lines)
instruction = f"""\
You are formatting a research team's GitHub commit history for a paper-recommendation pipeline. Produce TWO sections, separated by the literal marker line `{_SECTION_SEP}`.
SECTION 1 β€” Experiment history (for the scoring model):
A structured prompt that exactly matches the EXAMPLE FORMAT below. Pick the {n_events} most meaningful commits (skip dependency bumps, version bumps, CI changes, formatting). For each chosen commit, write one tight paragraph (1-3 sentences) describing the experiment in research terms. Use one of these category tags: [Research], [General], [Evaluation], [Bugfix], [Model Finetune], [Data Curation], [Refactor].
If a commit looks like part of an ongoing initiative (multiple commits on the same theme), include `[Category; iteration_chain_key: short_chain_name, iter #N]` to mark the chain β€” otherwise use the bare `[Category]` form.
End SECTION 1 with a single sentence that prompts paper suggestion.
SECTION 2 β€” Domain summary (for embedding-based retrieval):
2-4 sentences of clean natural language describing what the team works on, their technical focus areas, and the kinds of research papers they'd benefit from reading. NO bullets, NO bracketed tags, NO dates, NO meta-commentary about the format. Imagine you're writing an abstract that a search engine would index β€” pure topical content. The text should resemble the style and vocabulary of arxiv abstracts.
Repository: {owner}/{name}
Description: {meta.get('description') or '(none)'}
Topics: {", ".join(meta.get('topics') or []) or '(none)'}
Primary language: {meta.get('language') or '(unknown)'}
Raw commit history (most recent first):
{raw_history}
EXAMPLE FORMAT FOR SECTION 1 (do not copy content, only structure):
{FEW_SHOT_EXAMPLE}
Now output BOTH sections for {owner}/{name}. Begin with SECTION 1, then the separator on its own line, then SECTION 2:"""
try:
raw = llm_api.gemini_call(instruction, max_output_tokens=1200, temperature=0.2)
except Exception as e:
print(f"[team_context] Gemini call failed ({e}), falling back to heuristic.")
return _heuristic_result(owner, name, commits[:n_events], meta)
parts = raw.split(_SECTION_SEP, 1)
if len(parts) != 2:
# Gemini didn't follow the format. Use the full output as the
# team_context and derive a domain summary from description + topics.
print(f"[team_context] Section separator not found; using fallback domain summary.")
return TeamContextResult(
team_context=_strip_section_header(raw.strip()),
domain_summary=_domain_summary_from_meta(owner, name, meta),
)
return TeamContextResult(
team_context=_strip_section_header(parts[0].strip()),
domain_summary=_strip_section_header(parts[1].strip()),
)
def _strip_section_header(text: str) -> str:
"""Gemini sometimes echoes the section header ("SECTION 1 β€” ...") at
the top of each section. Strip it so downstream consumers see only
the actual content. Conservative β€” only matches the literal patterns
we used in the prompt."""
lines = text.split("\n")
while lines and (
lines[0].startswith("SECTION 1") or
lines[0].startswith("SECTION 2") or
not lines[0].strip()
):
lines.pop(0)
return "\n".join(lines).strip()
def _domain_summary_from_meta(owner: str, name: str, meta: dict) -> str:
"""Build a topical summary from repo description + topics. Used when
Gemini is unavailable or doesn't produce the structured output."""
desc = (meta.get("description") or "").strip()
topics = meta.get("topics") or []
language = meta.get("language") or ""
bits = []
if desc:
bits.append(desc)
if topics:
bits.append(f"The project focuses on {', '.join(topics)}.")
if language and not any(language.lower() in b.lower() for b in bits):
bits.append(f"Primarily written in {language}.")
if not bits:
return f"{owner}/{name} is a software project without published metadata."
return " ".join(bits)
def _heuristic_result(owner: str, name: str, commits: list[dict], meta: dict) -> TeamContextResult:
"""No-Gemini fallback: rule-based team_context + meta-derived summary."""
return TeamContextResult(
team_context=_heuristic_team_context(owner, name, commits),
domain_summary=_domain_summary_from_meta(owner, name, meta),
)
def _stub_result(owner: str, name: str, meta: dict) -> TeamContextResult:
"""Fallback for repos with no usable commit history."""
return TeamContextResult(
team_context=_stub_team_context(owner, name, meta),
domain_summary=_domain_summary_from_meta(owner, name, meta),
)
def _heuristic_team_context(owner: str, name: str, commits: list[dict]) -> str:
"""No-LLM fallback. Formats raw commits into the corpus structure
with category guessed from keywords. Worse quality than Gemini β€”
used only when GEMINI_API_KEY is unset or the Gemini call fails."""
lines = [f"Recently shipped by {owner}/{name} (sample of their experiment history):"]
for c in commits:
title_lower = c["title"].lower()
if any(k in title_lower for k in ("research", "new model", "experiment", "introduce")):
cat = "[Research]"
elif any(k in title_lower for k in ("bug", "fix")):
cat = "[Bugfix]"
elif any(k in title_lower for k in ("benchmark", "eval")):
cat = "[Evaluation]"
elif any(k in title_lower for k in ("refactor",)):
cat = "[Refactor]"
else:
cat = "[General]"
lines.append(f"- {c['date']} β€” {c['title']} {cat}")
if c["body"]:
summary = c["body"].split("\n", 1)[0][:250]
lines.append(f" {summary}")
lines.append("")
lines.append(f"Generate a paper suggestion for the {owner}/{name} team's reading list.")
return "\n".join(lines)
def _stub_team_context(owner: str, name: str, meta: dict) -> str:
"""Minimal context when commit history is unavailable (private repo,
empty repo, rate-limited). The model will still produce a recommendation
based on description + topics alone, but with much weaker calibration."""
desc = meta.get("description") or "(no description)"
topics = ", ".join(meta.get("topics") or []) or "(no topics)"
return (
f"Recently shipped by {owner}/{name} (limited history available):\n"
f"- Repository description: {desc}\n"
f"- Topics: {topics}\n\n"
f"Generate a paper suggestion for the {owner}/{name} team's reading list."
)
def build_team_context(repo_url: str, n_events: int = 4) -> TeamContextResult:
"""End-to-end: URL β†’ TeamContextResult(team_context, domain_summary).
The orchestrator uses `.team_context` as the scoring-time prompt and
`.domain_summary` as the embedding query for candidate retrieval."""
parsed = parse_repo_url(repo_url)
if not parsed:
raise ValueError(f"Not a github.com repo URL: {repo_url!r}")
owner, name = parsed
commits = fetch_github_commits(owner, name, n=100)
meta = fetch_repo_meta(owner, name)
return extract_team_context(owner, name, commits, meta, n_events=n_events)