idea-extraction-batch-pipeline
Skill0 starsChanged 7 months agoMITView on GitHub
Extract structured records from large conversation/document corpora using parallel LLM agents, with batch creation, merge, deduplication, taxonomy grouping, and interactive dashboard generation. Use when mining insights, ideas, or structured data from large text corpora (chat exports, meeting transcripts, research notes, support tickets) at scale.
In this repository37 other files it gives its agents.
- ablation-study-frameworkSkill
- async-http-websocket-patternsSkill
- brain-dumpSkill
- chaos-controlled-behavior-systemSkill
- daily-digestSkill
- database-reset-and-seedSkill
- duckdb-analytics-layerSkill
- event-driven-backtest-engineSkill
- event-study-full-pipelineSkill
- event-study-stats-suiteSkill
- failure-learning-systemSkill
- fastapi-sse-live-dashboardSkill
- goal-task-decompositionSkill
- high-information-gain-interviewerSkill
- idea-grouping-taxonomySkill
- interview-belief-extractorSkill
- journal-captureSkill
- journal-reviewSkill
- keyword-filter-frameworkSkill
- langgraph-agentic-pipelineSkill
- llm-as-judge-eval-frameworkSkill
- multi-tab-dashboard-rendererSkill
- multi-tier-memory-systemSkill
- parquet-storage-with-walSkill
- project-insightsSkill
- pydantic-typed-config-loaderSkill
- quality-trend-detectionSkill
- research-gap-finderSkill
- safe-commitSkill
- simtom-knowledge-isolationSkill
- statistical-signal-detectionSkill
- streaming-dataset-pipelineSkill
- structured-data-extraction-pipelineSkill
- structured-logging-setupSkill
- system-health-monitoringSkill
- textual-tui-reactiveSkill
- vector-db-ingestionSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 3 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 167
After each extraction round, append new records to the master file:
- Distinctive: line 260
After grouping, run a correction pass where agents review full descriptions and flag misassignments:
- Distinctive: line 293
After grouping and corrections, check whether the taxonomy itself needs updating:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Idea Extraction Batch Pipeline
A batch pipeline for extracting structured records from large text corpora using parallel LLM agents. Handles batching, parallel extraction, merge, deduplication, taxonomy-based grouping, and dashboard generation.
Architecture Overview
Source Corpus (JSONL chunks)
|
v
[1. BATCH] — Split into batches of 50 chunks
|
v
[2. EXTRACT] — N parallel LLM agents, one per batch
|
v
[3. MERGE] — Append new records to master file
|
v
[4. GROUP] — Assign records to taxonomy groups (batches of ~45)
|
v
[5. VALIDATE] — Cross-check assignments, flag mismatches
|
v
[6. DASHBOARD] — Generate interactive HTML from grouped dataStep 1: Prepare the Source Corpus
Your source data should be a JSONL file where each line is a "chunk" — a unit of text to extract from. Use Python (not sed) for slicing, especially on WSL/NTFS:
import itertools
def create_batches(
corpus_path: str,
output_dir: str,
start_offset: int,
num_batches: int = 10,
batch_size: int = 50,
):
"""Create batch files from a JSONL corpus.
Each batch is a JSONL file with `batch_size` chunks.
Use Python's itertools.islice — sed is extremely slow on WSL/NTFS.
"""
with open(corpus_path) as f:
lines = list(itertools.islice(f, start_offset, start_offset + num_batches * batch_size))
for i in range(num_batches):
batch = lines[i * batch_size : (i + 1) * batch_size]
batch_num = (start_offset // batch_size) + i
out_path = f"{output_dir}/batch_{batch_num}.jsonl"
with open(out_path, "w") as out:
out.writelines(batch)
print(f" Created {out_path} ({len(batch)} chunks)")Step 2: Define the Extraction Schema
Define a clear schema for what you're extracting. Each record needs a unique ID, core fields, and evidence pointers back to the source:
RECORD_SCHEMA = {
"record_id": str, # unique: "batch{N}_{seq}" (e.g., "batch42_3")
"title": str, # short descriptive title
"one_liner": str, # single-sentence summary
"description": str, # full description (2-5 sentences)
"problem": str, # what problem this addresses
"proposed_solution": str, # how to solve it
"tags": list[str], # classification tags
"stack": list[str], # technologies/tools mentioned
"blockers": list[dict], # obstacles: {"type": str, "detail": str}
"evidence": { # pointer back to source
"chunk_id": str,
"conversation_title": str,
"timestamp_start": float,
},
}Critical Rules for LLM Extraction
Include these in your extraction prompt:
- Blockers must be explicitly stated — never infer or assume blockers. Only include obstacles that are clearly described as problems in the source text.
- One record per distinct concept — don't merge separate ideas into one record.
- ID format must be deterministic —
batch{N}_{seq}prevents collisions across parallel agents.
Step 3: Parallel Agent Extraction
Launch multiple LLM agents in parallel, each processing one batch:
import json
import concurrent.futures
def extract_batch(batch_path: str, output_path: str, llm_client):
"""Process a single batch file with an LLM agent.
Reads chunks, sends to LLM with extraction prompt, writes records.
"""
with open(batch_path) as f:
chunks = [json.loads(line) for line in f]
batch_num = batch_path.split("batch_")[1].split(".")[0]
prompt = build_extraction_prompt(chunks, batch_num)
response = llm_client.invoke(prompt)
records = parse_extraction_response(response)
with open(output_path, "w") as out:
for record in records:
out.write(json.dumps(record) + "\n")
return {"batch": batch_num, "records": len(records)}
def run_parallel_extraction(batch_dir: str, output_dir: str, max_workers: int = 10):
"""Run extraction on all batch files using parallel agents.
10 parallel agents works but causes some contention; 7 is safer
if you see slowdowns.
"""
import glob
batch_files = sorted(glob.glob(f"{batch_dir}/batch_*.jsonl"))
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool:
futures = {}
for batch_file in batch_files:
batch_num = batch_file.split("batch_")[1].split(".")[0]
out_file = f"{output_dir}/batch_{batch_num}_records.jsonl"
future = pool.submit(extract_batch, batch_file, out_file)
futures[future] = batch_num
for future in concurrent.futures.as_completed(futures):
batch_num = futures[future]
try:
result = future.result()
results.append(result)
print(f" Batch {batch_num}: {result['records']} records extracted")
except Exception as e:
print(f" Batch {batch_num}: FAILED — {e}")
# Note failure and move on; don't retry in same session
results.append({"batch": batch_num, "records": 0, "error": str(e)})
total = sum(r["records"] for r in results)
failed = sum(1 for r in results if r.get("error"))
print(f"\nExtraction complete: {total} records from {len(results)} batches "
f"({failed} failed)")
return resultsStep 4: Merge Into Master File
After each extraction round, append new records to the master file:
def merge_into_master(
batch_output_dir: str,
master_path: str,
batch_pattern: str = "batch_*_records.jsonl",
):
"""Append all new batch records to the master file.
The master file is append-only — never overwrite it.
"""
import glob
new_files = sorted(glob.glob(f"{batch_output_dir}/{batch_pattern}"))
new_count = 0
with open(master_path, "a") as master:
for fpath in new_files:
with open(fpath) as f:
for line in f:
master.write(line)
new_count += 1
# Report total
total = sum(1 for _ in open(master_path))
print(f"Merged {new_count} new records. Master total: {total}")
return {"new": new_count, "total": total}Step 5: Taxonomy Grouping
Group records into a taxonomy. CRITICAL: never give an LLM more than ~50 records at once — it hallucinates after ~260. Always pass actual data as text input, never rely on agent memory.
def group_records(
master_path: str,
taxonomy: list[dict],
output_path: str,
group_batch_size: int = 45,
):
"""Assign records to taxonomy groups using LLM agents.
Process in batches of ~45 records to avoid LLM hallucination.
Always pass the actual record data as text input.
"""
with open(master_path) as f:
records = [json.loads(line) for line in f]
# Prepare lightweight summaries for grouping
summaries = [
{"record_id": r["record_id"], "title": r["title"], "tags": r.get("tags", [])}
for r in records
]
assignments = {}
for i in range(0, len(summaries), group_batch_size):
batch = summaries[i:i + group_batch_size]
# Build prompt with actual data embedded as text
prompt = build_grouping_prompt(batch, taxonomy)
response = llm_client.invoke(prompt)
batch_assignments = parse_grouping_response(response)
for record_id, group_id in batch_assignments.items():
assignments[record_id] = group_id
# Validate: every record assigned, all group codes valid
valid_groups = {g["group_id"] for g in taxonomy}
unassigned = [r["record_id"] for r in records if r["record_id"] not in assignments]
invalid = {rid: gid for rid, gid in assignments.items() if gid not in valid_groups}
if unassigned:
print(f"WARNING: {len(unassigned)} unassigned records")
if invalid:
print(f"WARNING: {len(invalid)} records assigned to invalid groups")
# Build grouped output
groups = {g["group_id"]: {**g, "records": []} for g in taxonomy}
for record in records:
gid = assignments.get(record["record_id"])
if gid and gid in groups:
groups[gid]["records"].append(record)
with open(output_path, "w") as f:
for group in groups.values():
f.write(json.dumps(group) + "\n")
return groupsStep 6: Validation and Correction Pass
After grouping, run a correction pass where agents review full descriptions and flag misassignments:
def run_correction_pass(
grouped_path: str,
num_reviewers: int = 6,
):
"""Review grouped records and flag misassignments.
Each reviewer agent gets a subset of groups to review.
They flag records that seem like poor fits.
"""
with open(grouped_path) as f:
groups = [json.loads(line) for line in f]
# Split groups among reviewers
per_reviewer = max(1, len(groups) // num_reviewers)
corrections = []
for i in range(0, len(groups), per_reviewer):
reviewer_groups = groups[i:i + per_reviewer]
# Send full descriptions (not just titles) to the reviewer
prompt = build_review_prompt(reviewer_groups)
response = llm_client.invoke(prompt)
batch_corrections = parse_corrections(response)
corrections.extend(batch_corrections)
print(f"Correction pass: {len(corrections)} reassignments suggested")
return correctionsStep 7: Taxonomy Health Check
After grouping and corrections, check whether the taxonomy itself needs updating:
def taxonomy_health_check(grouped_path: str, taxonomy: list[dict]):
"""Scan all groups for:
(a) records that are poor fits in their current group
(b) clusters of records that suggest a new group should be created
Run this after every regroup cycle.
"""
prompt = build_health_check_prompt(grouped_path, taxonomy)
response = llm_client.invoke(prompt)
suggestions = parse_health_check(response)
# suggestions = {"poor_fits": [...], "new_groups_suggested": [...]}
if suggestions["new_groups_suggested"]:
print(f"New groups suggested: {len(suggestions['new_groups_suggested'])}")
for sg in suggestions["new_groups_suggested"]:
print(f" - {sg['proposed_name']}: {sg['reason']}")
return suggestionsStep 8: Dashboard Generation
Generate an interactive HTML dashboard from the grouped data:
import json
from collections import Counter, defaultdict
from pathlib import Path
def generate_dashboard(
groups_path: str,
records_path: str,
output_path: str,
):
"""Generate an interactive HTML dashboard from grouped records.
Panels:
1. Connection graph (shared tags/stack between groups)
2. Timeline heatmap (when records were created, by group)
3. Blocker dashboard (what to fix first)
4. Priority scorer (weighted ranking of what to work on next)
"""
with open(groups_path) as f:
groups = [json.loads(line) for line in f]
with open(records_path) as f:
records = [json.loads(line) for line in f]
# Compute stats
tags = Counter()
for record in records:
for t in record.get("tags", []):
tags[t.lower()] += 1
stats = {
"records": len(records),
"groups": len(groups),
"with_blockers": sum(1 for g in groups if g.get("blockers")),
"top_tag": tags.most_common(1)[0][0] if tags else "N/A",
}
# Build visualizations (using plotly, matplotlib, or raw HTML/JS)
html = build_dashboard_html(groups, records, stats)
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
Path(output_path).write_text(html, encoding="utf-8")
print(f"Dashboard written to {output_path}")Priority Scoring Formula
Rank groups by a weighted combination of signals:
def score_group(group: dict, records: list[dict]) -> dict:
"""Score a group for prioritization.
Weights:
Depth (30%) — how many sub-records exist (more = more thinking done)
Blocker Freedom (25%) — fewer blockers = easier to start
Stack Overlap (25%) — uses tools you already know
Recency (20%) — more recently discussed = still top of mind
"""
depth = min(group["record_count"] / 3.0, 10)
blocker_score = max(0, 10 - len(group.get("blockers", [])) * 3)
# Stack overlap with your most-used tools
group_stack = set(s.lower() for s in group.get("stack", []))
overlap = len(group_stack & known_stack)
stack_score = min(overlap * 2.5, 10)
# Recency from timestamps
recency = compute_recency_score(group, records)
total = (depth * 0.30 + blocker_score * 0.25 +
stack_score * 0.25 + recency * 0.20)
return {
"group": group["title"],
"total": round(total, 1),
"depth": round(depth * 0.30, 1),
"blocker_freedom": round(blocker_score * 0.25, 1),
"stack_overlap": round(stack_score * 0.25, 1),
"recency": round(recency * 0.20, 1),
}Cadence Rules
For large corpora (10,000+ chunks), follow this cadence to manage context and quality:
| Every... | Do... |
| 10 batches (500 chunks) | Merge into master, report totals |
| 20 batches (1000 chunks) | Regroup ALL records + regenerate dashboard |
| 30-40 batches | Start a fresh conversation to reset context |
Lessons Learned (Hard-Won)
- Never use sed on WSL/NTFS for large file slicing — Python
itertools.isliceis orders of magnitude faster - Never give an LLM 500+ records to group at once — it hallucinates after ~260. Keep batches at 45-50 max.
- Always pass actual data as text input to grouping agents — don't rely on agent memory or references to earlier context
- If an agent fails, don't retry in the same conversation — note the failure and retry in a fresh session
- 10 parallel agents works but causes contention; 7 is safer if you see slowdowns
- Context bloat is real — by batch 80+ the conversation slows noticeably. Start fresh every 30-40 batches.
- Group summaries must be written from full descriptions, not just titles+tags. Titles-only gives ~85% quality.
- Master file is append-only — never overwrite. Grouped file gets regenerated each cycle.
- Track everything in a state section — batch number, chunk count, record count, last regroup. If context compresses, the state survives.
Common Pitfalls
- No unique IDs — records from parallel agents will collide if you use sequential IDs. Use
batch{N}_{seq}format. - Blockers from hallucination — LLMs love inventing obstacles. Explicitly instruct: "only include blockers stated as problems in the source text."
- Grouping drift — Taxonomy needs periodic health checks. Groups that grow too large should be split; orphan groups should be merged.
- Dashboard from stale data — Always regenerate the dashboard after regrouping, not just after extraction.
- Lost progress on crash — Keep the master file append-only and track state (last batch, counts) in a config file that survives context resets.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

