I design and build agentic AI systems that automate real business workflows, from multilingual customer support to automated lead research and qualification. Production-focused, with an emphasis on reliability, evaluation and integration, on top of 25+ years of software engineering.
4 AI projects · Multi-agent orchestration · RAG · MCP · Evaluation · Google Cloud
I came to AI as an experienced software engineer, not as a prompt writer. For 25+ years I have built and run production systems, and I apply the same discipline to LLM-based ones: queues that do not lose messages, idempotent writes, audit trails, and logs you can search when something goes wrong.
I built a multilingual RAG support assistant with AI ticket triage that answers customers in 30 languages and triages the support inbox, a multi-model localization pipeline, an open-source MCP server, and an agentic CRM on Google Cloud that turns inbound email into a researched and qualified lead.
Pipelines of specialised LLM agents, each its own service with its own permissions, coordinated by a workflow engine that owns the control flow: parallel fan-out, fan-in, retries with backoff, and a partial result instead of a lost one when an agent fails.
Multilingual retrieval-augmented assistants in production: embeddings, vector search, re-ranking, query enrichment from conversation context, and incremental indexing that re-embeds only what changed.
Labelled eval sets with asymmetric metrics, structured output validated against a schema, evidence quotes checked against the source, confidence thresholds that route uncertain cases to a person.
Webhooks, queues and CRM-style data flows around the models: idempotent ingestion, a single trusted writer into BigQuery with an audit log, and MCP servers that expose tools to assistants such as Claude and Cursor.
Inbound B2B email goes in; a researched, scored and qualified lead record comes out. Three specialised LLM agents run as separate Cloud Run services on Google Cloud, and a single trusted service owns every write to the BigQuery Golden Record. The agents only propose. Code decides.
An orchestrated pipeline of specialised agents, not an autonomous one: Cloud Workflows, not the model, decides the order of steps, the retries and what happens when an agent fails. Each agent has a fixed role, a fixed output schema and no tool that can change anything outside its own answer.
Intent, Research and Qualification have different inputs, failure modes and trust levels. Research reads a web page the sender controls; Intent reads only the email; Qualification consumes both. As separate services with their own service accounts, the agent that fetches sender-controlled pages holds no write access to the CRM.
They do not depend on each other, so a run takes as long as the slower of the two rather than their sum, and a slow or failed site fetch does not block intent classification. Qualification waits for both because it needs both results.
The order of steps is known in advance, so it is declared, not left to a model. Retries, fan-out, fan-in and partial failure are explicit in Cloud Workflows and can be read and audited, instead of being emergent behaviour of a prompt.
Spam is forced to a low score with no reply. A low-confidence lead cannot be high priority. Counts and dates are recomputed from all interactions. Quotes are checked against the source. Writes go through one validating service. None of these is left to a model’s judgement.
Its result is marked failed and the others continue. Qualification is told to rely on the email alone and lower its confidence. A nightly workflow republishes the last seven days of emails where an agent failed, and the idempotent writer fills in only what is missing.
What it automates: Sender research, intent classification, lead scoring, next-step choice and a reply draft for every inbound email.
The MX record points to Resend. A webhook carries only the email ID and metadata, so the body is fetched separately.
The only public endpoint. It verifies the webhook signature, claims the email ID in Firestore as received, publishes the ID to Pub/Sub, marks it published and answers 200. Only a published ID counts as a duplicate, so a delivery whose publish failed is retried on the next webhook; if the hand-over fails it answers 503 and Resend retries.
A push subscription delivers each message with an OIDC identity token. Failed deliveries retry with backoff from 10 to 600 seconds; after 5 attempts a message moves to a dead-letter topic, so nothing is dropped silently.
Private service. It fetches the full message from the Resend API and writes it to BigQuery with a MERGE that inserts only if the email ID is new, then publishes an email-stored event. Any failure returns 500 so Pub/Sub redelivers; every step is safe to repeat.
One Cloud Workflows execution per event. The worker knows nothing about orchestration, and more subscribers can be added without touching it.
Fan-out in Cloud Workflows. Calls are authenticated HTTP; transient failures (429, 502, 503, 504, timeouts) retry up to three times with exponential backoff. If an agent still fails, its result is marked failed and the others continue.
Fan-in. The agent receives the email and both results. A failed or missing result is left out and the model is told to rely on the email alone and lower its confidence.
The only service with write access to the CRM tables. It validates every agent result against its schema, then one BigQuery transaction upserts the interaction, rebuilds the contact and company rows and appends to an audit log with the state before and after.
Gemini 2.5 Flash returns a verdict that must match a fixed Pydantic schema: intent (demo request, pricing, partnership, support, recruiting, vendor solicitation, spam, other), urgency, summary, requested action, language, confidence and verbatim supporting quotes. Quotes that do not appear in the email are dropped.
Works from the sender’s domain. Public mailbox providers get no company at all. For a business domain it looks up MX, SPF and DMARC, fetches the home page and asks Gemini for a profile based only on that page text. If the site cannot be read, the result is unknown and no profile is invented.
Score 0 to 100, priority, rationale, one next step and a reply draft in the sender’s language. Rules in code have the last word: spam is forced to a low score with no reply, a low-confidence lead cannot be high priority, and the service has no tool for sending mail, so a draft stays a draft.
Agents can read the email table but cannot write to it. Only golden-record-writer changes CRM tables. Each service runs under its own service account with only the roles it needs, and private services accept calls only from the identity meant to call them. A manipulated model has nothing dangerous to do.
Every agent answers in a fixed schema, and the writer validates it again before storing anything. A schema guarantees the shape of an answer, not that it is right, so business rules in code apply on top.
Supporting quotes are checked to exist verbatim in the email or page and dropped otherwise. That proves where a quote came from, not that the conclusion follows from it, so quotes help a person review a decision rather than replace the review.
Email text, website text and earlier agent results are passed to the model as delimited data with an instruction not to follow them. This is a secondary measure; the real protection is the permissions above. The eval set includes prompt-injection attempts.
The sender controls the domain, so the page fetch connects only to public IP addresses, checks every redirect hop, pins the connection to the checked address, accepts only http(s) on default ports and limits time and size.
Every queue may deliver twice. Duplicates are caught by the Firestore claim in ingest, by the insert-if-new MERGE in the worker and by keyed upserts in the writer.
Counts and dates are rebuilt from all interactions, so a repeated run cannot inflate them. A company profile is replaced only by one with equal or higher confidence, and a failed agent never erases what an earlier run stored.
Cloud Scheduler runs a reprocess workflow nightly. It finds emails from the last seven days where an agent failed and republishes them; the idempotent writer fills in only what is missing.
Audit rows hold the fields that changed, before and after, and are written only when something changed. Rows get UUIDv7 identifiers, which sort by creation time and keep inserts and clustered scans local.
24 hand-written emails (demo requests, pricing, partnership, support, recruiting, vendor pitches, spam, prompt-injection attempts, short and non-English messages) run through Intent and Qualification: 95.8% intent accuracy and 83.3% next-step accuracy on this set, and no case exceeded the priority cap. Spam, vendor pitches and injection attempts never exceed low priority. The set is small, so one case moves a figure by about 4 points: it checks for the expensive mistakes and is not a statistical estimate.
Every model call logs model name, prompt tokens, output tokens and latency; the CRM shows them per email. Log-based metrics feed a Cloud Monitoring dashboard.
Cloud Monitoring emails on a message reaching the dead-letter queue, a failed workflow execution, a service logging ERROR or answering 5xx. A monthly budget alert watches cost.
FastAPI and Jinja2 views over the Golden Record: inbox, email page with each agent’s conclusion, draft reply, model, tokens and latency, audit entries, and merged contact and company records. Read-only account, Basic Auth, escaped output, UUIDv7 in URLs.
A production retrieval-augmented assistant for an international e-commerce company: a streaming chat widget on the site and a support-desk pipeline that classifies tickets and drafts replies, both on one retrieval function over 1,614 documents in 30 languages.
A fixed pipeline, not an autonomous agent: the code decides the steps, and a confidence score decides what is answered automatically and what a person reviews. The ticket side is a two-stage pipeline (extract the question, then retrieve and classify).
The chat widget and the ticket classifier call the same function, so an improvement to retrieval applies to both and they cannot drift apart.
A question in Dutch can match a document indexed in English or Finnish when the meaning is the same, so one knowledge base serves every language. Site search, by contrast, filters by the interface language.
The product is detected from a multilingual keyword dictionary, and results for that product get a boost while mismatching ones get a penalty, so the answer comes from the right product’s documents.
A low-confidence ticket gets an empty reply and goes to a person with the extracted question and context prepared. The threshold is the line between automatic handling and human review.
A SHA-256 cache decides which documents are new, changed or deleted, so the cost of keeping the knowledge base current tracks the change, not its size.
What it automates: Answering repetitive customer questions in the customer’s language, and extracting the question, classifying and drafting a reply for incoming support tickets.
A TypeScript ingestion script pulls FAQs, product copy, delivery times, shipping costs and Markdown docs from the locale files and config, one document set per locale.
Each document is hashed and compared with the cache. Only added and changed documents are re-embedded and deleted ones are removed, so the knowledge base stays current at the cost of the change.
OpenAI text-embedding-3-large, 3,072 dimensions, batched embedding and batched upserts.
A follow-up such as ‘And to Germany?’ is enriched with the earlier conversation before retrieval, so the customer does not have to repeat the question.
Embed the query, detect the product from a multilingual keyword dictionary, run vector search with no locale filter, then re-rank with a product boost and mismatch penalty. The chatbot and the ticket classifier share this single function.
Vercel AI SDK streams the answer into an assistant-ui widget. The model answers in the customer’s language with the retrieved context injected into the prompt.
Chat retrieval does not filter by locale. A question in Dutch can match a document indexed in English or Finnish when the meaning is the same. Site search, by contrast, filters by the interface language.
Candidates below a similarity threshold are dropped. Results matching the detected product get a boost, mismatching ones a penalty, and the top five go to the model.
The search endpoint adds fuzzy keyword matching to vector similarity, with normalisation for Cyrillic and Nordic characters and a stricter threshold when no keyword matches.
A Zoho Desk webhook first extracts the core question from the email, stripping greetings and order numbers, then runs retrieval on it.
The model assigns one of 18 categories, generates a draft reply and returns a confidence score. Low-confidence tickets get an empty reply and go to a person with the extracted question and context prepared.
The assistant does not guess when unsure. The threshold decides what is handled automatically and what a human reviews.
A Python pipeline that keeps 30 locales in sync from a single English source using three AI stages from two providers, with structural correctness enforced by tests instead of trusted to a model.
A fixed pipeline of stages, not agents: each stage has one job, and the order is set by code. The models handle language quality; deterministic code and tests handle structure.
A second model from a different provider reviews translation quality, which reduces reliance on self-evaluation. It does not guarantee that errors are independent, so the checks that must always hold are kept out of the models entirely.
Placeholder preservation, HTML tag preservation, key parity with the source and SEO length limits are enforced by a pytest suite. No model is trusted to keep them.
A structural violation (a lost placeholder, a broken tag, a missing key, a text over its length limit) fails the build, so it cannot ship. Each run fills only missing keys, so an interrupted run is finished by running it again.
Re-running touches only genuinely missing keys, so cost follows new content and not catalogue size, and existing translations are never overwritten.
What it automates: Finding missing translation keys, translating them to market-specific rules, reviewing the result and checking structure for every locale.
Python walks the English source, finds keys missing from each locale, including inside nested objects and lists, and removes keys that disappeared upstream. Existing translations are untouched.
An OpenAI call receives the text plus the market’s keyword spec and hard formatting rules for that key, such as title length budgets.
A model from a different provider, Anthropic, grades the translation against the source and flags dropped placeholders, calques, forbidden terms and length violations.
A separate AI stage does a naturalness and grammar pass in the target language, catching stilted but technically correct phrasing.
A pytest suite enforces what no model is trusted to hold: placeholder preservation, HTML tag preservation, key parity with the source and SEO length limits. A violation fails the build.
Deterministic code and tests guarantee that the output is well-formed. The models are trusted only for language quality.
The reviewing model comes from a different provider than the translating one, so the two are less likely to agree because they share the same blind spots.
Re-running fills only genuinely missing keys, so cost tracks new content and not catalogue size.
A status panel shows per locale and per stage what is synced, verified and proofread, so a content owner can see what is safe to ship without reading thirty languages.
An open-source Model Context Protocol server that lets Claude Desktop, Cursor and other MCP clients validate EU VAT numbers and look up rates directly in chat. Published to npm and listed in the official MCP Registry.
A tool provider, not an agent: the server contains no LLM. It exposes typed tools, and the client’s model decides when to call them. All five tools are annotated read-only and non-destructive.
The transport every desktop MCP client supports, so there is nothing to host. The server runs locally from a single npx command.
Four tools answer from a bundled VAT-rates dataset with no network call, no API key and no quota, so the server is useful before any account exists. Only live VIES validation calls the vatnode API.
A failed call returns an error result with a message and a hint, for example pointing to the API key when the key is rejected, so the assistant can tell the user what to fix instead of retrying blindly.
An AI client hands the server an API key. Trust comes from being able to read the code, so the package is MIT-licensed and the repository is public.
What it automates: Looking up VAT rates and number formats, and verifying a VAT number against the EU VIES service, from inside an AI assistant.
list_eu_vat_ratesofflineInput: none
Returns: Standard, reduced, super-reduced and parking rates for the EU member states and Northern Ireland.
get_country_vat_ratesofflineInput: countryCode (ISO 3166-1 alpha-2)
Returns: Rates, VAT number format and regex for one country, including some non-EU European jurisdictions.
On error: Unknown country code returns an error result.
check_vat_formatofflineInput: vatId
Returns: Normalised number, country, and whether it matches the country’s format. Does not contact VIES.
list_supported_countriesofflineInput: none
Returns: Every covered country, with a flag for which ones support live VIES validation.
validate_vat_numberlive, API keyInput: vatId
Returns: Live VIES verification through the vatnode API: validity, company name, address and registration date.
On error: Error result with the failure code, a message and a hint; needs VATNODE_API_KEY and uses one request from the monthly quota.
{
"mcpServers": {
"vatnode": {
"command": "npx",
"args": ["-y", "vatnode-mcp"],
"env": { "VATNODE_API_KEY": "optional, only for validate_vat_number" }
}
}
}The format every desktop MCP client supports, so there is no HTTP or SSE server to host. Installed with a single npx command.
Zod schemas describe each tool’s input and output. Four tools answer from a bundled VAT-rates dataset with no network call or API key; live VIES validation goes through the vatnode API.
Every outgoing API request carries a versioned User-Agent so MCP traffic is measurable in backend logs.
Published to npm with OIDC Trusted Publishing and signed provenance, so every release is tied to a commit of the public source and the pipeline holds no tokens.
The free tools work without an account, which lowers the cost of trying the server inside an assistant.
Listed in the official MCP Registry with a server manifest, which is how assistants and catalogues find tool servers.
An LLM returns a suggestion. Plain code validates it against a schema and business rules, and only one trusted service is allowed to write. Replies are drafts, and low-confidence cases go to a person with the context prepared.
Emails, web pages and earlier agent output are data, not instructions. Delimiters help, but the real protection is permissions: a manipulated model has no tool to send mail, no write access and only a fixed output schema.
I build the eval set early and look at the expensive mistakes first, such as spam getting a reply or a support request taken for a sale. I state the size of the set next to the result. Where quality matters, a model from a second provider reviews the first, and tests guard what no model should be trusted to keep.
AvailableHiring for an agentic AI role?
Send me the role and what the team is building – I respond within 24 hours.