d94049769e
Focuses now carry asked_turn_id so GET rebuilds stem and options on the same turn. Agent writes spokenPrompt; the live question slot is gone. Co-authored-by: Cursor <cursoragent@cursor.com>
439 lines
16 KiB
PL/PgSQL
439 lines
16 KiB
PL/PgSQL
-- Question-in-message: persist the asked turn and selected option on each
|
|
-- conversation focus so GET can rebuild the same assistant bubble after refresh.
|
|
-- New columns are nullable so existing cases remain readable as plain text.
|
|
|
|
begin;
|
|
|
|
alter table public.agentic_rectification_conversation_focuses
|
|
add column if not exists asked_turn_id uuid
|
|
references public.agentic_rectification_turns(id) on delete set null,
|
|
add column if not exists answer_option text
|
|
check (answer_option is null or answer_option in ('A', 'B', 'C', 'D', 'stop'));
|
|
|
|
create index if not exists agentic_rectification_focus_asked_turn_idx
|
|
on public.agentic_rectification_conversation_focuses (asked_turn_id)
|
|
where asked_turn_id is not null;
|
|
|
|
drop function if exists public.set_agentic_rectification_conversation_focus(
|
|
uuid, uuid, text, text, uuid, text, text, jsonb
|
|
);
|
|
|
|
create function public.set_agentic_rectification_conversation_focus(
|
|
p_user_id uuid,
|
|
p_case_id uuid,
|
|
p_question_id text,
|
|
p_intent text,
|
|
p_target_evidence_id uuid,
|
|
p_target_domain text,
|
|
p_target_kind text,
|
|
p_expected_answer_schema jsonb,
|
|
p_asked_turn_id uuid default null
|
|
)
|
|
returns jsonb
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_case public.agentic_rectification_cases%rowtype;
|
|
v_focus public.agentic_rectification_conversation_focuses%rowtype;
|
|
v_existing_identity jsonb;
|
|
v_incoming_identity jsonb;
|
|
begin
|
|
if p_user_id is null or p_case_id is null
|
|
or length(btrim(coalesce(p_question_id, ''))) = 0
|
|
or length(btrim(coalesce(p_intent, ''))) = 0
|
|
or p_expected_answer_schema is null
|
|
or jsonb_typeof(p_expected_answer_schema) <> 'object' then
|
|
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
|
|
end if;
|
|
if p_asked_turn_id is not null and not exists (
|
|
select 1 from public.agentic_rectification_turns
|
|
where id = p_asked_turn_id and case_id = p_case_id
|
|
) then
|
|
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select * into v_case
|
|
from public.agentic_rectification_cases
|
|
where id = p_case_id and user_id = p_user_id
|
|
for update;
|
|
if not found then
|
|
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
|
|
end if;
|
|
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
|
|
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
|
|
end if;
|
|
if p_target_evidence_id is not null and not exists (
|
|
select 1 from public.agentic_rectification_evidence
|
|
where id = p_target_evidence_id and case_id = p_case_id
|
|
) then
|
|
raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select * into v_focus
|
|
from public.agentic_rectification_conversation_focuses
|
|
where case_id = p_case_id and question_id = p_question_id;
|
|
if found then
|
|
if v_focus.status = 'active'
|
|
and v_focus.intent = p_intent
|
|
and v_focus.target_evidence_id is not distinct from p_target_evidence_id
|
|
and v_focus.target_domain is not distinct from p_target_domain
|
|
and v_focus.target_kind is not distinct from p_target_kind then
|
|
v_existing_identity := (v_focus.expected_answer_schema - 'prompt');
|
|
v_incoming_identity := (p_expected_answer_schema - 'prompt');
|
|
if jsonb_typeof(v_existing_identity -> 'choice') = 'object' then
|
|
v_existing_identity := jsonb_set(
|
|
v_existing_identity, '{choice}', (v_existing_identity -> 'choice') - 'prompt'
|
|
);
|
|
end if;
|
|
if jsonb_typeof(v_incoming_identity -> 'choice') = 'object' then
|
|
v_incoming_identity := jsonb_set(
|
|
v_incoming_identity, '{choice}', (v_incoming_identity -> 'choice') - 'prompt'
|
|
);
|
|
end if;
|
|
if v_existing_identity = v_incoming_identity then
|
|
update public.agentic_rectification_conversation_focuses
|
|
set expected_answer_schema = p_expected_answer_schema,
|
|
asked_turn_id = coalesce(asked_turn_id, p_asked_turn_id),
|
|
updated_at = pg_catalog.now()
|
|
where id = v_focus.id
|
|
returning * into v_focus;
|
|
return jsonb_build_object(
|
|
'focus', to_jsonb(v_focus),
|
|
'idempotent', true
|
|
);
|
|
end if;
|
|
end if;
|
|
raise exception 'agentic_rectification_focus_idempotency_conflict' using errcode = 'P0001';
|
|
end if;
|
|
|
|
update public.agentic_rectification_conversation_focuses
|
|
set status = 'superseded',
|
|
resolved_at = pg_catalog.now(),
|
|
updated_at = pg_catalog.now()
|
|
where case_id = p_case_id and status = 'active';
|
|
|
|
insert into public.agentic_rectification_conversation_focuses (
|
|
case_id, question_id, intent, target_evidence_id, target_domain, target_kind,
|
|
expected_answer_schema, status, asked_at, asked_turn_id
|
|
) values (
|
|
p_case_id, p_question_id, p_intent, p_target_evidence_id, p_target_domain, p_target_kind,
|
|
p_expected_answer_schema, 'active', pg_catalog.now(), p_asked_turn_id
|
|
) returning * into v_focus;
|
|
|
|
return jsonb_build_object(
|
|
'focus', to_jsonb(v_focus),
|
|
'idempotent', false
|
|
);
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.set_agentic_rectification_conversation_focus(
|
|
uuid, uuid, text, text, uuid, text, text, jsonb, uuid
|
|
) from public, anon, authenticated;
|
|
grant execute on function public.set_agentic_rectification_conversation_focus(
|
|
uuid, uuid, text, text, uuid, text, text, jsonb, uuid
|
|
) to service_role;
|
|
|
|
drop function if exists public.resolve_agentic_rectification_conversation_focus(
|
|
uuid, uuid, uuid, text, uuid
|
|
);
|
|
|
|
create function public.resolve_agentic_rectification_conversation_focus(
|
|
p_user_id uuid,
|
|
p_case_id uuid,
|
|
p_focus_id uuid,
|
|
p_status text,
|
|
p_evidence_id uuid,
|
|
p_answer_option text default null
|
|
)
|
|
returns jsonb
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_case public.agentic_rectification_cases%rowtype;
|
|
v_focus public.agentic_rectification_conversation_focuses%rowtype;
|
|
begin
|
|
if p_user_id is null or p_case_id is null or p_focus_id is null
|
|
or p_status not in ('resolved', 'declined', 'skipped', 'superseded') then
|
|
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
|
|
end if;
|
|
if p_answer_option is not null and p_answer_option not in ('A', 'B', 'C', 'D', 'stop') then
|
|
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select * into v_case
|
|
from public.agentic_rectification_cases
|
|
where id = p_case_id and user_id = p_user_id;
|
|
if not found then
|
|
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
|
|
end if;
|
|
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
|
|
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select * into v_focus
|
|
from public.agentic_rectification_conversation_focuses
|
|
where id = p_focus_id and case_id = p_case_id
|
|
for update;
|
|
if not found then
|
|
raise exception 'agentic_rectification_focus_not_found' using errcode = 'P0001';
|
|
end if;
|
|
if p_evidence_id is not null and not exists (
|
|
select 1 from public.agentic_rectification_evidence
|
|
where id = p_evidence_id and case_id = p_case_id
|
|
) then
|
|
raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
|
|
end if;
|
|
if v_focus.target_evidence_id is not null
|
|
and p_evidence_id is distinct from v_focus.target_evidence_id then
|
|
raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
|
|
end if;
|
|
if v_focus.status <> 'active' then
|
|
if v_focus.status = p_status
|
|
and (v_focus.target_evidence_id is null or v_focus.target_evidence_id is not distinct from p_evidence_id)
|
|
and v_focus.answer_option is not distinct from p_answer_option then
|
|
return jsonb_build_object(
|
|
'focus_id', v_focus.id, 'status', v_focus.status,
|
|
'evidence_id', v_focus.target_evidence_id, 'idempotent', true,
|
|
'answer_option', to_jsonb(v_focus.answer_option)
|
|
);
|
|
end if;
|
|
raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
|
|
end if;
|
|
|
|
update public.agentic_rectification_conversation_focuses
|
|
set status = p_status,
|
|
target_evidence_id = coalesce(target_evidence_id, p_evidence_id),
|
|
answer_option = coalesce(p_answer_option, answer_option),
|
|
resolved_at = pg_catalog.now(),
|
|
updated_at = pg_catalog.now()
|
|
where id = v_focus.id
|
|
returning * into v_focus;
|
|
|
|
return jsonb_build_object(
|
|
'focus_id', v_focus.id,
|
|
'status', p_status,
|
|
'evidence_id', coalesce(v_focus.target_evidence_id, p_evidence_id),
|
|
'idempotent', false,
|
|
'answer_option', to_jsonb(v_focus.answer_option)
|
|
);
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.resolve_agentic_rectification_conversation_focus(
|
|
uuid, uuid, uuid, text, uuid, text
|
|
) from public, anon, authenticated;
|
|
grant execute on function public.resolve_agentic_rectification_conversation_focus(
|
|
uuid, uuid, uuid, text, uuid, text
|
|
) to service_role;
|
|
|
|
create or replace function public.list_agentic_rectification_conversation_focuses(
|
|
p_user_id uuid,
|
|
p_case_id uuid
|
|
)
|
|
returns jsonb
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_case public.agentic_rectification_cases%rowtype;
|
|
v_focuses jsonb;
|
|
begin
|
|
if p_user_id is null or p_case_id is null then
|
|
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
|
|
end if;
|
|
select * into v_case
|
|
from public.agentic_rectification_cases
|
|
where id = p_case_id and user_id = p_user_id;
|
|
if not found then
|
|
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
|
|
end if;
|
|
select coalesce(jsonb_agg(to_jsonb(f) order by f.asked_at, f.created_at, f.id), '[]'::jsonb)
|
|
into v_focuses
|
|
from public.agentic_rectification_conversation_focuses f
|
|
where f.case_id = p_case_id;
|
|
return v_focuses;
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.list_agentic_rectification_conversation_focuses(uuid, uuid)
|
|
from public, anon, authenticated;
|
|
grant execute on function public.list_agentic_rectification_conversation_focuses(uuid, uuid)
|
|
to service_role;
|
|
|
|
create or replace function public.apply_agentic_rectification_choice_action(
|
|
p_user_id uuid,
|
|
p_case_id uuid,
|
|
p_action_id uuid,
|
|
p_question_id text,
|
|
p_option_id text,
|
|
p_expected_revision integer,
|
|
p_focus_id uuid,
|
|
p_focus_status text,
|
|
p_source_quote text,
|
|
p_derived_context jsonb,
|
|
p_narration text,
|
|
p_inference jsonb
|
|
)
|
|
returns jsonb
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_case public.agentic_rectification_cases%rowtype;
|
|
v_focus public.agentic_rectification_conversation_focuses%rowtype;
|
|
v_existing public.agentic_rectification_choice_actions%rowtype;
|
|
v_transition jsonb;
|
|
v_focus_result jsonb;
|
|
v_receipt jsonb;
|
|
v_id uuid;
|
|
v_probe_id text;
|
|
v_question_id text;
|
|
begin
|
|
if p_user_id is null or p_case_id is null or p_action_id is null
|
|
or p_option_id is null or p_option_id not in ('A', 'B', 'C', 'D', 'stop')
|
|
or p_expected_revision is null or p_expected_revision < 0
|
|
or p_focus_id is null
|
|
or p_focus_status is null or p_focus_status not in ('resolved', 'declined', 'skipped')
|
|
or p_derived_context is null or jsonb_typeof(p_derived_context) <> 'object'
|
|
or (p_inference is not null and jsonb_typeof(p_inference) <> 'object') then
|
|
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select * into v_case
|
|
from public.agentic_rectification_cases
|
|
where id = p_case_id and user_id = p_user_id
|
|
for update;
|
|
if not found then
|
|
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
|
|
end if;
|
|
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
|
|
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select * into v_existing
|
|
from public.agentic_rectification_choice_actions
|
|
where case_id = p_case_id
|
|
and action_id = p_action_id;
|
|
if found then
|
|
return jsonb_set(v_existing.receipt, '{idempotent}', 'true'::jsonb, true);
|
|
end if;
|
|
|
|
select * into v_focus
|
|
from public.agentic_rectification_conversation_focuses
|
|
where id = p_focus_id and case_id = p_case_id
|
|
for update;
|
|
if not found then
|
|
raise exception 'agentic_rectification_focus_not_found' using errcode = 'P0001';
|
|
end if;
|
|
if v_focus.status is distinct from 'active' then
|
|
raise exception 'agentic_rectification_stale_question' using errcode = 'P0001';
|
|
end if;
|
|
|
|
v_question_id := coalesce(nullif(btrim(coalesce(p_question_id, '')), ''), v_focus.question_id);
|
|
|
|
insert into public.agentic_rectification_choice_actions (
|
|
case_id, action_id, question_id, probe_id, option_id, expected_revision,
|
|
status, answer_class, source_quote, derived_context, focus_id, narration, receipt
|
|
) values (
|
|
p_case_id, p_action_id, v_question_id,
|
|
nullif(btrim(coalesce(p_inference ->> 'probe_id', p_derived_context ->> 'derivedFromProbeId', '')), ''),
|
|
p_option_id, p_expected_revision, 'received',
|
|
nullif(btrim(coalesce(p_inference ->> 'answer_class', p_derived_context ->> 'answerClass', '')), ''),
|
|
nullif(btrim(coalesce(p_source_quote, '')), ''),
|
|
p_derived_context, p_focus_id, nullif(btrim(coalesce(p_narration, '')), ''),
|
|
jsonb_build_object('status', 'received')
|
|
)
|
|
returning * into v_existing;
|
|
|
|
v_transition := null;
|
|
if p_inference is not null then
|
|
v_transition := public.append_agentic_rectification_inference_transition(
|
|
p_user_id,
|
|
p_case_id,
|
|
coalesce((p_inference ->> 'expected_revision')::integer, p_expected_revision),
|
|
p_inference ->> 'probe_id',
|
|
p_inference ->> 'open_probe_id',
|
|
p_inference ->> 'semantic_key',
|
|
p_inference ->> 'candidate_split_hash',
|
|
p_inference ->> 'answer_class',
|
|
p_inference ->> 'raw_answer',
|
|
p_inference -> 'inference_state',
|
|
coalesce(p_inference -> 'posterior_before', '{}'::jsonb),
|
|
coalesce(p_inference -> 'posterior_after', '{}'::jsonb),
|
|
coalesce(p_inference -> 'score_deltas', '{}'::jsonb),
|
|
p_inference ->> 'decision_state_fingerprint',
|
|
p_inference ->> 'reason',
|
|
p_inference ->> 'idempotency_key',
|
|
p_inference ->> 'candidate_set_id'
|
|
);
|
|
end if;
|
|
|
|
v_focus_result := public.resolve_agentic_rectification_conversation_focus(
|
|
p_user_id, p_case_id, p_focus_id, p_focus_status, null, p_option_id
|
|
);
|
|
|
|
v_probe_id := coalesce(
|
|
v_transition ->> 'probe_id',
|
|
p_inference ->> 'probe_id',
|
|
p_derived_context ->> 'derivedFromProbeId'
|
|
);
|
|
v_receipt := jsonb_build_object(
|
|
'action_id', p_action_id,
|
|
'status', 'applied',
|
|
'idempotent', false,
|
|
'question_id', v_question_id,
|
|
'option_id', p_option_id,
|
|
'probe_id', to_jsonb(v_probe_id),
|
|
'revision', coalesce((v_transition ->> 'revision')::integer, p_expected_revision),
|
|
'focus_id', v_focus_result -> 'focus_id',
|
|
'focus_status', v_focus_result -> 'status',
|
|
'transition_id', v_transition -> 'transition_id',
|
|
'source_quote', to_jsonb(nullif(btrim(coalesce(p_source_quote, '')), '')),
|
|
'derived_context', p_derived_context,
|
|
'narration', to_jsonb(nullif(btrim(coalesce(p_narration, '')), ''))
|
|
);
|
|
|
|
update public.agentic_rectification_choice_actions
|
|
set status = 'applied',
|
|
probe_id = v_probe_id,
|
|
transition_id = nullif(v_transition ->> 'transition_id', '')::uuid,
|
|
receipt = v_receipt,
|
|
updated_at = pg_catalog.now()
|
|
where id = v_existing.id
|
|
returning id into v_id;
|
|
|
|
update public.agentic_rectification_cases
|
|
set last_activity_at = pg_catalog.now(),
|
|
updated_at = pg_catalog.now()
|
|
where id = p_case_id;
|
|
|
|
return v_receipt;
|
|
exception
|
|
when unique_violation then
|
|
select * into v_existing
|
|
from public.agentic_rectification_choice_actions
|
|
where case_id = p_case_id
|
|
and action_id = p_action_id;
|
|
if found then
|
|
return jsonb_set(v_existing.receipt, '{idempotent}', 'true'::jsonb, true);
|
|
end if;
|
|
raise;
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.apply_agentic_rectification_choice_action(
|
|
uuid, uuid, uuid, text, text, integer, uuid, text, text, jsonb, text, jsonb
|
|
) from public, anon, authenticated;
|
|
grant execute on function public.apply_agentic_rectification_choice_action(
|
|
uuid, uuid, uuid, text, text, integer, uuid, text, text, jsonb, text, jsonb
|
|
) to service_role;
|
|
|
|
commit;
|