I recently shipped a lightweight BI analyst for an operations team that lives in spreadsheets. Instead of standing up Looker or Tableau, I gave them a Python script that shoves a CSV export into an LLM and gets back a reasoned report with anomaly detection. In this tutorial we will build that exact tool against Oxlo.ai using the standard OpenAI SDK. No new infrastructure, no token-counting surprises.
What you'll need
Before we start, make sure you have the following ready:
- Python 3.10 or newer
- An Oxlo.ai API key from https://portal.oxlo.ai
- The OpenAI SDK installed:
pip install openai
Step 1: Configure the Oxlo.ai client
I always verify the client before writing business logic. Create a file named bi_agent.py and add the following. Oxlo.ai is a drop-in replacement for the OpenAI client, so the only change is the base_url.
from openai import OpenAI
client = OpenAI(
base_url="https://api.oxlo.ai/v1",
api_key="YOUR_OXLO_API_KEY", # From https://portal.oxlo.ai
)
# Smoke test
ping = client.chat.completions.create(
model="llama-3.3-70b",
messages=[{"role": "user", "content": "Confirm connection"}],
)
print(ping.choices[0].message.content)
Step 2: Define the system prompt
The system prompt is the analyst's job description. I keep it rigid: cite exact numbers, show your work, and call out anomalies explicitly.
SYSTEM_PROMPT = """You are a senior business intelligence analyst examining raw CSV exports.
Rules:
- Base every claim on calculations from the provided data.
- Show your work when comparing aggregates.
- Flag anomalies by naming the specific row and explaining the deviation.
- If the data cannot answer the question, state that clearly.
- Format currency with two decimal places and use commas for thousands.
"""
Step 3: Prepare sample data
To keep the tutorial self-contained, I embed a small sales CSV directly in the script. In production I swap this for pd.read_csv, but a string means you can run the file immediately with no dependencies beyond the OpenAI SDK.
RAW_CSV = """date,region,product,units,revenue
2024-01-15,North,Widget-A,120,3600.00
2024-01-16,North,Widget-A,125,3750.00
2024-01-17,North,Widget-A,30,900.00
2024-01-15,South,Widget-B,80,2400.00
2024-01-16,South,Widget-B,82,2460.00
2024-01-17,South,Widget-B,85,2550.00
2024-01-15,East,Widget-A,200,6000.00
2024-01-16,East,Widget-A,210,6300.00
2024-01-17,East,Widget-A,205,6150.00
"""
Step 4: Build the analysis function
The core function prepends the CSV to the user question and sends the bundle to Oxlo.ai. I use llama-3.3-70b because it handles tabular reasoning well. Because Oxlo.ai charges a flat rate per request rather than per token, I do not have to worry about the cost spiking when I paste in a longer CSV. See https://oxlo.ai/pricing for current plan details.
def run_analysis(csv_text: str, question: str) -> str:
user_message = f"DATA:\n{csv_text}\n\nQUESTION:\n{question}"
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
temperature=0.2,
)
return response.choices[0].message.content
Step 5: Add structured JSON output
Plain text is great for Slack, but dashboards need JSON. I add a second function that forces JSON mode via the OpenAI-compatible response_format parameter. Oxlo.ai supports this on all modern chat models, so the output is machine-readable without regex parsing.
import json
def run_structured_analysis(csv_text: str, question: str) -> dict:
user_message = (
f"DATA:\n{csv_text}\n\nQUESTION:\n{question}\n\n"
"Respond with valid JSON only. Include keys: summary, top_region, "
"anomaly_detected, anomaly_details, recommendation."
)
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
temperature=0.2,
response_format={"type": "json_object"},
)
return json.loads(response.choices[0].message.content)
Run it
Here is the complete bi_agent.py assembled, plus the terminal output when I run it against the sample data.
from openai import OpenAI
import json
client = OpenAI(
base_url="https://api.oxlo.ai/v1",
api_key="YOUR_OXLO_API_KEY",
)
SYSTEM_PROMPT = """You are a senior business intelligence analyst examining raw CSV exports.
Rules:
- Base every claim on calculations from the provided data.
- Show your work when comparing aggregates.
- Flag anomalies by naming the specific row and explaining the deviation.
- If the data cannot answer the question, state that clearly.
- Format currency with two decimal places and use commas for thousands.
"""
RAW_CSV = """date,region,product,units,revenue
2024-01-15,North,Widget-A,120,3600.00
2024-01-16,North,Widget-A,125,3750.00
2024-01-17,North,Widget-A,30,900.00
2024-01-15,South,Widget-B,80,2400.00
2024-01-16,South,Widget-B,82,2460.00
2024-01-17,South,Widget-B,85,2550.00
2024-01-15,East,Widget-A,200,6000.00
2024-01-16,East,Widget-A,210,6300.00
2024-01-17,East,Widget-A,205,6150.00
"""
def run_analysis(csv_text: str, question: str) -> str:
user_message = f"DATA:\n{csv_text}\n\nQUESTION:\n{question}"
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
temperature=0.2,
)
return response.choices[0].message.content
def run_structured_analysis(csv_text: str, question: str) -> dict:
user_message = (
f"DATA:\n{csv_text}\n\nQUESTION:\n{question}\n\n"
"Respond with valid JSON only. Include keys: summary, top_region, "
"anomaly_detected, anomaly_details, recommendation."
)
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
temperature=0.2,
response_format={"type": "json_object"},
)
return json.loads(response.choices[0].message.content)
if __name__ == "__main__":
print("=== Freeform Report ===")
q1 = "Which region had the highest total revenue, and did any day look like an anomaly? Show your work."
print(run_analysis(RAW_CSV, q1))
print("\n=== Structured Report ===")
q2 = "Analyze the sales data and highlight any outliers."
structured = run_structured_analysis(RAW_CSV, q2)
print(json.dumps(structured, indent=2))
Running python bi_agent.py produces output similar to this:
=== Freeform Report ===
East region generated the highest total revenue at $18,450.00 across the three-day period.
North region on 2024-01-17 is an anomaly. Units dropped to 30, which is 76.0% below the prior two-day average of 122.5 units. Revenue fell to $900.00, pulling the North total down to $8,250.00.
=== Structured Report ===
{
"summary": "East leads revenue with $18,450.00, followed by North at $8,250.00 and South at $7,410.00.",
"top_region": "East",
"anomaly_detected": true,
"anomaly_details": "North on 2024-01-17: 30 units vs. prior avg 122.5, revenue $900.00 vs. prior avg $3,675.00.",
"recommendation": "Investigate North inventory or reporting delay for January 17."
}
Wrap-up
That is the entire agent. Two directions I would take next. First, replace the inline CSV with a SQL tool using Oxlo.ai function calling so the agent queries a live warehouse instead of static exports. Second, wrap the structured JSON output in a scheduled GitHub Action or cron job that emails a morning report to stakeholders without anyone touching Python.
Top comments (0)