Where every part of the platform lives, the command that puts it there, and what each piece of the code is for. Written to be followed by somebody who did not build it.
Three products, four repositories, four machines. Everything below was read off the running systems rather than remembered, so where this document and your memory disagree, believe this document and then check the machine.
| What | Address | Machine | Region |
|---|---|---|---|
| SmartMarket | marketz.smartzees.com | 54.254.25.0 | ap-southeast-1 |
| SmartService | servicez.smartzees.com | 35.91.251.211 | us-west-2 |
| SmartCommunity | livz.smartzees.com | 54.188.207.85 | us-west-2 |
| The landing page | smartzees.com | 35.91.251.211 | shares the Service box |
| The documents | servicez.smartzees.com/docs/ | 35.91.251.211 | and mirrored on Market |
| The open model | 184.34.122.122 | i-0c9e7a485d54e4e92 | us-west-2, another AWS account |
The three application machines are Lightsail, in AWS account 952427475294. The model is plain EC2 in the client's own account, 253918336085. That is why it is reached over the public internet and why a security group, not a private network, is the only thing in front of it.
Getting in:
ssh -i ~/Downloads/sailagentecsdevkey.pem ubuntu@35.91.251.211 # Service, landing, docs
ssh -i ~/Downloads/sailagentecsdevkey.pem ubuntu@54.254.25.0 # Market
ssh -i ~/Downloads/sailagentecsdevkey.pem ubuntu@54.188.207.85 # Community
The model machine has its own key and is normally reached from the AWS console instead, through EC2 Instance Connect, so nobody has to be sent a private key.
| Local folder | Remote | Branch | Product |
|---|---|---|---|
| plumber-assistant | jafgithub/ai-service-chat-se-abad | master | SmartService |
| AI-Orders-main | jafgithub/aichat-v2-abad | main | SmartMarket |
| community-assistant | AbadNaseer/ai-agent-smartcommunity | master | SmartCommunity |
| smartzees-landing | AbadNaseer/ai-agent-landingpage-smartzees | master | smartzees.com |
The last two are staged under a personal account because write access on the organisation copies has not been granted yet. When it is, the remote changes and nothing else does.
Each repository has the same shape: backend/ is FastAPI, frontend/ is Next.js exported to static files, and docs/ holds the written documents. The landing page has only a frontend.
Node is not on the system path on the build machine. Every frontend command below needs this first:
export PATH="$HOME/.local/node/bin:$PATH"
The pattern is the same everywhere and it is deliberate. Build locally, rsync to a staging directory in the deploy user's home, then move it into place with sudo. Never build on the server: these machines have under 2GB of memory and a Next.js build will take one down.
Backend, at /home/ubuntu/plumber/backend, unit plumber, port 8100.
rsync -az --exclude '__pycache__' --exclude '.venv' --exclude '.env' \
plumber-assistant/backend/app/ ubuntu@35.91.251.211:/home/ubuntu/plumber/backend/app/
ssh ubuntu@35.91.251.211 'sudo systemctl restart plumber'
Frontend, served from /var/www/serviceagent.
cd plumber-assistant/frontend && npm run build:serviceagent
rsync -az --delete out/ ubuntu@35.91.251.211:/home/ubuntu/deploy-serviceagent/
ssh ubuntu@35.91.251.211 \
'sudo rsync -a --delete /home/ubuntu/deploy-serviceagent/ /var/www/serviceagent/'
build:serviceagent rather than build: it sets an empty base path and an empty API url, because the app sits at the root of its own subdomain and the API is same origin.
Backend, at /var/www/ai-order/backend, unit aiorder.
rsync -az --exclude '__pycache__' --exclude '.env' \
AI-Orders-main/backend/app/ ubuntu@54.254.25.0:/home/ubuntu/deploy-market-app/
ssh ubuntu@54.254.25.0 \
'sudo rsync -a /home/ubuntu/deploy-market-app/ /var/www/ai-order/backend/app/ && sudo systemctl restart aiorder'
Frontend, served from /var/www/ai-order/frontend-dist.
cd AI-Orders-main/ai-order && npm run build:deploy
rsync -az --delete out/ ubuntu@54.254.25.0:/home/ubuntu/deploy-market/
ssh ubuntu@54.254.25.0 \
'sudo rsync -a --delete /home/ubuntu/deploy-market/ /var/www/ai-order/frontend-dist/'
The web root is frontend-dist, not frontend. There is a frontend directory beside it that is not served. Deploying into it looks like a successful deploy that changes nothing, and it has cost an afternoon before.
Backend, at /home/ubuntu/community/backend, unit community, port 8200.
rsync -az --exclude '__pycache__' --exclude '.venv' --exclude '.env' \
community-assistant/backend/app/ ubuntu@54.188.207.85:/home/ubuntu/community/backend/app/
ssh ubuntu@54.188.207.85 'sudo systemctl restart community'
Frontend, served from /var/www/community.
cd community-assistant/frontend && npm run build
rsync -az --delete out/ ubuntu@54.188.207.85:/home/ubuntu/deploy-community/
ssh ubuntu@54.188.207.85 \
'sudo rsync -a --delete /home/ubuntu/deploy-community/ /var/www/community/ && sudo chown -R www-data:www-data /var/www/community'
Port 8200 rather than 8100 is deliberate. This machine was cloned from the Service Assistant, and a stray request meant for that product must not find something listening here and be answered by it.
Static only, served from /var/www/smartzees on the Service machine.
cd smartzees-landing/app && npm run build
rsync -az --delete out/ ubuntu@35.91.251.211:/home/ubuntu/deploy-smartzees/
ssh ubuntu@35.91.251.211 \
'sudo rsync -a --delete /home/ubuntu/deploy-smartzees/ /var/www/smartzees/'
Served from /var/www/serviceagent-docs, which is outside the site root on purpose: the frontend deploy above uses --delete and would otherwise remove them.
python3 docs/_house/hub.py # rebuilds docs/index.html from its list
python3 docs/deployment/build.py # this document
rsync -az docs// ubuntu@35.91.251.211:/home/ubuntu/deploy-docs//
ssh ubuntu@35.91.251.211 \
'sudo rsync -a /home/ubuntu/deploy-docs/ /var/www/serviceagent-docs/'
Generated document sets (source.md plus the render_*.py scripts) deploy their build/web/ directory. Hand written ones (srs, lab, deployment, the two specifications) deploy the index.html beside the builder.
Only the ones somebody unfamiliar would otherwise have to read to understand. The rest are named for what they do.
| File | What it is for |
|---|---|
| services/llm.py (Market: services/ai.py) | The router. Everything wanting a sentence written asks this, and it decides whether our own model or Gemini answers, and falls back when the first cannot |
| services/gemini_service.py | Google's model. Also does speech to text and text to speech |
| services/ollama_service.py | Our own model over HTTP |
| services/tracing.py | Measures each stage of a request and keeps the last 50, which is what the live diagram reads |
| api/ai_public.py | Two read only routes with no token: which engine is switched on, and the recent traces. No question or reply text ever appears here |
| services/rag.py | The embedding model, all-MiniLM-L6-v2, 384 dimensions, loaded once |
| File | What it is for |
|---|---|
| services/ai_runtime.py | The engine switch itself, a small JSON file. This machine owns it for the whole platform |
| services/gpu_instance.py | Finds, starts and stops the model machine. Needs boto3 and AWS keys |
| api/ai_admin.py | The admin panel's engine controls, including start and stop |
| services/conversation.py | Turns what somebody said into a search, a booking step or a parking request |
| services/catalog_index.py, phrase_index.py | The service catalogue, held in memory so a search is a matrix multiply |
| services/parking.py, api/parking.py | Visitor passes and the QR code the gate reads |
| services/booking_service.py, booking_emails.py | Appointments, and the confirmations that go with them |
| File | What it is for |
|---|---|
| services/community_chat.py | The whole pipeline: scope to one association, retrieve, answer or refuse |
| services/docs_index.py | 209 passages in memory, and the 0.30 threshold under which no model is called at all |
| services/doc_library.py, doc_chunker.py | Which documents exist, and cutting a PDF into passages |
| services/platform_switch.py | Reads the engine switch from SmartService. This agent never writes it |
| api/documents.py | Upload, list, withdraw, download. The office screen talks to this |
| File | What it is for |
|---|---|
| services/catalog_index.py | About 25,600 products in memory. This is the 787x search improvement |
| sync_to_remote.py | Drains sync_outbox into the client's own database. See section 6 |
| services/shopping/ | Sourcing items the shop does not stock, through a provider interface |
| services/order_service.py | Totals, tax and tips. Tax is computed on the goods and the tip on the goods before tax |
One switch decides which engine answers, for all three products, and it lives on servicez.smartzees.com/admin under AI runtime. The other two read it over HTTP and never write it.
Settings on the Service machine, in .env:
GPU_INSTANCE_ID=i-0c9e7a485d54e4e92
AWS_REGION=us-west-2
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
OLLAMA_MODEL=llama3.2:3b
OLLAMA_TIMEOUT_SECONDS=45
The other two only need OLLAMA_URL and OLLAMA_MODEL, because they never start or stop anything.
OLLAMA_URL and GPU_INSTANCE_ID are two different modes and the first one wins. Set both on the Service machine and the panel will show a healthy engine it cannot stop. It also needs boto3 installed in its virtual environment, or every AWS call fails and the panel says the machine is unknown.
| Rule | Why |
|---|---|
| Exactly one machine may run aiorder-sync, and it is 54.254.25.0. The same for plumber-sync on 35.91.251.211. | These push rows into the client's live database. Cloned machines come up with the sync running, and two machines writing the same rows has happened three times |
| Stop aiorder-sync before any test that creates a customer or an order. | Test data reaches the client's production system otherwise. Start it again afterwards |
| Never widen port 11434 beyond the three application addresses. | Ollama has no authentication of any kind. The security group is the whole of its protection |
| Never build on a server. | Under 2GB of memory. A Next.js build will take the machine down |
| The model machine's shutdown behaviour must stay stop. | It switches itself off when idle. Set to terminate, the idle timer deletes it |
Checking the first rule takes one command and belongs in every handover:
for h in 35.91.251.211 54.254.25.0 54.188.207.85; do
echo -n "$h "
ssh ubuntu@$h 'systemctl is-active plumber-sync aiorder-sync | tr "\n" " "'
echo
done
Not "it returned 200", which a stale page also does. Ask it something.
curl -s -X POST https://servicez.smartzees.com/api/v1/chat \
-H 'Content-Type: application/json' \
-d '{{"message":"my boiler is leaking","session_id":"check"}}'
curl -s -X POST https://marketz.smartzees.com/api/v1/chat \
-H 'Content-Type: application/json' \
-d '{{"message":"milk","session_id":"check"}}'
curl -s -X POST https://livz.smartzees.com/api/v1/chat \
-H 'Content-Type: application/json' \
-d '{{"message":"quiet hours","session_id":"check","community":"serenity"}}'
A good answer from the third names a document. If it does not, the index did not load, whatever the status code said.
And the platform switch, which the other two agents depend on:
curl -s https://servicez.smartzees.com/api/v1/ai/provider
Written down because they look important and are not. Every one is a copy left behind when a machine was cloned.
| Machine | Leftover | Status |
|---|---|---|
| 54.188.207.85 | /var/www/serviceagent, /var/www/ai-order | Not served. From the clone this machine was made from |
| 54.254.25.0 | /var/www/plumber, /var/www/plumber.prev | Not served |
| 35.91.251.211 | /var/www/ai-order | Serves the documents mirror only, not the shop |
| Both app machines | plumber_assistant and ai_order databases where the product does not run | Unused. Left rather than dropped, because dropping a database to reclaim nothing is a bad trade |
None of these are served by nginx. If you are editing something and nothing changes, check you are on the right machine before you check anything else.
Written by Abad Naseer. Every measured number in this document is cited from the running system or from the code, and every target says that it is a target. Where the two disagree, the document says so rather than choosing the flattering one.