# AI Debate (Main · User Portal)

**Portal:** Main · **Routes:** `main.portal.debates.*` · **Tab:** "AI Debate" (next to PropertyLab AI) · **Gated by:** `['auth','main']`

## What it does
A member asks **one question** and a **panel of 5 AI models** answer it, then **debate each other** over
0–3 rounds, and finally **Property Lab AI delivers a synthesised verdict**. The whole run **streams live**
— every cell of the board fills in token-by-token. It's the "sharper second opinion" companion to the
single-model [AI Conversations](/docs/modules_handbook/main/ai-conversations/readMe.md) chat (the two are
tabs on the same area).

> The panel (config `ai.debate.debaters`): **Claude / OpenAI / Gemini / DeepSeek** as neutral
> "property analyst" debaters (each on its provider's default model, for diverse views) **+ Property Lab
> AI** (Claude **Opus** + the full PropertyLab persona — the house view + the verdict author; shown with
> the house icon `/images/logo/icon.png`, **not** the Anthropic logo, via a `logo` override on its
> `config('ai.debate.debaters')` entry, applied in `AiDebatesController::debatersForLead()`). Rounds:
> **0** = each model answers independently; **1–3** = each model sees the previous round's answers and
> rebuts. **Cost = 1 free credit per provider call** → `5 × (rounds+1) + 1` verdict = **6 / 11 / 16 / 21**
> credits for 0 / 1 / 2 / 3 rounds.

## How it works
- **Parallel — one streamed cell per request.** Each cell (`debater × round`, and the verdict) is its
  own SSE endpoint (`POST {id}/cell` `{debater, round}`). The **frontend orchestrates**: it fires **all
  of a round's debaters concurrently** (`Promise.all`) — so the panel streams **simultaneously** — and
  only starts the next round once the prior round's cells have **persisted** (rebuttal rounds + the
  verdict read the previous answers from the DB). After all cells, a small `POST {id}/finalize` sets the
  terminal status. `DebateRunner::streamCell()` runs one cell; the `StreamsServerSentEvents` trait
  (shared with AI Conversations) flushes each token immediately.
- **Funding ("perfect logic", per call) — reuses the chat policy.** Each cell resolves via
  `AiCreditService::planConversation($lead, $provider)`: **free credits first**, but only when the
  company (global) key for that provider exists; **then the member's own key**; with neither, that cell
  is **skipped** (no charge, marked unavailable). A credit is spent **once per successful credit-funded
  cell**, never on skip / fail / empty. The atomic, allowance-bounded `LeadAiCreditRepository::spend`
  keeps the free pool from overshooting even under the parallel cells.
- **Start rule (affordability).** A debate's full cost is `debaters × (rounds+1) + 1` (= **6 / 11 / 16 /
  21** for 0–3 rounds). The member can **pick any depth**, but a debate may only **start** when **free
  credits cover that cost _OR_ the member has their own key for all four providers** (which funds the
  overflow). The composer shows a live cost + warning, disabling **Run** only when neither holds;
  `AiDebatesController::canAffordRounds()` re-enforces this server-side in `store()` (so a direct API call
  can't bypass it). This is a UX/start gate — funding is still resolved per-cell at run time (above).
- **Storage (lead-keyed).** `ai_debates` (question, rounds, has_verdict, status, title) +
  `ai_debate_responses` — one cell per `(debater, round)` (+ the verdict, `debater = 'verdict'`,
  `round = rounds`; a **unique index** `(debate_id, debater, round)` makes the per-cell upsert atomic),
  with `provider`/`model`/`source` (credit|own)/`status`/`error`/tokens. Both extend `SoftDeleteModel` +
  `HasUuid` + `RecordsBlame`; deleting a debate soft-cascades its responses. Each cell is **persisted as
  it finishes**, so an interrupted run shows what completed on reload.
- **Idempotency, resume & retry.** Each cell endpoint is **per-cell locked** (`Cache::lock`) so the same
  cell can't run twice concurrently, and a cell already persisted **COMPLETE is skipped** (never re-run /
  re-charged). So a re-run **resumes**: completed cells are kept, only the unfinished ones run. `finalize`
  sets `COMPLETE` when ≥1 answer succeeded, else `FAILED` (so an all-skipped debate — e.g. before any key
  is configured — can be **retried** later); a `COMPLETE` debate is never re-run.
- **Create + auto-run.** `store` creates the debate (status PROCESSING) and redirects to `?d={uuid}`; the
  page **auto-runs** on load when the active debate is PROCESSING (fresh or interrupted → resumes); a
  FAILED debate shows a **Retry** button.
- **Frontend.** `Index.vue` owns the live `cells` map (keyed `${debater}#${round}`), seeds it from the
  persisted responses on open, and updates it from the stream events. `DebateBoard` lays out the
  question → one grid of `DebaterCard`s per round → the verdict; `DebaterCard` streams + renders markdown
  (`utils/markdown.js`); `DebateComposer` (question + rounds + live cost estimate) starts a debate;
  `DebateList` is the saved-debate sidebar. The **PropertyLab AI / AI Debate tabs** (`PortalAiTabs`) are
  shared `<Link>`s between `/property/ai-advisor` and `/property/ai-advisor/debates`. BYO-key + credits reuse the AI
  Conversations [ProvidersModal](/docs/modules_handbook/main/ai-conversations/readMe.md).
- **Admin read-only history.** An admin can replay a member's whole debate board — the question, every
  round's panel answers, and the verdict (static, no streaming) — on the lead's detail page under
  **Manage › Leads › Show › Portal Engagement › AI Debates**
  (`resources/js/Pages/Manage/Leads/Partials/Tabs/AiDebatesTab.vue`, fed as a plain prop by
  `LeadsController@show`; the debater columns are derived from the responses that actually ran). It is
  **view-only**: no streaming, no writes, no credits.

## Related files

**Backend — Models & Repository**
- [src/Ai/AiDebate.php](/src/Ai/AiDebate.php) · [src/Ai/AiDebateResponse.php](/src/Ai/AiDebateResponse.php) — the debate + its response cells; status / source / `DEBATER_VERDICT` constants.
- [src/Ai/Repositories/AiDebateRepository.php](/src/Ai/Repositories/AiDebateRepository.php) — `create` / `update` / `delete` / `saveResponse` (updateOrCreate per cell + touch activity).

**Backend — Orchestration & Services**
- [src/Ai/Services/DebateRunner.php](/src/Ai/Services/DebateRunner.php) — runs the rounds + verdict; per-call funding, prompt building, streaming, persistence.
- [src/Ai/Services/AiClient.php](/src/Ai/Services/AiClient.php) (`stream()`) · [AiCreditService.php](/src/Ai/Services/AiCreditService.php) (`planConversation`/`spend`) · [AiKeyService.php](/src/Ai/Services/AiKeyService.php).

**Backend — Controller, trait, request, routes**
- [app/Http/Controllers/Main/Portal/AiDebatesController.php](/app/Http/Controllers/Main/Portal/AiDebatesController.php) — index/store/destroy + the per-cell SSE `cell` + `finalize`; `debatersForLead` readiness + `canRun` gate.
- [app/Http/Controllers/Concerns/StreamsServerSentEvents.php](/app/Http/Controllers/Concerns/StreamsServerSentEvents.php) — shared SSE flushing trait (also used by AI Conversations).
- [app/Http/Requests/Main/Portal/AiDebates/StoreRequest.php](/app/Http/Requests/Main/Portal/AiDebates/StoreRequest.php) · [CellRequest.php](/app/Http/Requests/Main/Portal/AiDebates/CellRequest.php) · [routes/main.php](/routes/main.php) (`main.portal.debates.*` — incl. `{id}/cell` + `{id}/finalize`).

**Prompts & config**
- `AiRequest::PROMPT_DEBATE_ANSWER` / `PROMPT_DEBATE_VERDICT` ([config/ai_prompts.php](/config/ai_prompts.php)) · [resources/prompts/debate_analyst.md](/resources/prompts/debate_analyst.md) (neutral analyst persona) · [resources/prompts/debate_house.md](/resources/prompts/debate_house.md) (the house "Property Lab AI" debater + the verdict author — a decisive, judgement-giving deal coach). The house persona is kept **separate** from the member-facing chat persona [ai_conversation.md](/resources/prompts/ai_conversation.md) (which no longer gives buy/sell verdicts), so the debate still delivers a real verdict. Both persona texts are registered (`PROMPT_DEBATE_HOUSE` / `PROMPT_DEBATE_ANALYST`) and resolved through `AiRequest::promptSystem()`, so they are **admin-editable on the Manage AI Prompts page** like every other body (the `.md` files stay the defaults).
- [config/ai.php](/config/ai.php) — the `ai.debate` panel (debaters → provider/model/persona, `verdict_debater`, `max_rounds`).

**Frontend (Vue)**
- [resources/js/Pages/Main/Portal/AiDebates/Index.vue](/resources/js/Pages/Main/Portal/AiDebates/Index.vue) — the board orchestrator.
- Partials: [DebateBoard.vue](/resources/js/Pages/Main/Portal/AiDebates/Partials/DebateBoard.vue) · [DebaterCard.vue](/resources/js/Pages/Main/Portal/AiDebates/Partials/DebaterCard.vue) · [DebateComposer.vue](/resources/js/Pages/Main/Portal/AiDebates/Partials/DebateComposer.vue) · [DebateList.vue](/resources/js/Pages/Main/Portal/AiDebates/Partials/DebateList.vue)
- [resources/js/composables/useDebateStream.js](/resources/js/composables/useDebateStream.js) — the SSE reader · [resources/js/Components/PortalAiTabs.vue](/resources/js/Components/PortalAiTabs.vue) — the shared tabs.

**Migrations**
- [database/migrations/2026_06_18_000004_create_ai_debates_table.php](/database/migrations/2026_06_18_000004_create_ai_debates_table.php) · [2026_06_18_000005_create_ai_debate_responses_table.php](/database/migrations/2026_06_18_000005_create_ai_debate_responses_table.php) · [2026_06_18_000006_add_unique_slot_to_ai_debate_responses.php](/database/migrations/2026_06_18_000006_add_unique_slot_to_ai_debate_responses.php)

**See also:** [AI Conversations](/docs/modules_handbook/main/ai-conversations/readMe.md) (the single-model chat tab) · [AI Integration](/docs/modules_handbook/shared/ai/readMe.md) (`AiClient`, keys, credits, the request log).
