Spaces:
Sleeping
Sleeping
| """ | |
| Resy.com browser agent — finds restaurants eligible for the $20 monthly dining credit. | |
| Uses google-genai (Gemini Flash free tier) with Playwright browser tools. | |
| """ | |
| import asyncio | |
| import json | |
| import os | |
| import re | |
| from collections.abc import Callable | |
| from datetime import date | |
| from google import genai | |
| from google.genai import types | |
| from ..models import RestaurantOption | |
| from ..tools.browser import BrowserSession | |
| _MODEL = "gemini-2.0-flash-lite" | |
| _MAX_ITER = 20 | |
| def _system_prompt(cities: list[str], dinner_dates: list[date], party_size: int) -> str: | |
| pairs = "\n".join(f" - {c}: {d}" for c, d in zip(cities, dinner_dates)) | |
| return f"""You are a restaurant-discovery assistant on Resy.com. | |
| ## Mission | |
| Find 2–3 dinner options per city below. Return ONE JSON array when done. | |
| ## Resy credit facts | |
| - Delta Reserve Amex $20/month credit applies to ANY Resy booking. Set resy_credit_eligible=true always. | |
| - Global Dining Access (GDA) badge appears on select listings. Set global_dining_access=true only if you see it. | |
| ## Strategy | |
| 1. Navigate to resy.com for each city (e.g. https://resy.com/cities/hnl for Honolulu). | |
| 2. Search dinner availability on the dinner date, party size {party_size}, 7pm–9pm slots. | |
| 3. Find 2–3 restaurants per city. | |
| ## Cities and dinner dates: | |
| {pairs} | |
| Party size: {party_size} | |
| ## Output — JSON array only, no prose, no fences: | |
| [{{"city":"<city>","name":"<restaurant>","cuisine":"<type>","reservation_date":"<YYYY-MM-DD>", | |
| "party_size":{party_size},"resy_credit_eligible":true,"global_dining_access":<bool>}}]""" | |
| def _parse(text: str, party_size: int) -> list[RestaurantOption]: | |
| text = re.sub(r"```(?:json)?\s*", "", text).strip().rstrip("`").strip() | |
| s, e = text.find("["), text.rfind("]") | |
| if s == -1 or e == -1: | |
| return [] | |
| try: | |
| items = json.loads(text[s : e + 1]) | |
| except json.JSONDecodeError: | |
| return [] | |
| out = [] | |
| for item in items: | |
| try: | |
| rd = item.get("reservation_date") | |
| if isinstance(rd, str): | |
| rd = date.fromisoformat(rd) | |
| out.append(RestaurantOption( | |
| city=item["city"], name=item["name"], | |
| cuisine=item.get("cuisine", ""), | |
| reservation_date=rd, | |
| party_size=item.get("party_size", party_size), | |
| resy_credit_eligible=item.get("resy_credit_eligible", True), | |
| global_dining_access=item.get("global_dining_access", False), | |
| )) | |
| except (KeyError, ValueError): | |
| pass | |
| return out | |
| async def find_resy_restaurants( | |
| cities: list[str], | |
| dinner_dates: list[date], | |
| party_size: int, | |
| browser: BrowserSession, | |
| progress: Callable[[str], None], | |
| ) -> list[RestaurantOption]: | |
| client = genai.Client(api_key=os.environ["GEMINI_API_KEY"]) | |
| config = types.GenerateContentConfig( | |
| system_instruction=_system_prompt(cities, dinner_dates, party_size), | |
| tools=browser.gemini_tools, | |
| ) | |
| contents = [types.Content(role="user", parts=[types.Part(text=( | |
| "Search Resy for restaurants in each city in the system prompt. Return the JSON array when done." | |
| ))])] | |
| progress(f"Resy: searching {len(cities)} city/cities…") | |
| last_text = "" | |
| for i in range(_MAX_ITER): | |
| for attempt in range(4): | |
| try: | |
| response = await client.aio.models.generate_content( | |
| model=_MODEL, contents=contents, config=config | |
| ) | |
| break | |
| except Exception as e: | |
| if "429" in str(e) and attempt < 3: | |
| await asyncio.sleep(15 * (attempt + 1)) | |
| else: | |
| raise | |
| contents.append(response.candidates[0].content) | |
| try: | |
| if response.text: | |
| last_text = response.text | |
| except ValueError: | |
| pass | |
| fn_calls = response.function_calls | |
| if not fn_calls: | |
| break | |
| fn_parts = [] | |
| for fc in fn_calls: | |
| progress(f"Resy: {fc.name}…") | |
| try: | |
| result = await browser.execute_tool(fc.name, dict(fc.args)) | |
| except Exception as exc: | |
| result = f"Error: {exc}" | |
| fn_parts.append(types.Part( | |
| function_response=types.FunctionResponse(name=fc.name, response={"result": result}) | |
| )) | |
| contents.append(types.Content(role="user", parts=fn_parts)) | |
| results = _parse(last_text, party_size) | |
| progress(f"Resy: found {len(results)} restaurant(s).") | |
| return results | |