Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ OPENROUTER_API_KEY=replace-me
# Use a separate server-side key restricted to the Routes API and apply
# project quotas or budget alerts before enabling traffic-aware requests.
# GOOGLE_MAPS_ROUTES_API_KEY=your_google_maps_routes_api_key_here
# GOOGLE_MAPS_SAVED_ROUTE_LIMIT=20
# GOOGLE_MAPS_ROUTE_UPDATE_LIMIT=10

# ---------------------------------------------------------------------------
# Browser Use Cloud (optional browser automation for the agent)
Expand Down
13 changes: 13 additions & 0 deletions docs/google-maps-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ For a current driving estimate, the agent uses:
`OPTIMISTIC` and `PESSIMISTIC` are also supported for driving. Non-driving
modes use `NONE` because Google traffic models are limited to driving routes.

## Saved routes and scheduled updates

When SQLite storage is enabled, Blacki can save common routes and schedule
recurring traffic checks. It persists only Google place IDs, user-authored
labels, route preferences, and reminder metadata. Raw addresses, API responses,
and traffic snapshots are not stored.

Saved-route operations are owner-scoped. In Telegram they are intentionally
limited to private chats because group and topic sessions use a shared chat
identity. `GOOGLE_MAPS_SAVED_ROUTE_LIMIT` defaults to 20 saved routes per user,
and `GOOGLE_MAPS_ROUTE_UPDATE_LIMIT` defaults to 10 active traffic updates.
Scheduled checks must be at least 15 minutes apart.

## Location and time inputs

Plain location strings are sent as addresses. A known Google place ID can be
Expand Down
18 changes: 18 additions & 0 deletions src/blacki/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from blacki.calories.storage import SqliteCalorieStorage
from blacki.declarative_db.storage import SqliteDeclarativeDbStorage
from blacki.reminders.storage import SqliteReminderStorage
from blacki.routes.storage import SqliteSavedRouteStorage
from blacki.utils.preferences import SqlitePreferencesStorage
from blacki.workouts.storage import SqliteWorkoutStorage

Expand Down Expand Up @@ -138,6 +139,9 @@ class AppContainer:
_declarative_db_storage: SqliteDeclarativeDbStorage | None = field(
default=None, init=False, repr=False
)
_saved_route_storage: SqliteSavedRouteStorage | None = field(
default=None, init=False, repr=False
)

@classmethod
async def create(cls, sqlite_path: str | Path) -> Self:
Expand Down Expand Up @@ -182,6 +186,10 @@ async def _close_storages(self) -> None:
await self._declarative_db_storage.close()
self._declarative_db_storage = None

if self._saved_route_storage is not None:
await self._saved_route_storage.close()
self._saved_route_storage = None

async def initialize_all_storages(self) -> None:
"""Initialize all storage instances.

Expand All @@ -193,6 +201,7 @@ async def initialize_all_storages(self) -> None:
await self.workout_storage.initialize()
await self.preferences_storage.initialize()
await self.declarative_db_storage.initialize()
await self.saved_route_storage.initialize()

@property
def lock(self) -> asyncio.Lock:
Expand Down Expand Up @@ -245,3 +254,12 @@ def declarative_db_storage(self) -> SqliteDeclarativeDbStorage:
self.conn, self._lock
)
return self._declarative_db_storage

@property
def saved_route_storage(self) -> SqliteSavedRouteStorage:
"""Get or create the saved-route storage instance."""
if self._saved_route_storage is None:
from blacki.routes.storage import SqliteSavedRouteStorage

self._saved_route_storage = SqliteSavedRouteStorage(self.conn, self._lock)
return self._saved_route_storage
25 changes: 24 additions & 1 deletion src/blacki/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,18 @@
as the traffic model for non-driving modes. Treat avoid options as preferences,
not guarantees, and preserve all provider warnings and Google Maps attribution.
Ask one focused question when an endpoint or required departure time is missing.

Use save_common_route, update_common_route, and delete_common_route only after
an explicit user request to mutate a saved route. Never infer that a route
should be saved from a route lookup. Use user-authored labels and ask for a
more precise endpoint when place resolution is ambiguous. Use
list_common_routes for discovery and check_common_route for every fresh saved
route estimate. Saved routes are point-in-time checks, not live tracking.

Use schedule_common_route_update only when the user explicitly asks for
recurring traffic updates and supplies a schedule. Do not simulate continuous
tracking. A scheduled route update must use check_common_route and return its
summary with Google Maps attribution; never guess when a lookup fails.
</routes_policy>"""


Expand Down Expand Up @@ -199,7 +211,18 @@
}
),
"reminder": frozenset({"schedule_reminder", "list_reminders", "cancel_reminder"}),
"routes": frozenset({"get_route_estimate", "compare_route_scenarios"}),
"routes": frozenset(
{
"get_route_estimate",
"compare_route_scenarios",
"save_common_route",
"list_common_routes",
"check_common_route",
"update_common_route",
"delete_common_route",
"schedule_common_route_update",
}
),
"search": frozenset({"exa_search", "brave_search"}),
}

Expand Down
31 changes: 27 additions & 4 deletions src/blacki/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ def build_tools(config: ToolConfig) -> list[Any]:
logger.info("Brave Search tool enabled")

if config.google_maps_routes_api_key:
tools.extend(_build_google_routes_tools())
tools.extend(
_build_google_routes_tools(include_saved_routes=bool(config.sqlite_path))
)
logger.info("Google Maps Routes tools enabled")

if config.sqlite_path:
Expand Down Expand Up @@ -109,12 +111,33 @@ def _build_brave_search_tools() -> list[Any]:
return []


def _build_google_routes_tools() -> list[Any]:
def _build_google_routes_tools(*, include_saved_routes: bool = False) -> list[Any]:
"""Build Google Maps Routes tools."""
try:
from blacki.routes import compare_route_scenarios, get_route_estimate
from blacki.routes import (
check_common_route,
compare_route_scenarios,
delete_common_route,
get_route_estimate,
list_common_routes,
save_common_route,
schedule_common_route_update,
update_common_route,
)

return [get_route_estimate, compare_route_scenarios]
tools = [get_route_estimate, compare_route_scenarios]
if include_saved_routes:
tools.extend(
[
save_common_route,
list_common_routes,
check_common_route,
update_common_route,
delete_common_route,
schedule_common_route_update,
]
)
return tools
except ImportError as e:
logger.warning("Failed to load Google Maps Routes tools: %s", e)
return []
Expand Down
5 changes: 4 additions & 1 deletion src/blacki/reminders/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,12 @@ def _parse_reminder_datetime(datetime_val: str | int | float) -> datetime:
def _format_reminder(reminder: Reminder) -> dict[str, Any]:
"""Format a reminder for display."""
next_trigger_time = format_stored_instant_for_display(reminder.trigger_time)
message = reminder.message
if message.startswith('{"kind":"blacki.route_traffic_update"'):
message = "Scheduled traffic update for a saved route"
return {
"id": reminder.id,
"message": reminder.message,
"message": message,
"trigger_time": next_trigger_time,
"next_trigger_time": next_trigger_time,
"is_sent": reminder.is_sent,
Expand Down
16 changes: 16 additions & 0 deletions src/blacki/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
"""Google Maps Routes API tools."""

from .client import close_shared_routes_client
from .common_tools import (
CommonRouteChanges,
check_common_route,
delete_common_route,
list_common_routes,
save_common_route,
schedule_common_route_update,
update_common_route,
)
from .tools import RouteScenario, compare_route_scenarios, get_route_estimate

__all__ = [
"CommonRouteChanges",
"RouteScenario",
"check_common_route",
"close_shared_routes_client",
"compare_route_scenarios",
"delete_common_route",
"get_route_estimate",
"list_common_routes",
"save_common_route",
"schedule_common_route_update",
"update_common_route",
]
Loading