Download src/dedup_quality.py from Reizxn/makeitwork1: direct link, hf CLI and curl.
- Browser
- Download file 13.3 kB
-
https://huggingface.co/Reizxn/makeitwork1/resolve/main/src/dedup_quality.py
- Command line
-
hf download hf://Reizxn/makeitwork1/src/dedup_quality.py
-
curl -L -o dedup_quality.py https://huggingface.co/Reizxn/makeitwork1/resolve/main/src/dedup_quality.py
13.3 kB
| """ | |
| Deduplication, contamination check, and quality filter for SFT traces. | |
| Pipeline: | |
| 1. Load all traces from data/sft_traces_v2/ + existing sft_traces.jsonl | |
| 2. Deduplicate by query hash (exact match) and by fuzzy similarity (near-dupes) | |
| 3. Contamination check: remove traces whose queries appear in gold_traces.jsonl | |
| 4. Quality filter: remove traces that are too short, have empty reasoning, | |
| or have malformed structure | |
| 5. Shuffle and write final dataset | |
| Usage: | |
| python src/dedup_quality.py --input data/sft_traces_v2/ --output data/sft_traces_final.jsonl | |
| """ | |
| import argparse | |
| import hashlib | |
| import json | |
| import os | |
| import random | |
| import re | |
| import sys | |
| from collections import defaultdict | |
| from difflib import SequenceMatcher | |
| PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| def load_traces_from_dir(dir_path: str) -> list[dict]: | |
| """Load all .jsonl files from a directory.""" | |
| traces = [] | |
| if not os.path.exists(dir_path): | |
| return traces | |
| for fname in sorted(os.listdir(dir_path)): | |
| if fname.endswith('.jsonl'): | |
| fpath = os.path.join(dir_path, fname) | |
| with open(fpath, 'r', encoding='utf-8') as f: | |
| for line in f: | |
| try: | |
| trace = json.loads(line) | |
| if trace and 'query' in trace and 'trace' in trace: | |
| trace['_source'] = fname | |
| traces.append(trace) | |
| except json.JSONDecodeError: | |
| continue | |
| return traces | |
| def load_traces_from_file(fpath: str) -> list[dict]: | |
| """Load traces from a single .jsonl file.""" | |
| traces = [] | |
| if not os.path.exists(fpath): | |
| return traces | |
| with open(fpath, 'r', encoding='utf-8') as f: | |
| for line in f: | |
| try: | |
| trace = json.loads(line) | |
| if trace and 'query' in trace and 'trace' in trace: | |
| trace['_source'] = os.path.basename(fpath) | |
| traces.append(trace) | |
| except json.JSONDecodeError: | |
| continue | |
| return traces | |
| def query_hash(trace: dict) -> str: | |
| """Hash the query for exact dedup.""" | |
| return hashlib.md5(trace['query'].strip().lower().encode()).hexdigest() | |
| def normalize_query(query: str) -> str: | |
| """Normalize query for fuzzy matching.""" | |
| # Remove punctuation, lowercase, collapse whitespace | |
| q = re.sub(r'[^\w\s]', '', query.lower()) | |
| q = ' '.join(q.split()) | |
| return q | |
| def fuzzy_similarity(q1: str, q2: str) -> float: | |
| """Compute similarity between two queries.""" | |
| n1, n2 = normalize_query(q1), normalize_query(q2) | |
| if n1 == n2: | |
| return 1.0 | |
| # Quick length check | |
| if abs(len(n1) - len(n2)) > max(len(n1), len(n2)) * 0.5: | |
| return 0.0 | |
| return SequenceMatcher(None, n1, n2).ratio() | |
| def check_trace_quality(trace: dict) -> tuple[bool, str]: | |
| """Check if a trace meets quality standards. | |
| Returns (is_valid, reason_if_rejected) | |
| """ | |
| query = trace.get('query', '') | |
| trace_msgs = trace.get('trace', []) | |
| # Must have system, user, and at least 2 assistant turns | |
| if len(trace_msgs) < 4: | |
| return False, "too_few_messages" | |
| # Query must be substantial | |
| if len(query) < 15: | |
| return False, "query_too_short" | |
| # Check assistant turns have real content | |
| assistant_turns = [m for m in trace_msgs if m['role'] == 'assistant'] | |
| if len(assistant_turns) < 2: | |
| return False, "too_few_assistant_turns" | |
| for turn in assistant_turns: | |
| content = turn.get('content', '') | |
| if len(content) < 50: | |
| return False, "assistant_turn_too_short" | |
| # Must contain at least one special token | |
| if not any(tok in content for tok in ['<|reasoning|>', '<|search|>', '<|evidence|>', '<|finish|>']): | |
| return False, "no_special_tokens" | |
| # Must have at least one search | |
| has_search = any('<|search|>' in m.get('content', '') for m in assistant_turns) | |
| if not has_search: | |
| return False, "no_search_action" | |
| # Must have evidence or finish | |
| has_evidence = any('<|evidence|>' in m.get('content', '') for m in assistant_turns) | |
| has_finish = any('<|finish|>' in m.get('content', '') for m in assistant_turns) | |
| if not has_evidence and not has_finish: | |
| return False, "no_evidence_or_finish" | |
| # Check for reasoning density — at least one turn should have substantial reasoning | |
| max_reasoning_len = 0 | |
| for turn in assistant_turns: | |
| content = turn.get('content', '') | |
| # Extract reasoning sections | |
| reasoning_sections = re.findall(r'<\|reasoning\|>(.*?)<\|end\|>', content, re.DOTALL) | |
| for r in reasoning_sections: | |
| max_reasoning_len = max(max_reasoning_len, len(r.strip())) | |
| if max_reasoning_len < 30: | |
| return False, "reasoning_too_thin" | |
| return True, "ok" | |
| def deduplicate(traces: list[dict], similarity_threshold: float = 0.85) -> tuple[list[dict], dict]: | |
| """Remove duplicate and near-duplicate traces. | |
| Returns (deduplicated_traces, stats) | |
| """ | |
| stats = { | |
| 'exact_dups_removed': 0, | |
| 'fuzzy_dups_removed': 0, | |
| 'total_input': len(traces), | |
| } | |
| # Phase 1: Exact dedup by query hash | |
| seen_hashes = set() | |
| exact_deduped = [] | |
| for trace in traces: | |
| h = query_hash(trace) | |
| if h not in seen_hashes: | |
| seen_hashes.add(h) | |
| exact_deduped.append(trace) | |
| else: | |
| stats['exact_dups_removed'] += 1 | |
| # Phase 2: Fuzzy dedup by query similarity | |
| # Group by first word for efficiency | |
| groups = defaultdict(list) | |
| for trace in exact_deduped: | |
| first_word = normalize_query(trace['query']).split()[0] if normalize_query(trace['query']).split() else '' | |
| groups[first_word].append(trace) | |
| fuzzy_deduped = [] | |
| for first_word, group in groups.items(): | |
| if len(group) == 1: | |
| fuzzy_deduped.extend(group) | |
| continue | |
| # Compare within group | |
| kept = [] | |
| for trace in group: | |
| is_dup = False | |
| for kept_trace in kept: | |
| sim = fuzzy_similarity(trace['query'], kept_trace['query']) | |
| if sim >= similarity_threshold: | |
| is_dup = True | |
| stats['fuzzy_dups_removed'] += 1 | |
| break | |
| if not is_dup: | |
| kept.append(trace) | |
| fuzzy_deduped.extend(kept) | |
| stats['total_output'] = len(fuzzy_deduped) | |
| return fuzzy_deduped, stats | |
| def check_contamination(traces: list[dict], gold_traces: list[dict]) -> tuple[list[dict], dict]: | |
| """Remove traces whose queries match gold trace queries. | |
| Returns (clean_traces, stats) | |
| """ | |
| gold_queries = set() | |
| for gt in gold_traces: | |
| gold_queries.add(normalize_query(gt['query'])) | |
| clean = [] | |
| removed = 0 | |
| for trace in traces: | |
| nq = normalize_query(trace['query']) | |
| if nq in gold_queries: | |
| removed += 1 | |
| else: | |
| clean.append(trace) | |
| return clean, {'contamination_removed': removed, 'gold_queries': len(gold_queries)} | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Dedup and quality filter SFT traces") | |
| parser.add_argument("--input", type=str, default=os.path.join(PROJECT_DIR, "data", "sft_traces_v2"), | |
| help="Input directory with .jsonl files") | |
| parser.add_argument("--existing", type=str, default=os.path.join(PROJECT_DIR, "data", "sft_traces.jsonl"), | |
| help="Existing traces to merge with") | |
| parser.add_argument("--gold", type=str, default=os.path.join(PROJECT_DIR, "data", "gold_traces.jsonl"), | |
| help="Gold traces for contamination check") | |
| parser.add_argument("--output", type=str, default=os.path.join(PROJECT_DIR, "data", "sft_traces_final.jsonl"), | |
| help="Output file") | |
| parser.add_argument("--seed", type=int, default=42) | |
| parser.add_argument("--similarity", type=float, default=0.85, | |
| help="Fuzzy dedup similarity threshold") | |
| args = parser.parse_args() | |
| print("=" * 60) | |
| print("SFT TRACE DEDUPLICATION & QUALITY PIPELINE") | |
| print("=" * 60) | |
| # 1. Load all traces | |
| print("\n1. Loading traces...") | |
| new_traces = load_traces_from_dir(args.input) | |
| existing_traces = load_traces_from_file(args.existing) | |
| gold_traces = load_traces_from_file(args.gold) | |
| print(f" New traces: {len(new_traces):,}") | |
| print(f" Existing traces: {len(existing_traces):,}") | |
| print(f" Gold traces: {len(gold_traces):,}") | |
| all_traces = new_traces + existing_traces | |
| print(f" Total to process: {len(all_traces):,}") | |
| # 2. Quality filter | |
| print("\n2. Quality filtering...") | |
| quality_stats = defaultdict(int) | |
| quality_passed = [] | |
| for trace in all_traces: | |
| is_valid, reason = check_trace_quality(trace) | |
| if is_valid: | |
| quality_passed.append(trace) | |
| else: | |
| quality_stats[reason] += 1 | |
| print(f" Passed: {len(quality_passed):,}") | |
| print(f" Rejected: {sum(quality_stats.values()):,}") | |
| for reason, count in sorted(quality_stats.items(), key=lambda x: -x[1]): | |
| print(f" {reason}: {count}") | |
| # 3. Contamination check | |
| print("\n3. Contamination check (vs gold traces)...") | |
| clean_traces, contam_stats = check_contamination(quality_passed, gold_traces) | |
| print(f" Removed: {contam_stats['contamination_removed']}") | |
| print(f" Remaining: {len(clean_traces):,}") | |
| # 4. Deduplication | |
| print("\n4. Deduplication...") | |
| deduped_traces, dedup_stats = deduplicate(clean_traces, args.similarity) | |
| print(f" Exact dups removed: {dedup_stats['exact_dups_removed']:,}") | |
| print(f" Fuzzy dups removed: {dedup_stats['fuzzy_dups_removed']:,}") | |
| print(f" Final count: {dedup_stats['total_output']:,}") | |
| # 5. Shuffle and write | |
| print(f"\n5. Writing to {args.output}...") | |
| rng = random.Random(args.seed) | |
| rng.shuffle(deduped_traces) | |
| # Remove internal fields | |
| for trace in deduped_traces: | |
| trace.pop('_source', None) | |
| with open(args.output, 'w', encoding='utf-8') as f: | |
| for trace in deduped_traces: | |
| f.write(json.dumps(trace, ensure_ascii=False) + '\n') | |
| print(f" Written: {len(deduped_traces):,} traces") | |
| # Summary | |
| print("\n" + "=" * 60) | |
| print("SUMMARY") | |
| print("=" * 60) | |
| print(f" Input traces: {len(all_traces):,}") | |
| print(f" Quality rejected: {sum(quality_stats.values()):,}") | |
| print(f" Contamination removed: {contam_stats['contamination_removed']}") | |
| print(f" Exact dups removed: {dedup_stats['exact_dups_removed']:,}") | |
| print(f" Fuzzy dups removed: {dedup_stats['fuzzy_dups_removed']:,}") | |
| print(f" Final dataset: {len(deduped_traces):,}") | |
| # Category distribution | |
| cat_counts = defaultdict(int) | |
| for trace in deduped_traces: | |
| # Try to infer category from query pattern | |
| q = trace['query'].lower() | |
| if any(w in q for w in ['walk me through', 'implementation', 'step by step', 'control flow']): | |
| cat_counts['implementation'] += 1 | |
| elif any(w in q for w in ['trace how data', 'data flow', 'data path', 'interact']): | |
| cat_counts['cross_file'] += 1 | |
| elif any(w in q for w in ['architecture', 'module', 'structure of', 'map out']): | |
| cat_counts['architecture'] += 1 | |
| elif any(w in q for w in ['where is', 'used across', 'usage', 'called from']): | |
| cat_counts['usage'] += 1 | |
| elif any(w in q for w in ['error', 'failure', 'fail', 'debug']): | |
| cat_counts['error/debug'] += 1 | |
| elif any(w in q for w in ['api', 'contract', 'interface', 'parameters']): | |
| cat_counts['api'] += 1 | |
| elif any(w in q for w in ['depend', 'dependency', 'blast radius', 'impact']): | |
| cat_counts['dependency/impact'] += 1 | |
| elif any(w in q for w in ['fields', 'data structure', 'layout', 'memory']): | |
| cat_counts['data_structure'] += 1 | |
| elif any(w in q for w in ['compare', 'tradeoff', 'vs', 'contrast']): | |
| cat_counts['comparison'] += 1 | |
| elif any(w in q for w in ['security', 'validation', 'vulnerability']): | |
| cat_counts['security'] += 1 | |
| elif any(w in q for w in ['performance', 'bottleneck', 'hot', 'optimize']): | |
| cat_counts['performance'] += 1 | |
| elif any(w in q for w in ['pattern', 'design']): | |
| cat_counts['design_patterns'] += 1 | |
| else: | |
| cat_counts['other'] += 1 | |
| print(f"\n Category distribution:") | |
| for cat, count in sorted(cat_counts.items(), key=lambda x: -x[1]): | |
| print(f" {cat:20s}: {count:5d}") | |
| if __name__ == "__main__": | |
| main() | |