@method-ai-hq/sdk 0.0.0-stage → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +23 -2
- package/dist/packages/contracts/src/identity.d.ts +16 -0
- package/dist/packages/contracts/src/identity.js +41 -0
- package/dist/packages/contracts/src/method-package.d.ts +71 -0
- package/dist/packages/contracts/src/method-package.js +22 -0
- package/dist/packages/method-document/src/conversion-report.d.ts +138 -0
- package/dist/packages/method-document/src/conversion-report.js +23 -0
- package/dist/packages/sdk/examples/daily-briefing/README.md +31 -0
- package/dist/packages/sdk/examples/daily-briefing/TASK.md +3 -0
- package/dist/packages/sdk/examples/daily-briefing/approved-report.md +138 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_artifacts.py +39 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_check_inputs.py +47 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_files.py +31 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_manifest.py +23 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_markdown.py +67 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_progress.py +18 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_render.py +71 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_sessions.py +37 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_times.py +158 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_validation.py +101 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_website.py +215 -0
- package/dist/packages/sdk/examples/daily-briefing/checks.json +19 -0
- package/dist/packages/sdk/examples/daily-briefing/daily-briefing.method +158 -0
- package/dist/packages/sdk/examples/daily-briefing/inputs.json +1 -0
- package/dist/packages/sdk/examples/daily-briefing/package-lock.json +10 -0
- package/dist/packages/sdk/examples/daily-briefing/package.json +4 -0
- package/dist/packages/sdk/examples/daily-briefing/pyproject.toml +8 -0
- package/dist/packages/sdk/examples/daily-briefing/reader/reader.css +35 -0
- package/dist/packages/sdk/examples/daily-briefing/reader/reader.js +42 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/context.json +1 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/documents/document-001.md +219 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/media.json +1 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/metadata/documents.json +39 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0001.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0002.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0003.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0004.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0005.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0006.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0007.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0008.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0009.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0010.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0011.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0012.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0013.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0014.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0015.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0016.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0017.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0018.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0019.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0020.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0021.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0022.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0023.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0024.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0025.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0026.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0027.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0028.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/sessions.json +1 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/timeline.jsonl +28 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/untimed.jsonl +0 -0
- package/dist/packages/sdk/examples/daily-briefing/sample-output.zip +0 -0
- package/dist/packages/sdk/examples/daily-briefing/sample-report.md +51 -0
- package/dist/packages/sdk/examples/daily-briefing/uv.lock +128 -0
- package/dist/packages/sdk/examples/daily-briefing/vendor/marked-LICENSE.md +44 -0
- package/dist/packages/sdk/examples/daily-briefing/vendor/marked.mjs +77 -0
- package/dist/packages/sdk/examples/files.json +126 -0
- package/dist/packages/sdk/examples/message-routing/README.md +23 -0
- package/dist/packages/sdk/examples/message-routing/TASK.md +5 -0
- package/dist/packages/sdk/examples/message-routing/choose-destination.mjs +10 -0
- package/dist/packages/sdk/examples/message-routing/create-ticket.mjs +15 -0
- package/dist/packages/sdk/examples/message-routing/inputs.json +1 -0
- package/dist/packages/sdk/examples/message-routing/message-routing.method +45 -0
- package/dist/packages/sdk/examples/message-routing/result.fixture.json +6 -0
- package/dist/packages/sdk/examples/message-routing/routing.mjs +29 -0
- package/dist/packages/sdk/examples/message-routing/routing.test.mjs +27 -0
- package/dist/packages/sdk/examples/message-routing/ticket.method +88 -0
- package/dist/packages/sdk/examples/message-routing/ticket.test.mjs +64 -0
- package/dist/packages/sdk/examples/notes-summary/README.md +23 -0
- package/dist/packages/sdk/examples/notes-summary/TASK.md +3 -0
- package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/case.json +36 -0
- package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/examples.json +16 -0
- package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/files/0 +10 -0
- package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/recording.json +195 -0
- package/dist/packages/sdk/examples/notes-summary/notes/2026-10-05-standup.md +4 -0
- package/dist/packages/sdk/examples/notes-summary/notes/2026-10-07-planning.md +3 -0
- package/dist/packages/sdk/examples/notes-summary/notes-summary.method +54 -0
- package/dist/packages/sdk/examples/notes-summary/out/.keep +0 -0
- package/dist/packages/sdk/examples/notes-summary/read-notes.mjs +4 -0
- package/dist/packages/sdk/examples/notes-summary/save.mjs +7 -0
- package/dist/packages/sdk/examples/outbound-management/README.md +119 -0
- package/dist/packages/sdk/examples/outbound-management/TASK.md +5 -0
- package/dist/packages/sdk/examples/outbound-management/check_plan.py +16 -0
- package/dist/packages/sdk/examples/outbound-management/check_sources.py +14 -0
- package/dist/packages/sdk/examples/outbound-management/crm.py +153 -0
- package/dist/packages/sdk/examples/outbound-management/crm_test.py +141 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/README.md +15 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/crm.json +54 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/day-1/messages.json +29 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/day-2/messages.json +38 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/observations.json +12 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/plan.json +69 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/receipt.json +1 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/state.json +99 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/tasks.md +16 -0
- package/dist/packages/sdk/examples/outbound-management/inputs.json +4 -0
- package/dist/packages/sdk/examples/outbound-management/outbound.method +277 -0
- package/dist/packages/sdk/examples/outbound-management/read_crm.py +4 -0
- package/dist/packages/sdk/examples/outbound-management/save_day.py +4 -0
- package/dist/packages/sdk/examples/outbound-management/starter/crm.json +11 -0
- package/dist/packages/sdk/examples/social-briefing/README.md +41 -0
- package/dist/packages/sdk/examples/social-briefing/TASK.md +5 -0
- package/dist/packages/sdk/examples/social-briefing/inputs.json +5 -0
- package/dist/packages/sdk/examples/social-briefing/result.fixture.json +14 -0
- package/dist/packages/sdk/examples/social-briefing/social-briefing.method +110 -0
- package/dist/packages/sdk/examples/support-triage/README.md +77 -0
- package/dist/packages/sdk/examples/support-triage/TASK.md +3 -0
- package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/case.json +40 -0
- package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/recording.json +460 -0
- package/dist/packages/sdk/examples/support-triage/fake-helpdesk.mjs +38 -0
- package/dist/packages/sdk/examples/support-triage/helpdesk.mjs +33 -0
- package/dist/packages/sdk/examples/support-triage/inputs.json +1 -0
- package/dist/packages/sdk/examples/support-triage/read-ticket.mjs +8 -0
- package/dist/packages/sdk/examples/support-triage/route.mjs +4 -0
- package/dist/packages/sdk/examples/support-triage/sample-tickets.json +5 -0
- package/dist/packages/sdk/examples/support-triage/set-ticket.mjs +9 -0
- package/dist/packages/sdk/examples/support-triage/support-triage.method +165 -0
- package/dist/packages/sdk/examples/support-triage/triage.mjs +38 -0
- package/dist/packages/sdk/examples/support-triage/triage.test.mjs +68 -0
- package/dist/packages/sdk/observers/mail.delivery/fetch.mjs +87 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/bounce-by-recipient.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/bounce.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/delayed.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/delivered.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/empty.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/other-message.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/judge.mjs +13 -0
- package/dist/packages/sdk/observers/mail.delivery/observer.json +14 -0
- package/dist/packages/sdk/package.json +232 -0
- package/dist/packages/sdk/src/authoring-example.d.ts +91 -0
- package/dist/packages/sdk/src/authoring-example.js +50 -0
- package/dist/packages/sdk/src/authoring-instructions.d.ts +20 -0
- package/dist/packages/sdk/src/authoring-instructions.js +151 -0
- package/dist/packages/sdk/src/authoring-prompt.d.ts +2 -0
- package/dist/packages/sdk/src/authoring-prompt.js +8 -0
- package/dist/packages/sdk/src/authoring.d.ts +10 -0
- package/dist/packages/sdk/src/authoring.js +287 -0
- package/dist/packages/sdk/src/bindings.d.ts +4 -0
- package/dist/packages/sdk/src/bindings.js +124 -0
- package/dist/packages/sdk/src/bridge.d.ts +2 -0
- package/dist/packages/sdk/src/bridge.js +23 -0
- package/dist/packages/sdk/src/browser-runtime/pyproject.toml +5 -0
- package/dist/packages/sdk/src/browser-runtime/tools.json +189 -0
- package/dist/packages/sdk/src/browser-runtime/uv.lock +3894 -0
- package/dist/packages/sdk/src/browser-service.py +127 -0
- package/dist/packages/sdk/src/browser.d.ts +38 -0
- package/dist/packages/sdk/src/browser.js +203 -0
- package/dist/packages/sdk/src/capabilities.d.ts +13 -0
- package/dist/packages/sdk/src/capabilities.js +68 -0
- package/dist/packages/sdk/src/classification-client.d.ts +4 -0
- package/dist/packages/sdk/src/classification-client.js +27 -0
- package/dist/packages/sdk/src/cloud.d.ts +23 -0
- package/dist/packages/sdk/src/cloud.js +43 -0
- package/dist/packages/sdk/src/computer-settings.d.ts +28 -0
- package/dist/packages/sdk/src/computer-settings.js +57 -0
- package/dist/packages/sdk/src/connect.d.ts +47 -0
- package/dist/packages/sdk/src/connect.js +203 -0
- package/dist/packages/sdk/src/current-inspection.d.ts +3 -0
- package/dist/packages/sdk/src/current-inspection.js +146 -0
- package/dist/packages/sdk/src/current-runtime.d.ts +14 -0
- package/dist/packages/sdk/src/current-runtime.js +167 -0
- package/dist/packages/sdk/src/doctor.d.ts +17 -0
- package/dist/packages/sdk/src/doctor.js +68 -0
- package/dist/packages/sdk/src/executable.d.ts +2 -0
- package/dist/packages/sdk/src/executable.js +22 -0
- package/dist/packages/sdk/src/explain-effects.d.ts +32 -0
- package/dist/packages/sdk/src/explain-effects.js +192 -0
- package/dist/packages/sdk/src/explain.d.ts +115 -0
- package/dist/packages/sdk/src/explain.js +441 -0
- package/dist/packages/sdk/src/files.d.ts +9 -0
- package/dist/packages/sdk/src/files.js +50 -0
- package/dist/packages/sdk/src/guard-keys.d.ts +5 -0
- package/dist/packages/sdk/src/guard-keys.js +42 -0
- package/dist/packages/sdk/src/hosted-models.d.ts +49 -0
- package/dist/packages/sdk/src/hosted-models.js +81 -0
- package/dist/packages/sdk/src/improve.d.ts +55 -0
- package/dist/packages/sdk/src/improve.js +159 -0
- package/dist/packages/sdk/src/index.d.ts +13 -0
- package/dist/packages/sdk/src/index.js +13 -0
- package/dist/packages/sdk/src/inspect.d.ts +6 -0
- package/dist/packages/sdk/src/inspect.js +17 -0
- package/dist/packages/sdk/src/link.d.ts +12 -0
- package/dist/packages/sdk/src/link.js +53 -0
- package/dist/packages/sdk/src/local-cli.d.ts +24 -0
- package/dist/packages/sdk/src/local-cli.js +146 -0
- package/dist/packages/sdk/src/local-setup.d.ts +15 -0
- package/dist/packages/sdk/src/local-setup.js +42 -0
- package/dist/packages/sdk/src/method-client.d.ts +30 -0
- package/dist/packages/sdk/src/method-client.js +250 -0
- package/dist/packages/sdk/src/method-files.d.ts +29 -0
- package/dist/packages/sdk/src/method-files.js +93 -0
- package/dist/packages/sdk/src/method-help.d.ts +19 -0
- package/dist/packages/sdk/src/method-help.js +404 -0
- package/dist/packages/sdk/src/method-issues.d.ts +86 -0
- package/dist/packages/sdk/src/method-issues.js +192 -0
- package/dist/packages/sdk/src/method-sync.d.ts +46 -0
- package/dist/packages/sdk/src/method-sync.js +293 -0
- package/dist/packages/sdk/src/method.d.ts +5 -0
- package/dist/packages/sdk/src/method.js +359 -0
- package/dist/packages/sdk/src/model-checks.d.ts +96 -0
- package/dist/packages/sdk/src/model-checks.js +181 -0
- package/dist/packages/sdk/src/outbox.d.ts +120 -0
- package/dist/packages/sdk/src/outbox.js +297 -0
- package/dist/packages/sdk/src/own-model-key.d.ts +22 -0
- package/dist/packages/sdk/src/own-model-key.js +76 -0
- package/dist/packages/sdk/src/prepare.d.ts +24 -0
- package/dist/packages/sdk/src/prepare.js +245 -0
- package/dist/packages/sdk/src/process.d.ts +15 -0
- package/dist/packages/sdk/src/process.js +107 -0
- package/dist/packages/sdk/src/production.d.ts +182 -0
- package/dist/packages/sdk/src/production.js +284 -0
- package/dist/packages/sdk/src/progress.d.ts +12 -0
- package/dist/packages/sdk/src/progress.js +48 -0
- package/dist/packages/sdk/src/proposals.d.ts +73 -0
- package/dist/packages/sdk/src/proposals.js +284 -0
- package/dist/packages/sdk/src/quality.d.ts +82 -0
- package/dist/packages/sdk/src/quality.js +294 -0
- package/dist/packages/sdk/src/result-files.d.ts +5 -0
- package/dist/packages/sdk/src/result-files.js +149 -0
- package/dist/packages/sdk/src/result-transfer.d.ts +21 -0
- package/dist/packages/sdk/src/result-transfer.js +37 -0
- package/dist/packages/sdk/src/run-data.d.ts +11 -0
- package/dist/packages/sdk/src/run-data.js +80 -0
- package/dist/packages/sdk/src/run-method.d.ts +2 -0
- package/dist/packages/sdk/src/run-method.js +2 -0
- package/dist/packages/sdk/src/run-saved.d.ts +5 -0
- package/dist/packages/sdk/src/run-saved.js +138 -0
- package/dist/packages/sdk/src/run-worker.d.ts +6 -0
- package/dist/packages/sdk/src/run-worker.js +131 -0
- package/dist/packages/sdk/src/secrets.d.ts +37 -0
- package/dist/packages/sdk/src/secrets.js +196 -0
- package/dist/packages/sdk/src/update.d.ts +5 -0
- package/dist/packages/sdk/src/update.js +39 -0
- package/dist/packages/sdk/src/versions.d.ts +58 -0
- package/dist/packages/sdk/src/versions.js +144 -0
- package/dist/packages/sdk/src/worker.d.ts +120 -0
- package/dist/packages/sdk/src/worker.js +443 -0
- package/dist/packages/workflow-language/src/execution.d.ts +1 -0
- package/dist/packages/workflow-language/src/execution.js +1 -0
- package/dist/packages/workflow-language/src/inspection.d.ts +990 -0
- package/dist/packages/workflow-language/src/inspection.js +130 -0
- package/dist/packages/workflow-language/src/result-files.d.ts +160 -0
- package/dist/packages/workflow-language/src/result-files.js +67 -0
- package/dist/packages/workflow-language/src/schema.d.ts +207 -0
- package/dist/packages/workflow-language/src/schema.js +56 -0
- package/dist/packages/workflow-language/src/validate.d.ts +11 -0
- package/dist/packages/workflow-language/src/validate.js +77 -0
- package/package.json +230 -4
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Triage a support ticket
|
|
2
|
+
|
|
3
|
+
When a ticket arrives, this Method reads it from the help desk, chooses its queue, priority, and tags, drafts a first reply, and updates the ticket. When the team is not clear, the ticket goes to the triage queue and a person chooses the team.
|
|
4
|
+
|
|
5
|
+
| Step | Type | What it does |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `read` | run | Reads the ticket with `HELPDESK_TOKEN`. |
|
|
8
|
+
| `team` | classify, options | billing, technical, account, or `unclear`. |
|
|
9
|
+
| `refund` | classify, `answer: yes_no` | Does the customer ask for money back? |
|
|
10
|
+
| `urgency` | classify, `levels` | low, normal, high, or urgent. |
|
|
11
|
+
| `route` | run | The rule: the team's queue at 80% or more, else triage and a person; the refund tag at 50% or more. |
|
|
12
|
+
| `draft_reply` | call, `model: writer` | A first reply for an agent to check. The prompt shows two replies that the team liked. |
|
|
13
|
+
| `update` | run, changes the help desk | Sets queue, priority, and tags, and adds the reply as an internal note. |
|
|
14
|
+
| `ask_person` | ask, when a person is needed | "Which team should handle it?" |
|
|
15
|
+
| `move` | run, when a person is needed | Moves the ticket to the team that the person chose. |
|
|
16
|
+
|
|
17
|
+
What the file shows:
|
|
18
|
+
|
|
19
|
+
- **`models:`** names the model of the reply step: `writer: anthropic/claude-haiku-4.5`. Hosted models need no key.
|
|
20
|
+
- **`secrets:`** declares `HELPDESK_TOKEN`. Scripts read it from their environment. It never goes into the Method or the run records.
|
|
21
|
+
- **Three classify forms**, each with one question. The `unclear` option lets the classifier say that it cannot choose; a guess is not hidden.
|
|
22
|
+
- **The rule is in a script**, not in a prompt, so it is the same on every run and `triage.test.mjs` tests it.
|
|
23
|
+
- **An effect on each help desk change.** The http observer reads the ticket back. A 200 response shows only that the help desk accepted the request. The `update` effect checks the priority but not the queue, because the `move` step changes the queue after a person's answer.
|
|
24
|
+
- **`run_data: device`** keeps the run records on this computer. The classify and call steps still send the ticket text to the classifier and the model.
|
|
25
|
+
|
|
26
|
+
## Try it on this computer
|
|
27
|
+
|
|
28
|
+
`fake-helpdesk.mjs` is a small help desk with the three tickets in `sample-tickets.json`. It keeps them in memory.
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
method secret set HELPDESK_TOKEN # opens a private form; enter any test value
|
|
32
|
+
HELPDESK_TOKEN=THE_SAME_VALUE node fake-helpdesk.mjs 4310 &
|
|
33
|
+
method bind support-triage.method helpdesk --connection http://127.0.0.1:4310
|
|
34
|
+
method run support-triage.method --inputs inputs.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For your own help desk, run `method secret find` in this folder. It lists the key files nearby and the names in each, never the values, and prints the `method secret import` command for the file that has `HELPDESK_TOKEN`. Then bind `helpdesk` to your help desk's API URL. A help desk that needs a token for reads too gets it from `METHOD_OBSERVER_TOKEN`: the http observer sends that as a Bearer token.
|
|
38
|
+
|
|
39
|
+
T-1042 (charged twice) goes to Billing with the `refund_request` tag. T-1043 ("Can't get in") does not say enough, so the run stops at `ask_person` with exit code 2. Answer it with `--human`:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
echo '{"steps": {"ask_person:0": {"outputs": {"chosen_queue": "account"}}}}' > answer.json
|
|
43
|
+
method run support-triage.method --run-dir RUN_DIR --resume --human answer.json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The case in cases/
|
|
47
|
+
|
|
48
|
+
`cases/double-charge-to-billing` keeps the right result for T-1042: queue billing, tag `refund_request`, no person asked. It was made with `method case new support-triage.method --id double-charge-to-billing --passing-run RUN --expect expect.json`, where `expect.json` has three `equals` checks on `outputs.decision`. The classify answers in its recording came from a fixed test classifier, so the case checks the rule and not the classifier. `method test support-triage.method` replays it: steps that did not change return their recorded outputs. When you change the route rule, the route step runs again and the case shows whether T-1042 still goes to Billing.
|
|
49
|
+
|
|
50
|
+
## Tests
|
|
51
|
+
|
|
52
|
+
`node --test triage.test.mjs` tests the rule at its thresholds, the change that the scripts send, and the scripts against the fake help desk (the token, a repeated update, and a missing ticket).
|
|
53
|
+
|
|
54
|
+
## Put it in your app
|
|
55
|
+
|
|
56
|
+
Publish the Method and connect your app to it:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
method connect ../my-app
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`method connect` publishes the Method if it has no published version, makes a service key for the app, writes it to the app's `.env` without showing it, and prints the code to add. For a Python app the code is like this:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from withmethod import Method
|
|
66
|
+
|
|
67
|
+
method = Method() # reads METHOD_API_KEY from the environment or .env
|
|
68
|
+
|
|
69
|
+
def on_ask(question):
|
|
70
|
+
# question.step is "ask_person", question.question is the text, question.form the answer's JSON Schema.
|
|
71
|
+
return None # return {"chosen_queue": "billing"}, or None to send the question to the team inbox
|
|
72
|
+
|
|
73
|
+
run = method.run("METHOD_ID", {"ticket_id": ticket_id}, on_ask=on_ask, idempotency_key=f"ticket-{ticket_id}")
|
|
74
|
+
result = run["result"]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
In Node, use `new Method().run({method, inputs, idempotencyKey, onAsk})`. One idempotency key for each ticket means that a repeated webhook does not triage the ticket twice. A question that the app does not answer waits in the team inbox until a person answers it there or with `method answer RUN_ID`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "method-case/1",
|
|
3
|
+
"id": "double-charge-to-billing",
|
|
4
|
+
"status": "active",
|
|
5
|
+
"method_file": "support-triage.method",
|
|
6
|
+
"note": "A customer charged twice asks for the second charge back. The ticket goes to Billing with the refund_request tag, and no person is asked.",
|
|
7
|
+
"author": null,
|
|
8
|
+
"created": "2026-10-10T02:31:11.810Z",
|
|
9
|
+
"source": {
|
|
10
|
+
"run_dir": "/work/support-triage/run2",
|
|
11
|
+
"execution_id": "6707d77c-38bc-4e10-a5f3-cebd56ddeb71",
|
|
12
|
+
"method_sha256": "bbbb45763b9b8db47305c6af4940052c2282f4b8a1e607b36eb6732b97a41fb9",
|
|
13
|
+
"passing_run_dir": "/work/support-triage/run2"
|
|
14
|
+
},
|
|
15
|
+
"expect": [
|
|
16
|
+
{
|
|
17
|
+
"kind": "equals",
|
|
18
|
+
"ref": "outputs.decision.queue",
|
|
19
|
+
"value": "billing"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"kind": "equals",
|
|
23
|
+
"ref": "outputs.decision.tags",
|
|
24
|
+
"value": [
|
|
25
|
+
"refund_request"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"kind": "equals",
|
|
30
|
+
"ref": "outputs.decision.needs_person",
|
|
31
|
+
"value": false
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"runs": null,
|
|
35
|
+
"min_pass": null,
|
|
36
|
+
"supersedes": [],
|
|
37
|
+
"superseded_by": null,
|
|
38
|
+
"retention_until": "2027-10-10",
|
|
39
|
+
"redacted": true
|
|
40
|
+
}
|
package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/recording.json
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "method-recording/1",
|
|
3
|
+
"method_sha256": "bbbb45763b9b8db47305c6af4940052c2282f4b8a1e607b36eb6732b97a41fb9",
|
|
4
|
+
"execution_id": "6707d77c-38bc-4e10-a5f3-cebd56ddeb71",
|
|
5
|
+
"inputs": {
|
|
6
|
+
"ticket_id": "T-1042"
|
|
7
|
+
},
|
|
8
|
+
"initial_state": {},
|
|
9
|
+
"environment": {
|
|
10
|
+
"helpdesk": "http://127.0.0.1:4310"
|
|
11
|
+
},
|
|
12
|
+
"keys": {
|
|
13
|
+
"read": "805c11d820c39324d89adfabacd41b1f731a48ffe07e2586f9942ac241b7d04d",
|
|
14
|
+
"team": "918e0fd113af5eda3e22bc00dca64ab97dc73c6f394b677ea2a51601e7204dad",
|
|
15
|
+
"refund": "5b1ed20adcd1824089d1fd3d3682192ebd4fb73fdce0d263cb579850f3ad1611",
|
|
16
|
+
"urgency": "e5cea1552cda483b2de24883c9e5bf68a313410c8775e1d73bf20372c2ccc037",
|
|
17
|
+
"route": "d9ddd5a766745b9711eafd66a5b3b7a7e8426dcc34bbe03a79d8cc66e3a1562f",
|
|
18
|
+
"draft_reply": "a119986f42b583466a238edce83358d413d9fe65c401428990741225ebf5a0cd",
|
|
19
|
+
"update": "35ec1f944048dcfd8300d9b835a68aa7b3e0984dfa9d7bc101da694c4f67ee07",
|
|
20
|
+
"ask_person": "565ad1d8cd09f8dc42f0baaf09e45046666c47c66a670d74dd2b6cdb3e2b96a9",
|
|
21
|
+
"move": "0d7b538af952c7d552beb15e03495082c27176299976ea861a5f8496579c726a"
|
|
22
|
+
},
|
|
23
|
+
"iterations": {
|
|
24
|
+
"read": [
|
|
25
|
+
{
|
|
26
|
+
"inputs": {
|
|
27
|
+
"ticket_id": "T-1042",
|
|
28
|
+
"helpdesk": "http://127.0.0.1:4310"
|
|
29
|
+
},
|
|
30
|
+
"candidate": {
|
|
31
|
+
"ticket": {
|
|
32
|
+
"id": "T-1042",
|
|
33
|
+
"subject": "Charged twice this month",
|
|
34
|
+
"body": "Hi, my card was charged $49 two times on October 3 for the Team plan. We only have one workspace. Please refund the second charge."
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"team": [
|
|
40
|
+
{
|
|
41
|
+
"inputs": {
|
|
42
|
+
"subject": "Charged twice this month",
|
|
43
|
+
"body": "Hi, my card was charged $49 two times on October 3 for the Team plan. We only have one workspace. Please refund the second charge."
|
|
44
|
+
},
|
|
45
|
+
"candidate": {
|
|
46
|
+
"team": {
|
|
47
|
+
"choice": "billing",
|
|
48
|
+
"probabilities": {
|
|
49
|
+
"billing": 0.93,
|
|
50
|
+
"technical": 0.02,
|
|
51
|
+
"account": 0.03,
|
|
52
|
+
"unclear": 0.02
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"refund": [
|
|
59
|
+
{
|
|
60
|
+
"inputs": {
|
|
61
|
+
"body": "Hi, my card was charged $49 two times on October 3 for the Team plan. We only have one workspace. Please refund the second charge."
|
|
62
|
+
},
|
|
63
|
+
"candidate": {
|
|
64
|
+
"refund": {
|
|
65
|
+
"answer": true,
|
|
66
|
+
"probability": 0.96
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"urgency": [
|
|
72
|
+
{
|
|
73
|
+
"inputs": {
|
|
74
|
+
"subject": "Charged twice this month",
|
|
75
|
+
"body": "Hi, my card was charged $49 two times on October 3 for the Team plan. We only have one workspace. Please refund the second charge."
|
|
76
|
+
},
|
|
77
|
+
"candidate": {
|
|
78
|
+
"urgency": {
|
|
79
|
+
"level": "normal",
|
|
80
|
+
"score": 1.25,
|
|
81
|
+
"probabilities": {
|
|
82
|
+
"low": 0.1,
|
|
83
|
+
"normal": 0.6,
|
|
84
|
+
"high": 0.25,
|
|
85
|
+
"urgent": 0.05
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"route": [
|
|
92
|
+
{
|
|
93
|
+
"inputs": {
|
|
94
|
+
"team": {
|
|
95
|
+
"choice": "billing",
|
|
96
|
+
"probabilities": {
|
|
97
|
+
"billing": 0.93,
|
|
98
|
+
"technical": 0.02,
|
|
99
|
+
"account": 0.03,
|
|
100
|
+
"unclear": 0.02
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"refund": {
|
|
104
|
+
"answer": true,
|
|
105
|
+
"probability": 0.96
|
|
106
|
+
},
|
|
107
|
+
"urgency": {
|
|
108
|
+
"level": "normal",
|
|
109
|
+
"score": 1.25,
|
|
110
|
+
"probabilities": {
|
|
111
|
+
"low": 0.1,
|
|
112
|
+
"normal": 0.6,
|
|
113
|
+
"high": 0.25,
|
|
114
|
+
"urgent": 0.05
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"candidate": {
|
|
119
|
+
"decision": {
|
|
120
|
+
"priority": "normal",
|
|
121
|
+
"tags": [
|
|
122
|
+
"refund_request"
|
|
123
|
+
],
|
|
124
|
+
"queue": "billing",
|
|
125
|
+
"needs_person": false,
|
|
126
|
+
"rule": "team_meets_threshold",
|
|
127
|
+
"reason": "the classifier chose billing with 93% probability"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
"draft_reply": [
|
|
133
|
+
{
|
|
134
|
+
"inputs": {
|
|
135
|
+
"ticket": {
|
|
136
|
+
"id": "T-1042",
|
|
137
|
+
"subject": "Charged twice this month",
|
|
138
|
+
"body": "Hi, my card was charged $49 two times on October 3 for the Team plan. We only have one workspace. Please refund the second charge."
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"candidate": {
|
|
142
|
+
"reply": "Thank you for telling us about the two $49 charges on October 3. Our billing team will check your Team plan payment records for that date. We will write to you again when we know what caused the second charge.\n\nThe support team"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
"update": [
|
|
147
|
+
{
|
|
148
|
+
"inputs": {
|
|
149
|
+
"ticket_id": "T-1042",
|
|
150
|
+
"queue": "billing",
|
|
151
|
+
"priority": "normal",
|
|
152
|
+
"tags": [
|
|
153
|
+
"refund_request"
|
|
154
|
+
],
|
|
155
|
+
"note": "Thank you for telling us about the two $49 charges on October 3. Our billing team will check your Team plan payment records for that date. We will write to you again when we know what caused the second charge.\n\nThe support team",
|
|
156
|
+
"helpdesk": "http://127.0.0.1:4310"
|
|
157
|
+
},
|
|
158
|
+
"candidate": {
|
|
159
|
+
"updated": {
|
|
160
|
+
"id": "T-1042",
|
|
161
|
+
"queue": "billing",
|
|
162
|
+
"priority": "normal"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
"observations": {
|
|
169
|
+
"update/0/updated": [
|
|
170
|
+
{
|
|
171
|
+
"source": "http",
|
|
172
|
+
"ref": "GET /tickets/T-1042",
|
|
173
|
+
"data": {
|
|
174
|
+
"status": 200,
|
|
175
|
+
"json": true,
|
|
176
|
+
"fields": {
|
|
177
|
+
"priority": "normal"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
"method": {
|
|
184
|
+
"format": "method/3.4",
|
|
185
|
+
"name": "Triage a support ticket",
|
|
186
|
+
"goal": "Read a new help desk ticket, choose its queue and priority, draft a first reply, and update the ticket. Ask a person when the team is unclear.",
|
|
187
|
+
"run_data": "device",
|
|
188
|
+
"models": {
|
|
189
|
+
"writer": {
|
|
190
|
+
"agent": "claude"
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"secrets": {
|
|
194
|
+
"HELPDESK_TOKEN": "API token for the help desk. read-ticket.mjs and set-ticket.mjs send it as a Bearer token."
|
|
195
|
+
},
|
|
196
|
+
"inputs": {
|
|
197
|
+
"ticket_id": {
|
|
198
|
+
"type": "text",
|
|
199
|
+
"description": "ID of the new ticket in the help desk, such as T-1042."
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
"environment": {
|
|
203
|
+
"helpdesk": {
|
|
204
|
+
"type": "service",
|
|
205
|
+
"description": "Help desk API. GET /tickets/ID reads a ticket; PATCH /tickets/ID sets its queue, priority, tags, and internal note."
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"steps": {
|
|
209
|
+
"read": {
|
|
210
|
+
"name": "Read the ticket",
|
|
211
|
+
"purpose": "Reads the ticket from the help desk with HELPDESK_TOKEN and returns its id, subject, and text. Fails when the help desk does not return the ticket. Changes nothing.",
|
|
212
|
+
"in": {
|
|
213
|
+
"ticket_id": "inputs.ticket_id",
|
|
214
|
+
"helpdesk": "environment.helpdesk"
|
|
215
|
+
},
|
|
216
|
+
"do": {
|
|
217
|
+
"kind": "run",
|
|
218
|
+
"runtime": "node",
|
|
219
|
+
"entrypoint": "read-ticket.mjs"
|
|
220
|
+
},
|
|
221
|
+
"out": {
|
|
222
|
+
"ticket": {
|
|
223
|
+
"type": "record",
|
|
224
|
+
"fields": {
|
|
225
|
+
"id": "text",
|
|
226
|
+
"subject": "text",
|
|
227
|
+
"body": "text"
|
|
228
|
+
},
|
|
229
|
+
"description": "The ticket as the help desk returned it."
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
"team": {
|
|
234
|
+
"name": "Choose the team",
|
|
235
|
+
"in": {
|
|
236
|
+
"subject": "ticket.subject",
|
|
237
|
+
"body": "ticket.body"
|
|
238
|
+
},
|
|
239
|
+
"do": {
|
|
240
|
+
"kind": "classify",
|
|
241
|
+
"question": "Which team should handle the main request in this support ticket?",
|
|
242
|
+
"options": {
|
|
243
|
+
"billing": "Billing - invoices, charges, refunds, and plan changes.",
|
|
244
|
+
"technical": "Technical - errors, outages, and problems using the product.",
|
|
245
|
+
"account": "Account - sign-in, users, and access.",
|
|
246
|
+
"unclear": "Unclear - the ticket does not say enough to choose, or no listed team fits."
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
"out": "team"
|
|
250
|
+
},
|
|
251
|
+
"refund": {
|
|
252
|
+
"name": "Check for a refund request",
|
|
253
|
+
"in": {
|
|
254
|
+
"body": "ticket.body"
|
|
255
|
+
},
|
|
256
|
+
"do": {
|
|
257
|
+
"kind": "classify",
|
|
258
|
+
"question": "Does the customer ask for money back?",
|
|
259
|
+
"answer": "yes_no"
|
|
260
|
+
},
|
|
261
|
+
"out": "refund"
|
|
262
|
+
},
|
|
263
|
+
"urgency": {
|
|
264
|
+
"name": "Score the urgency",
|
|
265
|
+
"in": {
|
|
266
|
+
"subject": "ticket.subject",
|
|
267
|
+
"body": "ticket.body"
|
|
268
|
+
},
|
|
269
|
+
"do": {
|
|
270
|
+
"kind": "classify",
|
|
271
|
+
"question": "How urgent is this support ticket for the customer?",
|
|
272
|
+
"levels": [
|
|
273
|
+
"low",
|
|
274
|
+
"normal",
|
|
275
|
+
"high",
|
|
276
|
+
"urgent"
|
|
277
|
+
]
|
|
278
|
+
},
|
|
279
|
+
"out": "urgency"
|
|
280
|
+
},
|
|
281
|
+
"route": {
|
|
282
|
+
"name": "Choose the queue and priority",
|
|
283
|
+
"purpose": "Chooses the team's queue when the team is not unclear and its probability is at least 80%. Otherwise chooses the triage queue and marks the ticket for a person. The priority is the most likely urgency level. Adds the tag refund_request when the probability of a refund request is at least 50%. Returns the rule used. Changes nothing.",
|
|
284
|
+
"in": {
|
|
285
|
+
"team": "team",
|
|
286
|
+
"refund": "refund",
|
|
287
|
+
"urgency": "urgency"
|
|
288
|
+
},
|
|
289
|
+
"do": {
|
|
290
|
+
"kind": "run",
|
|
291
|
+
"runtime": "node",
|
|
292
|
+
"entrypoint": "route.mjs"
|
|
293
|
+
},
|
|
294
|
+
"out": {
|
|
295
|
+
"decision": {
|
|
296
|
+
"type": "record",
|
|
297
|
+
"description": "The queue, priority, and tags, whether a person must choose the team, and the rule used.",
|
|
298
|
+
"fields": {
|
|
299
|
+
"queue": "text",
|
|
300
|
+
"priority": "text",
|
|
301
|
+
"tags": {
|
|
302
|
+
"type": "list",
|
|
303
|
+
"items": "text"
|
|
304
|
+
},
|
|
305
|
+
"needs_person": "boolean",
|
|
306
|
+
"rule": "text",
|
|
307
|
+
"reason": "text"
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
"draft_reply": {
|
|
313
|
+
"name": "Draft the first reply",
|
|
314
|
+
"in": {
|
|
315
|
+
"ticket": "ticket"
|
|
316
|
+
},
|
|
317
|
+
"do": {
|
|
318
|
+
"kind": "call",
|
|
319
|
+
"model": "writer",
|
|
320
|
+
"prompt": "Write the first reply to this support ticket, for a support agent to check and send.\nThank the customer in one sentence. Then say in one or two sentences what happens next.\nIf the ticket does not say what is wrong, ask for the one detail that is missing.\nDo not promise a refund, a date, or a fix. Sign it \"The support team\".\n\nSubject: {{ticket.subject}}\n{{ticket.body}}\n\nTwo replies that the team liked:\n\nTicket: \"I was charged twice for October.\"\nReply: \"Thank you for telling us about the double charge. Our billing team will check your October invoices and reply within one business day. The support team\"\n\nTicket: \"The export button does nothing.\"\nReply: \"Thank you for the report. Which browser do you use, and does the export fail for every report or only for one? With that, our technical team can look into it. The support team\"\n"
|
|
321
|
+
},
|
|
322
|
+
"out": {
|
|
323
|
+
"reply": {
|
|
324
|
+
"type": "text",
|
|
325
|
+
"description": "The reply draft for a support agent to check."
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
"update": {
|
|
330
|
+
"name": "Update the ticket",
|
|
331
|
+
"purpose": "Sets the ticket's queue, priority, and tags, and adds the reply draft as an internal note, with one PATCH request. A repeated request sets the same values, so a retry is safe. A ticket whose team is unclear goes to the triage queue.",
|
|
332
|
+
"in": {
|
|
333
|
+
"ticket_id": "inputs.ticket_id",
|
|
334
|
+
"queue": "decision.queue",
|
|
335
|
+
"priority": "decision.priority",
|
|
336
|
+
"tags": "decision.tags",
|
|
337
|
+
"note": "reply",
|
|
338
|
+
"helpdesk": "environment.helpdesk"
|
|
339
|
+
},
|
|
340
|
+
"do": {
|
|
341
|
+
"kind": "run",
|
|
342
|
+
"runtime": "node",
|
|
343
|
+
"entrypoint": "set-ticket.mjs"
|
|
344
|
+
},
|
|
345
|
+
"changes": [
|
|
346
|
+
"environment.helpdesk"
|
|
347
|
+
],
|
|
348
|
+
"effects": {
|
|
349
|
+
"updated": {
|
|
350
|
+
"intent": "The ticket has the chosen priority. The queue is not part of this effect, because the move step changes it after a person's answer.",
|
|
351
|
+
"in": {
|
|
352
|
+
"ticket_id": "inputs.ticket_id",
|
|
353
|
+
"priority": "decision.priority"
|
|
354
|
+
},
|
|
355
|
+
"observe": {
|
|
356
|
+
"kind": "http",
|
|
357
|
+
"path": "/tickets/{inputs.ticket_id}",
|
|
358
|
+
"expect": {
|
|
359
|
+
"fields": {
|
|
360
|
+
"priority": "{inputs.priority}"
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
"out": {
|
|
367
|
+
"updated": {
|
|
368
|
+
"type": "record",
|
|
369
|
+
"fields": {
|
|
370
|
+
"id": "text",
|
|
371
|
+
"queue": "text",
|
|
372
|
+
"priority": "text"
|
|
373
|
+
},
|
|
374
|
+
"description": "The queue and priority that the help desk returned."
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
"ask_person": {
|
|
379
|
+
"name": "Ask a person for the team",
|
|
380
|
+
"when": "decision.needs_person",
|
|
381
|
+
"after": "update",
|
|
382
|
+
"in": {
|
|
383
|
+
"ticket": "ticket",
|
|
384
|
+
"reason": "decision.reason"
|
|
385
|
+
},
|
|
386
|
+
"ask": "Ticket {{ticket.id}} is in the triage queue: {{reason}}.\nSubject: {{ticket.subject}}\n\nWhich team should handle it: billing, technical, or account?\n",
|
|
387
|
+
"out": {
|
|
388
|
+
"chosen_queue": {
|
|
389
|
+
"type": "text",
|
|
390
|
+
"description": "billing, technical, or account"
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
"move": {
|
|
395
|
+
"name": "Move the ticket to the chosen team",
|
|
396
|
+
"when": "decision.needs_person",
|
|
397
|
+
"purpose": "Rejects an answer that is not billing, technical, or account. Then sets that queue with one PATCH request and keeps the priority and tags. A repeated request sets the same values.",
|
|
398
|
+
"in": {
|
|
399
|
+
"ticket_id": "inputs.ticket_id",
|
|
400
|
+
"queue": "chosen_queue",
|
|
401
|
+
"priority": "decision.priority",
|
|
402
|
+
"tags": "decision.tags",
|
|
403
|
+
"helpdesk": "environment.helpdesk"
|
|
404
|
+
},
|
|
405
|
+
"do": {
|
|
406
|
+
"kind": "run",
|
|
407
|
+
"runtime": "node",
|
|
408
|
+
"entrypoint": "set-ticket.mjs",
|
|
409
|
+
"args": [
|
|
410
|
+
"--team-only"
|
|
411
|
+
]
|
|
412
|
+
},
|
|
413
|
+
"changes": [
|
|
414
|
+
"environment.helpdesk"
|
|
415
|
+
],
|
|
416
|
+
"effects": {
|
|
417
|
+
"moved": {
|
|
418
|
+
"intent": "The ticket is in the queue that the person chose.",
|
|
419
|
+
"in": {
|
|
420
|
+
"ticket_id": "inputs.ticket_id",
|
|
421
|
+
"queue": "chosen_queue"
|
|
422
|
+
},
|
|
423
|
+
"observe": {
|
|
424
|
+
"kind": "http",
|
|
425
|
+
"path": "/tickets/{inputs.ticket_id}",
|
|
426
|
+
"expect": {
|
|
427
|
+
"fields": {
|
|
428
|
+
"queue": "{inputs.queue}"
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
"out": {
|
|
435
|
+
"moved": {
|
|
436
|
+
"type": "record",
|
|
437
|
+
"fields": {
|
|
438
|
+
"id": "text",
|
|
439
|
+
"queue": "text",
|
|
440
|
+
"priority": "text"
|
|
441
|
+
},
|
|
442
|
+
"description": "The queue and priority that the help desk returned."
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
"accept": {
|
|
446
|
+
"unused_output": "The receipt stays in the run record. The result cannot name it, because the step is skipped when the team is clear."
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
"result": {
|
|
451
|
+
"decision": "decision",
|
|
452
|
+
"reply": "reply",
|
|
453
|
+
"ticket": "updated"
|
|
454
|
+
},
|
|
455
|
+
"files": [
|
|
456
|
+
"helpdesk.mjs",
|
|
457
|
+
"triage.mjs"
|
|
458
|
+
]
|
|
459
|
+
}
|
|
460
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// A small help desk for trying support-triage.method on this computer. It keeps tickets in memory.
|
|
2
|
+
// Run: HELPDESK_TOKEN=... node fake-helpdesk.mjs [PORT] (default port 4310; tickets from sample-tickets.json)
|
|
3
|
+
import {createServer} from 'node:http';
|
|
4
|
+
import {readFileSync} from 'node:fs';
|
|
5
|
+
|
|
6
|
+
/** Starts the help desk. GET /tickets/ID is open, like the read-only observer; PATCH needs the token. */
|
|
7
|
+
export function startHelpdesk({token, tickets, port = 0}) {
|
|
8
|
+
const store = new Map(tickets.map(ticket => [ticket.id, structuredClone(ticket)]));
|
|
9
|
+
const patches = [];
|
|
10
|
+
const server = createServer(async (req, res) => {
|
|
11
|
+
let text = '';
|
|
12
|
+
for await (const chunk of req) text += chunk;
|
|
13
|
+
const send = (status, body) => { res.writeHead(status, {'content-type': 'application/json'}); res.end(JSON.stringify(body)); };
|
|
14
|
+
const id = /^\/tickets\/([A-Za-z0-9_-]+)$/.exec(req.url ?? '')?.[1];
|
|
15
|
+
const ticket = id && store.get(id);
|
|
16
|
+
if (!ticket) return send(404, {error: 'not_found'});
|
|
17
|
+
if (req.method === 'GET') return send(200, ticket);
|
|
18
|
+
if (req.method !== 'PATCH') return send(405, {error: 'method_not_allowed'});
|
|
19
|
+
if (req.headers.authorization !== `Bearer ${token}`) return send(401, {error: 'unauthorized'});
|
|
20
|
+
const {queue, priority, tags, internal_note} = JSON.parse(text || '{}');
|
|
21
|
+
patches.push({id, queue, priority, tags, internal_note});
|
|
22
|
+
Object.assign(ticket, {queue, priority, tags});
|
|
23
|
+
if (internal_note && !ticket.notes.includes(internal_note)) ticket.notes.push(internal_note);
|
|
24
|
+
return send(200, ticket);
|
|
25
|
+
});
|
|
26
|
+
return new Promise(resolve => server.listen(port, '127.0.0.1', () => resolve({
|
|
27
|
+
url: `http://127.0.0.1:${server.address().port}`, store, patches,
|
|
28
|
+
close: () => new Promise(done => { server.closeAllConnections(); server.close(done); }),
|
|
29
|
+
})));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (process.argv[1]?.endsWith('fake-helpdesk.mjs')) {
|
|
33
|
+
const token = process.env.HELPDESK_TOKEN;
|
|
34
|
+
if (!token) { console.error('Set HELPDESK_TOKEN to the token that PATCH requests must send.'); process.exit(1); }
|
|
35
|
+
const tickets = JSON.parse(readFileSync(new URL('./sample-tickets.json', import.meta.url), 'utf8'));
|
|
36
|
+
const desk = await startHelpdesk({token, tickets, port: Number(process.argv[2] ?? 4310)});
|
|
37
|
+
console.log(`Help desk at ${desk.url} with tickets ${tickets.map(t => t.id).join(', ')}. Stop it with Ctrl-C.`);
|
|
38
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Help desk requests for the scripts of support-triage.method. Every request sends HELPDESK_TOKEN and stops after 10 seconds.
|
|
2
|
+
export async function readInput() {
|
|
3
|
+
let text = '';
|
|
4
|
+
for await (const chunk of process.stdin) text += chunk;
|
|
5
|
+
return JSON.parse(text);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function helpdesk(base, path, {method = 'GET', body} = {}) {
|
|
9
|
+
if (typeof base !== 'string' || !/^https?:\/\//.test(base)) throw Error('The helpdesk connection must be an http or https URL.');
|
|
10
|
+
const token = process.env.HELPDESK_TOKEN;
|
|
11
|
+
if (!token) throw Error('HELPDESK_TOKEN is not set. Run method secret find, or method secret set HELPDESK_TOKEN.');
|
|
12
|
+
const response = await fetch(new URL(path, base.endsWith('/') ? base : base + '/'), {
|
|
13
|
+
method, signal: AbortSignal.timeout(10_000), redirect: 'error',
|
|
14
|
+
headers: {authorization: `Bearer ${token}`, accept: 'application/json', ...(body ? {'content-type': 'application/json'} : {})},
|
|
15
|
+
...(body ? {body: JSON.stringify(body)} : {}),
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) throw Error(`The help desk returned ${response.status} for ${method} ${path}.`);
|
|
18
|
+
return response.json();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ticketPath(id) {
|
|
22
|
+
if (typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,64}$/.test(id)) throw Error('Expected a ticket ID of letters, digits, - and _.');
|
|
23
|
+
return `tickets/${id}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function main(run) {
|
|
27
|
+
try {
|
|
28
|
+
process.stdout.write(JSON.stringify(await run(await readInput())) + '\n');
|
|
29
|
+
} catch (error) {
|
|
30
|
+
process.stderr.write(error.message + '\n');
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"ticket_id": "T-1042"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {helpdesk, main, ticketPath} from './helpdesk.mjs';
|
|
2
|
+
|
|
3
|
+
await main(async ({ticket_id, helpdesk: base}) => {
|
|
4
|
+
const ticket = await helpdesk(base, ticketPath(ticket_id));
|
|
5
|
+
if (ticket.id !== ticket_id || typeof ticket.subject !== 'string' || typeof ticket.body !== 'string')
|
|
6
|
+
throw Error(`The help desk did not return ticket ${ticket_id} with a subject and a body.`);
|
|
7
|
+
return {ticket: {id: ticket.id, subject: ticket.subject, body: ticket.body}};
|
|
8
|
+
});
|