Backend — AI services
Two AI-powered batch flows — Import (generate draft locations from a guided questionnaire) and Improvement (suggest improvements to existing locations). Both call Google Gemini directly over HTTP, write results to a batch model, and surface progress via the Process / ProcessService state machine.
For provider details (Gemini model name, pricing config, env vars), see Backend → Third-party integration.
Provider
Provider: Google Gemini via generativelanguage.googleapis.com/v1beta REST API
Model: gemini-2.5-flash-lite (default, configurable via GEMINI_MODEL) — config/services.php:51
Config key: services.gemini — config/services.php:49-58
Required env vars
| Variable | Purpose |
|---|---|
GEMINI_API_KEY | Bearer API key sent as ?key= query param on every Gemini request |
GEMINI_MODEL | Model name override (defaults to gemini-2.5-flash-lite) |
GEMINI_2_5_FLASH_LITE_INPUT_PER_MILLION | Cost per 1M input tokens (default 0.10) — for cost tracking only |
GEMINI_2_5_FLASH_LITE_OUTPUT_PER_MILLION | Cost per 1M output tokens (default 0.40) — for cost tracking only |
GOOGLE_API_KEY | Google Places / Geocoding API key — used in the post-processing enrichment step, not the Gemini call |
HTTP client configuration
Both controllers call Gemini with identical HTTP settings — AiLocationImportController.php:479-499, AiLocationImprovementController.php:527-545:
Http::connectTimeout(10)— TCP connect timeout, 10 secondsHttp::timeout(90)— total response timeout, 90 seconds- Generation config:
temperature: 0.2,responseMimeType: 'application/json'
A ConnectionException is caught and re-thrown as a ValidationException with a user-visible message. HTTP error responses (non-2xx) are also caught and surfaced as ValidationException, with the upstream Gemini error message included only in local (app()->isLocal()) environments.
The extendExecutionBudget() helper calls set_time_limit($seconds) (default 120 s) at the start of each Gemini request method — AiLocationImportController.php:42-46 — to prevent PHP’s max execution time from killing a long Gemini response. The apply path calls it with 300 s — AiLocationImportController.php:1067.
Import flow
Controller: app/Http/Controllers/AiLocationImportController.php
Purpose: Given a guided questionnaire (brand, business type, location count, geography, etc.), ask Gemini to generate draft location records. The user reviews drafts and approves/rejects before final import to the workspace.
Key method: requestGeminiDraft
AiLocationImportController.php:463-558
- Builds a structured prompt via
buildPrompt()—AiLocationImportController.php:135-248. The prompt requests a JSON object with alocationsarray; each element has name, address, city, state, zip, country, phone, website, lat, lng, tags, categories, and custom_fields. Custom field definitions from the workspace are injected into the prompt as a hint. - Posts to
https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey}. - Extracts
candidates[0].content.parts[0].text, strips markdown fences viastripJsonFences(), thenjson_decode()s the result. - Returns
['model_name', 'usage_metadata', 'locations'].
Prompt version: v4 — AiLocationImportController.php:40. Stored on the batch record for reproducibility.
Google Places enrichment
After Gemini returns draft locations, each item is passed through enrichDraftPayloadWithGoogle() — AiLocationImportController.php:381-461. This calls the Google Places Text Search API then the Places Details API to fill missing address components, phone, website, hours, and coordinates. The enrichment step is best-effort; failures return the original payload unchanged.
Batch lifecycle
| Status | Meaning |
|---|---|
draft | Default; batch has items pending review |
imported | All items have been imported (none remain pending/approved) |
discarded | Batch was deleted by the user |
Item statuses: pending_review, approved, rejected, imported, failed.
Jobs
| Job | Queue | timeout | tries | Notes |
|---|---|---|---|---|
GenerateAiLocationImportJob | aigen | 180 s | 2 | Calls generateIntoBatch(); moves process to waiting_review on success |
ApplyAiLocationImportJob | ai_apply | 180 s | 2 | Calls importApprovedBatch(); dispatched synchronously via dispatchSync() from the controller — AiLocationImportController.php:1331 |
⚠️ Warning:
ApplyAiLocationImportJobis dispatched withdispatchSync(), meaning it runs in the HTTP request cycle, not the queue. This is intentional (the apply step is fast), but it means the apply request blocks until all approved items are persisted. For large batches,extendExecutionBudget(300)is called to raise the PHP time limit —AiLocationImportController.php:1067.
Generate job is enqueued with GenerateAiLocationImportJob::dispatch($process->id)->onQueue(QueueName::Aigen->value) — AiLocationImportController.php:1236.
Route surface
Registered under ['auth:api', 'auth_session', 'audit'] middleware — routes/api.php:69-74:
| Method | Path | Handler |
|---|---|---|
GET | /ai-location-imports | index |
POST | /ai-location-imports | store — creates batch + dispatches generate job |
GET | /ai-location-imports/{id} | show |
PATCH | /ai-location-imports/{batchId}/items/{itemId} | updateItem — approve/reject/edit a draft |
POST | /ai-location-imports/{id}/import | import — auto-approves valid pending items, dispatches apply job |
DELETE | /ai-location-imports/{id} | destroy — sets status to discarded |
Improvement flow
Controller: app/Http/Controllers/AiLocationImprovementController.php
Purpose: Given an existing workspace’s location dataset, ask Gemini to suggest per-location field improvements and group-level structural recommendations (new tag, new category, tag merge, tag split). The user reviews suggestions and applies selected ones.
Key method: requestGeminiImprovement
AiLocationImprovementController.php:511-601
- Builds a structured prompt via
buildPrompt(dataset, options, customFieldDefinitions)—AiLocationImprovementController.php:406-509. The full workspace location dataset (serialized to JSON) is embedded in the prompt, plus workspace metadata, active map/finder context, and option flags (prioritize_missing_fields,allow_new_tags,allow_new_categories,prioritize_naming_cleanup). - Posts to the same Gemini endpoint with the same connect/timeout settings.
- Expects a JSON root with
location_updates(array of per-location suggestions) andgroup_recommendations(structural suggestion object). - Returns
['model_name', 'usage_metadata', 'location_updates', 'group_recommendations'].
Prompt version: v1 — AiLocationImprovementController.php:43.
Token usage tracking
Both controllers call extractUsageMetadata() — AiLocationImportController.php:98-114, AiLocationImprovementController.php:178-194. This extracts promptTokenCount, candidatesTokenCount, totalTokenCount, thoughtsTokenCount, and toolUsePromptTokenCount from usageMetadata in the Gemini response and stores them as usage_metadata on the batch record. The raw Gemini usageMetadata object is preserved under the raw key.
Group recommendations
AiLocationImprovementController.php:603-656 — the improvement flow returns up to 5–12 group-level recommendations alongside per-location suggestions. Valid types: new_tag, new_category, merge, split. Each recommendation gets a deterministic 8-char id (md5 of type + payload fields). Recommendations are stored in group_recommendations on the batch; user decisions are stored in group_user_payload.
Accepted recommendations are applied via finalizeRecommendations() / applyAcceptedRecommendations() — AiLocationImprovementController.php:1444-1566. Tag merges are executed as two SQL operations: delete pivot rows where the variant location already has the target tag, then UPDATE location_tag SET tag_id = {target} for the remainder.
Batch lifecycle
| Status | Meaning |
|---|---|
draft | Has items pending review |
applied | All items applied (none remain pending/approved) |
discarded | Deleted by user |
Item statuses: pending_review, approved, rejected, applied, failed.
Jobs
| Job | Queue | timeout | tries | Notes |
|---|---|---|---|---|
GenerateAiLocationImprovementJob | aigen | 180 s | 2 | Calls generateIntoBatch(); moves process to waiting_review on success |
ApplyAiLocationImprovementJob | ai_apply | 180 s | 2 | Dispatched asynchronously via dispatch()->onQueue() — AiLocationImprovementController.php:1405 |
The improvement apply job is queued (unlike the import apply job which runs synchronously). Progress is reported via ProcessService events.
Route surface
routes/api.php:76-84:
| Method | Path | Handler |
|---|---|---|
GET | /ai-location-improvements | index |
POST | /ai-location-improvements | store — creates batch + dispatches generate job |
GET | /ai-location-improvements/{id} | show |
PATCH | /ai-location-improvements/{batchId}/items/{itemId} | updateItem |
PATCH | /ai-location-improvements/{id}/groups | updateGroups |
PATCH | /ai-location-improvements/{id}/recommendations/{recId} | updateRecommendation |
POST | /ai-location-improvements/{id}/apply | apply — dispatches apply job |
POST | /ai-location-improvements/{id}/finalize-recommendations | finalizeRecommendations |
DELETE | /ai-location-improvements/{id} | destroy |
Confidence scoring
Both flows compute a confidence value (high, medium, low) for each item and store it on the item record:
- Import:
lowif any validation errors;highif ≥ 6 core fields filled and ≤ 1 warning; otherwisemedium—AiLocationImportController.php:687-716. - Improvement:
lowif any validation errors;highif ≥ 3 fields changed and 0 warnings; otherwisemedium—AiLocationImprovementController.php:717-735.
Admin AI log management
routes/api.php:163-167 — admin-only routes under role:admin:
GET /admin/ai-logs— list all process records for AI jobsPOST /admin/ai-logs/{process}/cancel— cancel a running jobDELETE /admin/ai-logs/finished— clear completed/failed recordsDELETE /admin/ai-logs/{process}— delete a specific record
Handled by AdminConsoleController.
Per-plan limits
🔴 [NEEDS CLARIFICATION: No per-tier AI usage limits were found in the middleware stack or controller logic. EnsureSubscriptionTier is not applied to the AI import or improvement routes in routes/api.php. Whether rate limits or quota enforcement exist at the infrastructure layer (queue throttling, Gemini quota) is not confirmed.]