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:
-
Replace manual list management with ItemTracker:
-
Use StatusTracker for visit/completion updates:
-
Use UIBuilder for UI configurations:
Best Practices¶
- Use Type Hints: Always specify the item type for
ItemTracker[T] - Singleton Services: Use
get_location_enrichment_service()for the enrichment service - Async Enrichment: Run geocoding/enrichment in background tasks
- Persist After Changes: Call persistence callback after state changes
- Fuzzy Matching: Use appropriate thresholds (0.6-0.8) for name matching
Future Enhancements¶
- Add
BillTrackercomponent for expense splitting - Add
PollTrackercomponent for voting - Add
NotificationBuilderfor push notifications - Add caching layer for enrichment results
- Add batch UI updates for better performance