Architecture case study · Azure AI + geospatial data
Energy Copilot: from public data to a defensible energy prospect
A map-first product for finding photovoltaic opportunities in Luxembourg, applying public tenders and answering cited questions about tariffs and law. The architecture keeps model reasoning useful while moving consequential calculations, access control and evidence into deterministic services.
Français : étude d’architecture d’un copilote Azure AI pour la prospection photovoltaïque au Luxembourg (données géospatiales publiques, appels d’offres, tarifs déterministes, RAG, droit cité via Lex). Le déploiement public est hors ligne ; cette page documente l’architecture, les décisions, les contrôles, les preuves et les limites.
Repository description: Production Azure AI co-pilot for Luxembourg solar-PV prospecting: geospatial data, tenders, deterministic tariffs, RAG and MCP law.
Topics: agentic-ai, azure-openai, luxembourg, maplibre, mcp, postgis, rag, solar-energy, terraform.
What was built, and what it proves
Business problem
Solar, tender, price and legal data is public but fragmented and difficult to turn into a prospect a developer can act on.
Product answer
One map combines roofs and sites, tender applicability, economics and a cited assistant.
Engineering answer
Scheduled Python producers feed PostGIS/pgvector; a bounded .NET agent composes five tools.
Trust answer
Numbers come from code, legal evidence from Lex MCP, SQL is read-only and unsupported answers are refused.
01 · The model plans; bounded services decide
The online path is deliberately small. The model can select tools and explain outputs, but it cannot write data, calculate a tariff or order the interface to display unsupported results.
Safety boundary: tool output, not model prose, drives map pins and cards. Typed payloads are validated; the AI explains evidence but does not become the evidence.
sqlRead-only analysis and geo candidates; the exact query is provenance.
lawDated Luxembourg and reviewed EU text through the public Lex MCP server.
search_corpusHybrid retrieval over current energy documents in PostgreSQL.
compute_tariffTyped inputs into deterministic code; the model never calculates.
find_offersCurrent tenders and machine-checkable applicability constraints.
Request lifecycle: one question, five visible stages
- Normalize and route. The host supplies schemas and bounded tools.
- Call tools, sometimes in parallel. Geo and dated-law lookups can run together.
- Validate outputs. Database role, bouncer, timeouts, caps and contracts.
- Ground or refuse. No usable evidence produces a refusal.
- Render deterministically. Payload shape produces pins, cards or text.
Azure delivery plane: small because one engineer operates it
Static Web Apps served the client. Container Apps ran the API. PostgreSQL Flexible Server owned relational, geospatial and vector data. Azure OpenAI handled chat and vision; Content Understanding extracted tender constraints. Terraform defines infrastructure; GitHub Actions deploys through OIDC.
A second search store or more agents must earn their operating cost through measured need.
02 · Batch producers create reusable facts
Ingestion does expensive interpretation once. Requests read typed, provenance-carrying rows rather than repeatedly asking a model to rediscover structure.
Official and public sources
- National INSPIRE buildings + solar cadastre
- TED and Luxembourg tenders
- PVGIS, prices, tariffs and aid
- Grid-operator documents + public Lex MCP
Python producers
- Coordinate transforms + geometric joins
- Three-class existing-PV vision
- Content Understanding extraction
- History, provenance and fail-loud status
PostgreSQL consumers
- PostGIS roofs, parcels and materialized sites
- Offers and typed constraints
- FTS + pgvector document chunks
- Read-only API tools and map endpoints
National buildings became the primary source
The first design used the solar cadastre and silently missed large industrial roofs. National building polygons now lead; the cadastre is a nullable suitability signal joined spatially. Unknown remains unknown.
Vision with abstention: yes / no / maybe
Ambiguous roofs stay maybe; an outage leaves the field null. Every downstream surface preserves that distinction.
Documents become filterable predicates
Content Understanding stores verbatim text plus typed constraints. "Apply tender" is deterministic filtering, not query-time invented eligibility.
Two retrieval responsibilities
The current energy corpus uses PostgreSQL FTS + pgvector with rank fusion. Legal search is not duplicated: Lex provides dated Luxembourg and EU law, history, comparison and provenance.
03 · Controls sit where the failure happens
Prompt instructions help, but consequential controls are enforced in code, database permissions, typed contracts or deployment gates.
| Failure | Control | Residual risk |
|---|---|---|
| Fabricated tariff | Deterministic engine + C#/Python parity; model only extracts and narrates. | New formulas require code; uncovered questions are refused. |
| Unsafe database access | analyst_ro role, forbidden-column bouncer, timeout and row cap. | A permitted SELECT may still be slow, wrong or infer data. |
| Confident false absence | Zero rows trigger witness queries and an explicit diagnosis. | Main and witness queries can share a wrong assumption. |
| Ungrounded prose | Session-wide evidence guard; the judge sees actual rows, not citation labels. | Rendered rows have no separate prose-grounding score. |
| Wrong UI action | Typed payload shape drives pins and cards; no model render tool. | Bad aliases and coordinates still need validation. |
| Hidden uncertainty | Vision abstains to maybe; estimates and unknowns remain labelled. | A human must confirm ambiguous roofs. |
AI governance
The interface identifies the AI. It provides research support, not automated decisions about people. Legal and monetary answers are cited or refused; inferred labels require confirmation.
Privacy
No account or profile is required. Conversation turns support bounded continuity and are inaccessible to the analytical role. Inputs are not used for fine-tuning. A formal notice and erasure path remain gaps.
Cloud identity
GitHub deploys through OIDC. Runtime credentials are Container App secrets; the PostgreSQL bootstrap password remains in protected Terraform state. Managed-identity runtime auth is the next hardening step.
Network
HTTPS, a restricted CORS allowlist and a PostgreSQL firewall were live. Private endpoints, VNet integration, per-user authorization and retrieval ACLs are not.
04 · One decision surface, not fourteen interruptions
Only the decisions that explain the product's shape stay here. Full alternatives, evidence and reversal history remain in the repository's decision records.
Models never produce consequential numbers
- Instead of
- Compute with a model, then verify.
- Cost
- Every formula needs code and tests.
- Reverse if
- Refusal makes normal economics questions unusable.
One agent over five contract-driven tools
- Instead of
- One routed expert per topic.
- Cost
- Tool schemas become critical infrastructure.
- Reverse if
- Ordinary questions repeatedly hit the round cap.
Generic SQL finder under structural controls
- Instead of
- A rigid filter API.
- Cost
- Role, bouncer and performance need ownership.
- Reverse if
- Leakage or cost survives those controls.
Content Understanding for tenders
- Instead of
- PDF scraping plus another extractor.
- Cost
- Managed-service and model compatibility risk.
- Reverse if
- Constraint errors make Apply unreliable.
Postgres hybrid retrieval before AI Search
- Instead of
- A second search service from day one.
- Cost
- Retrieval quality is owned in-house.
- Reverse if
- Golden-set hit@5 falls below 0.80.
OIDC and Terraform delivery
- Instead of
- Stored CI credentials and portal state.
- Cost
- More identity and IaC setup discipline.
- Reverse if
- Never; extend managed identity to runtime.
National buildings supersede cadastre
- Instead of
- Patching a source missing valuable roofs.
- Cost
- Migration, re-derivation and spatial join.
- Reverse if
- Source noise outweighs regained coverage.
Benchmark model tiers; reject premium
- Instead of
- Assuming a larger model is better.
- Cost
- Token spend and evaluation work.
- Reverse if
- A larger independent-judge run reverses it.
05 · Claims are paired with something inspectable
Deterministic behaviour was tested on every change. Hosted-model evaluations ran before releases; they are not misrepresented as per-commit gates.
| Model | Groundedness | Relevance | Refusal recall | Median tokens | p50 |
|---|---|---|---|---|---|
gpt-5-nano | 0.79 | 0.93 | 14 / 16 | 10,822 | 40.7 s |
gpt-5-mini production | 0.76 | 0.94 | 15 / 16 | 10,227 | 17.0 s |
gpt-5 | 0.77 | 0.83 | 15 / 16 | 14,534 | 48.3 s |
Caveats: one 40-question run; gpt-5-mini was the judge; candidate capacity differed, so latency is directional. The evidence rejected the premium tier and retained mini because refusal behaviour matters most here.
Code gates, every push
.NET release build, tariff parity, Python tests, schema coverage and frontend tests and build.
Model gates, before release
Groundedness, outcome, law retrieval and agent behaviour against hosted models.
Delivery evidence, after deployment
Immutable image, OIDC login, health smoke, public telemetry and source freshness (while the deployment ran).
06 · A credible personal production system, not enterprise theatre
Real when it ran
- Public deployment and live telemetry
- Automated pipelines with freshness status
- Read-only and deterministic tool boundaries
- Terraform, OIDC and tested CI/CD
- Measured model and retrieval choices
- Visible abstention, refusal and provenance
Enterprise adoption adds
- Entra runtime identity, private endpoints and VNet
- Per-principal authorization and retrieval ACLs
- Purview-class catalog, retention and lineage governance
- DPIA, model-risk approval and security review
- SLOs, load tests, on-call and disaster recovery
- Segregation of duties and release approvals
| Accepted risk | Why accepted here | Trigger / next control |
|---|---|---|
| Runtime secrets remain. | Public read-only workload; CI credential risk was removed first. | Managed identity before private or enterprise data. |
| Model-written SELECT may infer or be wrong. | Generality is useful and visible data is public. | Templates or per-user views when private data appears. |
| Model evals are pre-release. | They cost hosted tokens and wall time. | Automate after higher release frequency or a regression. |
| No permanent UAT environment. | A personal, public-data product cannot justify duplicate standing resources. Pull requests must pass deterministic gates; main deploys an immutable image, then a live health smoke confirms it. | An isolated integration stage, approvals and segregation of duties when a team, private data, stateful migrations or an SLA make the blast radius material. |
| Single-region app and database. | Personal traffic cannot justify redundant standing cost. | Test regional recovery when an SLA requires it. |
| A global cost guard may stop the app. | On a personal subscription, solvency outranks uptime. | Service budgets and chargeback before commercialization. |
The last row is not theoretical: the public deployment is offline today, which is exactly what a cost guard on a personal subscription does. Lex, the legal corpus this product consumed, is online and documented separately.
The signal is judgment, not service count
This demonstrates problem framing, data engineering, bounded agent design, RAG, Azure delivery, evaluation and honest risk ownership. The strongest choices are the capabilities deliberately left out until evidence makes them worth operating.