# AI Analytics Injection Instructions

## Goal

Inject the existing shared analytics service into the OpenAI-backed flows so every relevant AI request can send usage logs to the Zavedenia analytics API.

Use the already-created shared service:

- `app/shared/services/ai_analytics.py`

Do not create another analytics client or duplicate HTTP POST logic inside business services.

---

## Scope

Inject analytics for these flows:

- `translation`
- `wine_pairing`
- `beer_pairing`
- `menu_extraction`
- `create_restaurant_offer` text translation step

Do not change request or response formats of existing public endpoints.

Do not add analytics logic directly inside route handlers unless strictly necessary for extracting request metadata.

Prefer one central integration point through the shared OpenAI client.

---

## Existing Building Blocks

### Shared analytics service

File:

- `app/shared/services/ai_analytics.py`

Current responsibilities:

- reads analytics config from env
- posts usage payload to `https://api.zavedenia.com/ai/AiUsageLogs.php`
- supports:
  - `city`
  - `zavedenia_id`
  - `service`
  - `model`
  - `tokens_input`
  - `tokens_output`
  - `data_input`
  - `data_output`
  - `execution_time`

### Shared OpenAI client

File:

- `app/shared/clients/openai_client.py`

This is the correct primary injection point because all JSON/text OpenAI calls already pass through it.

---

## Required Design

### 1. Inject analytics service into `UnifiedOpenAIClient`

Update `app/shared/clients/openai_client.py`:

- add a nullable analytics service dependency on the client instance
- add a setter, for example:
  - `set_analytics_service(self, analytics_service)`
- keep analytics best-effort
  - analytics failure must never fail the main OpenAI request
  - only log warning/error when analytics submission fails

### 2. Initialize analytics service in `app/main.py`

Create the analytics service once during startup and inject it into the shared OpenAI client.

Expected pattern:

- load `AiAnalyticsConfig.from_env()`
- construct `AiAnalyticsService`
- call `openai_client.set_analytics_service(...)`
- close it on shutdown

Also add the service auth env if needed:

- `AI_ANALYTICS_SERVICE_KEY`

If missing, fall back to the global auth key the same way other services do.

### 3. Extend `json_completion(...)`

In `app/shared/clients/openai_client.py`, extend the `json_completion(...)` signature with optional analytics context.

Recommended argument:

```python
analytics_context: Optional[Dict[str, Any]] = None
```

This context should carry optional restaurant metadata and any extra request-specific payload fields needed for analytics.

Recommended keys:

- `city`
- `zavedenia_id`
- `data_input`
- `data_output`

### 4. Build analytics payload after successful OpenAI completion

After a successful OpenAI response:

- compute execution time in milliseconds
- extract usage information from `response.usage`
- extract:
  - `prompt_tokens`
  - `completion_tokens`
  - `total_tokens`
  - `cached_tokens` when available
  - `reasoning_tokens` when available
- extract model from the actual response model
- extract finish reason from `response.choices[0].finish_reason`

Then call the analytics service.

### 5. Analytics must be non-blocking from a business perspective

The analytics call itself can still be awaited, but:

- if analytics fails, swallow the exception after logging it
- return the OpenAI result normally

Do not raise analytics-related exceptions to API consumers.

---

## Payload Mapping Rules

Map OpenAI usage into this shape:

```json
{
  "service": "openai",
  "model": "gpt-4o-mini",
  "tokens_input": {
    "prompt_tokens": 1240,
    "cached_tokens": 180,
    "total_tokens": 1420
  },
  "tokens_output": {
    "completion_tokens": 356,
    "reasoning_tokens": 44,
    "total_tokens": 400
  },
  "data_input": {},
  "data_output": {},
  "execution_time": 1487
}
```

### Important

`service` in the analytics payload should be:

- `"openai"` for OpenAI-backed calls

Do not send internal app service names like `translation` or `wine_pairing` in the top-level analytics `service` field unless the analytics contract is intentionally changed.

If you want to preserve the internal app service name, include it inside `data_input`, for example:

```json
{
  "app_service": "translation"
}
```

---

## Data Input Rules

Build `data_input` from the original call.

### For prompt-based calls

Recommended structure:

```json
{
  "type": "chat_completion",
  "user_message": "...",
  "max_tokens": 500,
  "temperature": 0,
  "operation_id": "wine_pairing",
  "app_service": "wine_pairing"
}
```

### For messages-based calls

Store a sanitized representation of the messages array.

Important for menu extraction:

- never send raw base64 image data URLs to analytics
- replace image data URLs with a placeholder like:
  - `"[omitted_data_url]"`

### If a service has extra business metadata

Merge it into `data_input`.

Examples:

- target language
- source language
- filename
- occasion
- user prompt

---

## Data Output Rules

Build `data_output` from the parsed JSON response.

Recommended structure:

```json
{
  "type": "chat_completion_response",
  "content": { ...parsed_json... },
  "finish_reason": "stop"
}
```

If parsed JSON is unavailable, use a truncated raw content string.

---

## Service-Specific Context Rules

### Translation service

File:

- `app/services/translation/service.py`

Pass `analytics_context` for each OpenAI request.

At minimum include:

- `data_input.type = "chat_completion"`
- `data_input.app_service = "translation"`

If available, also include:

- source language
- target languages
- request type
- whether English is included

### Wine pairing service

File:

- `app/services/wine_pairing/service.py`

Pass:

- `data_input.app_service = "wine_pairing"`
- request language
- optional user prompt
- counts:
  - number of foods
  - number of wines

For Vivino lookup calls:

- still log analytics
- use:
  - `data_input.app_service = "vivino_lookup"`

### Beer pairing service

File:

- `app/services/beer_pairing/service.py`

Pass:

- `data_input.app_service = "beer_pairing"`
- request language
- optional user prompt
- counts:
  - number of foods
  - number of beers

For Untappd lookup calls:

- log separately
- use:
  - `data_input.app_service = "untappd_lookup"`

### Menu extraction service

File:

- `app/services/menu_extraction/service.py`

Pass:

- `data_input.app_service = "menu_extraction"`
- source language
- target language
- batch index
- total batches

Do not send raw image bytes or base64 image URLs to analytics.

If `city` is available upstream, pass it through `analytics_context`.

### Insert menu items service

File:

- `app/services/insert_menu_items/service.py`

This service does not call OpenAI directly, but it does call `menu_extraction_service.extract_menu(...)`.

Use this service to pass downstream analytics metadata into menu extraction, for example:

- `city`
- filename
- source language
- target language

### Create restaurant offer service

File:

- `app/services/create_restaurant_offer/service.py`

Inject analytics for the text translation step only, because that step uses `openai_client.json_completion(...)`.

Pass:

- `data_input.app_service = "create_restaurant_offer"`
- occasion
- target language
- optionally `zavedenia_id` if it can be derived safely from the request

Do not change the image generation flow unless you explicitly decide to support analytics for image generation separately.

---

## Restaurant Metadata Rules

Top-level analytics fields:

- `city`
- `zavedenia_id`

These should be optional.

Do not invent values.

If a service/request does not currently expose them, send nothing for those fields.

Known sources today:

- `city` is available in insert-menu-items flow
- `zId` / `offer_id` may be available in create-restaurant-offer flow, but only map to `zavedenia_id` if the value is truly numeric and semantically the same identifier

---

## Safety Constraints

- do not break existing endpoint schemas
- do not duplicate analytics POST code inside each service
- do not send secrets inside `data_input` or `data_output`
- do not send raw image base64 payloads
- do not fail the main request because analytics failed
- keep analytics integration additive and backward compatible

---

## Recommended Implementation Order

1. Update `app/shared/clients/openai_client.py`
2. Wire analytics startup/shutdown in `app/main.py`
3. Pass `analytics_context` from:
   - `translation`
   - `wine_pairing`
   - `beer_pairing`
   - `menu_extraction`
   - `create_restaurant_offer`
4. Pass upstream context from `insert_menu_items` into `menu_extraction`
5. Add tests

---

## Minimum Tests

Add tests that verify:

1. analytics is called after successful OpenAI response
2. analytics failure does not fail the main request
3. payload includes token usage correctly
4. `cached_tokens` defaults to `0` when absent
5. `reasoning_tokens` defaults to `0` when absent
6. image data URLs are sanitized in menu extraction analytics
7. optional `city` and `zavedenia_id` are passed only when present

---

## Expected End Result

After implementation:

- every OpenAI JSON completion should optionally emit analytics
- the analytics integration should live centrally in the shared OpenAI client
- business services should only provide contextual metadata
- existing public API contracts must remain unchanged
