Social Services Navigator · AfifAI
Case study · AI for social impact · 2025–2026

An AI guide that finds the right support service in two model calls

Residents of a crisis-affected region in southern Israel describe what they need in their own words, in one of five languages. The chat asks only what it must (where they live, how old they are, what kind of help), then returns up to three real services from a catalog of 354, filtered by geography and age in code rather than by the model.

.NET 8 / C#PostgreSQLAmazon Bedrock AgentCoreClaude on BedrockPythonReact + TypeScriptAWS S3 / RDS / Lambda
Client
Israeli social-impact organization coordinating support services for a region (confidential)
Team
A team of 4 that I led
My role
Team lead & architect (hands-on)
Timeline
Dec 2025 – Aug 2026 · in production
Status
In production
At a glance
−93%model context per ranking call: ≈245K → ≈17K characters
3+ to 2model calls per search turn (1 when the chat asks a follow-up)
≈10 stypical answer, down from minutes on the first agent design (estimate)
0unverified service IDs reach the user. Every ID is checked in code.
≤5 minfrom a catalog edit to the live chat, with no redeploy
354services in the catalog, each with location, scope and age rules
5UI languages, RTL and LTR, ≈850 strings each
86REST endpoints across 2 APIs, 31 database tables
The problem

Hundreds of services, and no easy way to find the right one

After a regional crisis, dozens of organizations opened therapy, rehabilitation, legal, financial and community programs for local residents. Each one has its own rules. Some serve one local authority only, some the whole region, some the whole country. Many have age limits. A resident looking for physiotherapy for herself, or emotional support for a teenage son, had to know which organization to call and whether she was even eligible.

The client wanted one place to ask. It had to answer in the resident's language, never invent a service, and stay correct as organizations kept adding and changing programs.

What I owned

Architecture, backend and the AI layer

  • Designed the platform: two .NET 8 APIs (resident and admin), a PostgreSQL schema of 31 tables, a background messaging worker and three React front ends.
  • Wrote most of the backend: chat sessions and history, the service catalog with translations, organizations and the service-provider role, OTP sign-in, e-mail and SMS.
  • Took the AI matching through three designs, from a Lambda prompt to a Bedrock agent to a controlled AgentCore Runtime agent in Python, and wrote the runbooks for each.
  • Built the catalog pipeline: a 30-column CSV import, an outbox-driven sync to S3 and a catalog snapshot that the agent reloads without a redeploy.
  • Set up the AWS production environment (RDS, S3, Lambda, Bedrock, IAM) and led the team on the front ends and releases.
Architecture

The model asks and ranks. The code filters.

The diagram is wider than the screen: swipe sideways to see all of it →

The CMS is the only source of truth. Every service change writes an outbox row. The sync step rebuilds that service's document and, after a full reindex or CSV import, writes one catalog snapshot that the agent reloads on a 5-minute TTL. The old engine stays behind a configuration switch, so the team could compare the two paths and fall back without a deploy.

Design decisions

Let the code do the parts that must be exact

Own the loop instead of an autonomous agent

The managed agent decided by itself how often to search the knowledge base, usually 3 or more times per turn. The new runtime runs a fixed sequence: triage, filter, rank. Nothing is retrieved until town, age and need are known. Trade-off: a Python service next to a .NET backend, and the orchestration is ours to maintain.

Geography and age in code, not in the prompt

Which local authority a settlement belongs to, whether a service is local, regional or national, and its age range are all checked by a deterministic filter. The model only ranks by professional domain. Trade-off: the settlement-to-authority table has to be maintained by hand.

A catalog snapshot instead of a vector index

With 354 short, structured records, the filter cuts the list to a median of ≈70 candidates before the model sees anything. A JSON snapshot in S3 removed chunking, ingestion jobs and citation-token problems. Trade-off: this works while the catalog fits in memory; a much larger catalog would bring retrieval back.

Verify every ID the model returns

The rank call can only pick from the candidate list it was given. Any ID outside that list is dropped before the backend loads service cards. If nothing valid is left, the user gets an honest “no match in your area” and a suggestion.

An emergency path with no model at all

The emergency button skips the agent and returns a configured list of emergency services in the user's language. The most urgent answer never waits on a model or depends on one.

Engineering log

Three engines, and what pushed us to the next one

Most of the hard problems came from running the matching on real catalog data and real questions. A selection:

Jan 2026

The whole catalog in every prompt

The first engine was a Lambda that sent the full compressed catalog with every request: about 245K characters of service data, cached but resent on each turn and growing with every new service.

Kept it as a working baseline and moved on to a retrieval design built on a managed knowledge base.

Apr 2026

Service IDs became citation tokens

Default chunking separated each service's ID line from its text. The model returned the knowledge base's citation tokens instead of real IDs, the backend couldn't parse them, and valid matches disappeared.

Recreated the data source with no chunking, enriched each document with treatment type, sector, scope and age, and added a prompt rule for numeric IDs.

May 2026

Duplicates and raw JSON in the chat

The agent ran at temperature 1. Answers drifted out of the JSON contract, repeated services already shown, and sometimes showed the user raw {"answer": …}.

Temperature 0, a hardened parser on the backend, and the list of already-shown service IDs sent on every turn as the source of truth.

Jun 2026

Right domain, wrong local authority

Residents of one regional council were offered services that serve only a neighbouring council. The model was resolving geography from a lookup table written into the prompt.

Moved geography into a deterministic filter: settlement → authority, then service scope. The model never sees out-of-area services.

Jun 2026

Answers that took minutes

Traces showed several autonomous knowledge-base iterations plus pre- and post-processing on every turn, with throttling and retries on top.

Replaced the managed agent with a controlled AgentCore Runtime agent: one call to triage, one to rank, a cached system prompt, and count or name lookups answered from the catalog with no retrieval.

Jun 2026

The chat kept asking for the town

When no candidates were left, the agent asked for the town again, and the conversation looped.

Two explicit endings: “your town is outside the area we cover” or “I've shown everything relevant in your area”, each with a next step.

Jun 2026

National services crowded out local ones

The ranker sees at most 60 candidates. Ordered by ID, national programs could fill the window and push out a local service that fit better.

Sort local first, then by keyword match on the need, and only then cap. Added a “never pad the list” rule: fewer results, or none, rather than an off-domain one.

Jul 2026

Catalog changes needed a deploy

The runtime shipped with a bundled CSV, so every catalog update meant redeploying the agent.

The backend writes a catalog snapshot to S3 on reindex and CSV import. The agent reloads it every 5 minutes and keeps the bundled CSV only as a cold fallback.

Same question, two engines

What happens in one search turn

A simplified replay of one request, “physiotherapy for a 30-year-old woman in a town in the region”. The steps are illustrative. The counts come from the code and the 354-service catalog. No client data is shown.

Outcome

Where it landed

AreaBeforeAfterChangeSource
Context per ranking callWhole catalog, ≈245K chars≤60 filtered candidates, ≈17K chars−93%derived
Model calls per search turn3+ autonomous iterations2 (triage + rank), 1 to ask−33% or morederived
Typical answer timeMinutes≈10 s≈10×+ fasterestimate
Invalid service IDs shownCitation tokens, matches droppedOnly IDs from the candidate list0 by designderived
Catalog change to live chatAgent redeploySnapshot reload, TTL 300 s≤5 minmeasured
Out-of-area resultsGeography resolved by the modelDeterministic authority + scope filter0 in validationderived
Catalog onboardingService-by-service entryOne CSV import (30 fields) + auto-reindexdays → minutesestimate
Emergency answerThrough the modelConfigured list, no model call0 model callsderived
  • Live in production with dev, QA, stage and prod branches on every repo.
  • Three roles on one backend: residents, service-provider organizations that manage their own services, and administrators who approve them.
  • Usage and session reports exported from the admin system for the client's own reporting.
  • The catalog grew from 318 to 354 services between data versions with no change to the agent.
  • Runbooks for production setup, agent deployment and the migration between engines, so the client's team can operate it.
  • Next steps: managed memory for registered users across sessions, and an evaluation set of questions in all five languages.
Stack

Technology stack

Backend
.NET 8, C#, EF Core, PostgreSQL on RDS, background worker
AI
Bedrock AgentCore Runtime (Python), Claude Sonnet via Converse, prompt caching; earlier Bedrock Agents + Knowledge Bases, Lambda
Front ends
React, TypeScript, Vite, Redux Toolkit, i18next (he, ar, en, ru, am)
AWS
S3 (pre-signed uploads, catalog snapshot), RDS, Lambda, IAM, CDK
Integrations
Transactional e-mail, SMS gateway for OTP, Excel exports
Scale
5 repos, ≈73K lines, 1,248 commits in ≈8 months

Free scoping call · 30 minutes · No commitment

Bring one process. Leave with a plan and a number to beat.

Write a few lines: the process or system, what hurts today (time, errors, cost or an AI pilot that stalled) and who uses it. After a short call you get a written plan and a fixed quote within ≈3 days.

Get a free scoping callafif.mansour@gmail.com

Opens a pre-filled email · reply within one business day

The model reads. The code counts.

All projects