๐ŸŒฟ Hire Gardener

Core Source Code โ€” AI Vendor Communication Workflow

Python Streamlit Ollama ยท Llama3 WhatsApp Cloud API

๐Ÿ” 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.

๐Ÿ“„ config.py โ€” Single source of truth for all settings
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", "")
๐Ÿ“„ messaging/__init__.py โ€” Pluggable messaging layer
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.py โ€” Persona prompt + LLM call with fallback
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.

๐Ÿ“„ messaging/mock.py โ€” Vendor personas and reply logic
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.

๐Ÿ“„ workflow.py โ€” Stage 3: Quote request with media
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

โš™๏ธ Technical Implementation Notes

Key Design Decisions