๐ About This Code Showcase
This showcase highlights the four core modules that power Hire Gardener's AI vendor communication workflow.
The key design principle is a pluggable messaging layer โ one config flag switches the entire app between simulation mode (Llama3 plays vendor roles) and live mode (real WhatsApp API). The workflow logic never changes.
๐๏ธ Project File Structure
hire-gardener/
โโโ ui.py โ Streamlit app (main entry point)
โโโ workflow.py โ 6-stage orchestration logic
โโโ agent.py โ Llama3 message composition
โโโ config.py โ MODE flag + all settings
โโโ requirements.txt
โโโ .env.example
โโโ README.md
โโโ messaging/
โโโ __init__.py โ Auto-loads mock or real mode
โโโ mock.py โ Llama3 vendor simulation
โโโ whatsapp.py โ Meta WhatsApp Cloud API
โ๏ธ Core Design: The Mode Switch
The entire mock/live switch lives in two files. config.py reads the environment, and messaging/__init__.py loads the correct module automatically.
import os
from dotenv import load_dotenv
load_dotenv()
# Switch between "mock" (Llama3 simulation) and "real" (WhatsApp API)
MODE = os.getenv("MODE", "mock")
# Ollama โ local LLM running on your machine
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://127.0.0.1:11434")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3:latest")
# WhatsApp Cloud API โ only used when MODE=real
WHATSAPP_API_TOKEN = os.getenv("WHATSAPP_API_TOKEN", "")
WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "")
from config import MODE
if MODE == "real":
from .whatsapp import get_vendor_response # Real WhatsApp API
else:
from .mock import get_vendor_response # Llama3 simulation
# Same function signature in both modules โ workflow never needs to change
__all__ = ["get_vendor_response"]
๐ค Agent Module โ Message Composition
All outbound messages are composed by Llama3 with a persona system prompt. Every function has a hardcoded fallback so the app keeps working even if Ollama is offline.
AGENT_SYSTEM = """You are a personal assistant helping a property owner find
a grass cutting service in Malaysia. You communicate naturally,
like a real person sending WhatsApp messages. Keep messages short
and conversational โ 2 to 4 sentences maximum. Use a natural mix
of English and Malay where appropriate (e.g. "boleh", "terima kasih").
Never reveal you are an AI."""
def call_llm(prompt, system=AGENT_SYSTEM):
try:
response = requests.post(
f"{OLLAMA_BASE_URL}/api/generate",
json={
"model": OLLAMA_MODEL,
"prompt": prompt,
"system": system,
"stream": False,
"options": {"temperature": 0.7}
},
timeout=60
)
return response.json().get("response", "").strip()
except Exception:
return None # Caller uses fallback text if None
def compose_outreach(area):
result = call_llm(
f"Write a short WhatsApp message asking a grass cutting service "
f"if they cover {area}. Friendly, casual, 2โ3 sentences."
)
# Fallback if Ollama is offline
return result or (
f"Hi, I'm looking for a grass cutting service in {area}. "
"Do you provide this service in the area? Thanks!"
)
๐ญ Vendor Simulation โ Mock Mode
Each vendor has a persona system prompt that drives Llama3's responses. Vendor 3 (Pak Razif) is programmed to decline at the quoting stage โ adding realism to the simulation.
PERSONAS = {
1: {
"system": (
"You are Ahmad, owner of Ahmad Landscaping in KL. "
"Speak in friendly Malay-English mix โ use 'boleh', 'ok bro', 'insyaAllah'. "
"Charge RM150โ180. Available Monday or Tuesday next week."
),
"rate": "160", "availability": "Monday next week", "declines": False
},
2: {
"system": (
"You are Sarah from Green Garden KL. Professional English. "
"Charge RM200โ220. Available this Saturday morning."
),
"rate": "210", "availability": "This Saturday", "declines": False
},
3: {
"system": (
"You are Razif, a solo gardener based in Shah Alam. "
"For outreach: reply positively. For quoting: apologise and "
"say the area is too far from Shah Alam."
),
"rate": None, "availability": None, "declines": True
}
}
def get_vendor_response(vendor: dict, message: str, stage: str) -> tuple:
"""Returns (reply_text, quote_data | None)"""
persona_id = vendor.get("persona_id", 1)
persona = PERSONAS[persona_id]
if stage == "quoting" and persona["declines"]:
prompt = (
f"Reply politely saying the area is too far from Shah Alam: '{message}'"
)
else:
prompt = f"Reply naturally to this WhatsApp message in character: '{message}'"
reply = call_llm(prompt, persona["system"]) or persona["fallbacks"].get(stage, "Ok, noted.")
if stage == "quoting":
quote_data = {
"declined": persona["declines"],
"rate": persona["rate"],
"availability": persona["availability"]
}
return reply, quote_data
return reply, None
๐ Workflow Engine โ 6-Stage Orchestration
The workflow module calls agent functions and the messaging layer in sequence. It never imports from WhatsApp or mock directly โ only through the pluggable messaging module.
def stage_3_quote(vendors: list, conversations: dict) -> tuple:
"""Send quote request + media to all vendors. Returns (conversations, quotes)."""
quotes = []
for vendor in vendors:
msg = compose_quote_request()
# Display version shows media indicators in the UI
display_msg = msg + "\n\n๐ท [Area photo attached]\n๐ฅ [Area video attached]"
conversations[vendor["id"]].append({
"role": "agent", "text": display_msg, "has_media": True
})
# LLM gets the clean message (no UI indicators)
reply, quote_data = get_vendor_response(vendor, msg, "quoting")
conversations[vendor["id"]].append({"role": "vendor", "text": reply})
quotes.append({
**quote_data,
"vendor_id": vendor["id"],
"vendor_name": vendor["name"],
"number": vendor["number"],
"persona_id": vendor.get("persona_id", 1)
})
return conversations, quotes