These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

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.geminiconfig/services.php:49-58

Required env vars

VariablePurpose
GEMINI_API_KEYBearer API key sent as ?key= query param on every Gemini request
GEMINI_MODELModel name override (defaults to gemini-2.5-flash-lite)
GEMINI_2_5_FLASH_LITE_INPUT_PER_MILLIONCost per 1M input tokens (default 0.10) — for cost tracking only
GEMINI_2_5_FLASH_LITE_OUTPUT_PER_MILLIONCost per 1M output tokens (default 0.40) — for cost tracking only
GOOGLE_API_KEYGoogle 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 seconds
  • Http::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

  1. Builds a structured prompt via buildPrompt()AiLocationImportController.php:135-248. The prompt requests a JSON object with a locations array; 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.
  2. Posts to https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey}.
  3. Extracts candidates[0].content.parts[0].text, strips markdown fences via stripJsonFences(), then json_decode()s the result.
  4. Returns ['model_name', 'usage_metadata', 'locations'].

Prompt version: v4AiLocationImportController.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

StatusMeaning
draftDefault; batch has items pending review
importedAll items have been imported (none remain pending/approved)
discardedBatch was deleted by the user

Item statuses: pending_review, approved, rejected, imported, failed.

Jobs

JobQueuetimeouttriesNotes
GenerateAiLocationImportJobaigen180 s2Calls generateIntoBatch(); moves process to waiting_review on success
ApplyAiLocationImportJobai_apply180 s2Calls importApprovedBatch(); dispatched synchronously via dispatchSync() from the controller — AiLocationImportController.php:1331

⚠️ Warning: ApplyAiLocationImportJob is dispatched with dispatchSync(), 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:

MethodPathHandler
GET/ai-location-importsindex
POST/ai-location-importsstore — 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}/importimport — 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

  1. 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).
  2. Posts to the same Gemini endpoint with the same connect/timeout settings.
  3. Expects a JSON root with location_updates (array of per-location suggestions) and group_recommendations (structural suggestion object).
  4. Returns ['model_name', 'usage_metadata', 'location_updates', 'group_recommendations'].

Prompt version: v1AiLocationImprovementController.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

StatusMeaning
draftHas items pending review
appliedAll items applied (none remain pending/approved)
discardedDeleted by user

Item statuses: pending_review, approved, rejected, applied, failed.

Jobs

JobQueuetimeouttriesNotes
GenerateAiLocationImprovementJobaigen180 s2Calls generateIntoBatch(); moves process to waiting_review on success
ApplyAiLocationImprovementJobai_apply180 s2Dispatched 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:

MethodPathHandler
GET/ai-location-improvementsindex
POST/ai-location-improvementsstore — creates batch + dispatches generate job
GET/ai-location-improvements/{id}show
PATCH/ai-location-improvements/{batchId}/items/{itemId}updateItem
PATCH/ai-location-improvements/{id}/groupsupdateGroups
PATCH/ai-location-improvements/{id}/recommendations/{recId}updateRecommendation
POST/ai-location-improvements/{id}/applyapply — dispatches apply job
POST/ai-location-improvements/{id}/finalize-recommendationsfinalizeRecommendations
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: low if any validation errors; high if ≥ 6 core fields filled and ≤ 1 warning; otherwise mediumAiLocationImportController.php:687-716.
  • Improvement: low if any validation errors; high if ≥ 3 fields changed and 0 warnings; otherwise mediumAiLocationImprovementController.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 jobs
  • POST /admin/ai-logs/{process}/cancel — cancel a running job
  • DELETE /admin/ai-logs/finished — clear completed/failed records
  • DELETE /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.]