Spaces:
Sleeping
Sleeping
| 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"]} | |