Building scalable cloud infrastructure and AI quality systems. 5 years working across GCP, Azure, and hybrid environments with Terraform, Docker, and Python.
I’m a software engineer at Lumin Innovations with a background spanning cloud infrastructure, data governance, and AI quality systems. My work sits at the intersection of infrastructure automation and operational reliability — I design systems that are version-controlled, observable, and reproducible by default.
At Lumin I built Python and Bash ingestion pipelines containerized in Docker and provisioned GCP environments with Terraform. Before that, I designed layered GCP ingestion architectures at Florida Atlantic University and containerized QA frameworks at Wipro in India.
My philosophy: every infrastructure decision should be code, every data flow should be traceable, and every AI quality issue should have a root cause, not a workaround. I'm drawn to roles where systems reliability and data integrity are first-class concerns.
Python + Bash ingestion application packaged in Docker, GCP storage and compute provisioned with Terraform. Every Linux environment runs the same version-controlled build — no drift between dev and production.
The same ingestion code behaved differently depending on which Linux box it ran on. Environments were built by hand, so dev and production drifted apart and failures surfaced in one place but not the other — the slowest kind of bug to chase, because reproducing it is the hard part.
$ terraform apply -var-file=prod.tfvars
Terraform will perform the following actions:
+ google_storage_bucket.ingest
+ google_compute_instance.pipeline_vm
+ google_service_account.ingest_sa
google_storage_bucket.ingest ......... created
google_compute_instance.pipeline_vm .. created
google_service_account.ingest_sa ..... created
Apply complete. Infrastructure matches the committed
configuration — the same plan runs in every environment.
Every Linux environment now runs the same version-controlled build. Rebuilding an environment is a Terraform apply rather than a day of manual setup, and “works on my machine” stopped being a valid explanation for a pipeline failure.
Python + Bash tools analyzing 5,000+ AI-generated responses, flagging quality gaps at 98% accuracy. Root-cause separation between content gaps and prompt-handling failures cut engineering debugging time by 30%.
Thousands of AI chat responses needed review, and a wrong answer had two possible causes: the knowledge base genuinely lacked the fact, or the fact was there and the model failed to use it. Nobody knew which without investigating, so every failure was checked twice — once by content owners, once by engineers — and one of those investigations was always wasted.
def classify_failure(response, kb_hit, prompt_trace):
# One failure, one owner. The split below is the whole point:
# before it, engineers checked both causes on every bad answer.
if not kb_hit.found:
# the knowledge base genuinely lacks the fact
return Failure("content_gap", owner="kb_owner", area=kb_hit.area)
if not prompt_trace.cited(kb_hit.entry_id):
# it was retrievable and still missed — model side, not content
return Failure("prompt_handling", owner="engineering",
trace=prompt_trace.id)
return None # grounded and cited — not a failure
Governance framework with per-area ownership and automated Bash audits routing 200+ stale knowledge-base entries monthly to owners — cutting content staleness by 45% and replacing a fully manual process.
No one owned any particular area of the knowledge base. Reviews happened when someone noticed a problem, which meant content decayed quietly and the people best placed to fix an entry were never told it had gone stale. Auditing it by hand did not scale, and it was the first thing dropped under deadline pressure.
#!/usr/bin/env bash
# Route stale knowledge-base entries to the owner of their area.
set -euo pipefail
STALE_AFTER_DAYS=180
OWNERS="config/area-owners.tsv"
while IFS=$'\t' read -r entry area last_review; do
age=$(( ( $(date +%s) - $(date -d "$last_review" +%s) ) / 86400 ))
[[ "$age" -lt "$STALE_AFTER_DAYS" ]] && continue
owner=$(awk -F'\t' -v a="$area" '$1==a{print $2}' "$OWNERS")
# fail loud: an unmapped area must never be silently skipped
: "${owner:?no owner mapped for area $area}"
jira_create_review "$entry" "$owner" "$age"
done < <(kb_export --fields id,area,last_review)
Microsoft Copilot Studio agent grounded in the Town’s records retention schedule. Replaced manual lookups with a conversational interface — each answer cites the schedule entry so staff can verify before acting.
Town staff had to find the right row in a long records retention schedule before they could keep or destroy anything. The lookup was manual, easy to get wrong, and carried real consequences — destroying a record early is not a recoverable mistake.
Staff How long do we keep building inspection reports?
Advisor Retain for the period set by the Town’s records retention
schedule, then dispose using that entry’s disposition
instruction.
Source Records Retention Schedule
— Building & Inspections
Open the cited entry and confirm before disposing of
any record.
Retention questions are answered conversationally in Microsoft 365 instead of by manually searching the schedule — and because the citation travels with the answer, a person can still verify before acting. The design treats the agent as a faster route to the source, not a replacement for it.
Python pipeline cleaning 1,300+ Arabica cupping reviews from the Coffee Quality Institute, applying quality rules, loading to Azure SQL. Every rejected record is logged with its failing rule — no silent drops. Data lineage published to GitHub.
The Coffee Quality Institute dataset is public and messy — missing scores, unparseable harvest years, absent countries of origin. The normal way to handle that is to drop bad rows during the load, which leaves a clean table and no idea what is missing from it or why. An analyst downstream cannot tell the difference between “no data” and “data we quietly threw away”.
REJECT_RULES = {
"missing_cup_points": lambda r: pd.isna(r.total_cup_points),
"score_out_of_range": lambda r: not 0 <= r.total_cup_points <= 100,
"missing_country": lambda r: pd.isna(r.country_of_origin),
"bad_harvest_year": lambda r: parse_year(r.harvest_year) is None,
}
def validate(df):
clean, rejected = [], []
for row in df.itertuples():
failed = [name for name, rule in REJECT_RULES.items() if rule(row)]
if failed:
# keep the row AND the reason — a drop you cannot query
# is indistinguishable from data that never existed
rejected.append({**row._asdict(), "failed_rules": ";".join(failed)})
else:
clean.append(row)
return pd.DataFrame(clean), pd.DataFrame(rejected)
Every loaded score traces back to its source row, and every excluded row names the rule that excluded it. The lineage and data dictionary are published, so the model is reusable rather than one-off.
C# ASP.NET Core MVC web app for logging, assigning, and closing permit requests. Normalized SQL Server schema with T-SQL migration for legacy CSV data, before/after execution plans for index design, deployed to Azure App Service.
Permit requests lived in flat CSV exports. Status was free text, so the same state was spelled three ways; assignment was a name typed into a cell; and there was no referential integrity, no audit trail, and no safe way for two people to work at once.
-- Legacy CSV -> normalized schema
INSERT INTO PermitRequest (RequestRef, PermitTypeId, Status, SubmittedUtc, AssignedToId)
SELECT s.request_ref,
pt.PermitTypeId, -- lookup, not free text
COALESCE(NULLIF(LTRIM(RTRIM(s.status)), ''), 'Open'),
TRY_CONVERT(datetime2, s.submitted_at, 126),
u.UserId
FROM staging.LegacyPermitCsv AS s
JOIN PermitType AS pt ON pt.Code = s.permit_type_code
LEFT JOIN AppUser AS u ON u.Email = s.assigned_email
-- refuse unparseable dates rather than silently inserting NULL
WHERE TRY_CONVERT(datetime2, s.submitted_at, 126) IS NOT NULL;
-- The queue screen filters on status + date, then joins the assignee.
-- The plan showed that join driving the cost, so cover all three.
CREATE NONCLUSTERED INDEX IX_PermitRequest_Status_Submitted
ON PermitRequest (Status, SubmittedUtc DESC)
INCLUDE (RequestRef, AssignedToId);
Staff log, assign and close requests against a schema that enforces its own integrity, with legacy records migrated rather than abandoned. Index choices are backed by execution plans captured before and after, so the tuning is evidence rather than intuition.
Aspect-based sentiment analysis over the Yelp Open Dataset, scoring one business against five nearby competitors across six service aspects. Streamlit dashboard with radar comparison, gap analysis and a filterable review browser.
A small business collects hundreds of reviews and reads almost none of them. The star rating tells an owner they are a 4.1; it does not tell them what to fix first, or where a competitor two streets away is beating them. Aggregate sentiment has the same flaw — one number cannot distinguish slow service from mediocre food.
# Yelp JSON -> load_yelp.py -> SQLite -> analyze.py -> app.py
ASPECTS = {
"service": {"staff", "barista", "friendly", "rude"},
"wait_time": {"wait", "slow", "queue", "line"},
# ... coffee, food, price, ambience
}
def score_review(text):
# sentence level, not review level: one review can praise the
# coffee and damn the wait, and both facts need to survive
scores = defaultdict(list)
for sent in sent_tokenize(text):
hits = [a for a, words in ASPECTS.items()
if words & set(sent.lower().split())]
if not hits:
continue # unattributable sentiment is dropped, not spread
compound = vader.polarity_scores(sent)["compound"]
for aspect in hits:
scores[aspect].append(compound)
return {a: mean(v) for a, v in scores.items()}
The dashboard turns a wall of reviews into one answer: the target business trails its competitors most on food (−0.15), then service (−0.09) and wait time (−0.07) — a ranked list of what to fix rather than a rating to feel bad about. The known limits are stated in the repository rather than hidden: keyword matching misses phrasing an LLM would catch, and the public dataset covers 11 metro areas, so the live demo runs on Philadelphia.
7,000+ telecom customer records cleaned and segmented to answer the three questions a retention team actually asks: how much recurring revenue is at risk, which segments leave, and which interventions would pay for themselves.
A churn rate is a number, not a decision. Knowing that 26% of customers leave tells a retention team nothing about who to call first, and acquiring a replacement customer costs far more than keeping the one you have. The question worth answering is not how much churn there is but which segments it concentrates in.
Churn concentrates sharply in month-to-month contracts and short-tenure customers, which turns a vague retention budget into a specific target list. The repository carries the cleaning steps, the SQL and Python analysis, and the dashboard specification, so the method is reusable rather than a one-off notebook.
Multi-year retail transaction data cleaned, modelled and analysed into KPIs, profitability drivers and regional performance — the full path from raw transactional rows to a dashboard a manager would actually open.
Retail organisations generate transactional data continuously and convert very little of it into decisions. The data arrives at the grain of a line item; the questions arrive at the grain of a quarter, a region or a product line. Closing that gap is modelling work, not reporting work.
Month-over-month revenue movement, regional performance and the top product and customer cohorts are visible in one view, with the modelling and the queries published so the numbers can be checked rather than taken on trust.