Automation · · 5 min read
Polars: turn a CRM export into a traceable AI sales brief
Use typed CSV input, integer cents and account-level aggregation to prepare a small, checkable JSON brief before connecting an AI writer.
By Sociologix Editorial

Calculate the brief before asking AI to explain it
A sales assistant needs a reliable snapshot before it can write a useful account update. Instead of asking a model to interpret every row of a CRM export, calculate the open opportunity counts and values first. Keep the contributing opportunity IDs so a reviewer can trace a sentence back to its inputs.
This AI-assisted editorial guide uses Polars, a Python DataFrame library, to transform four fictional opportunities into two account summaries. The example runs locally and prints JSON. It does not contact an AI provider, connect a CRM, send outreach or predict sales. The proposed AI-writing step comes after the tested data preparation.
1. Pin Polars and agree on one export format
We verified Polars 2.1.0 on October 28, 2026. It uses the MIT license and requires Python 3.11 or later. Our local run used Python 3.13.15. The official Polars website and documentation describe the open-source library; this example does not require its managed cloud service. [1] [2]
The CSV has five columns in a fixed order: opportunity_id, account_id, stage, currency and value_cents. Values are whole USD cents, not formatted dollar strings. Here, qualified and proposal mean open; won and lost are excluded. Those are explicit rules for this fixture, not a universal definition of your CRM’s stages.
Run these PowerShell commands in a new scratch folder. Save the Python block in the next section as sales_brief.py before executing the final line. Installation needs internet access; the supplied fixture needs no network connection.
mkdir account-brief-demo
cd account-brief-demo
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install "polars==2.1.0"
# Save sales_brief.py below first.
.\.venv\Scripts\python.exe sales_brief.py2. Validate, group and retain the source IDs
The function checks the header before reading with a complete schema. Polars requires that a full schema follow the CSV’s column order. Parsing errors are not ignored and extra fields are not deliberately truncated. These settings do not prove that every malformed CSV is safe: the documentation expects conforming CSV input. Use an approved export for this exercise. [3]
Application checks reject missing values, blank identifiers, repeated opportunity IDs, unknown stages, non-USD currency and out-of-range amounts. IDs are compared exactly, including case and surrounding nonblank whitespace; this is not fuzzy entity resolution. The 1 MB text limit is checked before parsing, while the 10,000-row limit is checked after the small file is read.
Integer cents avoid binary floating-point arithmetic for the totals. Together, our row and per-row value limits keep this sum below the signed 64-bit range. Missing amounts are rejected before aggregation because a sum with no non-null values can otherwise return zero. [4]
import csv
import io
import json
import platform
import polars as pl
SCHEMA = {"opportunity_id": pl.String, "account_id": pl.String,
"stage": pl.String, "currency": pl.String, "value_cents": pl.Int64}
def make_brief(csv_text):
if len(csv_text.encode("utf-8")) > 1_000_000:
raise ValueError("Demo accepts at most 1 MB of UTF-8 CSV")
header = next(csv.reader(io.StringIO(csv_text)), [])
if header != list(SCHEMA):
raise ValueError("Unexpected column names or order")
df = pl.read_csv(io.StringIO(csv_text), schema=SCHEMA,
ignore_errors=False, truncate_ragged_lines=False)
if df.height == 0 or df.height > 10_000:
raise ValueError("Expected 1 to 10,000 opportunities")
if df.select(pl.any_horizontal(pl.all().is_null()).any()).item():
raise ValueError("Missing required value")
if df.filter((pl.col("opportunity_id").str.strip_chars() == "") |
(pl.col("account_id").str.strip_chars() == "")).height:
raise ValueError("Blank identifier")
if df["opportunity_id"].n_unique() != df.height:
raise ValueError("Duplicate opportunity ID")
if df.filter(~pl.col("stage").is_in(["qualified", "proposal", "won", "lost"]) |
(pl.col("currency") != "USD") |
~pl.col("value_cents").is_between(0, 1_000_000_000_000)).height:
raise ValueError("Unsupported stage, currency or amount")
summary = (df.filter(pl.col("stage").is_in(["qualified", "proposal"]))
.group_by("account_id")
.agg(pl.len().alias("open_opportunities"),
pl.col("value_cents").sum().alias("open_value_cents"),
pl.col("opportunity_id").sort().alias("source_opportunity_ids"))
.sort("account_id"))
return {"snapshot_id": "synthetic-demo-v1", "currency": "USD",
"accounts": summary.to_dicts()}
SAMPLE = """opportunity_id,account_id,stage,currency,value_cents
D-01,A-01,qualified,USD,5000000
D-02,A-01,proposal,USD,3000000
D-03,A-02,won,USD,9000000
D-04,A-02,qualified,USD,2500000
"""
if __name__ == "__main__":
print("Python", platform.python_version(), "Polars", pl.__version__)
print(json.dumps(make_brief(SAMPLE), indent=2))
3. Inspect the observed output and failure cases
The executed fixture returned A-01 with two open opportunities, 8,000,000 cents and source IDs D-01 and D-02. A-02 had one open opportunity, 2,500,000 cents and source ID D-04. D-03 was excluded because its stage was won. In dollars, those open pipeline values are $80,000 and $25,000; they are not booked revenue or forecasts.
The code sorts account rows and source IDs explicitly, then converts each row to a Python dictionary for JSON serialization. The grouping combines counts, summed values and the contributing ID list. An account with no open opportunities is absent from the output, rather than represented by a zero-valued row. [5]
Fifteen local checks passed. They covered the two account results, exclusion of the won opportunity, preserved source IDs and a JSON round trip. Negative cases covered a duplicate ID, missing amount, nonnumeric amount, mixed currency, unknown stage, reordered header, negative amount, empty input, fractional cents and an oversized text input.
To reproduce a failure, change the first USD to EUR or replace 5000000 with 1.5. The function should stop rather than return a partial brief. In a real import, keep the failed batch available for authorized review. Do not silently drop a row or convert an unknown amount to zero.
4. Give an AI writer a bounded job
A proposed next step is to give an approved AI writer only the checked summary and a narrow instruction: describe each account’s open count and value, cite its opportunity IDs, and do not invent a close date, probability or next action. Label the unit as USD cents in the machine payload and format dollars deliberately in the final report. No model response was generated or evaluated in this test.
Keep the original export, export timestamp and a unique snapshot identifier in your internal record. The demo’s synthetic-demo-v1 is just a fixture label. In production, give each snapshot its own identifier and link it to the exact source file and transformation version. Duplicate checking here covers only one export; it does not reconcile repeated imports or changing deal history.
Even without names or email addresses, account IDs and pipeline values can remain commercially sensitive. Confirm the recipient, provider and access policy before any later AI handoff. Validate the drafted numbers and source references against the JSON, then let a person approve any customer-facing communication.
Start with one team’s approved export and stage definitions. This example uses eager processing for a deliberately small file; it is not a large-file performance benchmark or a production upload endpoint. Currency conversion, account matching, CRM writes and automated outreach require separate implementation and tests.
Sources & further reading
Give your AI sales assistant a reliable data foundation
Sociologix can help connect approved CRM data, cloud automation and AI workers with traceable summaries and review steps. Bring one recurring sales report and the rules your team uses to prepare it.
Talk to Sociologix