[No QA] Migrate ProposalPolice to Responses + Conversations API - #97199
Draft
roryabraham wants to merge 15 commits into
Draft
[No QA] Migrate ProposalPolice to Responses + Conversations API#97199roryabraham wants to merge 15 commits into
roryabraham wants to merge 15 commits into
Conversation
Replaces the deprecated Assistants thread/run polling helpers (promptAssistant, parseAssistantResponse) with createConversation, addConversationItems, and a generic parseJSONResponse<T>, so callers can use the Responses API with persistent Conversations instead.
ProposalUtils holds the proposal/bot-detection helpers previously private to proposalPoliceComment.ts, so the new conversation-tracking utils can share them without a reverse import. GithubUtils.pinIssue is a best-effort GraphQL pin (the REST API has no equivalent) used to surface ProposalPolice's per-issue duplicate-check tracking comment; failures are swallowed since pinning is a convenience, not something the caller depends on.
Splits the dashboard-configured Assistant prompt into small, focused fragments (template definition, template/edit examples, decision tree, bot actions, duplicate detection) with per-call-type assemblers, so each Responses API call only gets the instructions it actually needs instead of the whole prompt every time. Also drops a stale "re-state the problem" section from the examples that no longer matches the proposal template. Adds JSON-schema definitions and type guards for the three response shapes (template-check, edit-check, duplicate-check), replacing the "respond with JSON" instructions previously baked into the prompt text.
Pure, independently-testable helpers for the duplicate-check Conversation flow: finding a tracked Conversation ID from a hidden marker on a bot-authored comment, building the tracking comment body, seeding items for prior proposals, and chunking items to OpenAI's 20-per-call Conversation limit.
Swaps promptAssistant for promptResponses on gpt-5.6-luna (replacing the Assistant's GPT-4o) for template-check and edit-check, and replaces the per-prior-proposal Assistants loop with a single Responses call against a persistent per-issue Conversation for duplicate-check. Removes the now-unused PROPOSAL_POLICE_ASSISTANT_ID input/secret, and exports `run` (guarded behind a JEST_WORKER_ID check on the auto-invocation) so it can be unit tested directly. Fixes #72725
Covers OpenAIUtils (promptResponses, createConversation, addConversationItems, parseJSONResponse), the ProposalPoliceConversation tracking helpers, and proposalPoliceComment's run() end-to-end (NO_ACTION, ACTION_REQUIRED, ACTION_EDIT, duplicate withdrawal, bot-author skip, and the Conversation create/reuse flow). No tests previously existed for either of these.
Regenerates every action's ncc bundle via npm run gh-actions-build. Most of these only pick up the new GithubUtils.pinIssue method (a shared lib bundled into every action); proposalPoliceComment's bundle reflects its full migration off the Assistants API.
- Require the model's action to be ACTION_HIDE_DUPLICATE, not just a high similarity score, before withdrawing a proposal as a duplicate (guards against the two fields disagreeing). - Skip the duplicate-check Responses call entirely when an issue has no prior proposals to compare against, instead of always spending an API call on a comparison that can't find anything. - Add a concurrency group (scoped by issue number) to the workflow so two comments posted close together on the same issue can't each create their own tracking Conversation. - Add tests for both behavior changes.
Round-2 branch-reviewer catch: the duplicate-check Responses call was the only mechanism appending items to a Conversation (via its `conversation` param's auto-append behavior), so skipping that call for an issue's first proposal (introduced in the previous commit) also skipped ever recording it - permanently hiding it from every future duplicate check on that issue. Now the proposal is recorded directly via addConversationItems when the call is skipped. Adds a regression test that runs the action twice in sequence (first proposal, then a near-duplicate second one) to prove the first proposal is actually comparable.
- Post the tracking comment (and pin) immediately after creating a Conversation, before sending any remaining seed-item batches, so a failure mid-seeding can't leave the Conversation permanently untracked and fragment duplicate-detection history across issues with a large pre-existing proposal backlog. - Validate the model-reported duplicateCommentId actually matches a real proposal comment before using it to build the withdrawal notice's link. - Escape angle brackets in untrusted comment/proposal text before interpolating it into our XML-style wrapper tags, so a comment containing a literal closing tag can't be mistaken by the model for the end of our own wrapper. - Switch the auto-invocation guard from checking JEST_WORKER_ID to the require.main === module pattern already used by every other action in .github/actions/javascript/*. - Add tests: multi-batch seeding (>20 prior proposals) with an assertion on tracking-comment-before-remaining-seed ordering, and escaping/tagging coverage for all four prompt input builders.
- Exclude the new proposal's own comment ID from the duplicate-check originalProposal lookup, guarding against a model self-match hallucination linking the withdrawal notice to itself. - Add tests for GithubUtils.pinIssue (correct GraphQL call, and that errors are swallowed rather than thrown), the one piece of new logic from this migration that didn't yet have coverage.
Round-5 branch-reviewer note: no test asserted the actual model value passed to promptResponses, so a future accidental edit to PROPOSAL_POLICE_MODEL wouldn't be caught by the suite. Exports the constant and asserts it's used for all three call types (duplicate, template, and edit check). (The model ID itself, gpt-5.6-luna, was independently confirmed to be a real, GA OpenAI model as of 2026-07-09 before this migration began.)
Pinning is a scarce, repo-wide resource (max 3 pinned issues for the entire repo), used by maintainers for things like contributing guides or roadmap items. ProposalPolice would have attempted to consume one of those slots on the first proposal of every "Help Wanted" issue, which could fire constantly on an active repo and conflict with real pins - for zero functional benefit, since the hidden marker text in the tracking comment (not pin status) is the actual mechanism used to find a tracked Conversation. Reverts the corresponding +1 eslint-seatbelt allowance bump for GithubUtilsTest.ts back to its original value now that the pinIssue test (and its one unsafe-type-assertion) is gone too.
Codecov Report✅ All modified and coverable lines are covered by tests. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Explanation of Change
ProposalPolice currently runs on the OpenAI Assistants API: the system prompt is configured manually in the OpenAI dashboard (not checked into code), and duplicate-proposal detection makes one Assistants thread/run per prior proposal on the issue (each with up to 90s of polling), since Assistants are being deprecated in favor of the Responses API.
This migrates ProposalPolice to:
OpenAIUtils.promptResponses) on gpt-5.6-luna (replacing the Assistant's GPT-4o) for the template-check and edit-check calls — no more thread/run polling.prompts/proposalPolice/*) split into small, focused instruction fragments per call type (template-check / edit-check / duplicate-check), replacing the single dashboard-configured prompt blob — each call only gets the instructions it actually needs.Also extracted
ProposalUtils(proposal/bot-detection helpers) as a shared, independently-testable utility.No tests previously existed for ProposalPolice or
OpenAIUtils; this PR adds coverage for all of the new logic (OpenAIUtilsTest,ProposalPoliceConversationTest,ProposalPoliceInputTest,proposalPoliceCommentTest).Fixed Issues
$ #72725
PROPOSAL:
Tests
This is a GitHub Actions bot with no UI, so "manual testing" means exercising the compiled action directly:
npm run test -- OpenAIUtilsTest ProposalPoliceConversationTest ProposalPoliceInputTest proposalPoliceCommentTest GithubUtilsTestand confirm all pass.npm run typecheck-tsgoandnpm run lint-changedand confirm both are clean.npm run gh-actions-buildand confirm.github/actions/javascript/proposalPoliceComment/index.jsis rebuilt with no further diff.NO_ACTION).Steps 4-8 (live end-to-end verification against real GitHub/OpenAI API calls) have not been run yet as of opening this draft — only steps 1-3 have been verified locally. Marking as draft until 4-8 are confirmed on a real test issue.
Offline tests
None — this is a GitHub Actions bot with no client-side/offline behavior.
QA Steps
None — see
[No QA]in the PR title. This is a backend-only GitHub Actions change with no user-facing app behavior.PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Not applicable — this is a backend-only GitHub Actions change with no UI.
Android: Native
N/A
Android: mWeb Chrome
N/A
iOS: Native
N/A
iOS: mWeb Safari
N/A
MacOS: Chrome / Safari
N/A