# Agents — Brigata di Cucina

Dettaglio dei 7 agenti LLM, 2 componenti deterministici e lo **Strategy Engine** che compongono il sistema.

## Strategy Engine (NUOVO — Deterministic)

**Modello:** nessuno (deterministico) | **File:** `strategy_engine.py`

Cervello analitico del ristorante. Legge le tabelle `intel_*` (populate da `intel_scraper` ogni ~5min) e produce raccomandazioni data-driven per ogni turno.

**Input (da intel_* tables):**
- `intel_all_bids` → prezzi vincenti per ingrediente, competizione
- `intel_competitor_menus` → prezzi piatti dei concorrenti
- `intel_competitor_meals` → piatti più ordinati dai clienti, success rate
- `intel_rankings` → classifica ristoranti
- `intel_blog_facts` → fatti strategici dal blog Cronache del Cosmo
- `intel_blog_client_profiles` → profili strutturati dei clienti (velocità, prezzo, qualità, rarità, cultura)

**Output:**
```python
{
    "recipe_pool": [...],       # 14 ricette ordinate per profittabilità
    "bid_prices": {...},        # {ingredient: bid_per_unit} calibrato su intel
    "ingredient_needs": {...},  # {ingredient: total_qty_needed}
    "n_recipes": 14,
    "servings_per_recipe": 1-3, # auto-regolato su demand storico
    "max_auction_budget": int,  # max 20% del bilancio
    "competitive_intel": {...}, # summary per Chef LLM context
}
```

**Auto-regolazione:**
- Servings: parte da 1, sale a 2-3 se demand > capacity (basato su turn_summary)
- Bid prices: calibrati su avg_winning_price da intel_all_bids + margine per competizione
- Menu prices: 10% sotto competitors (da intel_competitor_menus) oppure tier per prestige
- Recipe selection: scoring basato su popolarità, prestige, #ingredienti, overlap

---

## Patron (Orchestratore)

**Modello:** gpt-oss-120b | **File:** `agents/patron.py`

Cervello strategico del ristorante. Governa le transizioni di fase, attiva gli agenti.

**Tool MCP:** `update_restaurant_is_open`

| Fase | Azione |
|---|---|
| game_started | Sync ricette, legge turn_summary precedente, chiama Strategy Engine |
| speaking | Invia recipe_pool + competitive_intel + previous_notes al Chef |
| closed_bid | Invia budget e bid_prices al Chef |
| waiting | Invia direttiva al Chef per finalizzare il menu |
| serving | Nessuna azione (Maitre + Brigata autonomi) |
| stopped | Attiva Evaluator per analisi |

**Cambiamento chiave:** il Patron NON calcola più la strategia internamente — delega tutto allo Strategy Engine. Tutte le direttive di fase (speaking, closed_bid, waiting) vanno allo **Chef**, che gestisce l'intero ciclo.

---

## Chef (Analisi Strategica + Aste + Menu — 3 Fasi Multi-Turn)

**Modello:** gpt-oss-120b | **File:** `agents/chef.py`

Gestisce l'**intero ciclo** del turno: analisi strategica → asta ingredienti → menu finale.
Usa un Agent **stateful** (`stateless=False`) con `Memory` per mantenere il contesto LLM tra le 3 fasi.

**Tool MCP:** `save_menu`, `closed_bid`

**Conversazione multi-turn (3 fasi nella stessa sessione LLM):**

**Fase 1 — speaking (analisi strategica, NO tool):**
1. Riceve dal Patron: `recipe_pool`, `competitive_intel`, `previous_notes`, `price_tiers`
2. Raccoglie contesto ricco: blog intel, turn summary, prezzi concorrenti
3. **L'LLM analizza** tutti i dati e produce un'analisi strategica
4. La risposta viene salvata in memory per le fasi successive
5. `planned_recipes` = pool candidato completo (l'asta è per tutti)

**Fase 2 — closed_bid (asta, tool `closed_bid`):**
1. Riceve dal Patron: budget + bid_prices (pre-calcolati da Strategy Engine)
2. Costruisce la lista bids deterministicamente: `[{ingredient, bid, quantity}]`
3. Safety: se costo totale > budget, scala proporzionalmente
4. **L'LLM chiama `closed_bid`** con parametri ESATTI (continua la conversazione)

**Fase 3 — waiting (menu finale, tool `save_menu`):**
1. Verifica inventario reale post-asta (API refresh)
2. Determina ricette fattibili con inventario attuale
3. Se <3 fattibili: scansiona tutte le ricette DB come backup (solo ≤6 ingredienti)
4. **L'LLM DECIDE il menu** con pieno contesto delle fasi 1+2, chiama `save_menu`
5. Fallback: selezione deterministica + MCP diretto se LLM fallisce
6. Notifica Patron (`menu_ready`)

**Memory reset:** a ogni `turn_started`, `reset_memory()` cancella la conversazione.

**Cambiamento chiave:** Lo Chef gestisce tutto il ciclo in un'unica conversazione multi-turn. Non si appoggia più al Magazzini per aste/inventario. Il Datapizza monitoring mostra un singolo trace "agent chef" con 3 interazioni LLM nella stessa sessione.

---

## Magazzini (IDLE — Solo Mercato, Disattivato)

**Modello:** gpt-oss-120b | **File:** `agents/magazzini.py`

**Stato attuale:** inattivo. Non riceve più direttive dal Patron.
L'asta è gestita direttamente dal Chef. Il mercato (`ENABLE_MARKET_SELLING`, `ENABLE_MARKET_BUYING`) è disattivato.

**Tool MCP:** `create_market_entry`, `execute_transaction`, `delete_market_entry`

L'agente resta istanziato ma idle. Potrebbe essere riattivato in futuro per market buying post-asta.

---

## Maitre (Servizio Clienti)

**Modello:** gpt-oss-120b | **File:** `agents/maitre.py`

Interpreta ordini e avvia preparazioni. Gestisce più clienti in parallelo.

**Tool MCP:** `prepare_dish`

**Flusso per ogni `client_spawned`:**
1. LLM interpreta `orderText` e identifica il piatto dal menu
2. Controlla `known_intolerances` per il tipo di cliente
3. Se OK: chiama `prepare_dish` (HTTP 200 immediato, non bloccante)
4. Registra in `pending_orders`

**Nota:** Emergency buy rimosso — era broken e causava race condition nella Brigata (cambiava lo status degli ordini da "preparing" a "failed_missing_ingredients" prima che la Brigata potesse servire il piatto).

---

## Brigata (Middleware Serving)

**Modello:** nessuno (deterministico) | **File:** `infra/brigata.py`

Processo deterministico in ascolto di `preparation_complete`. Non usa LLM.

**Tool MCP:** `serve_dish`

**Flusso per ogni `preparation_complete`:**
1. Cerca in `pending_orders` il primo cliente che aspetta quel piatto
2. Chiama `serve_dish(dish_name, client_id)`
3. Logga l'esito in `service_log`
4. Se fallisce per intolleranza: scrive in `known_intolerances`

**Matching strategy:** La ricerca del client è robusta:
- Normalizzazione nomi piatti: strip + lowercase + rimozione accenti (NFKD)
- Fuzzy matching: substring match accent-insensitive come fallback
- Retry: fino a 12 tentativi × 0.5s (≈6s) per gestire la race condition con Maitre
- SSE key variants: supporta `dish`, `dishName`, `dish_name`
- Diagnostic logging: quando non trova match, logga tutti gli ordini pendenti

---

## PR (Comunicazioni Esterne)

**Modello:** gpt-oss-120b (CAMBIATO da 20b) | **File:** `agents/pr.py`

Gestisce tutte le comunicazioni con gli altri ristoranti.

**Tool MCP:** `send_message`

---

## Evaluator (Analisi Post-Turno)

**Modello:** gpt-oss-120b (CAMBIATO da 20b) | **File:** `agents/evaluator.py`

Analizza le performance del turno e produce insight strutturati per il turno successivo.

**Lettura dati:** legge da `intel_all_bids` (via API + fallback DB), `service_log`, `market_history`, menu corrente.

**Pre-aggregazione dati (deterministico, nel codice Python):**
Il codice computa PRIMA dell'LLM call:
- **Breakdown per piatto**: ordini, serviti, falliti, revenue per piatto; piatti a menu mai ordinati
- **Breakdown per archetipo cliente**: clienti per tipo (Esploratore, Astrobarone, Saggio, Famiglia), serviti vs falliti
- **Economia del turno**: revenue (piatti + mercato), costi (aste + mercato), profitto netto
- **Dettaglio aste**: per ogni ingrediente: offerto, pagato, vinto/perso, quantità
- **Spreco ingredienti**: inventario residuo a fine turno, per ingrediente
- **Intolleranze**: eventi di intolleranza rilevati nel turno

**Output LLM — solo formattazione MD, nessun consiglio:**
L'LLM formatta i dati pre-aggregati in 6 sezioni markdown fattuali (nessuna raccomandazione):
1. BILANCIO (revenue, costi, profitto, saldo)
2. PIATTI (per ogni piatto: ordini, serviti, falliti, revenue)
3. CLIENTI (per tipo: arrivati, serviti, falliti)
4. ASTE (per ingrediente: offerta, prezzo, esito)
5. SPRECO (ingredienti rimasti a fine turno)
6. INTOLLERANZE (eventi rilevati)

**Consumatori delle notes:**
- **Patron** → legge `notes` e le passa al Chef come `previous_notes`
- **Chef LLM** → riceve metriche numeriche + notes nel prompt per la decisione menu
- **Strategy Engine** → legge solo campi numerici (`dishes_served`, `dishes_failed`) per `_compute_servings()`

---

## Stagista Nerd (Blog Intelligence)

**Modello:** gpt-oss-120b (chat completion diretta, NON Agent) | **File:** `agents/stagista.py`

Funzioni async stand-alone (non Agent, non MessageBus). Invocate dall'`intel_scraper` ogni volta che vengono scoperte nuove pagine sul blog Cronache del Cosmo (hackablog.datapizza.tech).

**Implementazione:** chiamate `httpx` dirette a `{REGOLO_BASE_URL}/chat/completions` con `response_format: {"type": "json_object"}` e `temperature: 0.0`. Nessun framework Agent — pura estrazione dati.

**Compiti:**
1. Per ogni articolo NEWS/EVENT: estrae un array di **fatti** strutturati (`intel_blog_facts`)
   - Tipi: `market_trend`, `pricing_signal`, `demand_shift`, `event`, `strategy_hint`, `regulation`
2. Per ogni articolo BIO: estrae un **profilo cliente** strutturato (`intel_blog_client_profiles`)
   - Campi: speed_preference, price_sensitivity, quality_expectation, rarity_preference, cultural_depth, summary

**Flusso:**
1. `intel_scraper` chiama `blog_scraper.scrape_blog()` per scoprire nuove pagine (solo se slug non già in DB)
2. Per ogni pagina nuova, chiama `await extract_facts_from_news()` o `await extract_profile_from_bio()`
3. **Retry automatico:** ad ogni ciclo, `scrape_and_process_blog()` cerca anche pagine NEWS senza facts e BIO senza profili in DB, e le riprocessa (gestisce fallimenti LLM transienti come rate-limit/timeout)
4. L'LLM è forzato a restituire JSON puro (response_format json_object)
5. I risultati vengono salvati in `intel_blog_facts` / `intel_blog_client_profiles`
6. Ogni chiamata LLM ha try/except individuale: un fallimento su un articolo non blocca gli altri

**Consumatori dei dati:**
- **Strategy Engine** → legge `get_blog_intel_markdown()`, lo include nel `competitive_intel` passato al Chef
- **Chef** → riceve il blog intel come markdown nel contesto competitivo
- **Maitre** → legge `get_blog_client_profile(client_name)` per ogni cliente che arriva

---

## SSE Middleware (Event Router)

**File:** `infra/sse.py`

| Evento SSE | Destinatario |
|---|---|
| `game_started` | Patron |
| `game_phase_changed` | Patron + broadcast |
| `client_spawned` | Maitre |
| `preparation_complete` | Brigata |
| `new_message` | PR |

---

## Data Flow (AGGIORNATO)

```
intel_scraper (ogni 5 min)
  → scrape /restaurants → intel_restaurant_snapshots, intel_rankings
  → scrape /meals → intel_competitor_meals
  → scrape /bid_history → intel_all_bids
  → scrape /market/entries → intel_market_entries
  → compute_price_benchmarks → intel_price_benchmarks
  → scrape_blog → intel_blog_pages (solo nuove pagine)
    → Stagista Nerd (LLM) → intel_blog_facts, intel_blog_client_profiles
    → retry automatico: riprocessa news senza facts e bio senza profili

game_started
  → Patron → StrategyEngine.compute_strategy()
      ← legge intel_all_bids, intel_competitor_menus, intel_competitor_meals, intel_rankings
      ← legge intel_blog_facts, intel_blog_client_profiles (via get_blog_intel_markdown)
      → produce recipe_pool, bid_prices, ingredient_needs
  → Patron → Chef: phase_directive(speaking) con recipe_pool + competitive_intel + previous_notes
  → Chef LLM (fase 1): analizza dati, produce analisi strategica (salvata in memory)

closed_bid
  → Patron → Chef: phase_directive(closed_bid) con budget + bid_prices
  → Chef LLM (fase 2, continua conversazione): chiama closed_bid con bids deterministici

waiting
  → Patron → Chef: phase_directive(waiting)
  → Chef LLM (fase 3, continua conversazione): verifica inventario → decide menu → save_menu
  → Chef → Patron: menu_ready
  → Patron: apre ristorante
```

---

## Comunicazione tra agenti

```
Patron ──phase_directive──→ Chef (speaking, closed_bid, waiting), PR, Evaluator
Chef ──menu_ready──→ Patron
PR ──trade_proposal──→ Magazzini (solo se market riattivato)
```

**Nota:** Lo Chef gestisce aste + menu direttamente. Magazzini è idle (non riceve più direttive).

---

## Feature Flags

| Flag | Default | Effetto |
|---|---|---|
| `ENABLE_SCALPING` | off | Magazzini compra ingredienti cheap per rivenderli |
| `ENABLE_MARKET_BUYING` | on | Magazzini cerca ingredienti mancanti sul mercato |
| `ENABLE_MARKET_SELLING` | **off** | ~~Magazzini vende surplus~~ (disattivato: nessuno compra) |
| `ENABLE_PROACTIVE_OUTREACH` | off | PR contatta proattivamente altri ristoranti |
| `ENABLE_EMERGENCY_BUY` | **off** | ~~Emergency buy durante serving~~ (rimosso: broken, causava race condition) |
