feat(fusion_helpdesk_central): findings-first wizard, explicit Generate button

The old flow fired OpenAI on wizard open with just ticket + chatter,
so the AI summary was just a paraphrase of what the user originally
reported — your engineering analysis (scope, limitations, recommended
approach) never made it to the owner. Restructure to a two-step flow:

  1. Open wizard → empty findings + empty summary, NO OpenAI call
  2. You write findings: scope / effort / approach / risk
  3. Click 'Generate Summary from Findings' → OpenAI runs with
     ticket + chatter + findings, where the prompt explicitly tells
     the model to weight findings MORE THAN the original report
  4. Review/edit, then Send

Bulk wizard mirrors the flow per line: each row gets its own
findings + summary, one 'Generate All Summaries' button fans out
parallel OpenAI calls using each line's own findings.

Updated SUMMARY_PROMPT to:
- Tell the model the support engineer's findings are authoritative
- Emit a bullet structure that leads with the recommendation, not
  the user's restated ask
- Side with findings over the original report when they conflict

New tests cover:
- default_get does NOT fire OpenAI (regression guard for auto-AI)
- Findings text actually reaches the OpenAI prompt
- Send works with a manually-typed summary (no AI in the loop)
- Existing bulk + validation paths still pass with the new shape

Also folds in the deferred code-review #7: ThreadPoolExecutor now
explicitly cancels pending futures on timeout via
shutdown(wait=False, cancel_futures=True) so a slow OpenAI day can't
hold the wizard open for ceil(N/workers)*15s.

Bumps fusion_helpdesk_central to 19.0.2.3.0.

Smoke-tested live on nexa: opening the wizard makes zero OpenAI calls;
clicking Generate with findings='My findings: scope is XL, ~8h' makes
exactly one call and the findings text is verifiably in the prompt
body received by call_openai_chat.
This commit is contained in:
gsinghpal
2026-05-27 13:49:02 -04:00
parent 7349f3180d
commit c520803c84
6 changed files with 303 additions and 85 deletions

View File

@@ -23,22 +23,35 @@ _logger = logging.getLogger(__name__)
SUMMARY_PROMPT = """You are summarising a customer support ticket for a busy executive
who needs to decide whether to approve the work.
The support engineer has reviewed this ticket and written their findings
below (scope, limitations, recommended approach, anything the original
reporter wouldn't have known). Treat those findings as authoritative —
they reflect the actual engineering reality, not just the user's
description of the problem. If the findings contradict the original
report, side with the findings and call out the gap.
Output rules:
- 4-6 short bullet points, plain text (no markdown).
- First bullet: the ask, in one sentence.
- Second bullet: the business impact if approved.
- Third bullet: the business impact if NOT approved (or "none material").
- Optional bullets: cost / effort signals if any are mentioned.
- First bullet: the ask, in one sentence, framed in the support
engineer's terms (not just a paraphrase of the original report).
- Second bullet: the support engineer's recommendation, if they made one.
- Third bullet: business impact if approved.
- Fourth bullet: business impact if NOT approved (or "none material").
- Optional bullets: scope / effort / risk signals from the findings.
- Final bullet: open questions the approver should think about.
- Do not invent facts. If the thread doesn't say, write "not stated".
- Do not invent facts. If the findings or thread don't say, write
"not stated".
- No greetings, no sign-offs, no preamble.
Ticket title: {name}
Original report:
Original report from the user:
{description_plain}
Replies so far:
Replies in the ticket thread:
{messages_plain}
Support engineer's findings (your most important input):
{findings_plain}
"""
# Bound the prompt sent to OpenAI so a runaway thread doesn't blow cost or
@@ -47,13 +60,17 @@ Replies so far:
OPENAI_PROMPT_MAX_CHARS = 8000
def build_summary_prompt(ticket_name, description_plain, messages):
"""Render SUMMARY_PROMPT with ticket + chatter content.
def build_summary_prompt(ticket_name, description_plain, messages,
findings=''):
"""Render SUMMARY_PROMPT with ticket + chatter + support findings.
`messages` is a list of dicts with at minimum {author, date, body_plain}.
We render one line per message so the model can attribute who said what.
Empty inputs collapse to "(none)" so the prompt never has bare blanks
that the model could interpret as a missing section.
`findings` is the support engineer's free-text notes from the wizard —
their scope/effort/recommendation that the AI should weight more
heavily than the original user description.
Empty inputs collapse to explicit markers so the prompt never has
bare blanks the model could misread as missing sections.
"""
name = (ticket_name or '').strip() or '(untitled)'
desc = (description_plain or '').strip() or '(no description)'
@@ -67,8 +84,13 @@ def build_summary_prompt(ticket_name, description_plain, messages):
msgs = '\n---\n'.join(lines)
else:
msgs = '(no replies yet)'
findings_text = (findings or '').strip() or (
'(none provided — base the summary on the user\'s report and '
'thread alone)'
)
return SUMMARY_PROMPT.format(
name=name, description_plain=desc, messages_plain=msgs,
findings_plain=findings_text,
)