Skip to content

MiniApp Component Architecture

Created: December 2, 2025 Purpose: Reusable components for building MiniApp handlers


Overview

The MiniApp component system provides reusable building blocks for creating rich, interactive handlers like Trip Planner, Todo List, Bill Split, and Poll. These components extract common patterns to reduce code duplication and ensure consistency.

Architecture Diagram

app/miniapps/
├── handlers/                    # MiniApp implementations
│   ├── base_handler.py          # Abstract base class
│   ├── trip_planner.py          # Uses all components
│   ├── todo_list.py             # Can use ItemTracker, UIBuilder
│   ├── bill_split.py            # Can use ItemTracker, StatusTracker
│   └── poll.py                  # Can use ItemTracker, UIBuilder
├── components/                  # Reusable components
│   ├── item_tracker.py          # Generic list management
│   ├── status_tracker.py        # Status updates with ratings
│   ├── query_engine.py          # Natural language filtering
│   ├── ui_builder.py            # UI configuration builder
│   └── location_enrichment.py   # Geocoding & Places API
└── schemas/                     # Domain data models
    └── venue.py                 # Venue & TripState models

Components

1. ItemTracker

File: app/miniapps/components/item_tracker.py

Generic item list management with fuzzy name matching. Works with any item type that extends TrackedItem.

Base Class: TrackedItem

from dataclasses import dataclass, field
from typing import Optional
import uuid

@dataclass
class TrackedItem:
    """Base class for trackable items."""
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    name: str = ""
    status: str = "pending"
    category: str = ""
    group: str = ""  # For grouping (e.g., district, project)
    notes: str = ""
    rating: Optional[int] = None
    created_at: Optional[str] = None
    updated_at: Optional[str] = None

Usage

from app.miniapps.components import ItemTracker, TrackedItem
from dataclasses import dataclass

# Define your item type
@dataclass
class TodoItem(TrackedItem):
    priority: str = "medium"
    due_date: Optional[str] = None

# Create tracker
tracker: ItemTracker[TodoItem] = ItemTracker()

# Add items
item = TodoItem(name="Buy groceries", priority="high")
tracker.add(item)

# Find by name (fuzzy matching)
found = tracker.find_by_name("groceries")  # Returns TodoItem

# Filter items
high_priority = tracker.filter(lambda x: x.priority == "high")

# Group by category
by_category = tracker.group_by("category")  # Dict[str, List[TodoItem]]

# Count by status
status_counts = tracker.count_by("status")  # Dict[str, int]

Key Methods

Method Description
add(item) Add item to tracker
remove(item_id) Remove item by ID
find_by_id(id) Get item by exact ID
find_by_name(name, threshold=0.6) Fuzzy search by name
filter(predicate) Filter items with lambda
group_by(field) Group items by field value
count_by(field) Count items per field value
get_all() Get all items

2. StatusTracker

File: app/miniapps/components/status_tracker.py

Handles status updates with natural language rating extraction.

Usage

from app.miniapps.components import StatusTracker, StatusUpdate

tracker = StatusTracker()

# Parse natural language status update
update = tracker.parse_status_update(
    "Visited Tsukiji Market - amazing sushi, 5 stars!"
)
# Returns: StatusUpdate(
#     item_name="Tsukiji Market",
#     new_status="visited",
#     rating=5,
#     notes="amazing sushi"
# )

# Apply update to item
venue = get_venue("Tsukiji Market")
tracker.apply_update(venue, update)

# Format response
response = tracker.format_status_response(venue, update)
# Returns: "Marked Tsukiji Market as visited! (5★) - amazing sushi"

Rating Extraction Patterns

The tracker understands various rating expressions:

Pattern Extracted Rating
"5 stars", "5/5" 5
"4 out of 5" 4
"loved it", "amazing" 5
"liked it", "good" 4
"okay", "meh" 3
"didn't like it" 2
"terrible", "awful" 1

3. QueryEngine

File: app/miniapps/components/query_engine.py

Natural language query filtering for items.

Usage

from app.miniapps.components import QueryEngine, create_venue_query_engine

# Use pre-configured venue engine
engine = create_venue_query_engine()

# Parse natural language query
result = engine.parse_query("show me restaurants in Shibuya")
# Returns: QueryResult(
#     category_filter="restaurant",
#     group_filter="Shibuya",
#     status_filter=None,
#     time_filter=None
# )

# Apply filters to items
filtered = engine.apply_filters(venues, result)

Custom Query Engine

from app.miniapps.components import QueryEngine

# Configure for your domain
engine = QueryEngine(
    category_keywords={
        "task": ["task", "todo", "item"],
        "meeting": ["meeting", "call", "sync"],
        "reminder": ["reminder", "alert"],
    },
    time_keywords={
        "today": ["today", "now"],
        "tomorrow": ["tomorrow"],
        "this_week": ["this week", "week"],
    },
    status_keywords={
        "pending": ["pending", "todo", "open"],
        "done": ["done", "complete", "finished"],
    }
)

4. UIBuilder

File: app/miniapps/components/ui_builder.py

Fluent API for building UI JSON configurations.

Usage

from app.miniapps.components import (
    UIBuilder, build_progress_indicator, build_nav_item,
    build_action, build_filter_section, build_filter_option
)

# Build a UI configuration
ui = (
    UIBuilder()
    .set_header("Tokyo Trip", "12 venues planned")
    .add_stats_grid([
        {"label": "Visited", "value": "5"},
        {"label": "Remaining", "value": "7"},
    ])
    .add_card_list(
        cards=[
            {"title": "Tsukiji Market", "subtitle": "Ginza • Restaurant"},
            {"title": "Shibuya 109", "subtitle": "Shibuya • Shopping"},
        ],
        card_style="venue"
    )
    .add_map_view(
        center={"lat": 35.6762, "lng": 139.6503},
        markers=[...],
        zoom=12
    )
    .add_actions([
        build_action("add", "Add Venue", "plus"),
        build_action("filter", "Filter", "funnel"),
    ])
    .build()
)

Helper Functions

# Progress indicator (e.g., 5/12 visited)
progress = build_progress_indicator(
    current=5,
    total=12,
    label="venues visited"
)

# Navigation item
nav = build_nav_item(
    id="restaurants",
    label="Restaurants",
    count=8,
    icon="utensils"
)

# Action button
action = build_action(
    id="add_venue",
    label="Add Venue",
    icon="plus",
    style="primary"
)

# Filter section with options
filters = build_filter_section(
    title="Category",
    options=[
        build_filter_option("restaurant", "Restaurants", count=8),
        build_filter_option("bar", "Bars", count=4),
    ]
)

5. LocationEnrichmentService

File: app/miniapps/components/location_enrichment.py

Geocoding, Google Places enrichment, and distance calculations.

Usage

from app.miniapps.components import get_location_enrichment_service

# Get singleton service
service = get_location_enrichment_service()

# Geocode a venue
success = await service.geocode_venue(venue, destination="Tokyo")
# Sets: venue.latitude, venue.longitude, venue.place_id

# Enrich with Places data
success = await service.enrich_venue(venue)
# Sets: venue.opening_hours, venue.price_level, venue.google_rating,
#       venue.phone_number, venue.website, venue.dietary_options

# Combined geocode + enrich
result = await service.geocode_and_enrich(
    venue,
    destination="Tokyo",
    on_complete=lambda: save_to_db(venue)
)

# Batch processing with rate limiting
results = await service.batch_geocode_and_enrich(
    venues,
    destination="Tokyo",
    on_venue_complete=lambda v: save_to_db(v),
    delay_between=0.5  # seconds
)

# Get walking time between venues
walking_time = await service.get_walking_time(venue1, venue2)
# Returns: "15 mins" or None

# Check if venue is open
is_open = await service.check_is_open(venue)
# Returns: True, False, or None

EnrichmentResult

@dataclass
class EnrichmentResult:
    success: bool
    item_name: str
    geocoded: bool = False
    enriched: bool = False
    error: Optional[str] = None

Schemas

Venue

File: app/miniapps/schemas/venue.py

Rich venue model for trip planning.

from app.miniapps.schemas import Venue, TripState

# Create a venue
venue = Venue(
    name="Ichiran Ramen",
    category="restaurant",
    district="Shibuya",
    address="1-22-7 Jinnan, Shibuya",
    vibe=["popular", "solo-friendly"],
    must_try=["Original Tonkotsu"],
    best_for=["lunch", "late-night"],
    source_note="Friend recommendation"
)

# Serialize
data = venue.to_dict()

# Deserialize
venue = Venue.from_dict(data)

# Get emoji for category
emoji = venue.get_emoji()  # 🍴

# Format for display
title = venue.format_card_title()  # "○ 🍴 Ichiran Ramen"
short = venue.format_short()  # "○ 🍴 Ichiran Ramen (Shibuya)"

TripState

Complete trip state with statistics.

trip = TripState(
    destination="Tokyo",
    dates={"start": "2025-03-01", "end": "2025-03-10"},
    venues=[venue1, venue2, venue3]
)

# Update districts from venues
trip.update_districts()

# Get statistics
stats = trip.get_stats()
# {"total": 3, "visited": 1, "planned": 2, "districts": 2}

# Get category counts
counts = trip.get_category_counts()
# {"restaurant": 2, "bar": 1}

Integration Example

Here's how Trip Planner uses all components together:

from app.miniapps.handlers.base_handler import BaseMiniAppHandler
from app.miniapps.components import (
    ItemTracker, StatusTracker, create_venue_query_engine,
    UIBuilder, get_location_enrichment_service,
)
from app.miniapps.schemas import Venue, TripState

class TripPlannerHandler(BaseMiniAppHandler):
    def __init__(self):
        super().__init__()
        self.venue_tracker: ItemTracker[Venue] = ItemTracker()
        self.status_tracker = StatusTracker()
        self.query_engine = create_venue_query_engine()
        self.enrichment_service = get_location_enrichment_service()

    async def handle_add_venue(self, venue_data: dict) -> dict:
        # Create venue
        venue = Venue.from_dict(venue_data)

        # Add to tracker
        self.venue_tracker.add(venue)

        # Enrich in background
        asyncio.create_task(
            self.enrichment_service.geocode_and_enrich(
                venue,
                destination=self.trip_state.destination,
                on_complete=self._persist_state
            )
        )

        return {"success": True, "venue_id": venue.id}

    async def handle_mark_visited(self, text: str) -> dict:
        # Parse natural language
        update = self.status_tracker.parse_status_update(text)

        # Find venue
        venue = self.venue_tracker.find_by_name(update.item_name)
        if not venue:
            return {"error": "Venue not found"}

        # Apply update
        self.status_tracker.apply_update(venue, update)

        # Generate response
        response = self.status_tracker.format_status_response(venue, update)
        return {"message": response}

    def build_ui(self) -> dict:
        venues = self.venue_tracker.get_all()
        stats = self.trip_state.get_stats()

        return (
            UIBuilder()
            .set_header(
                self.trip_state.destination,
                f"{stats['total']} venues"
            )
            .add_stats_grid([
                {"label": "Visited", "value": str(stats['visited'])},
                {"label": "Planned", "value": str(stats['planned'])},
            ])
            .add_card_list([
                {
                    "id": v.id,
                    "title": v.format_card_title(),
                    "subtitle": f"{v.district}{v.category}",
                }
                for v in venues
            ])
            .build()
        )

Migration Guide

Updating Existing Handlers

To migrate an existing handler to use these components:

  1. Replace manual list management with ItemTracker:

    # Before
    self.items = []
    def find_item(name):
        for item in self.items:
            if name.lower() in item.name.lower():
                return item
    
    # After
    from app.miniapps.components import ItemTracker
    self.tracker: ItemTracker[YourItem] = ItemTracker()
    item = self.tracker.find_by_name(name)
    

  2. Use StatusTracker for visit/completion updates:

    # Before
    def parse_rating(text):
        # 50 lines of regex...
    
    # After
    from app.miniapps.components import StatusTracker
    tracker = StatusTracker()
    update = tracker.parse_status_update(text)
    

  3. Use UIBuilder for UI configurations:

    # Before
    return {
        "type": "app_ui",
        "header": {...},
        "sections": [...],
        # 100+ lines of nested dicts
    }
    
    # After
    from app.miniapps.components import UIBuilder
    return (
        UIBuilder()
        .set_header("Title", "Subtitle")
        .add_card_list(cards)
        .build()
    )
    


Best Practices

  1. Use Type Hints: Always specify the item type for ItemTracker[T]
  2. Singleton Services: Use get_location_enrichment_service() for the enrichment service
  3. Async Enrichment: Run geocoding/enrichment in background tasks
  4. Persist After Changes: Call persistence callback after state changes
  5. Fuzzy Matching: Use appropriate thresholds (0.6-0.8) for name matching

Future Enhancements

  • Add BillTracker component for expense splitting
  • Add PollTracker component for voting
  • Add NotificationBuilder for push notifications
  • Add caching layer for enrichment results
  • Add batch UI updates for better performance