Skip to content

NLP Enrichment with LLMs

This recipe demonstrates how to enrich posts in your database using Large Language Models (LLMs). SMDT supports various providers including OpenAI, Anthropic, Gemini, Ollama, and Hugging Face via the text_generation enricher.

Overview

The TextGenerationEnricher allows you to run any LLM prompt against the body of posts in your database and save the structured response into the post_enrichments table.

Common use cases include:

  • Sentiment Analysis
  • Topic Classification
  • Toxicity Detection
  • Summarization
  • Entity Extraction

Prerequisites

Ensure you have the necessary API keys or a local LLM server running (e.g., Ollama). If you plan to use the built-in privacy layer with pii_policy (rather than just privacy_fields on their own), install the pii extra: pip install 'smdt[pii]'.

Configuration Reference

TextGenerationConfig is highly configurable, but the common case (point at one of the built-in providers) only needs a model and an api_key, via a provider factory:

FactoryProviderPre-filled base_url
TextGenerationConfig.for_openai(model, api_key, **kwargs)OpenAIhttps://api.openai.com/v1
TextGenerationConfig.for_anthropic(model, api_key, **kwargs)Anthropichttps://api.anthropic.com/v1/messages
TextGenerationConfig.for_gemini(model, api_key, **kwargs)Geminihttps://generativelanguage.googleapis.com/v1beta/openai/
TextGenerationConfig.for_ollama(model, base_url="http://localhost:11434/v1", **kwargs)Ollama (local)as given, defaults to local

Every other field is passed through **kwargs and is identical to constructing TextGenerationConfig(...) directly. There's no factory for Hugging Face Inference Endpoints (provider_kind="hf-text") since each one has its own endpoint URL rather than one shared host — construct TextGenerationConfig(...) directly for that case (see the example below).

Prompting

ParameterDefaultDescription
user_templateSummary promptThe prompt sent to the model. Use {body} as a placeholder for the post content.
system_prompt"You are a helpful assistant."The system instruction that sets the behavior context for the model.
temperature0.2Controls randomness (0.0=deterministic, 1.0=creative). Lower values are better for classification.
max_tokensNoneMax number of tokens to generate.
prompt_pathNonePath to a YAML/JSON file containing prompts (advanced usage).

Execution & Performance

ParameterDefaultDescription
batch_size32Number of posts to fetch from the database at once. Lower this for large local models.
requests_per_minute120Client-side rate limiting to avoid hitting API caps.
only_missingTrueIf True, skips posts that already have an enrichment entry for this model_id. Set to False to re-process everything.
reset_cacheFalseIf True, clears internal caches before starting.

Output

ParameterDefaultDescription
do_save_to_dbTrueWhether to save results to the post_enrichments table.
output_dirNoneIf do_save_to_db=False, results are written to JSONL files in this directory.

Privacy layer and preprocessing

ParameterDefaultDescription
privacy_fields[]Row fields to redact/hash before they're sent anywhere, e.g. ["body"]. Empty means the privacy layer is off.
pii_policyNoneOptional PiiPolicy for Presidio-based PII detection (phone numbers, emails, names, ...) on privacy_fields. Without one, privacy_fields still get baseline mention/email/URL redaction.
pepperNoneSecret pepper for the hasher. Required once privacy_fields is non-empty.
preprocessors[]Your own list of row-transform functions, applied after the privacy layer and before the LLM sees the row (e.g. to clean up artifacts the privacy layer left behind).

See Privacy Layer below for a full example.

Provider Examples

1. OpenAI

python
import os
from smdt.store.standard_db import StandardDB
from smdt.enrichers.runner import run_enricher
from smdt.enrichers.text_generation import TextGenerationConfig

db = StandardDB(db_name="my_local_db")

config = TextGenerationConfig.for_openai(
    model="gpt-4o-mini",
    api_key=os.environ["OPENAI_API_KEY"],
    model_id_postfix="v1_sentiment",  # Saved as: text_generation_v1_sentiment
    system_prompt="You are an expert sentiment analyst.",
    user_template=(
        "Classify the sentiment of the following social media post as "
        "POSITIVE, NEGATIVE, or NEUTRAL. Return only the class label.\n\n"
        "Post: {body}"
    ),
    temperature=0.0,
    batch_size=20,
    requests_per_minute=60,
    only_missing=True,
)

print("Starting Text Generation Enricher (OpenAI)...")
run_enricher("text_generation", db=db, config=config)
print("Enrichment complete.")

2. Anthropic (Claude)

python
import os
from smdt.store.standard_db import StandardDB
from smdt.enrichers.runner import run_enricher
from smdt.enrichers.text_generation import TextGenerationConfig

db = StandardDB(db_name="my_local_db")

config = TextGenerationConfig.for_anthropic(
    model="claude-3-5-sonnet-20241022",
    api_key=os.environ["ANTHROPIC_API_KEY"],
    model_id_postfix="v1_sentiment_claude",
    system_prompt="You are a helpful assistant.",
    user_template="Analyze the sentiment of this post: {body}",
    only_missing=True,
    batch_size=10,
    reset_cache=True,
    max_tokens=1000,
)

print("Starting Text Generation Enricher (Anthropic)...")
run_enricher("text_generation", db=db, config=config)
print("Enrichment complete.")

3. Gemini

python
import os
from smdt.store.standard_db import StandardDB
from smdt.enrichers.runner import run_enricher
from smdt.enrichers.text_generation import TextGenerationConfig

db = StandardDB(db_name="my_local_db")

config = TextGenerationConfig.for_gemini(
    model="gemini-1.5-pro",
    api_key=os.environ["GEMINI_API_KEY"],
    model_id_postfix="v1_sentiment_gemini",
    system_prompt="You are a helpful assistant.",
    user_template="Analyze the sentiment of this post: {body}",
    only_missing=True,
    batch_size=10,
)

print("Starting Text Generation Enricher (Gemini)...")
run_enricher("text_generation", db=db, config=config)
print("Enrichment complete.")

4. Local LLM with Ollama

You can use a locally running Ollama instance to save costs and keep data private (no api_key needed).

python
from smdt.store.standard_db import StandardDB
from smdt.enrichers.runner import run_enricher
from smdt.enrichers.text_generation import TextGenerationConfig

db = StandardDB(db_name="my_local_db")

config = TextGenerationConfig.for_ollama(
    model="llama3",
    model_id_postfix="llama3_topic",
    system_prompt="You are a helpful classifier.",
    user_template="Identify the main topic of this text: {body}",
    batch_size=5,  # Lower batch size for local inference
    only_missing=True,
)

print("Starting Local Enrichment (Ollama)...")
run_enricher("text_generation", db=db, config=config)
print("Done.")

5. Hugging Face Inference Endpoints

Each Hugging Face Inference Endpoint has its own URL, so there's no for_hf_text factory — construct TextGenerationConfig directly with provider_kind="hf-text".

python
import os
from smdt.store.standard_db import StandardDB
from smdt.enrichers.runner import run_enricher
from smdt.enrichers.text_generation import TextGenerationConfig

db = StandardDB(db_name="my_local_db")

config = TextGenerationConfig(
    provider_kind="hf-text",
    chat_model_id="meta-llama/Llama-2-7b-chat-hf",
    base_url="https://api-inference.huggingface.co/models/meta-llama/Llama-2-7b-chat-hf",
    api_key=os.environ["HF_API_KEY"],
    model_id_postfix="hf_classification",
    system_prompt="You are a helpful classifier.",
    user_template="Identify the main topic of this text: {body}",
    only_missing=True,
)

print("Starting Text Generation Enricher (Hugging Face)...")
run_enricher("text_generation", db=db, config=config)
print("Enrichment complete.")

Privacy layer (optional)

Server-backed enrichers like text_generation send body to a third-party API by default. privacy_fields/pii_policy enable a built-in redaction/hashing layer (reusing smdt.pseudonymizer) that runs before anything leaves the machine:

python
import os
from smdt.store.standard_db import StandardDB
from smdt.enrichers.runner import run_enricher
from smdt.enrichers.text_generation import TextGenerationConfig
from smdt.pseudonymizer.pii_policy import DEFAULT_PII_POLICY

db = StandardDB(db_name="my_local_db")

config = TextGenerationConfig.for_anthropic(
    model="claude-3-5-sonnet-20241022",
    api_key=os.environ["ANTHROPIC_API_KEY"],
    model_id_postfix="v1_sentiment_claude",
    system_prompt="You are a helpful assistant.",
    user_template="Analyze the sentiment of this post: {body}",
    only_missing=True,
    batch_size=10,
    # Privacy layer: mentions, emails, phone numbers, and names in
    # "body" are redacted or hashed before the row leaves the machine.
    privacy_fields=["body"],
    pii_policy=DEFAULT_PII_POLICY,
    pepper=os.environ["PSEUDONYMIZATION_PEPPER"].encode(),
)

run_enricher("text_generation", db=db, config=config)

If base_url points at a known commercial API host (OpenAI, Anthropic, Gemini, or the Hugging Face Inference API) and privacy_fields is left empty, SMDT logs a warning so this isn't a silent default. Without a pii_policy, privacy_fields still get baseline mention/email/URL redaction via the dependency-free Redactor; pii_policy (e.g. DEFAULT_PII_POLICY) upgrades this to Presidio-based detection covering phone numbers, credit cards, and person names.

Need to clean something up afterward (e.g. leftover formatting artifacts the redaction pass introduces)? Add your own preprocessors=[...] — it runs after the privacy layer and before the LLM sees the row.

Viewing Results

The method for retrieving results depends on your do_save_to_db setting.

Option A: Database Storage (do_save_to_db=True)

This is the default. Results are stored in the post_enrichments table, with the LLM's response under the text key. You can query them using SQL:

sql
SELECT 
    p.body AS original_text, 
    pe.body ->> 'text' AS llm_response 
FROM posts p
JOIN post_enrichments pe ON p.post_id = pe.post_id
WHERE pe.model_id = 'text_generation_v1_sentiment'
LIMIT 10;

Or using Python:

python
from smdt.store.standard_db import StandardDB

db = StandardDB("my_local_db", initialize=False)
conn = db.connect()
try:
    with conn.cursor() as cur:
        cur.execute(
            """
            SELECT body ->> 'text' 
            FROM post_enrichments 
            WHERE model_id = %s 
            LIMIT 5
            """,
            ("text_generation_v1_sentiment",)
        )
        for row in cur.fetchall():
            print(f"Result: {row[0]}")
finally:
    conn.close()

Option B: Local File Storage (do_save_to_db=False)

If you set do_save_to_db=False, the enricher writes results to JSONL files in the directory specified by output_dir.

The files are named following the pattern {model_id}_{date}.jsonl.

python
import json
import glob
import os

# The directory you specified in your config
output_dir_path = "./my_llm_outputs" 

# Find all JSONL files
files = glob.glob(os.path.join(output_dir_path, "*.jsonl"))

for filepath in files:
    print(f"Reading {filepath}...")
    with open(filepath, "r", encoding="utf-8") as f:
        for line in f:
            record = json.loads(line)
            post_id = record["post_id"]

            # The actual LLM output is inside the 'body' dictionary
            llm_text = record["body"]["text"] 
            
            print(f"[{post_id}] {llm_text}")