Skip to content

feat(primer-api): add AI-powered Primer bot for Slack#7713

Open
hectahertz wants to merge 2 commits intomainfrom
hectahertz/feat-primer-bot
Open

feat(primer-api): add AI-powered Primer bot for Slack#7713
hectahertz wants to merge 2 commits intomainfrom
hectahertz/feat-primer-bot

Conversation

@hectahertz
Copy link
Copy Markdown
Contributor

@hectahertz hectahertz commented Mar 30, 2026

Closes #

This adds an AI-powered assistant for the #primer Slack channel. It supplements the existing Moveworks-based Primer Bot (which does static FAQ matching on @mentions) by providing dynamic, context-aware answers powered by GitHub Models (GPT-4o).

The architecture avoids hosting anything. A Slack Workflow triggers a GitHub Action via repository_dispatch, which fetches relevant docs from primer.style, sends them to GPT-4o with the question, and posts the answer back to Slack as a thread reply.

User posts question in #primer
  → Someone reacts with :robot_face:
    → Slack Workflow fires
      → Sends repository_dispatch to GitHub Actions
        → Action fetches Primer component docs from primer.style
        → Sends context + question to GitHub Models (gpt-4o)
        → Posts answer back to Slack via Web API (thread reply)

How it compares with the existing Primer Bot:

Moveworks Bot (existing) Primer AI (this PR)
Trigger @mention :robot_face: emoji
Response type FAQ match Dynamic LLM reasoning
Data source Static FAQ markdown Live primer.style docs
Latency ~1s ~15-30s (Action cold start)
Infra Moveworks (managed) GitHub Actions (zero hosting)

Example invocations

Local CLI testing (no Slack needed):

export GITHUB_MODELS_TOKEN=ghp_...
npx tsx packages/primer-api/src/action.ts "How do I use ActionList with sections?"

Expected output:

--- Answer ---
You can create sections in ActionList using `ActionList.Group`. Import from `@primer/react`:

`import {ActionList} from '@primer/react'`

Wrap related items in `ActionList.Group` with a `title` prop:

‍‍‍jsx
<ActionList>
  <ActionList.Group title="Navigation">
    <ActionList.Item>Home</ActionList.Item>
    <ActionList.Item>Settings</ActionList.Item>
  </ActionList.Group>
  <ActionList.Group title="Actions">
    <ActionList.Item variant="danger">Delete</ActionList.Item>
  </ActionList.Group>
</ActionList>
‍‍‍

Docs: <https://primer.style/product/components/action-list>

Model: gpt-4o-2024-11-20
Components: ActionList

Manual dispatch via GitHub UI:

  1. Go to Actions > "Primer Bot" > "Run workflow"
  2. Enter: "What's the difference between Button and IconButton?"
  3. Check Action logs for the answer (no Slack posting in manual mode)

HTTP server for local dev:

npm run dev -w packages/primer-api

curl -X POST http://localhost:3847/ask \
  -H 'Content-Type: application/json' \
  -d '{"question": "When should I use Banner vs Flash?"}'

Expected response:

{
  "answer": "`Banner` is the newer, recommended component for displaying important messages...",
  "model": "gpt-4o-2024-11-20",
  "componentsMentioned": ["Banner", "Flash"]
}

Full Slack flow (after setup):

  1. Someone posts in #primer: "How do I add loading state to a Button?"
  2. Another person reacts with :robot_face:
  3. Slack Workflow fires, sends repository_dispatch to GitHub
  4. ~20s later, Primer AI posts a thread reply with the answer, including code examples and a link to the Button docs on primer.style

Changelog

New

  • packages/primer-api/ - New package with the Primer bot logic (knowledge retrieval, LLM integration, Slack posting)
  • .github/workflows/primer-bot.yml - GitHub Action workflow triggered by repository_dispatch or workflow_dispatch

Changed

N/A

Removed

N/A

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; this is a private internal tool ("private": true), not published to npm

Testing & Reviewing

To test locally:

npm install
npm run build -w packages/react
export GITHUB_MODELS_TOKEN=ghp_...  # needs models:read scope
npx tsx packages/primer-api/src/action.ts "How do I use ActionList?"

To test the HTTP server:

npm run dev -w packages/primer-api
curl -s http://localhost:3847/ask -X POST -H 'Content-Type: application/json' -d '{"question":"What component should I use for a dropdown menu?"}'

TypeScript:

cd packages/primer-api && npx tsc --noEmit  # should pass clean
Setup for full Slack integration (not required for review)

Needs three GitHub Actions secrets:

  1. MODELS_TOKEN - GitHub PAT with models:read scope
  2. SLACK_BOT_TOKEN - Slack bot token with chat:write scope

Then a Slack Workflow that triggers on :robot_face: reaction and sends a repository_dispatch webhook. Full setup instructions in packages/primer-api/README.md.

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Storybook)
  • Changes are SSR compatible
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge
  • (GitHub staff only) Integration tests pass at github/github-ui

@hectahertz hectahertz requested a review from a team as a code owner March 30, 2026 12:24
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 30, 2026

⚠️ No Changeset found

Latest commit: d427938

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new internal @primer/api workspace that powers an AI assistant workflow for the #primer Slack channel, using GitHub Actions + GitHub Models/OpenAI and primer.style docs retrieval.

Changes:

  • Introduces packages/primer-api with Slack-posting Action entry point, local HTTP API, prompt templates, and primer.style doc retrieval.
  • Adds a Primer Bot GitHub Actions workflow triggered by repository_dispatch and workflow_dispatch.
  • Updates package-lock.json to include the new workspace and its dependencies.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/primer-api/tsconfig.json TypeScript build configuration for the new workspace.
packages/primer-api/src/prompts.ts Defines system/user prompt construction for Slack-oriented responses.
packages/primer-api/src/llm.ts OpenAI-compatible chat completion wrapper integrating retrieved context.
packages/primer-api/src/knowledge.ts Component matching + primer.style doc fetching and prompt-context formatting.
packages/primer-api/src/index.ts Local/dev HTTP server exposing /ask and /health.
packages/primer-api/src/config.ts Env-based configuration (GitHub Models vs OpenAI, ports, optional auth).
packages/primer-api/src/action.ts GitHub Action entry point: read event payload, call LLM, post to Slack thread.
packages/primer-api/package.json Declares the new private workspace package and scripts/deps.
packages/primer-api/README.md Setup and usage docs for Slack workflow + local testing.
packages/primer-api/.env.example Example env vars for local runs.
.github/workflows/primer-bot.yml Workflow wiring for dispatch triggers and running the bot.
package-lock.json Lockfile updates for new workspace + dependencies.

Comment on lines +21 to +26
const server = createServer(async (req, res) => {
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server sets Access-Control-Allow-Origin: * and listens on all interfaces by default (server.listen(config.port)). If someone runs this outside localhost, it becomes a publicly callable LLM proxy (and potentially exposes Slack posting if configured). Consider binding to 127.0.0.1 by default and/or making CORS opt-in / restricted to known origins.

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +123
export function formatContext(ctx: RetrievedContext): string {
const sections: string[] = []

sections.push(`Available Primer React components: ${ctx.componentList}`)

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatContext always injects a comma-joined list of all Primer React components into the prompt. This can add a lot of tokens/cost and may push the prompt toward context window limits without improving answer quality. Consider omitting it, truncating it, or only including it when no relevant component match is found.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +22
return `## Context from Primer documentation
${context}
## Question
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildUserPrompt uses Markdown headers (## ...). Since the system prompt explicitly forbids Slack-style headers (#) in the assistant output, including header syntax in the user message/context can increase the chance the model mirrors that formatting. Consider switching to plain-text delimiters (e.g. Context: / Question:) instead of ## headings.

Suggested change
return `## Context from Primer documentation
${context}
## Question
return `Context from Primer documentation:
${context}
Question:

Copilot uses AI. Check for mistakes.
export async function ask(question: string, config: Config): Promise<AskResult> {
const openai = getClient(config)

// Retrieve relevant context from MCP data layer
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline comment says context is retrieved from an "MCP data layer", but retrieveContext in this package fetches primer.style docs and uses @primer/react/generated/components.json. Update the comment to match the actual implementation so future maintenance/debugging isn’t misleading.

Suggested change
// Retrieve relevant context from MCP data layer
// Retrieve relevant Primer docs and component metadata for the question

Copilot uses AI. Check for mistakes.
*
* Triggered by repository_dispatch with event_type 'primer-bot-question'.
* Reads the question from the payload, generates an answer using the LLM,
* and posts it back to Slack via incoming webhook.
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file header comment mentions posting to Slack via an "incoming webhook", but the implementation uses chat.postMessage with a bot token. Please update the comment to reflect the actual Slack API being used.

Suggested change
* and posts it back to Slack via incoming webhook.
* and posts it back to Slack using the Slack Web API (chat.postMessage) with a bot token.

Copilot uses AI. Check for mistakes.
description: 'Question to ask the Primer bot'
required: true

permissions: {}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

permissions: {} at the workflow level will leave GITHUB_TOKEN with no permissions, which causes actions/checkout to fail (it needs contents: read). Add at least permissions: contents: read (either at the workflow level or job level).

Suggested change
permissions: {}
permissions:
contents: read

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +13
function readBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: Uint8Array[] = []
req.on('data', (chunk: Uint8Array) => chunks.push(chunk))
req.on('end', () => resolve(Buffer.concat(chunks).toString()))
req.on('error', reject)
})
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readBody buffers the entire request body with no size limit. If this server is run anywhere beyond local dev, a large request can cause memory pressure/DoS before the later question.length check. Add a maximum body size (e.g. stop reading after N bytes and return 413).

Copilot uses AI. Check for mistakes.
@primer
Copy link
Copy Markdown
Contributor

primer bot commented Mar 30, 2026

🤖 Lint issues have been automatically fixed and committed to this PR.

@github-actions github-actions bot temporarily deployed to storybook-preview-7713 March 30, 2026 12:35 Inactive
@primer
Copy link
Copy Markdown
Contributor

primer bot commented Mar 30, 2026

🤖 Lint issues have been automatically fixed and committed to this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants