Spaces:
Paused
Paused
| """Validate markdown files for RAG parser compatibility.""" | |
| import logging | |
| import sys | |
| from pathlib import Path | |
| from typing import Any | |
| # Add src to path (go up from scripts/ to project root, then into src/) | |
| sys.path.insert(0, str(Path(__file__).parent.parent / "src")) | |
| from rag.markdown_parser import MarkdownParser | |
| # Chunk size thresholds (in characters) | |
| CHUNK_MIN_OPTIMAL = 400 # Below this is acceptable but not optimal | |
| CHUNK_MAX_OPTIMAL = 2000 # Above this is acceptable but not optimal | |
| CHUNK_MIN_WARN = 200 # Below this triggers warning (too small) | |
| CHUNK_MAX_WARN = 2000 # Above this triggers warning (large, consider splitting) | |
| CHUNK_MAX_FAIL = 3000 # Above this triggers failure (too large, must split) | |
| # Token estimation (rough approximation: 1 token β 4 chars) | |
| CHARS_PER_TOKEN = 4 | |
| logger = logging.getLogger(__name__) | |
| def validate_file(file_path: Path) -> dict[str, Any]: | |
| """ | |
| Validate a single markdown file for RAG compatibility. | |
| Returns dict with validation results. | |
| """ | |
| parser = MarkdownParser() | |
| result = { | |
| "file": file_path.name, | |
| "path": str(file_path), | |
| "status": "PASS", | |
| "frontmatter_valid": False, | |
| "sections": [], | |
| "chunks": [], | |
| "issues": [], | |
| "warnings": [], | |
| } | |
| try: | |
| # Read file | |
| content = file_path.read_text(encoding="utf-8") | |
| if metadata := parser.parse_frontmatter(content): | |
| result["frontmatter_valid"] = True | |
| result["metadata"] = metadata | |
| else: | |
| result["issues"].append("No valid frontmatter found") | |
| result["status"] = "FAIL" | |
| # Parse sections | |
| sections = parser.extract_sections(content) | |
| if not sections: | |
| result["warnings"].append("No ## sections found in file") | |
| # Parse full file to get chunks | |
| chunks = parser.parse_file(file_path) | |
| result["chunks"] = chunks | |
| # Analyze chunk sizes | |
| for chunk in chunks: | |
| text = chunk["text"] | |
| size = len(text) | |
| tokens_est = size // CHARS_PER_TOKEN | |
| section_title = chunk["metadata"].get("section_title", "Unknown") | |
| chunk_info = { | |
| "section": section_title, | |
| "chars": size, | |
| "tokens": tokens_est, | |
| "status": "OK", | |
| } | |
| # Check size against thresholds | |
| if size > CHUNK_MAX_FAIL: | |
| chunk_info["status"] = "TOO_LARGE" | |
| result["issues"].append( | |
| f"Section '{section_title}' is too large: {size} chars (~{tokens_est} tokens)" | |
| ) | |
| result["status"] = "FAIL" | |
| elif size > CHUNK_MAX_WARN: | |
| chunk_info["status"] = "LARGE" | |
| result["warnings"].append( | |
| f"Section '{section_title}' is large: {size} chars (~{tokens_est} tokens). Consider splitting." | |
| ) | |
| if result["status"] == "PASS": | |
| result["status"] = "WARNING" | |
| elif size < CHUNK_MIN_WARN: | |
| chunk_info["status"] = "SMALL" | |
| result["warnings"].append( | |
| f"Section '{section_title}' is small: {size} chars (~{tokens_est} tokens). Consider merging." | |
| ) | |
| if result["status"] == "PASS": | |
| result["status"] = "WARNING" | |
| result["sections"].append(chunk_info) | |
| except FileNotFoundError: | |
| result["status"] = "FAIL" | |
| result["issues"].append("File not found") | |
| except UnicodeDecodeError: | |
| result["status"] = "FAIL" | |
| result["issues"].append("File encoding error (not UTF-8)") | |
| except Exception: | |
| logger.exception("Unexpected error while validating RAG content") | |
| result["status"] = "FAIL" | |
| result["issues"].append( | |
| "Unexpected internal error during validation (see logs for details)" | |
| ) | |
| return result | |
| def _print_frontmatter(result: dict[str, Any]) -> None: | |
| """Print frontmatter validation status.""" | |
| if result["frontmatter_valid"]: | |
| print("β Frontmatter: Valid YAML") | |
| if "metadata" in result: | |
| for key, val in result["metadata"].items(): | |
| if key not in ["source_file", "doc_type"]: | |
| print(f" β’ {key}: {val}") | |
| print(f" β’ doc_type: {result['metadata'].get('doc_type', 'unknown')}") | |
| else: | |
| print("β Frontmatter: Missing or invalid") | |
| def _print_sections(result: dict[str, Any]) -> None: | |
| """Print section structure analysis.""" | |
| if result["sections"]: | |
| print(f"\nβ Section Structure: {len(result['sections'])} sections found") | |
| for section in result["sections"]: | |
| status_icon = ( | |
| "β " | |
| if section["status"] == "OK" | |
| else "β οΈ" | |
| if section["status"] in ["LARGE", "SMALL"] | |
| else "β" | |
| ) | |
| print( | |
| f" {status_icon} {section['section']}: {section['chars']} chars (~{section['tokens']} tokens)" | |
| ) | |
| def _print_issues_warnings(result: dict[str, Any]) -> None: | |
| """Print issues and warnings.""" | |
| if result["issues"]: | |
| print("\nβ Issues Found:") | |
| for issue in result["issues"]: | |
| print(f" β’ {issue}") | |
| if result["warnings"]: | |
| print("\nβ οΈ Warnings:") | |
| for warning in result["warnings"]: | |
| print(f" β’ {warning}") | |
| def _print_final_status(result: dict[str, Any]) -> None: | |
| """Print final validation status.""" | |
| status_icons = {"PASS": "β ", "WARNING": "β οΈ", "FAIL": "β"} | |
| icon = status_icons.get(result["status"], "β") | |
| print(f"\n{icon} {result['status']}: ", end="") | |
| if result["status"] == "PASS": | |
| print("File is RAG-ready!") | |
| elif result["status"] == "WARNING": | |
| print("File is usable but could be improved") | |
| else: | |
| print("File needs fixes before use") | |
| def print_validation_result(result: dict[str, Any]) -> None: | |
| """Print validation result in a nice format.""" | |
| print(f"\nπ Validating: {result['path']}") | |
| print("β" * 60) | |
| _print_frontmatter(result) | |
| _print_sections(result) | |
| if result["chunks"]: | |
| _print_chunk_analysis(result) | |
| _print_issues_warnings(result) | |
| _print_final_status(result) | |
| print("β" * 60) | |
| def _print_chunk_analysis(result: dict[str, Any]) -> None: | |
| """Print chunk size analysis and distribution.""" | |
| sizes = [len(c["text"]) for c in result["chunks"]] | |
| print("\nπ Chunk Analysis:") | |
| print(f" Total chunks: {len(sizes)}") | |
| print( | |
| f" Size range: {min(sizes)} - {max(sizes)} chars " | |
| f"({min(sizes) // CHARS_PER_TOKEN} - {max(sizes) // CHARS_PER_TOKEN} tokens)" | |
| ) | |
| # Distribution | |
| optimal = sum(CHUNK_MIN_OPTIMAL <= s <= CHUNK_MAX_OPTIMAL for s in sizes) | |
| if optimal == len(sizes): | |
| print(f" β All chunks in optimal range ({CHUNK_MIN_OPTIMAL}-{CHUNK_MAX_OPTIMAL} chars)") | |
| else: | |
| print( | |
| f" {optimal}/{len(sizes)} chunks in optimal range " | |
| f"({CHUNK_MIN_OPTIMAL}-{CHUNK_MAX_OPTIMAL} chars)" | |
| ) | |
| def validate_directory(directory: Path) -> None: | |
| """Validate all .md files in a directory.""" | |
| md_files = list(directory.glob("*.md")) | |
| if not md_files: | |
| print(f"No .md files found in {directory}") | |
| return | |
| results = [] | |
| for file_path in md_files: | |
| result = validate_file(file_path) | |
| results.append(result) | |
| print_validation_result(result) | |
| # Summary | |
| print("\n" + "=" * 60) | |
| print("SUMMARY") | |
| print("=" * 60) | |
| pass_count = sum(r["status"] == "PASS" for r in results) | |
| warning_count = sum(r["status"] == "WARNING" for r in results) | |
| fail_count = sum(r["status"] == "FAIL" for r in results) | |
| print(f"Total files: {len(results)}") | |
| print(f"β Pass: {pass_count}") | |
| print(f"β οΈ Warning: {warning_count}") | |
| print(f"β Fail: {fail_count}") | |
| print("=" * 60) | |
| if __name__ == "__main__": | |
| if len(sys.argv) < 2: | |
| # Default: validate all RAG content | |
| print("Validating all RAG database content...\n") | |
| lessons_dir = Path("data/rag_database/lessons") | |
| exercises_dir = Path("data/rag_database/exercises") | |
| if lessons_dir.exists(): | |
| print("\n" + "=" * 60) | |
| print("LESSONS") | |
| print("=" * 60) | |
| validate_directory(lessons_dir) | |
| if exercises_dir.exists(): | |
| print("\n" + "=" * 60) | |
| print("EXERCISES") | |
| print("=" * 60) | |
| validate_directory(exercises_dir) | |
| else: | |
| path = Path(sys.argv[1]) | |
| if path.is_file(): | |
| result = validate_file(path) | |
| print_validation_result(result) | |
| elif path.is_dir(): | |
| validate_directory(path) | |
| else: | |
| print(f"Error: {path} is not a file or directory") | |
| sys.exit(1) | |