# PropertyLab Research Advisor

Nav: Property Research in the existing portal, `/property/research` (`main.portal.research.index`).

The module is disabled by default (`PROPERTYLAB_RESEARCH_ENABLED=false`). It reuses the portal's authenticated, contact-verified Lead identity. Every workflow lookup is scoped to that owning Lead, including staff using their own portal workspace. CRM permissions do not grant access to another person's research conversation.

The existing `Src\Ai\Services\AiClient`, encrypted credentials, and `AiCreditService` fund real model calls. Research requests select OpenAI explicitly. `PROPERTYLAB_RESEARCH_MODEL` optionally selects an existing compatible OpenAI model; otherwise an OpenAI-specific prompt pin or the existing OpenAI catalog default is used. No new provider-key environment variable is introduced. Gateway-client installations must verify that the existing gateway routes Research to OpenAI before enabling `PROPERTYLAB_GATEWAY_OPENAI_VERIFIED`; until then AI remains unavailable and deterministic exploration remains usable.

`POST /property/research/turns` consumes JSON with `conversationId`, `clientTurnId`, `text`, `briefRevision`, optional selection references and scope. It returns ordinary HTTP SSE, consumed using Fetch rather than an Inertia visit. Every frame has event ID, sequence, conversation ID, turn ID and data. The accepted run is persisted before streaming; session locking is released before provider calls. A separate authorized cancellation POST, a newer question, or a saved context revision invalidates outstanding work. Provider timeouts are temporarily bounded to the remaining turn budget and restored after each call. Deployment verification must exercise real proxy buffering and concurrent PHP capacity.

The JSON planner can select only `ResearchRepository::toolDefinitions()` actions, with the same strict PHP argument validation as direct exploration. It can inspect a resolved scheme before choosing additional analytical tools. An ambiguous resolver result cannot silently become an analysis; destination coordinates require an explicit matching user confirmation in the saved brief. Unsupported requests do not produce synthetic observations.

Every analytical turn persists one immutable result. `ResultComposer` retains separate price/location sections, their units, scope and coverage while namespacing chart, series, metric and claim IDs. Canonical entity IDs are preserved for map linking. Intermediate search candidates do not become the final analyzed map set. Restricted selections are disclosed. The model selects verified claim IDs for its explanation; unvalidated prose never streams into the answer. Source claims currently retain their canonical English wording; broader multilingual quality requires evaluation before it is advertised.

One completed, bounded advisor turn spends one existing portal credit when funded by free credits. Duplicate client request IDs replay without a second model call or debit. Failed or cancelled runs do not debit a credit. The existing gateway may meter several underlying provider calls independently. Local AI raw request logging is always disabled for this feature; remote provider/gateway retention follows the existing service configuration.

Conversations persist a bounded, validated state document containing the buyer brief, shortlist, checklist, current scene and saved scenarios. Optimistic revisions prevent lost edits. Saved scenario IDs are immutable; editing assumptions requires a new scenario ID. Scene documents reference owned immutable results and retain camera/layer/selection settings without hover state or caller-supplied analytical values. Deleting a conversation removes its private turns, messages and results. The default export includes public scheme observations and shortlist identities; it excludes messages, private notes, asking assumptions, destinations and nested scenarios.

`POST /property/research/tools` accepts `{conversationId,revision,tool,arguments}` and returns `{result,revision}`. It uses the identical deterministic services and persistence without an AI call. `GET /property/research/results/{id}` returns `{result}`; the `/evidence` suffix returns the bounded stored evidence, provenance, coverage and limitations. These are authenticated endpoints, not public file links.

Focused tests use `DatabaseTransactions` so they preserve a separately imported research snapshot. They cover portal gates, owner isolation, direct research without AI, private state/replay, immutable scenarios, stale revisions, cancellation, idempotency, credits, OpenAI selection, invalid plans, user-controlled destination confirmation and composed price/journey evidence. Run against a dedicated testing database; the isolated development runner is `php vendor/bin/phpunit -c phpunit.propertylab.local.xml --filter 'AdvisorGroundingTest|AdvisorCompositionTest|PropertyLabAdvisorTest'`.
