Webex Contact Center AI Agent Helper Bot
Find a file
jmcqueen 034e657fcb feat(close): programmatic SS-ticket closure for the CC agent
Extends the app so a Contact Center AI agent can close SS tickets
end-to-end (customer-confirmed fix, cancellation, duplicate) without a
human touch — the natural next step after ticket creation landed.

The SS Resolved (161) workflow validator requires four fields on the
ticket at execution time (components + 3 CMDB customfields for Business
Service / System / Cause). The transition screen itself only exposes
resolution, so the closer has to PUT these into the ticket first and
*then* fire the transition. This change wires up the whole flow with
sensible per-subType defaults, so the AI can close a typical ticket
with just { subType, comment }.

Design (see Forgejo #10 for the full write-up):

- Generalized services/jira/storesCache into a reusable factory
  createAssetsObjectCache. Same in-memory Map + on-disk JSON snapshot +
  startAt/maxResults pagination + personal-PAT auth. Store cache becomes
  one instance; three new caches join it for the CMDB validator fields
  (businessServices=100, systems=103, causes=107 — all in schema 68, so
  the same permission workaround from Forgejo #8 keeps working).

- Boot-time init warms all four caches in parallel; a new
  services/jira/caches.js barrel exposes them for lifecycle (init) and
  by-name lookup (admin routes). Full sync takes ~5s in the tenant
  (2661 stores dominate — the three CMDB caches together hold ~653
  entries and finish in ~1s).

- Admin surface rebuilt around a plural /admin/caches path:
    GET  /admin/caches                    — status of all four
    GET  /admin/caches/:name/status       — one cache
    POST /admin/caches/:name/refresh      — force resync of one
    POST /admin/caches/refreshAll         — parallel refresh
  Old /admin/storesCache/{status,refresh} are kept as aliases.

- Config renamed: config.storesCache -> config.caches (dir instead of
  path; filenames auto-derived per cache). Env vars renamed to
  CACHES_* (dir/refresh-hours/stale-hours/page-size/max-pages).
  .env.example updated.

- ssCloseDefaults.js maps every supported SS subType to a
  {component, businessService, system, cause} tuple. Values verified
  against the live caches so they resolve at runtime. Falls back to a
  safe __default__ tuple (Help Desk / Store Technology / "I can't find
  my option - Misc" / Unknown) for un-mapped subTypes — those catch-all
  values are the tenant's designed "I don't know" escape valves.

- issues.js closeTicket completely reworked:
    * setSSValidatorFields helper: reads current ticket state, resolves
      each of the four fields via caller > existing-on-ticket > default,
      PUTs them using the Cloud CMDB shape [{id: "<ws>:<objectId>"}]
      (same fix as Forgejo #8 for Store Number).
    * Only writes fields that need writing; preserves human triage by
      default (preserveExistingClassification=true).
    * Fixes Forgejo #9: only sends the resolution field when the
      chosen transition targets statusCategory=done. Non-done
      transitions (e.g. "Waiting for customer") no longer 400 on
      "Field 'resolution' cannot be set."

- closeHelpers.js adds three CC-agent intent wrappers:
    confirmFixed(key, {subType, ...})       → resolution=Done
    customerCancelled(key, {subType, ...})  → resolution=Won't Do
    markDuplicate(key, {primaryKey, ...})   → resolution=Duplicate +
                                              formal Duplicate issueLink
                                              to the primary
  Each posts a standardized internal audit-trail comment ("Closed via
  WxCC AI agent: …") that documents the automated action for the
  humans who inherit the ticket.

- New routes: POST /ticket/:key/{confirmFixed,customerCancelled,
  duplicate} plus an expanded body on the existing /close route.

Verified end-to-end: POST /ticket/SS-20948/confirmFixed with just
subType=Report a Technology issue transitioned the ticket to Resolved
with all four validator fields populated from defaults (Help Desk /
Store Technology / I can't find my option - Misc / Unknown), plus
resolution=Done. Direct Jira REST GET confirms every field persisted.

Closes Forgejo #9. Refs Forgejo #10.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 11:20:41 -04:00
bin Docs: prefer .env for ASSETS_SYNC_TOKEN; Keychain is optional dev path 2026-07-07 10:22:40 -04:00
src feat(close): programmatic SS-ticket closure for the CC agent 2026-07-07 11:20:41 -04:00
.dockerignore Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
.env.example feat(close): programmatic SS-ticket closure for the CC agent 2026-07-07 11:20:41 -04:00
.gitignore Workaround #1: cache Assets store lookups via personal PAT sync 2026-07-07 10:18:28 -04:00
discover-ss-fields.js Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
discover-ss-request-types.js Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
docker-compose.yml Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
Dockerfile Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
package-lock.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
package.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
README.md feat(close): programmatic SS-ticket closure for the CC agent 2026-07-07 11:20:41 -04:00
ss-fields-266.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-267.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-268.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-269.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-270.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-271.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-272.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-273.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-274.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-275.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-426.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-493.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-request-types-clean.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-request-types.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00

wxcc-ai

Node/Express service that wires Webex Contact Center summaries, Jira tickets (core + JSM Store Support), and xAI Grok summarization into a single set of internal HTTP endpoints.

Setup

npm install
cp .env.example .env
# fill in JIRA_*, XAI_*, and optionally the Assets values
npm run dev

The server listens on PORT (default 1866).

Environment variables

See .env.example for the full list. The important ones:

Var Purpose
JIRA_CLOUD_ID If set, requests use https://api.atlassian.com/ex/jira/{cloudId}.
JIRA_BASE_URL Fallback for legacy site URLs (https://your-site.atlassian.net).
JIRA_EMAIL, JIRA_API_TOKEN Basic auth to Jira. JIRA_AUTH_TYPE=bearer switches to bearer.
JIRA_SERVICE_DESK_ID Numeric JSM service desk id for Store Support.
JIRA_ASSETS_WORKSPACE_ID Assets workspace id (auto-discovered if omitted).
JIRA_ASSETS_STORE_SCHEMA_ID, _OBJECT_TYPE_ID Locate the Store schema/type.
JIRA_ASSETS_STORE_NUMBER_ATTRIBUTE[_ID] Attribute name (or id) holding the store number in Assets.
JIRA_STORE_CUSTOM_FIELD_ID Custom field on the JSM request that holds the Store Assets reference.
XAI_API_KEY, XAI_BASE_URL Grok credentials for summary generation.

Endpoints

Base path: /api/wxccai.

Read

  • GET /getticket?jiraKey=CS-1234 — Grok-summarized single ticket.
  • GET /open-tickets-by-reporter?email=user@example.com — Grok-summarized list of open tickets a person reported.
  • GET /ticket/:key/status — raw status fields (no Grok).
  • GET /ticket/:key/transitions — available workflow transitions.

Write

  • PATCH /ticket/:key — body: { summary?, description?, priority?, labels?, assigneeAccountId?, additional? }
  • POST /ticket/:key/comment — body: { text, internal? }
  • POST /ticket/:key/close — body: { transitionName?, resolution?, comment?, internal?, component?, businessService?, system?, cause?, subType?, preserveExistingClassification?, skipValidatorFields? }. Auto-picks the first "done" transition when transitionName is omitted. When the chosen transition is done-category, the four SS workflow-validator fields (components, Business Service, System, Cause) are populated first (see "Closing SS tickets" below). Non-done transitions skip the resolution field (fixes issue #9).
  • POST /ticket/:key/confirmFixed — CC-agent convenience. Body: { subType?, comment?, component?, businessService?, system?, cause?, internal? }. Closes with resolution=Done.
  • POST /ticket/:key/customerCancelled — CC-agent convenience. Body: { subType?, reason?, component?, businessService?, system?, cause?, internal? }. Closes with resolution=Won't Do.
  • POST /ticket/:key/duplicate — CC-agent convenience. Body: { primaryKey (required), subType?, comment?, component?, businessService?, system?, cause?, internal? }. Creates a formal Duplicate issue link to primaryKey, then closes with resolution=Duplicate.

Store Support (JSM requests)

  • GET /ssRequestTypes — supported subType values + Assets config summary.
  • POST /createSSRequest — body includes subType, summary, storeNumber (auto-resolved via Assets), onBehalfOf, description, additional.

Webex webhook

  • POST /issueTranscript/:jiraKey — attaches audio + JSON transcript + human-readable transcript, then posts a restricted-visibility summary comment.

Admin

  • GET /admin/caches — snapshot of all four Assets object caches (stores, businessServices, systems, causes): { caches: [{ name, count, lastSyncAt, ageSeconds, syncing, assetsSyncConfigured, ... }] }. Safe for health checks.
  • GET /admin/caches/:name/status — same shape, one cache.
  • POST /admin/caches/:name/refresh — force an immediate resync of one cache via the personal PAT (see below). Takes a few seconds.
  • POST /admin/caches/refreshAll — refresh every cache in parallel.
  • GET /admin/storesCache/status and POST /admin/storesCache/refresh — backward-compat aliases for the stores cache endpoints above.

Debug (non-production only)

  • GET /debug/assetsProbe?storeNumber=305 — runs several AQL variants against Jira Assets and returns visible schemas + object-type detail + a computed diagnosis. Returns 404 when NODE_ENV=production.

Assets object caches (Assets workaround)

Several parts of the SS lifecycle need to translate a human-readable name (a store number, a Business Service name, a System name, a Cause code) into a Jira Assets object id before the value can be written to a CMDB custom field. The shared service account is silently filtered out of the underlying object schema (68), so the app maintains four local caches that are populated from a personal Atlassian PAT (a real human account with the right Assets role):

Cache Object type Used for
stores 109 customfield_10261 Store Number on new SS tickets
businessServices 100 customfield_10224 Business Service workflow validator on close
systems 103 customfield_10225 System workflow validator on close
causes 107 customfield_10233 Cause workflow validator on close

All four are backed by the same shared factory (services/jira/assetsObjectCache.js) and use the same PAT credentials. The service account is still used for everything else (creating tickets, comments, attachments, transitions).

Both storage patterns end up in the same place — process.env.ASSETS_SYNC_TOKEN — so the runtime code path is identical. Pick whichever fits the host.

Setup A — production / Linux host (.env)

Put the values directly in .env (which is gitignored) and lock the file down:

cat >> .env <<'EOF'
ASSETS_SYNC_EMAIL=you@ae.com
ASSETS_SYNC_TOKEN=<paste-your-atlassian-api-token>
EOF

chmod 600 .env    # only the bot user can read it

Then start normally:

npm start

Rotate the token in Atlassian → Account → Security → API tokens whenever the trust boundary on the host changes (new operator, offboarding, suspected leak). The app reloads it on the next process start.

Setup B — local dev on macOS (Keychain)

If you're running the app on a Mac and would rather not keep the PAT in .env, use the wrapper script — it pulls the token from Keychain into ASSETS_SYNC_TOKEN before exec'ing the process:

security add-generic-password \
    -s jira-assets-sync \
    -a you@ae.com \
    -w '<paste-your-atlassian-api-token>' \
    -U

echo 'ASSETS_SYNC_EMAIL=you@ae.com' >> .env   # email in .env, token stays in Keychain

./bin/load-assets-sync-secret.sh npm start

If ASSETS_SYNC_TOKEN is already in the process env (Setup A above), the wrapper is a no-op and skips the Keychain lookup.

Runtime behavior

On boot the app loads each on-disk cache from $CACHES_DIR/{name}.json (default ./data/), kicks off a background refresh for any snapshot missing or older than CACHES_STALE_AFTER_HOURS, and schedules a periodic full resync every CACHES_REFRESH_HOURS. resolveStoreAssetReference serves lookups from memory (sub-ms) with a live PAT lookup as fallback for brand-new stores. The three close-time caches (business services, systems, causes) are much smaller (dozens to a few hundred entries) and rarely change.

Force a refresh at any time:

curl -X POST http://localhost:1866/api/wxccai/admin/caches/stores/refresh
curl -X POST http://localhost:1866/api/wxccai/admin/caches/refreshAll

Closing SS tickets

The Resolved transition on SS tickets fires a workflow validator that requires four fields to be populated:

  • components — Jira native (63 options in the SS project)
  • customfield_10224 Business Service — Assets CMDB
  • customfield_10225 System — Assets CMDB
  • customfield_10233 Cause — Assets CMDB (the value "Unknown" exists as a designed catch-all)

closeTicket fills these in before calling the Resolved transition. Resolution order per field:

  1. Explicit value in the request body (component, businessService, system, cause)
  2. Whatever is already on the ticket (if preserveExistingClassification is true, the default — respects human triage)
  3. The per-subType default from src/config/ssCloseDefaults.js
  4. The __default__ entry (Help Desk / Store Technology / I can't find my option - Misc / Unknown)

For CC-agent-driven closes, the shortest path is to send just subType and comment; everything else is defaulted. Use the convenience routes:

# Caller confirms the issue is resolved
curl -X POST http://localhost:1866/api/wxccai/ticket/SS-20948/confirmFixed \
  -H 'Content-Type: application/json' \
  -d '{"subType":"Report a Technology issue"}'

# Caller wants to cancel
curl -X POST http://localhost:1866/api/wxccai/ticket/SS-20949/customerCancelled \
  -H 'Content-Type: application/json' \
  -d '{"subType":"Broken device / hardware","reason":"changed their mind"}'

# Duplicate of an earlier ticket (creates a formal Duplicate issueLink)
curl -X POST http://localhost:1866/api/wxccai/ticket/SS-20950/duplicate \
  -H 'Content-Type: application/json' \
  -d '{"primaryKey":"SS-20948"}'

To override the auto-detected classification (for a subType not in the defaults map, or when the caller volunteers specific context), pass any of the four fields explicitly. CMDB names are matched case-insensitively; you can also pass a raw Assets objectId as a shortcut for businessService / system / cause:

curl -X POST http://localhost:1866/api/wxccai/ticket/SS-XXXXX/confirmFixed \
  -H 'Content-Type: application/json' \
  -d '{"subType":"Broken device / hardware","system":"Printer","cause":"Broken Equipment"}'

Jira Assets gotcha

createSSRequest resolves a store number to an Assets object reference (customfield_10261). Two independent permission layers must both grant access, or every AQL query silently returns total: 0:

  1. OAuth scopes on the API token: read:cmdb-schema:jira, read:cmdb-type:jira, read:cmdb-object:jira, read:cmdb-attribute:jira (and the write: equivalents for updates).
  2. Object Schema role membership in Jira Assets itself — the underlying user needs to be added to a role on the Store schema in Jira → Assets → Object schemas → Configure → Roles.

If AQL keeps returning total: 0 with HTTP 200, run the probe endpoint above; the diagnosis field will tell you exactly which layer is missing.

Docker

npm run docker:build
npm run docker:run

The image runs as a non-root user and exposes 1866. HEALTHCHECK pings GET /health.

Repo hygiene

  • Secrets live only in .env, which is .gitignored.
  • The two discover-ss-*.js helper scripts read from .env — never hardcode credentials in them.
  • The debug logger no longer echoes the outbound Authorization header. Rotate any token that appears in older logs/*.log files.