Spaces:
Sleeping
Sleeping
| import os | |
| import time | |
| import requests | |
| from typing import Dict, List, Any | |
| from openai import OpenAI | |
| from dotenv import load_dotenv | |
| # Load .env file | |
| load_dotenv() | |
| # LLM model with environment variable override | |
| MODEL = os.getenv("LLM_MODEL", "qwen/qwen3-coder:free") | |
| def generate_docs_for_repo(parsed_files: List[Dict[str, Any]]) -> Dict[str, str]: | |
| """ | |
| Generates Markdown documentation for an entire repository in a SINGLE API call. | |
| Args: | |
| parsed_files: List of dicts, each with keys: | |
| - 'filename': str (the file path) | |
| - 'parsed_data': dict with 'functions', 'classes', 'imports' from parser.py | |
| Returns: | |
| Dict mapping filename -> markdown documentation string | |
| """ | |
| if not parsed_files: | |
| return {} | |
| # Get API key from environment | |
| api_key = os.getenv('OPENROUTER_API_KEY') | |
| if not api_key: | |
| raise ValueError("OPENROUTER_API_KEY environment variable not set") | |
| # Initialize OpenAI client with OpenRouter base URL | |
| client = OpenAI( | |
| api_key=api_key, | |
| base_url='https://openrouter.ai/api/v1' | |
| ) | |
| # Build ONE comprehensive prompt for all files | |
| files_summary = [] | |
| for file_item in parsed_files: | |
| filename = file_item['filename'] | |
| parsed_data = file_item['parsed_data'] | |
| functions_text = "\n".join([ | |
| f" - {func['name']}(): {func['docstring'] or 'No docstring'}" | |
| for func in parsed_data.get('functions', []) | |
| ]) | |
| classes_text = "\n".join([ | |
| f" - {cls['name']}: {cls['docstring'] or 'No docstring'}" | |
| for cls in parsed_data.get('classes', []) | |
| ]) | |
| imports_text = "\n".join([ | |
| f" - {imp}" | |
| for imp in parsed_data.get('imports', []) | |
| ]) | |
| file_summary = f"""### File: {filename} | |
| Functions: | |
| {functions_text or " None"} | |
| Classes: | |
| {classes_text or " None"} | |
| Imports: | |
| {imports_text or " None"} | |
| """ | |
| files_summary.append(file_summary) | |
| # Single comprehensive prompt with detailed formatting instructions | |
| combined_prompt = f"""You are a professional code documentation expert. I have a Python repository with {len(parsed_files)} file(s) to document. | |
| For EACH file below, write RICH, DETAILED Markdown documentation following this EXACT structure: | |
| ## filename.py | |
| ### Overview | |
| [ONE LINE SUMMARY: What this file does in plain English] | |
| ### Key Components | |
| [CREATE A TABLE with columns: Name | Type | Purpose | Parameters] | |
| [Include ALL important functions and classes] | |
| [Format: | function_name() | Function | Brief purpose | parameter_types |] | |
| [Format: | ClassName | Class | Brief purpose | __init__ params |] | |
| ### Usage Example | |
| [REALISTIC code example with actual variable names and imports] | |
| [Show how to actually use the main components] | |
| [Include multiple lines if needed] | |
| ### Notes | |
| [Important gotchas, warnings, or tips about this file] | |
| [Performance considerations if relevant] | |
| [Dependencies or requirements if relevant] | |
| --- | |
| IMPORTANT FORMATTING RULES: | |
| 1. Use ## for file headers ONLY | |
| 2. Use ### for section headers (Overview, Key Components, Usage Example, Notes) | |
| 3. For Key Components table, use markdown table format with pipes | |
| 4. Code examples must be in ```python code blocks | |
| 5. Always end with --- (three dashes) between files | |
| 6. Be verbose and helpful - this is for developers who need to understand code quickly | |
| 7. Include actual sample data/variable names in usage examples | |
| 8. Explain WHY and HOW, not just WHAT | |
| === REPOSITORY FILES === | |
| {''.join(files_summary)} | |
| === DOCUMENTATION === | |
| Now write comprehensive documentation for each file following the structure above exactly.""" | |
| # Make ONE API call for the entire repository | |
| markdown_output = "" | |
| retry_count = 0 | |
| max_retries = 1 | |
| while retry_count <= max_retries: | |
| try: | |
| stream = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| { | |
| 'role': 'user', | |
| 'content': combined_prompt | |
| } | |
| ], | |
| stream=True, | |
| max_tokens=8000, | |
| timeout=30, | |
| extra_headers={ | |
| 'HTTP-Referer': 'http://localhost:3000', | |
| 'X-Title': 'RepoAnalyzer' | |
| } | |
| ) | |
| # Collect all streamed chunks | |
| for chunk in stream: | |
| if chunk.choices[0].delta.content: | |
| markdown_output += chunk.choices[0].delta.content | |
| break # Success, exit retry loop | |
| except requests.exceptions.ConnectionError as e: | |
| print(f"❌ Cannot reach OpenRouter - check internet connection: {e}") | |
| raise | |
| except requests.exceptions.Timeout as e: | |
| print(f"❌ Request timed out (30s): {e}") | |
| raise | |
| except requests.exceptions.HTTPError as e: | |
| # Check for 429 (rate limit) | |
| if e.response and e.response.status_code == 429: | |
| if retry_count < max_retries: | |
| print(f"⚠️ Rate limit hit (HTTP 429) - waiting 60 seconds before retry...") | |
| time.sleep(60) | |
| retry_count += 1 | |
| continue | |
| else: | |
| print(f"❌ Rate limit hit (HTTP 429) - max retries exhausted") | |
| raise | |
| else: | |
| print(f"❌ HTTP Error {e.response.status_code if e.response else 'unknown'}: {e}") | |
| raise | |
| except Exception as e: | |
| # Print full exception details so we can debug | |
| print(f"❌ Unexpected error: {type(e).__name__}: {e}") | |
| import traceback | |
| print("Full traceback:") | |
| traceback.print_exc() | |
| raise | |
| # Parse the response and split by filename | |
| result = {} | |
| current_filename = None | |
| current_doc = "" | |
| for line in markdown_output.split('\n'): | |
| # Check if this is a file header (## filename or ### filename) | |
| if line.startswith('##') and not line.startswith('###'): | |
| # Save previous file if exists | |
| if current_filename and current_doc.strip(): | |
| result[current_filename] = current_doc.strip() | |
| # Extract filename from header | |
| header_text = line.replace('##', '').strip() | |
| # Try to match to an actual filename | |
| for file_item in parsed_files: | |
| if file_item['filename'] in header_text or header_text in file_item['filename']: | |
| current_filename = file_item['filename'] | |
| current_doc = "" | |
| break | |
| else: | |
| # Fallback: use the header text as-is | |
| current_filename = header_text | |
| current_doc = "" | |
| elif current_filename: | |
| current_doc += line + "\n" | |
| # Save the last file | |
| if current_filename and current_doc.strip(): | |
| result[current_filename] = current_doc.strip() | |
| # Ensure all files have documentation (fallback to combined output if parsing failed) | |
| if not result: | |
| # If we couldn't parse individual files, return the combined output for all files | |
| for file_item in parsed_files: | |
| result[file_item['filename']] = markdown_output | |
| else: | |
| # For files without specific documentation, add a note | |
| for file_item in parsed_files: | |
| if file_item['filename'] not in result: | |
| result[file_item['filename']] = f"Documentation for {file_item['filename']} is included in the comprehensive repository documentation above." | |
| return result | |
| def generate_readme_for_repo(repo_url: str, docs_list: List[Dict[str, str]]) -> str: | |
| """ | |
| Generates a comprehensive README.md for a repository using AI. | |
| Args: | |
| repo_url: The repository URL | |
| docs_list: List of dicts with 'filename' and 'documentation' keys | |
| Returns: | |
| README markdown content as a string | |
| """ | |
| api_key = os.getenv('OPENROUTER_API_KEY') | |
| if not api_key: | |
| raise ValueError("OPENROUTER_API_KEY environment variable not set") | |
| client = OpenAI( | |
| api_key=api_key, | |
| base_url='https://openrouter.ai/api/v1' | |
| ) | |
| # Extract project name from repo URL | |
| project_name = repo_url.strip('/').split('/')[-1] | |
| # Prepare documentation summary | |
| docs_summary = "\n".join([ | |
| f"- {doc.get('filename', 'unknown')}: {doc.get('documentation', '')[:200]}..." | |
| for doc in docs_list[:10] # Limit to first 10 files to stay within token limits | |
| ]) | |
| prompt = f"""You are a technical writer. Create a professional README.md for this Python project based on the documentation: | |
| Project Repository: {repo_url} | |
| Project Name: {project_name} | |
| Generated File Documentations: | |
| {docs_summary} | |
| Create a comprehensive README.md that includes: | |
| 1. **Project Title and Description** - 2-3 sentences about what the project does | |
| 2. **Features** - A bulleted list of 5-7 key features | |
| 3. **Installation** - Python-specific installation steps (venv, pip-install pattern) | |
| 4. **Usage** - Example code snippets of how to use the project | |
| 5. **Project Structure** - Brief descriptions of the main files/directories | |
| 6. **Contributing** - Generic contributing guidelines | |
| 7. **License** - Standard license section (MIT suggested) | |
| Format the output as proper Markdown. Make it professional and ready for GitHub.""" | |
| try: | |
| stream = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| { | |
| 'role': 'user', | |
| 'content': prompt | |
| } | |
| ], | |
| stream=True, | |
| max_tokens=2000, | |
| timeout=30, | |
| extra_headers={ | |
| 'HTTP-Referer': 'http://localhost:3000', | |
| 'X-Title': 'RepoAnalyzer' | |
| } | |
| ) | |
| readme_content = "" | |
| for chunk in stream: | |
| if chunk.choices[0].delta.content: | |
| readme_content += chunk.choices[0].delta.content | |
| return readme_content.strip() | |
| except requests.exceptions.ConnectionError as e: | |
| raise ConnectionError(f"Cannot reach OpenRouter: {e}") | |
| except requests.exceptions.Timeout as e: | |
| raise TimeoutError(f"Request timed out (30s): {e}") | |
| except requests.exceptions.HTTPError as e: | |
| if e.response and e.response.status_code == 429: | |
| raise Exception("Rate limit hit (HTTP 429) - please wait 60 seconds") | |
| raise | |
| def generate_gitignore_for_repo(repo_url: str, file_extensions: set, imports: set) -> str: | |
| """ | |
| Generates a .gitignore file for a repository based on project type. | |
| Args: | |
| repo_url: The repository URL | |
| file_extensions: Set of file extensions found in the repo (e.g., {'.py', '.js', '.json'}) | |
| imports: Set of top-level imported modules | |
| Returns: | |
| .gitignore content as a string | |
| """ | |
| api_key = os.getenv('OPENROUTER_API_KEY') | |
| if not api_key: | |
| raise ValueError("OPENROUTER_API_KEY environment variable not set") | |
| client = OpenAI( | |
| api_key=api_key, | |
| base_url='https://openrouter.ai/api/v1' | |
| ) | |
| # Detect project type based on file extensions and imports | |
| project_types = [] | |
| if '.py' in file_extensions: | |
| project_types.append('Python') | |
| if '.js' in file_extensions or '.jsx' in file_extensions: | |
| project_types.append('Node.js/JavaScript') | |
| if '.ts' in file_extensions or '.tsx' in file_extensions: | |
| project_types.append('TypeScript') | |
| if '.go' in file_extensions: | |
| project_types.append('Go') | |
| if 'django' in imports or 'flask' in imports: | |
| project_types.append('Web Framework (Django/Flask)') | |
| if 'numpy' in imports or 'pandas' in imports or 'sklearn' in imports: | |
| project_types.append('Data Science') | |
| project_type_str = ', '.join(project_types) if project_types else 'Python' | |
| prompt = f"""Generate a professional .gitignore file for this {project_type_str} project at {repo_url}. | |
| Include these REQUIRED entries first: | |
| .env | |
| __pycache__/ | |
| *.pyc | |
| venv/ | |
| node_modules/ | |
| faiss_indexes/ | |
| .DS_Store | |
| Then add appropriate entries for this project type: {project_type_str} | |
| Include entries for: | |
| - Build artifacts and temporary files | |
| - IDE and editor files (.vscode/, .idea/, *.swp) | |
| - OS-specific files | |
| - Virtual environment and dependency files | |
| - Cache and database files | |
| - Any framework-specific directories | |
| Format as a standard .gitignore file with clear comments describing each section. | |
| Return ONLY the .gitignore content, no explanations.""" | |
| try: | |
| stream = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| { | |
| 'role': 'user', | |
| 'content': prompt | |
| } | |
| ], | |
| stream=True, | |
| max_tokens=1500, | |
| timeout=30, | |
| extra_headers={ | |
| 'HTTP-Referer': 'http://localhost:3000', | |
| 'X-Title': 'RepoAnalyzer' | |
| } | |
| ) | |
| gitignore_content = "" | |
| for chunk in stream: | |
| if chunk.choices[0].delta.content: | |
| gitignore_content += chunk.choices[0].delta.content | |
| return gitignore_content.strip() | |
| except requests.exceptions.ConnectionError as e: | |
| raise ConnectionError(f"Cannot reach OpenRouter: {e}") | |
| except requests.exceptions.Timeout as e: | |
| raise TimeoutError(f"Request timed out (30s): {e}") | |
| except requests.exceptions.HTTPError as e: | |
| if e.response and e.response.status_code == 429: | |
| raise Exception("Rate limit hit (HTTP 429) - please wait 60 seconds") | |
| raise | |
| # Test script to debug OpenRouter connection | |
| print("🧪 Testing OpenRouter API connection...") | |
| print(f"OPENROUTER_API_KEY set: {'Yes' if os.getenv('OPENROUTER_API_KEY') else 'No'}") | |
| print(f"MODEL: {MODEL}") | |
| print() | |
| api_key = os.getenv('OPENROUTER_API_KEY') | |
| if not api_key: | |
| print("❌ ERROR: OPENROUTER_API_KEY not found in environment!") | |
| print(" Make sure .env file exists with OPENROUTER_API_KEY=your_key_here") | |
| exit(1) | |
| try: | |
| print("📤 Sending test request to OpenRouter...") | |
| response = requests.post( | |
| "https://openrouter.ai/api/v1/chat/completions", | |
| headers={ | |
| "Authorization": f"Bearer {api_key}", | |
| "Content-Type": "application/json", | |
| "HTTP-Referer": "http://localhost:3000", | |
| "X-Title": "RepoAnalyzer-Test" | |
| }, | |
| json={ | |
| "model": MODEL, | |
| "messages": [{"role": "user", "content": "say hello"}], | |
| "max_tokens": 10 | |
| }, | |
| timeout=30 | |
| ) | |
| print(f"\n✅ Status Code: {response.status_code}") | |
| print(f"Response Headers: {dict(response.headers)}") | |
| print(f"\nResponse Body:\n{response.text}\n") | |
| if response.status_code == 200: | |
| print("✅ Connection successful!") | |
| elif response.status_code == 401: | |
| print("❌ Unauthorized (401) - Check your OPENROUTER_API_KEY") | |
| elif response.status_code == 429: | |
| print("❌ Rate limited (429) - Too many requests") | |
| else: | |
| print(f"⚠️ Unexpected status code: {response.status_code}") | |
| except requests.exceptions.ConnectionError as e: | |
| print(f"❌ Connection Error: Cannot reach OpenRouter") | |
| print(f" {e}") | |
| except requests.exceptions.Timeout as e: | |
| print(f"❌ Timeout Error: Request took longer than 30 seconds") | |
| print(f" {e}") | |
| except Exception as e: | |
| print(f"❌ Error: {type(e).__name__}: {e}") | |
| import traceback | |
| traceback.print_exc() | |