# import_requests.py # This script fetches metadata for Rust crates from crates.io, enriches it with AI insights using a local LLM, # and performs dependency analysis. It also handles retries, caching, and logging. import requests import json import logging import time import os import re import shutil import tarfile import tempfile import subprocess import sys from typing import Optional, Dict, List, Any, Union from concurrent.futures import ThreadPoolExecutor, as_completed from bs4 import BeautifulSoup import tiktoken import requests_cache from datetime import datetime from llama_cpp import Llama # Constants MODEL_PATH = os.path.expanduser("~/models/deepseek/deepseek-coder-6.7b-instruct.Q4_K_M.gguf") LLAMA_BIN = os.path.expanduser("~/llama.cpp/build/bin/llama-cli") MAX_TOKENS = 256 MODEL_TOKEN_LIMIT = 4096 PROMPT_TOKEN_MARGIN = 3000 CHECKPOINT_INTERVAL = 5 # Save intermediary results every N crates MAX_RETRIES = 3 # Setup logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.StreamHandler(), logging.FileHandler(f"crate_enrichment_{time.strftime('%Y%m%d-%H%M%S')}.log") ] ) # GitHub Token GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", "") # Caching session = requests_cache.CachedSession('crate_cache', expire_after=3600) # Retry decorator def retry_with_backoff(max_retries=3, backoff_factor=1.0): """Retry decorator with exponential backoff""" def decorator(func): def wrapper(*args, **kwargs): retries, wait_time = 0, backoff_factor while retries < max_retries: try: return func(*args, **kwargs) except Exception as e: retries += 1 if retries == max_retries: logging.error(f"All {max_retries} retries failed: {str(e)}") return kwargs.get('default_return', None) logging.warning(f"Attempt {retries} failed, retrying in {wait_time:.1f}s: {str(e)}") time.sleep(wait_time) wait_time *= 2 return None return wrapper return decorator def estimate_tokens(prompt: str) -> int: encoding = tiktoken.get_encoding("cl100k_base") return len(encoding.encode(prompt)) def truncate_content(content: str, max_tokens: int = 1000) -> str: paragraphs = content.split("\n\n") result, current_tokens = "", 0 encoding = tiktoken.get_encoding("cl100k_base") for para in paragraphs: tokens = len(encoding.encode(para)) if current_tokens + tokens <= max_tokens: result += para + "\n\n" current_tokens += tokens else: break return result.strip() def clean_output(output: str, task: str = "general") -> str: """Task-specific output cleaning""" if not output: return "" # Remove any remaining prompt artifacts output = output.split("<|end|>")[0].strip() if task == "classification": # For classification tasks, extract just the category categories = ["AI", "Database", "Web Framework", "Networking", "Serialization", "Utilities", "DevTools", "ML", "Cryptography", "Unknown"] for category in categories: if re.search(r'\b' + re.escape(category) + r'\b', output, re.IGNORECASE): return category return "Unknown" elif task == "factual_pairs": # For factual pairs, ensure proper formatting pairs = [] facts = re.findall(r'✅\s*Factual:?\s*(.*?)(?=❌|\Z)', output, re.DOTALL) counterfacts = re.findall(r'❌\s*Counterfactual:?\s*(.*?)(?=✅|\Z)', output, re.DOTALL) # Pair them up for i in range(min(len(facts), len(counterfacts))): pairs.append(f"✅ Factual: {facts[i].strip()}\n❌ Counterfactual: {counterfacts[i].strip()}") return "\n\n".join(pairs) else: # General cleaning - more permissive than before lines = [line.strip() for line in output.splitlines() if line.strip()] return "\n".join(lines) # Load model ONCE at script startup model = Llama( model_path=MODEL_PATH, n_ctx=2048, n_gpu_layers=32, # Use as many GPU layers as possible verbose=False ) # Then use model for inference def run_llama(prompt: str, temp: float = 0.2, max_tokens: int = 256) -> Optional[str]: """Run the LLM with customizable parameters per task""" try: token_count = estimate_tokens(prompt) if token_count > PROMPT_TOKEN_MARGIN: logging.warning(f"Prompt too long ({token_count} tokens). Truncating.") prompt = truncate_content(prompt, PROMPT_TOKEN_MARGIN - 100) output = model( prompt, max_tokens=max_tokens, temperature=temp, stop=["<|end|>", "<|user|>", "<|system|>"] # Stop at these tokens ) raw_text = output["choices"][0]["text"] return clean_output(raw_text) except Exception as e: logging.error(f"Model inference failed: {str(e)}") raise def summarize_feature(crate_name: str, features: list) -> Optional[str]: """Generate summaries for crate features with better prompting""" try: if not features: return "No features documented for this crate." # Format features with their dependencies feature_text = "" for f in features[:8]: # Limit to 8 features for context size feature_name = f.get("name", "") deps = f.get("dependencies", []) deps_str = ", ".join(deps) if deps else "none" feature_text += f"- {feature_name} (dependencies: {deps_str})\n" prompt = ( f"<|system|>You are a Rust programming expert analyzing crate features.\n" f"<|user|>For the Rust crate `{crate_name}`, explain these features and what functionality they provide:\n\n" f"{feature_text}\n\n" f"Provide a concise explanation of each feature's purpose and when a developer would enable it.\n" f"<|end|>" ) # Use moderate temperature for informative but natural explanation return run_llama(prompt, temp=0.2, max_tokens=350) except Exception as e: logging.warning(f"Feature summarization failed for {crate_name}: {str(e)}") return "Feature summary not available." def classify_use_case(crate_name: str, desc: str, keywords: list, readme_summary: str = "", key_deps: list = None) -> Optional[str]: """Classify the use case of a crate with rich context""" key_deps = key_deps or [] try: joined = ", ".join(keywords[:10]) if keywords else "None" key_deps_str = ", ".join(key_deps[:5]) if key_deps else "None" desc = truncate_content(desc, 300) readme_summary = truncate_content(readme_summary, 300) # Few-shot prompting with examples prompt = ( f"<|system|>You are a Rust expert classifying crates into the most appropriate category.\n" f"<|user|>\n" f"# Example 1\n" f"Crate: `tokio`\n" f"Description: An asynchronous runtime for the Rust programming language\n" f"Keywords: async, runtime, futures\n" f"Key Dependencies: mio, bytes, parking_lot\n" f"Category: Networking\n\n" f"# Example 2\n" f"Crate: `serde`\n" f"Description: A generic serialization/deserialization framework\n" f"Keywords: serde, serialization\n" f"Key Dependencies: serde_derive\n" f"Category: Serialization\n\n" f"# Crate to Classify\n" f"Crate: `{crate_name}`\n" f"Description: {desc}\n" f"Keywords: {joined}\n" f"README Summary: {readme_summary}\n" f"Key Dependencies: {key_deps_str}\n\n" f"Category (pick only one): [AI, Database, Web Framework, Networking, Serialization, Utilities, DevTools, ML, Cryptography, Unknown]\n" f"<|end|>" ) # Use lower temperature for classification tasks result = run_llama(prompt, temp=0.1, max_tokens=20) # Extract just the category name using regex categories = ["AI", "Database", "Web Framework", "Networking", "Serialization", "Utilities", "DevTools", "ML", "Cryptography", "Unknown"] for category in categories: if re.search(r'\b' + re.escape(category) + r'\b', result, re.IGNORECASE): return category return "Unknown" # Default if no category is found except Exception as e: logging.warning(f"Classification failed for {crate_name}: {str(e)}") return "Unknown" def score_crate(data: dict) -> float: score = (data.get("downloads", 0) / 1000) + (data.get("github_stars", 0) * 10) score += len(truncate_content(data.get("readme", ""), 1000)) / 500 return round(score, 2) def factual_pairs(crate: dict) -> Optional[str]: try: desc = truncate_content(crate.get("description", ""), 300) readme_summary = truncate_content(crate.get("readme_summary", ""), 300) prompt = ( f"<|system|>Create 5 factual/counterfactual pairs for the Rust crate. " f"Factual statements must be true. Counterfactuals should be plausible but incorrect - " f"make them subtle and convincing rather than simple negations.\n" f"<|user|>\n" f"Crate: {crate['name']}\n" f"Description: {desc}\n" f"Repo: {crate.get('repository', '')}\n" f"README Summary: {readme_summary}\n" f"Key Features: {', '.join([f['name'] for f in crate.get('features', [])][:5])}\n\n" f"Format each pair as:\n" f"✅ Factual: [true statement about the crate]\n" f"❌ Counterfactual: [plausible but false statement]\n\n" f"Create 5 pairs.\n" f"<|end|>" ) # Use higher temperature for creative outputs return run_llama(prompt, temp=0.6, max_tokens=400) except Exception as e: logging.warning(f"Factual pairs generation failed for {crate['name']}: {str(e)}") return None def extract_code_snippets(readme: str) -> list: """Extract code snippets from markdown README""" snippets = [] if not readme: return snippets # Find Rust code blocks pattern = r"```(?:rust|(?:no_run|ignore|compile_fail|mdbook-runnable)?)\s*([\s\S]*?)```" matches = re.findall(pattern, readme) for code in matches: if len(code.strip()) > 10: # Only include non-trivial snippets snippets.append(code.strip()) return snippets[:5] # Limit to 5 snippets def download_crate_source(crate_name: str, version: str, temp_dir: str) -> Optional[str]: """Download and extract crate source code""" try: url = f"https://crates.io/api/v1/crates/{crate_name}/{version}/download" download_path = os.path.join(temp_dir, f"{crate_name}-{version}.tar.gz") with session.get(url, stream=True) as r: r.raise_for_status() with open(download_path, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) extract_path = os.path.join(temp_dir, f"{crate_name}-{version}") os.makedirs(extract_path, exist_ok=True) with tarfile.open(download_path) as tar: tar.extractall(path=extract_path) return extract_path except Exception as e: logging.error(f"Failed to download source for {crate_name}: {str(e)}") return None def analyze_dependencies(crates_data: list) -> dict: """Analyze dependencies between crates""" dependency_graph = {} crate_names = {crate["name"] for crate in crates_data} for crate in crates_data: deps = [] for dep in crate.get("dependencies", []): if dep.get("crate_id") in crate_names: deps.append(dep.get("crate_id")) dependency_graph[crate["name"]] = deps # Find most depended-upon crates reverse_deps = {} for crate, deps in dependency_graph.items(): for dep in deps: if dep not in reverse_deps: reverse_deps[dep] = [] reverse_deps[dep].append(crate) return { "dependency_graph": dependency_graph, "reverse_dependencies": reverse_deps, "most_depended": sorted(reverse_deps.items(), key=lambda x: len(x[1]), reverse=True)[:10] } def save_checkpoint(data: list, filename_prefix: str = "checkpoint") -> str: """Save intermediary results with timestamp""" timestamp = time.strftime("%Y%m%d-%H%M%S") filename = f"{filename_prefix}_{timestamp}.jsonl" with open(filename, "w") as out: for item in data: out.write(json.dumps(item) + "\n") logging.info(f"Saved checkpoint to {filename}") return filename @retry_with_backoff(max_retries=MAX_RETRIES) def fetch_crate_metadata(crate: str) -> Optional[dict]: try: r = session.get(f"https://crates.io/api/v1/crates/{crate}") r.raise_for_status() data = r.json() crate_data = data["crate"] latest = crate_data["newest_version"] # Get readme readme_response = session.get(f"https://crates.io/api/v1/crates/{crate}/readme") readme = readme_response.text if readme_response.ok else "" # Get dependencies deps_response = session.get(f"https://crates.io/api/v1/crates/{crate}/{latest}/dependencies") deps = deps_response.json().get("dependencies", []) if deps_response.ok else [] # Get features - using the versions endpoint features = [] versions_response = session.get(f"https://crates.io/api/v1/crates/{crate}/{latest}") if versions_response.ok: version_data = versions_response.json().get("version", {}) features_dict = version_data.get("features", {}) features = [{"name": k, "dependencies": v} for k, v in features_dict.items()] # Repository info and GitHub stars repo = crate_data.get("repository", "") gh_stars = 0 # Check if it's a GitHub repo if "github.com" in repo and GITHUB_TOKEN: match = re.search(r"github.com/([^/]+)/([^/]+)", repo) if match: owner, repo_name = match.groups() repo_name = repo_name.split('.')[0] # Handle .git extensions gh_url = f"https://api.github.com/repos/{owner}/{repo_name}" gh_headers = {"Authorization": f"token {GITHUB_TOKEN}"} if GITHUB_TOKEN else {} gh = session.get(gh_url, headers=gh_headers) if gh.ok: gh_data = gh.json() gh_stars = gh_data.get("stargazers_count", 0) # Check if it's hosted on lib.rs lib_rs_data = {} if "lib.rs" in repo: lib_rs_url = f"https://lib.rs/crates/{crate}" lib_rs_response = session.get(lib_rs_url) if lib_rs_response.ok: soup = BeautifulSoup(lib_rs_response.text, 'html.parser') # Get README from lib.rs if not already available if not readme: readme_div = soup.find('div', class_='readme') if readme_div: readme = readme_div.get_text(strip=True) # Get lib.rs specific stats stats_div = soup.find('div', class_='crate-stats') if stats_div: downloads_text = stats_div.find(string=re.compile(r'[\d,]+ downloads')) if downloads_text: lib_rs_data["librs_downloads"] = int(re.sub(r'[^\d]', '', downloads_text)) # Extract code snippets from readme code_snippets = extract_code_snippets(readme) # Extract sections from readme readme_sections = extract_readme_sections(readme) if readme else "" result = { "name": crate, "version": latest, "description": crate_data.get("description", ""), "repository": repo, "keywords": crate_data.get("keywords", []), "categories": crate_data.get("categories", []), "readme": readme, "downloads": crate_data.get("downloads", 0), "github_stars": gh_stars, "dependencies": deps, "code_snippets": code_snippets, "features": features, # Now populated with actual features "readme_sections": readme_sections, # New line added **lib_rs_data } return result except Exception as e: logging.error(f"Failed fetching metadata for {crate}: {str(e)}") raise def enrich_crate(crate: dict) -> dict: """Apply AI enrichments to crate data""" try: # First generate a README summary to use in other prompts if crate["readme"]: try: readme_content = crate.get("readme_sections", "") or truncate_content(crate.get("readme", ""), 2000) prompt = f"<|system|>Extract key features from README.\n<|user|>Summarize key aspects of this Rust crate from its README:\n{readme_content}\n<|end|>" crate["readme_summary"] = run_llama(prompt, temp=0.3, max_tokens=300) except Exception as e: logging.warning(f"README summary failed for {crate['name']}: {str(e)}") crate["readme_summary"] = None # Extract key dependencies for context key_deps = [dep.get("crate_id") for dep in crate.get("dependencies", [])[:5] if dep.get("kind") == "normal"] # Now use this enriched context for classification crate["feature_summary"] = summarize_feature(crate["name"], crate["features"]) crate["use_case"] = classify_use_case( crate["name"], crate["description"], crate["keywords"], crate.get("readme_summary", ""), key_deps ) crate["score"] = score_crate(crate) crate["factual_counterfactual"] = factual_pairs(crate) return crate except Exception as e: logging.error(f"Failed to enrich {crate['name']}: {str(e)}") return crate def get_crate_list() -> list: return [ # Original crates "serde", "tokio", "reqwest", "rand", "clap", "rayon", "uuid", "actix-web", "sqlx", "candle-core", "onnxruntime", # ML/AI crates "tokenizers", "safetensors", "linfa", "ndarray", "smartcore", "burn", "tract", "tch", # Add more crates here... # Other specialized crates "movingai", "ug-metal", "surrealml-core", "tauri", # Add more crates here... ] def main(): start_time = time.time() crates = get_crate_list() logging.info(f"Fetching and enriching {len(crates)} crates...") # Create timestamped output directory timestamp = time.strftime("%Y%m%d-%H%M%S") output_dir = f"crate_data_{timestamp}" os.makedirs(output_dir, exist_ok=True) enriched = [] # Step 1: Fetch metadata for all crates with ThreadPoolExecutor(max_workers=4) as pool: futures = {pool.submit(fetch_crate_metadata, name): name for name in crates} for i, future in enumerate(as_completed(futures)): crate_name = futures[future] try: data = future.result() if data: enriched.append(data) logging.info(f"Fetched metadata for {crate_name} ({i+1}/{len(crates)})") # Save checkpoint periodically if (i+1) % CHECKPOINT_INTERVAL == 0 or i+1 == len(crates): save_checkpoint(enriched, f"{output_dir}/metadata_checkpoint") except Exception as e: logging.error(f"Failed processing {crate_name}: {str(e)}") # Step 2: Enrich crates with AI insights for i, crate in enumerate(enriched): try: logging.info(f"Enriching {crate['name']} ({i+1}/{len(enriched)})") enriched[i] = enrich_crate(crate) # Save checkpoint periodically if (i+1) % CHECKPOINT_INTERVAL == 0 or i+1 == len(enriched): save_checkpoint(enriched, f"{output_dir}/ai_enriched_checkpoint") except Exception as e: logging.error(f"Failed to enrich {crate['name']}: {str(e)}") # Step 3: Perform dependency analysis logging.info("Analyzing crate dependencies...") dependency_analysis = analyze_dependencies(enriched) # Save final results final_output = f"{output_dir}/enriched_crate_metadata_{timestamp}.jsonl" with open(final_output, "w") as out: for item in enriched: out.write(json.dumps(item) + "\n") # Save dependency analysis with open(f"{output_dir}/dependency_analysis_{timestamp}.json", "w") as out: json.dump(dependency_analysis, out, indent=2) # Generate summary report summary = { "total_crates": len(enriched), "total_time": f"{time.time() - start_time:.2f}s", "timestamp": datetime.now().isoformat(), "most_popular": sorted(enriched, key=lambda x: x.get("score", 0), reverse=True)[:5], "most_depended_upon": dependency_analysis["most_depended"][:5] } with open(f"{output_dir}/summary_report_{timestamp}.json", "w") as out: json.dump(summary, out, indent=2) logging.info(f"✅ Done. Enriched {len(enriched)} crates in {time.time() - start_time:.2f}s") logging.info(f"Results saved to {output_dir}/") if __name__ == "__main__": # Check disk space before starting if shutil.disk_usage("/").free < 1_000_000_000: # 1GB logging.warning("Low disk space! This may affect performance.") try: main() except Exception as e: logging.critical(f"Script failed: {str(e)}") sys.exit(1)