skills/ai-parsing-data/SKILL.md
Pull structured data from messy text using AI. Use when parsing invoices, extracting fields from emails, scraping entities from articles, converting unstructured text to JSON, extracting contact info, parsing resumes, reading forms, pulling data from transcripts (VTT, LiveKit, Recall), extracting fields from Langfuse traces, or any task where messy text goes in and clean structured data comes out. Also use when emails are messy and lack structure, or structured data extraction from unstructured content is unreliable., extract entities from text, parse PDF with AI, structured extraction from unstructured text, OCR plus AI extraction, convert email to structured data, pull fields from documents automatically, AI data entry automation, invoice parsing, resume parsing with AI, medical record extraction.
npx skillsauth add lebsral/dspy-programming-not-prompting-lms-skills ai-parsing-dataInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Guide the user through building AI that pulls structured data out of messy text. Uses DSPy extraction — define the output shape, and the AI fills it in.
Ask the user:
For pulling a known set of fields from text:
import dspy
# Configure any LM provider
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
dspy.configure(lm=lm)
class ParseContact(dspy.Signature):
"""Extract contact information from the text."""
text: str = dspy.InputField(desc="Text containing contact information")
name: str = dspy.OutputField(desc="Person's full name")
email: str = dspy.OutputField(desc="Email address")
phone: str = dspy.OutputField(desc="Phone number")
parser = dspy.ChainOfThought(ParseContact)
ChainOfThought adds reasoning before extraction, which helps the model think through which text maps to which field — typically 5-15% more accurate than bare Predict on ambiguous inputs.
For complex or nested output, use Pydantic models. DSPy handles the serialization automatically:
from pydantic import BaseModel, Field
from typing import Optional
class Address(BaseModel):
street: str
city: str
state: str
zip_code: str
class Person(BaseModel):
name: str
age: Optional[int] = None
email: Optional[str] = None
address: Address
skills: list[str]
class ParsePerson(dspy.Signature):
"""Extract person details from the text."""
text: str = dspy.InputField()
person: Person = dspy.OutputField()
parser = dspy.ChainOfThought(ParsePerson)
result = parser(text="John Doe, 32, lives at 123 Main St, Springfield IL 62701. Expert in Python and SQL.")
print(result.person) # Person(name='John Doe', age=32, ...)
Use Optional for fields that might not appear in every document — this tells the model it's OK to return None instead of guessing.
Small models (<4B params) produce frequent JSON syntax errors — unclosed braces, missing quotes, trailing commas. Switching to YAML output eliminates these failures entirely while preserving structured data. In one production case (3.6M historical name records), frontier models achieved ~70% accuracy while fine-tuned 0.8B-4B models using YAML output hit 94-96%.
import yaml
class ParsePersonYAML(dspy.Signature):
"""Extract person details from the text. Return the result as YAML, not JSON."""
text: str = dspy.InputField()
person_yaml: str = dspy.OutputField(desc="extracted person data in YAML format")
parser = dspy.Predict(ParsePersonYAML)
result = parser(text="John Doe, 32, [email protected]")
# Parse YAML back into structured data
person_data = yaml.safe_load(result.person_yaml)
Use this pattern when running sub-4B parameter models (Qwen, Phi, Gemma) locally. Larger models (GPT-4o, Claude) handle JSON fine — stick with Pydantic output fields for those.
When you need to pull a variable number of items (entities, line items, experiences):
class Entity(BaseModel):
name: str
type: str = Field(description="Type: person, organization, location, or date")
class ParseEntities(dspy.Signature):
"""Extract all named entities from the text."""
text: str = dspy.InputField()
entities: list[Entity] = dspy.OutputField(desc="All entities found in the text")
parser = dspy.ChainOfThought(ParseEntities)
from pathlib import Path
# Single file
text = Path("document.txt").read_text()
result = parser(text=text)
# Directory of files
documents = []
for path in Path("documents/").glob("*.txt"):
documents.append({"file": path.name, "text": path.read_text()})
import pandas as pd
df = pd.read_csv("emails.csv") # column: body
results = []
for _, row in df.iterrows():
result = parser(text=row["body"])
results.append(result.person.model_dump()) # Pydantic → dict
# Save extracted data
pd.DataFrame(results).to_csv("extracted.csv", index=False)
Transcripts are a common parsing source — extracting caller info, action items, decisions, or structured summaries from conversations.
WebVTT (.vtt) files:
import re
def load_vtt(path):
"""Extract text from a VTT transcript, stripping timestamps."""
text = open(path).read()
lines = [line.strip() for line in text.split("\n")
if line.strip() and not line.startswith("WEBVTT")
and not re.match(r"\d{2}:\d{2}", line)
and not line.strip().isdigit()]
return " ".join(lines)
LiveKit transcripts:
import json
def load_livekit_transcript(path):
"""Extract text from a LiveKit transcript JSON export."""
data = json.load(open(path))
segments = data.get("segments", data.get("results", []))
return " ".join(seg.get("text", "") for seg in segments)
Recall.ai transcripts:
def load_recall_transcript(transcript_data):
"""Extract text from a Recall.ai transcript response."""
return " ".join(
entry["words"] for entry in transcript_data if entry.get("words")
)
Example: extracting structured data from a call transcript:
class CallSummary(BaseModel):
caller_name: Optional[str] = None
issue_summary: str
resolution: Optional[str] = None
follow_up_needed: bool
action_items: list[str]
class ParseCallTranscript(dspy.Signature):
"""Extract structured information from a customer call transcript."""
transcript: str = dspy.InputField(desc="Full call transcript text")
summary: CallSummary = dspy.OutputField()
parser = dspy.ChainOfThought(ParseCallTranscript)
transcript = load_livekit_transcript("call_001.json")
result = parser(transcript=transcript)
Extract structured data from AI interactions logged in Langfuse:
from langfuse import Langfuse
langfuse = Langfuse()
traces = langfuse.fetch_traces(limit=100).data
# Parse each trace's input/output for structured fields
for trace in traces:
if trace.input:
text = trace.input.get("message", str(trace.input))
result = parser(text=text)
Real-world text is messy. Use a reward function with dspy.Refine to catch bad extractions and retry:
class ValidatedParser(dspy.Module):
def __init__(self):
self.parse = dspy.ChainOfThought(ParseContact)
def forward(self, text):
return self.parse(text=text)
def contact_reward(args, pred):
score = 1.0
if not pred.email or "@" not in pred.email:
score -= 0.4 # Email should contain @
phone_digits = pred.phone.replace("-", "").replace(" ", "") if pred.phone else ""
if len(phone_digits) < 10:
score -= 0.3 # Phone number should have at least 10 digits
return max(score, 0.0)
validated_parser = dspy.Refine(
module=ValidatedParser(),
N=3,
reward_fn=contact_reward,
threshold=0.7,
)
dspy.Refine retries the extraction up to N times, keeping the attempt with the highest reward score. Penalize each failed constraint proportionally to its importance.
The model does the heavy lifting, then regex sweeps for anything it missed. This pattern improved F1 from 0.733 (model-only) to 0.929 (hybrid) in a production privacy extraction system.
import re
def hybrid_extract(text, parser, patterns):
"""Run model extraction, then fill gaps with regex patterns."""
result = parser(text=text)
# Regex backstop — catch fields the model missed
for field, pattern in patterns.items():
model_value = getattr(result, field, None)
if not model_value:
match = re.search(pattern, text)
if match:
result.__dict__[field] = match.group(1) if match.groups() else match.group()
return result
# Define regex patterns for common fields
patterns = {
"email": r"[\w.+-]+@[\w-]+\.[\w.-]+",
"phone": r"\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}",
"zip_code": r"\b\d{5}(?:-\d{4})?\b",
}
result = hybrid_extract(messy_text, parser, patterns)
Use this when your input has a mix of structured patterns (emails, phones, dates) and free-form text. The model handles ambiguous fields like names and summaries; regex catches well-formatted fields the model occasionally skips.
When a field genuinely isn't in the text, you want the model to say so rather than hallucinate a value. Use Optional types in your Pydantic model, and add a validation note in the signature docstring:
class ParseContact(dspy.Signature):
"""Extract contact info from the text. Return None for fields not present — do not guess."""
text: str = dspy.InputField()
name: str = dspy.OutputField(desc="Person's full name")
email: Optional[str] = dspy.OutputField(desc="Email address, or None if not found")
phone: Optional[str] = dspy.OutputField(desc="Phone number, or None if not found")
from dspy.evaluate import Evaluate
def parsing_metric(example, prediction, trace=None):
"""Score based on field-level accuracy (partial credit)."""
correct = 0
total = 0
for field in ["name", "email", "phone"]:
expected = getattr(example, field, None)
predicted = getattr(prediction, field, None)
if expected is not None:
total += 1
if predicted and expected.lower().strip() == predicted.lower().strip():
correct += 1
return correct / total if total > 0 else 0.0
evaluator = Evaluate(devset=devset, metric=parsing_metric, num_threads=4, display_progress=True)
score = evaluator(parser)
print(f"Baseline accuracy: {score}%")
For Pydantic outputs, compare field-by-field or use the model's .model_dump() to compare dicts. Partial credit (scoring each field independently) is better than all-or-nothing for extraction tasks — it tells you which specific fields are causing problems.
# Optimize
optimizer = dspy.BootstrapFewShot(metric=parsing_metric, max_bootstrapped_demos=4)
optimized = optimizer.compile(parser, trainset=trainset)
# Evaluate improvement
improved = evaluator(optimized)
print(f"Optimized accuracy: {improved}%")
# Save for production
optimized.save("parser.json")
# Load later
parser = dspy.ChainOfThought(ParseContact)
parser.load("parser.json")
For parsing many documents at once:
import json
results = []
errors = []
for doc in documents:
try:
result = optimized(text=doc["text"])
results.append({
"source": doc["file"],
**result.person.model_dump() # flatten Pydantic fields
})
except Exception as e:
errors.append({"source": doc["file"], "error": str(e)})
# Save results
with open("extracted.json", "w") as f:
json.dump(results, f, indent=2)
if errors:
print(f"{len(errors)} documents failed to parse — check errors list")
/ai-summarizing/ai-decomposing-tasks/ai-improving-accuracy/ai-generating-datastr, int, float, bool, list, dict, and nested Pydantic models.None defaults — use field: Optional[str] = dspy.OutputField(default=None) or the model will hallucinate values for missing fields instead of returning None.max_tokens higher and add a "be exhaustive" instruction in the signature docstring./ai-do if you do not have it — it routes any AI problem to the right skill and is the fastest way to work: npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill ai-dotools
See what is happening during optimizer.compile() instead of waiting blind. Use when you want to watch optimization progress, see scores as they come in, know if your optimizer is working, check if optimization is stuck, understand why optimization is taking too long, get live progress during compile, monitor convergence, detect overfitting during optimization, interpret optimization results, or pick the right tool for watching optimization. Also used for optimizer progress bar, is my optimizer doing anything, optimization seems stuck, how long will optimization take, watch GEPA run, watch MIPROv2 run, live optimization dashboard, optimizer not improving, scores not going up, optimization taking forever, see what optimizer is doing, debug slow optimization, optimization visibility, optimizer metrics, track compile progress, optimization observability.
testing
Use when you want the highest-quality prompt optimization DSPy offers — jointly optimizes instructions and few-shot demos, with auto=light/medium/heavy presets. Common scenarios - you want the best possible accuracy from prompt optimization, jointly tuning instructions and few-shot demonstrations, using auto presets for different compute budgets, or when COPRO or BootstrapFewShot alone are not reaching your accuracy target. Related - ai-improving-accuracy, dspy-copro, dspy-bootstrap-few-shot. Also used for dspy.MIPROv2, best DSPy optimizer, highest quality optimization, auto=light medium heavy, joint instruction and demo optimization, most powerful prompt optimizer, MIPROv2 vs COPRO vs BootstrapFewShot, which optimizer should I use, state of the art prompt optimization, when to use MIPROv2, optimize both instructions and examples, heavy optimization for production, best optimizer for accuracy.
testing
Use LangWatch for DSPy auto-tracing and real-time optimizer progress. Use when you want to set up LangWatch, langwatch.dspy.init, auto-tracing DSPy, real-time optimization dashboard, optimizer progress tracking, app.langwatch.ai, or DSPy optimizer dashboard. Also used for langwatch setup, pip install langwatch, langwatch trace, optimizer progress, real-time optimization, watch optimizer run, LangWatch self-hosted, langwatch docker, langwatch vs langtrace, langwatch autotrack_dspy.
data-ai
Use when you want to optimize instructions without few-shot examples — a lightweight alternative to COPRO when you do not have or do not want to use demonstrations. Common scenarios - optimizing instructions when you do not have or do not want to use few-shot demonstrations, lightweight instruction search as a first step, tasks where examples in the prompt confuse the model, or when you want fast instruction optimization without the cost of COPRO. Related - ai-improving-accuracy, dspy-copro, dspy-miprov2. Also used for dspy.GEPA, instruction optimization without demos, lightweight prompt optimization, optimize instructions only, no few-shot examples needed, GEPA vs COPRO, quick instruction search, when demonstrations hurt performance, zero-shot optimization, instruction-only optimizer, simplest instruction tuner, fast prompt optimization, skip few-shot and just tune instructions, optimize Pydantic field descriptions, GEPA structured output, GEPA does not optimize field desc.