cogniVerse-backend / agents /github_agent.py
Cogniverse CI
πŸš€ Deploy backend from GitHub @ aa0adf8
50f886a
Raw
History Blame Contribute Delete
11.2 kB
import os
import requests
import json
import re
import logging
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langchain_groq import ChatGroq
from utils.logger import get_logger
from core.prompts import FORMATTING_DIRECTIVE
logger = get_logger(__name__)
# ── Helpers ───────────────────────────────────────────────────────────────────
def _get_headers() -> dict:
headers = {"Accept": "application/vnd.github.v3+json"}
token = os.getenv("GITHUB_TOKEN")
if token:
headers["Authorization"] = f"token {token}"
return headers
def _extract_owner_repo(repo_url: str) -> tuple:
match = re.search(r"github\.com/([^/\s]+)/([^/\s]+)", repo_url)
if match:
owner = match.group(1)
repo = match.group(2).replace(".git", "").strip()
repo = repo.rstrip(".,;:'\"()[]{}?!")
return owner, repo
parts = repo_url.strip("/ ").split("/")
if len(parts) >= 2:
owner = parts[-2]
repo = parts[-1].replace(".git", "").strip().rstrip(".,;:'\"()[]{}?!")
return owner, repo
return "", ""
def fetch_repo_metadata(owner: str, repo: str) -> dict:
try:
url = f"https://api.github.com/repos/{owner}/{repo}"
resp = requests.get(url, headers=_get_headers(), timeout=10)
if resp.status_code == 200:
d = resp.json()
return {
"name": d.get("full_name", f"{owner}/{repo}"),
"description": d.get("description") or "No description provided.",
"language": d.get("language") or "Unknown",
"stars": d.get("stargazers_count", 0),
"forks": d.get("forks_count", 0),
"open_issues": d.get("open_issues_count", 0),
"created_at": (d.get("created_at") or "")[:10],
"updated_at": (d.get("updated_at") or "")[:10],
"clone_url": d.get("clone_url", f"https://github.com/{owner}/{repo}.git"),
"topics": ", ".join(d.get("topics", [])) or "None",
}
elif resp.status_code == 404:
return {"error": "Repository not found (404). It may be private or the URL may be wrong."}
elif resp.status_code == 403:
return {"error": "GitHub API rate limit exceeded. Add GITHUB_TOKEN to .env."}
return {"error": f"GitHub returned status {resp.status_code}"}
except Exception as e:
return {"error": str(e)}
def fetch_file(owner: str, repo: str, path: str = "README.md") -> str:
for branch in ["main", "master"]:
try:
url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}"
resp = requests.get(url, timeout=15)
if resp.status_code == 200:
text = resp.text
return text[:15000] + "\n...[TRUNCATED]..." if len(text) > 15000 else text
except Exception:
pass
return f"[Could not fetch {path} β€” not found in main or master branch]"
def search_repos(topic: str) -> list:
try:
url = f"https://api.github.com/search/repositories?q={requests.utils.quote(topic)}&sort=updated&order=desc&per_page=8"
resp = requests.get(url, headers=_get_headers(), timeout=10)
if resp.status_code == 200:
items = resp.json().get("items", [])[:6]
return [{
"name": i.get("full_name"),
"description": (i.get("description") or "N/A")[:100],
"stars": i.get("stargazers_count", 0),
"open_issues": i.get("open_issues_count", 0),
"language": i.get("language") or "N/A",
"last_updated": (i.get("updated_at") or "")[:10],
} for i in items]
except Exception as e:
logger.error("github_search_failed", error=str(e))
return []
# ── Agent Node ────────────────────────────────────────────────────────────────
_SYSTEM_PROMPT = f"""{FORMATTING_DIRECTIVE}
You are the Cognibot GitHub Architecture Agent. You will be provided with pre-fetched GitHub data.
Your job is to synthesize that data into a rich analysis.
### OUTPUT REQUIREMENTS (follow ALL of these):
1. **Repository Overview** - bold title, description, language, stars, forks, open issues.
2. **Tech Stack Table** - a Markdown table listing all technologies/tools you can identify from the README.
3. **Architecture Flowchart** - a Mermaid.js `graph TD` flowchart showing how the main components connect.
4. **How to Contribute** - bullet points extracted from CONTRIBUTING.md or the README.
5. **Clone Command** - a bash code block with the exact `git clone` command.
Keep headings clear. Do NOT skip any section.
"""
def _detect_intent(user_msg: str) -> str:
"""Detect whether this is a repo analysis or topic search."""
msg_lower = user_msg.lower()
if "github.com" in msg_lower or "analyze" in msg_lower:
return "analyze"
if "find top" in msg_lower or "topic" in msg_lower or "search" in msg_lower:
return "search"
return "analyze"
def _extract_repo_url(text: str) -> str:
match = re.search(r"https?://github\.com/[^\s\)\"']+", text)
return match.group(0) if match else ""
def _extract_topic(text: str) -> str:
patterns = [
r"topic ['\"]?([a-zA-Z0-9_\-]+)['\"]?",
r"for ['\"]?([a-zA-Z0-9_\-]+)['\"]?",
r"about ['\"]?([a-zA-Z0-9_\-]+)['\"]?",
]
for p in patterns:
m = re.search(p, text, re.IGNORECASE)
if m:
return m.group(1)
words = text.split()
return words[-1] if words else "machine-learning"
def github_agent_node(state: dict) -> dict:
"""Fetch GitHub data directly, then feed it to the LLM for rich analysis."""
messages = state.get("messages", [])
user_msg = ""
for m in reversed(messages):
if isinstance(m, HumanMessage):
content = m.content
if isinstance(content, list):
user_msg = next((x.get("text","") for x in content if x.get("type")=="text"), "")
else:
user_msg = content
break
logger.info("github_agent_started", user_msg=user_msg[:100])
intent = _detect_intent(user_msg)
llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0.3)
# ── Branch 1: Repo Analysis ───────────────────────────────────────────────
if intent == "analyze":
repo_url = _extract_repo_url(user_msg)
if not repo_url:
# Try to get from message
repo_url = user_msg.strip()
owner, repo = _extract_owner_repo(repo_url)
logger.info("github_analyzing", owner=owner, repo=repo)
if not owner or not repo:
ai_msg = AIMessage(content=f"❌ I couldn't extract a valid GitHub repository URL from your message. Please provide a full URL like `https://github.com/owner/repo`.")
return {"messages": [ai_msg], "agent_trace": state.get("agent_trace", []) + ["github_agent"]}
# Fetch data in parallel-style (sequential is fine, it's fast)
meta = fetch_repo_metadata(owner, repo)
readme = fetch_file(owner, repo, "README.md")
contributing = fetch_file(owner, repo, "CONTRIBUTING.md")
if "error" in meta:
ai_msg = AIMessage(content=f"❌ GitHub API Error: {meta['error']}")
return {"messages": [ai_msg], "agent_trace": state.get("agent_trace", []) + ["github_agent"]}
# Build a rich context prompt
context = f"""
## Repository: {meta['name']}
- **Description**: {meta['description']}
- **Primary Language**: {meta['language']}
- **Stars**: {meta['stars']} | **Forks**: {meta['forks']} | **Open Issues**: {meta['open_issues']}
- **Topics**: {meta['topics']}
- **Created**: {meta['created_at']} | **Last Updated**: {meta['updated_at']}
- **Clone URL**: {meta['clone_url']}
## README.md Content:
{readme}
## CONTRIBUTING.md Content:
{contributing}
"""
logger.info("github_context_built", repo=f"{owner}/{repo}", readme_len=len(readme))
prompt_messages = [
SystemMessage(content=_SYSTEM_PROMPT),
HumanMessage(content=f"Analyze the repository `{owner}/{repo}` using this data:\n\n{context}")
]
# High-Resolution Self-Correction: Inject feedback if we are in a retry loop
feedback = state.get("eval_feedback")
if feedback and state.get("retry_count", 0) > 0:
prompt_messages.append(HumanMessage(content=(
f"⚠️ YOUR PREVIOUS RESPONSE FAILED QUALITY AUDIT.\n"
f"{feedback}\n"
"Please regenerate your response and fix ALL the issues mentioned above."
)))
try:
response = llm.invoke(prompt_messages)
content = response.content
logger.info("github_llm_response", content_len=len(content) if content else 0)
if not content:
content = f"# {meta['name']}\n\n**Description**: {meta['description']}\n\n**Stars**: {meta['stars']} | **Forks**: {meta['forks']}\n\n**Clone**: `git clone {meta['clone_url']}`"
except Exception as e:
logger.error("github_llm_failed", error=str(e))
content = f"❌ LLM error: {e}"
ai_msg = AIMessage(content=content)
return {"messages": [ai_msg], "agent_trace": state.get("agent_trace", []) + ["github_agent"]}
# ── Branch 2: Topic Search ────────────────────────────────────────────────
else:
topic = _extract_topic(user_msg)
logger.info("github_searching_topic", topic=topic)
repos = search_repos(topic)
if not repos:
ai_msg = AIMessage(content=f"❌ No repositories found for topic `{topic}`. Try a different keyword.")
return {"messages": [ai_msg], "agent_trace": state.get("agent_trace", []) + ["github_agent"]}
repos_json = json.dumps(repos, indent=2)
prompt_messages = [
SystemMessage(content=_SYSTEM_PROMPT),
HumanMessage(content=(
f"Here are the top GitHub repositories for the topic **'{topic}'**, "
f"ranked by recent activity (not just stars):\n\n```json\n{repos_json}\n```\n\n"
"Present this as a **Markdown Table** with columns: Rank, Repository, Language, Stars, Open Issues, Last Updated, Description. "
"Then add a brief summary of which repository would be best to contribute to and why."
))
]
try:
response = llm.invoke(prompt_messages)
content = response.content
except Exception as e:
content = f"❌ LLM error: {e}"
ai_msg = AIMessage(content=content)
return {"messages": [ai_msg], "agent_trace": state.get("agent_trace", []) + ["github_agent"]}