Categorizer
Easily categorize string records into predefined categories with a combination of LLM, regex and keywords.
- Define your categories, feed your input — records come back categorized.
- Nested categories are allowed (any depth).
- For efficiency, regex patterns and keywords can pre-handle records where applicable, and the LLM acts as the fallback — you only pay for what rules can't resolve.
Install
pip install -r categorizer/requirements.txt # pydantic, pyyaml, langchain-openai, python-dotenv
echo 'OPENAI_API_KEY=sk-...' >> .env # only needed for the LLM phase
The key is read from a .env file in your project (auto-loaded, no export
needed). An already-exported OPENAI_API_KEY environment variable takes
precedence over .env.
Quick start
from categorizer import Categorizer, Taxonomy, cat, to_dataframe
taxonomy = Taxonomy(categories=[ # Python builder…
cat("Food & Dining",
cat("Coffee", keywords=["KOFEYNYA"]),
cat("Groceries")),
cat("Healthcare",
cat("Medications", keywords=["ECZANE"])),
])
# …or: Taxonomy.from_yaml("categories.yaml") / from_json(...) / from_dict({...})
c = Categorizer(taxonomy)
results = c.categorize([
"coffee - kofeynya 30 dollars", # resolved by keyword, free
"dinner at a small italian place", # resolved by the LLM
])
for r in results:
print(r.path, r.method, r.rationale) # ['Food & Dining', 'Coffee'] keyword …
df = to_dataframe(results) # flat table: lvl1, lvl2, method, ok, …
Async: await c.acategorize(records). Inputs: list of strings, dicts,
Record objects, or a pandas DataFrame.
Runnable demo: python -m categorizer.example
The three phases
- Patterns (regex, instant, free) — per-source rules that assign a full
category path in one hit; a record's
sourcefield selects the rule set. - Keywords (substring, instant, free) — per-category trigger words assign the full path to that category.
- LLM (semantic, costs money) — only the leftovers. Level-by-level walk
with structured output, so an out-of-taxonomy answer is impossible.
Records sharing a
keywordare called once and share the result (method="cache"), and the cache persists across calls.
Each phase can be toggled (use_patterns / use_keywords / use_llm).
Batch mode
batch_size=N (default 1) sends up to N records per LLM call — per tree
level, per group — not the whole run in one call:
- Only records that reach the LLM phase batch at all: patterns, keywords, and the cache resolve their records for free first.
- A group = records at the same position in the tree with the same
source. Each group is chunked into batches of ≤ N; all batches fly concurrently undermax_concurrency. - The walk stays level-by-level: one call judges a batch against the top-level categories, then records regroup by the parent each one chose and batch again for that parent's children.
Example: 10 records on a 2-level taxonomy = 20 calls per-record, but ~7 batched — 1 call at level 1, then one smaller call per chosen parent at level 2.
Reliability: batch items are id-keyed, so a missing, duplicated, or invalid
item in a response automatically re-runs as a single call at that level, and a
failed batch call degrades its whole chunk to singles — nothing is silently
dropped. allow_no_fit verdicts work inside batches.
When to use it: batching is the cost and rate-limit lever — fewer
requests, and the taxonomy tokens amortized across N records. It is not a
latency lever: for small runs, N parallel single calls finish faster than one
call writing N rationales. Reach for it when record counts vastly outnumber
max_concurrency or requests-per-minute limits bind, and keep N modest
(≤ ~10–20): per-record accuracy degrades as batches grow.
Config files
The taxonomy is a typed Pydantic model; file formats are just loaders —
YAML in, JSON in, dict in, Python builder in, same validated Taxonomy out.
Typos, duplicate siblings, and patterns pointing at nonexistent categories
fail fast with clear errors.
categories.yaml — the taxonomy
(full example: categorizer/examples/categories.yaml):
categories:
- name: Food & Dining
rules: # hard constraints shown to the LLM
- "Supermarket purchases (Migros, BIM, ...) belong here, not in Retail Purchases."
children:
- name: Coffee
keywords: [KOFEYNYA] # substring auto-triggers
description: "" # optional context for the LLM
patterns.yaml — per-source regex rules
(full example: categorizer/examples/bank_patterns.yaml):
sources:
QNB Finansbank Enpara:
- pattern: "Gelen Transfer"
path: [Incoming P2P Transfers, Incoming Money]
Editor autocomplete / CI validation:
Taxonomy.write_file_schema("categories.schema.json")
Patterns.write_file_schema("patterns.schema.json")
# YAML header: # yaml-language-server: $schema=categories.schema.json
Steering tip: the LLM decides one level at a time and only sees that level's options. Put
rules/descriptionat the level where the wrong turn happens — a helper on a level-2 child cannot fix a level-1 mistake.
What you get back
Each record yields a Result: path (root→leaf names), method
(pattern | keyword | llm | cache), per-level rationale, ok flag, and
error if unresolved. One bad record never stops the batch.
Records that genuinely don't belong anywhere are signaled, not crammed into
the nearest bin: Result.unplaced carries a grounded verdict —
no_fit(off_axis | uncovered | under_bar) when no category can be truthfully
grounded, or cant_tell when the text is too opaque to identify. The rationale
states why. No-fit is an outcome, never a category.
Knobs
Categorizer(
taxonomy, patterns,
model="gpt-5.4-nano", # any OpenAI chat model
llm=my_langchain_model, # or bring any LangChain chat model
use_patterns=True, use_keywords=True, use_llm=True,
allow_no_fit=True, # False → forced choice: every record gets a category
batch_size=1, # >1 → judge N records per LLM call (cuts requests and
# amortizes the taxonomy tokens; accuracy degrades as
# N grows — keep ≤ ~10-20; misses auto-repair as singles)
cache=True,
max_concurrency=32,
on_progress=lambda p: ..., # Progress(phase, done, total)
# extra kwargs go to ChatOpenAI, e.g. reasoning_effort="low", timeout=30
)
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file categorizer-2.0.0.tar.gz.
File metadata
- Download URL: categorizer-2.0.0.tar.gz
- Upload date:
- Size: 20.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.25
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eea06d563c9628e372f868d3a4447971c6abbaa4933fdf555bcbb0c821888580
|
|
| MD5 |
862aefb1137ab4c129c55f56be16c949
|
|
| BLAKE2b-256 |
4930e62b0e51cd4a9f58486bdf00463e187565233c4e8c82e1cea9b766c14731
|
File details
Details for the file categorizer-2.0.0-py3-none-any.whl.
File metadata
- Download URL: categorizer-2.0.0-py3-none-any.whl
- Upload date:
- Size: 20.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.25
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e15d4b06ca849e74b39349080bf40ea28611ef6f991d1c1043ca6e4d20fda7b
|
|
| MD5 |
2fdfa21928e38534d41f14afc3451302
|
|
| BLAKE2b-256 |
2a7c7b2e6c3a7b95dd07858897753662e27287687f8b8f2ed6da827b6ffff52b
|