support_desk 0.1.3 → 0.3.0

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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -0
  3. data/README.md +1101 -22
  4. data/Rakefile +16 -0
  5. data/app/assets/stylesheets/support_desk.css +10 -0
  6. data/app/controllers/support_desk/tickets_controller.rb +28 -4
  7. data/app/helpers/support_desk/engine_helper.rb +23 -1
  8. data/app/views/support_desk/console/tickets/_actions.html.erb +15 -0
  9. data/app/views/support_desk/console/tickets/_assignment.html.erb +16 -2
  10. data/app/views/support_desk/console/tickets/_composer.html.erb +27 -0
  11. data/app/views/support_desk/console/tickets/_context_card.html.erb +33 -0
  12. data/app/views/support_desk/console/tickets/_draft.html.erb +114 -0
  13. data/app/views/support_desk/console/tickets/_message.html.erb +34 -1
  14. data/app/views/support_desk/console/tickets/_new_conversation_form.html.erb +91 -0
  15. data/app/views/support_desk/console/tickets/_ticket_row.html.erb +28 -0
  16. data/app/views/support_desk/console/tickets/_timeline.html.erb +68 -14
  17. data/app/views/support_desk/console/tickets/index.html.erb +14 -2
  18. data/app/views/support_desk/console/tickets/new.html.erb +25 -0
  19. data/app/views/support_desk/console/tickets/show.html.erb +5 -0
  20. data/app/views/support_desk/tickets/_human_door.html.erb +24 -0
  21. data/app/views/support_desk/tickets/_ticket_row.html.erb +13 -0
  22. data/config/console_routes.rb +1 -1
  23. data/config/locales/support_desk.console.en.yml +89 -0
  24. data/config/locales/support_desk.console.es.yml +91 -0
  25. data/config/locales/support_desk.en.yml +18 -0
  26. data/config/locales/support_desk.es.yml +29 -0
  27. data/config/routes.rb +7 -1
  28. data/lib/generators/support_desk/assistant_generator.rb +193 -0
  29. data/lib/generators/support_desk/console_generator.rb +1 -1
  30. data/lib/generators/support_desk/install_generator.rb +26 -0
  31. data/lib/generators/support_desk/templates/add_assistants_to_support_desk.rb.erb +236 -0
  32. data/lib/generators/support_desk/templates/add_opened_by_to_support_desk_tickets.rb.erb +79 -0
  33. data/lib/generators/support_desk/templates/assistant/service.rb.erb +60 -0
  34. data/lib/generators/support_desk/templates/assistant/turn_job.rb.erb +72 -0
  35. data/lib/generators/support_desk/templates/assistant/turn_job_test.rb.erb +69 -0
  36. data/lib/generators/support_desk/templates/console/controller.rb.erb +1 -1
  37. data/lib/generators/support_desk/templates/initializer.rb +63 -0
  38. data/lib/generators/support_desk/upgrade_generator.rb +66 -0
  39. data/lib/support_desk/assistant_policy.rb +213 -0
  40. data/lib/support_desk/brief.rb +283 -0
  41. data/lib/support_desk/configuration.rb +699 -3
  42. data/lib/support_desk/console.rb +634 -30
  43. data/lib/support_desk/console_engine.rb +2 -0
  44. data/lib/support_desk/console_routes.rb +20 -8
  45. data/lib/support_desk/context_card.rb +33 -1
  46. data/lib/support_desk/doctor.rb +195 -2
  47. data/lib/support_desk/engine.rb +10 -0
  48. data/lib/support_desk/errors.rb +42 -0
  49. data/lib/support_desk/events.rb +12 -5
  50. data/lib/support_desk/macros.rb +41 -12
  51. data/lib/support_desk/models/assignment.rb +3 -1
  52. data/lib/support_desk/models/assistant.rb +153 -0
  53. data/lib/support_desk/models/concerns/agent.rb +39 -4
  54. data/lib/support_desk/models/concerns/requester.rb +45 -12
  55. data/lib/support_desk/models/desk.rb +26 -3
  56. data/lib/support_desk/models/draft.rb +266 -0
  57. data/lib/support_desk/models/event.rb +15 -1
  58. data/lib/support_desk/models/ticket/assistance.rb +675 -0
  59. data/lib/support_desk/models/ticket.rb +781 -120
  60. data/lib/support_desk/outcome.rb +38 -0
  61. data/lib/support_desk/queue.rb +41 -5
  62. data/lib/support_desk/summary.rb +1 -1
  63. data/lib/support_desk/test_helpers.rb +160 -3
  64. data/lib/support_desk/timeline.rb +40 -11
  65. data/lib/support_desk/topic.rb +22 -0
  66. data/lib/support_desk/topic_tree.rb +7 -1
  67. data/lib/support_desk/transcript.rb +237 -0
  68. data/lib/support_desk/version.rb +1 -1
  69. data/lib/support_desk/wizard.rb +4 -12
  70. data/lib/support_desk.rb +153 -0
  71. data/lib/tasks/support_desk.rake +73 -0
  72. metadata +35 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 587ddd67c318b1c6b4cbe7b496a65ec2bfb70b39818556a5d90ad81415db17c8
4
- data.tar.gz: 3ab9972eca4fc07680e1b33d139400d7f1c6ef9116e3778967f070d05f5111e1
3
+ metadata.gz: 8d7e90a546ededcbf2f0f13d30a2929655e0d6bb343f11d114b0e539b8985bfb
4
+ data.tar.gz: 502d37cedab338e5f42db97b021ed01dd8ee46844600fc31d3240e9a1a09597c
5
5
  SHA512:
6
- metadata.gz: 27e60ca1419b9fd778e6d57ac35dea358b7f422829b92c36f00d123153b967a0171c8f7edfeeef8715e597b2e169fa48629325d4314aa2fdc2593f8c27662924
7
- data.tar.gz: f04e5ba138fd6e847c0aad98ef5f42eeaf1018900eccd4f3b72af15a9ba82a00e9506bf2412e900145918a2eb97299b5b0fbb2d1b0bcabba0c23b890ae027fc3
6
+ metadata.gz: fae5879d3023eed0f45c24e275bf63fdaf5ac2f8e982a7d6b9ea6c9db4afe12036025c6ef20a1280031456978908414849719fb9ea28a6d53d5dd47a3c6995aa
7
+ data.tar.gz: 9e8e5df4a7e7f836c297a3509d4c2d23111a6f9365fdd34208e410db11e249aa0445c515fe19421efa4f883b43503cf65bc32efc207007ff46a55d9102b8a470
data/CHANGELOG.md CHANGED
@@ -4,6 +4,132 @@ All notable changes to this project are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.3.0] - 2026-09-19
8
+
9
+ **A machine can answer, and a person still owns every word it sends.** An
10
+ assistant is an agent with a policy: she has a seat, a name, a turn budget
11
+ and a level that says what she may produce here — and at the default level
12
+ that is a proposal a human reads and sends. Nothing in this release turns
13
+ itself on. Without `config.assistant` the gem behaves exactly as 0.2 did.
14
+
15
+ ### Added
16
+
17
+ - `config.assistant :rose do |rose| … end` — one block per assistant, with `name`, `avatar`, `autonomy`, `disclosure`, `max_turns`, `responds_within`, `may_open_conversations`, the three system lines (`hand_off_line`, `human_requested_line`, `disclosure_line`) and the two hooks (`hand_off_when`, `cap`). `config.assistants`, `config.assistant?(key)`, `config.default_assistant`, and `desk.assistant = :rose` (an explicit `nil` disables one desk rather than inheriting).
18
+ - **`disclosure` is required and has no default.** Four modes — `:signature_and_notice`, `:signature`, `:notice`, `:none` — and boot fails until one is chosen. Whether a customer is told they are talking to a machine is a legal decision in a jurisdiction the gem knows nothing about.
19
+ - `SupportDesk::AssistantPolicy` — what she may do on ONE case, right now, and why. `LEVELS` (`off observe draft reply resolve`), `VERBS_BY_LEVEL`, `level` / `because` / `ceilings` / `floors`, `may?` / `may_observe?` / `may_draft?` / `may_reply?` / `may_hold?` / `may_close?`, `allowed_verbs` / `forbidden_verbs` / `to_h`. Ceilings lower what she may ever produce (her autonomy, the topic's cap, the host's `cap` block, the case's cap, a pause); floors lower it because of the case's state. `because` names the one rule that decided it.
20
+ - `topic :payments, assistant: :draft` — a topic cap, and it only ever tightens: `Topic#assistant_cap` is the minimum over the node and every ancestor.
21
+ - `SupportDesk::Assistant` — the record a message can be authored by, an assignment can point at and an audit log can name. `SupportDesk.assistant(key = nil)`, `SupportDesk.reset_assistants!`, `SupportDesk.ai_actor?(record)`; `deactivate!(by:)` / `activate!(by:)` is the cross-process kill switch, read fresh from the row before every write.
22
+ - `Ticket#respond!(body, by:, turn:, …)` — the one verb a harness needs. Policy decides whether it is sent, proposed as a draft, or withheld with the reason on the record; the answer is a `SupportDesk::Outcome` (`sent?` / `drafted?` / `withheld?` / `escalated?` / `turn`).
23
+ - `Ticket#draft!`, `Ticket#escalate!(reason:, summary:)`, `Ticket#request_human!(by: requester)`, `Ticket#pause_assistant!` / `#resume_assistant!`.
24
+ - **The turn.** `assistant_revision` is an integer bumped by every registered message and every transition; `ticket.assistant_turn` spells it, and EVERY assistant action requires it and consumes it. A late, retried or redelivered action raises `SupportDesk::StaleTurn` and writes nothing — which is why this gem has no idempotency keys, claim rows or leases.
25
+ - `SupportDesk::Draft` — a reply she proposes and a person sends. `send!(by:, seen_turn:, body:)` posts it as the HUMAN's message (they read it, they own it, the requester sees their signature) and keeps the original; `reject!(by:, reason:)` records why, which is the number that tells you whether she is ready for a higher level. `stale?`, `edited?`, `final_body`, `confidence_percent`, and the `reviewed` / `verbatim` / `edited` scopes.
26
+ - `SupportDesk::Transcript` — the conversation as ordered turns (`role`, `name`, `body`, `at`, `attachments`, `assisted`), with four roles rather than the clocks' three. `to_a` / `to_h` / `to_text` / `last(n)` / `since(message)` / `size`, and `ticket.transcript(limit:)`.
27
+ - `SupportDesk::Brief` — everything a machine needs to answer one case, as versioned data. `ticket.brief(include_internal: false, transcript_limit: 50)`, `to_h` / `to_text` / `policy`. Facts only: `may` / `may_not` are the authorization, and there is not one imperative sentence in it.
28
+ - `Requester#support_context` — key/value pairs about the PERSON, the way `Supportable#support_context` is pairs about the thing they are asking about. Empty by default; rendered by `ContextCard#requester_pairs` and included in the brief.
29
+ - Readers and scopes for the new state: `Ticket#assistant`, `#assistant_policy`, `#assistant_turn`, `#assistant_in_play?`, `#held_by_assistant?`, `#human_required?`, `#assistant_paused?`, `#assistant_turns_left`, `#assistant_message?(message)`, `#drafts` / `#pending_draft`; and `held_by_assistants`, `held_by_humans`, `needs_human`, `assistant_paused`, `assistant_capped`, `resolved_by_assistant`, `with_pending_draft`, `assistant_idle_since(time)`. `Desk#assistant` / `#assistant?` name the one that works a desk.
30
+ - Console: `send_draft`, `reject_draft`, `pause_assistant`, `resume_assistant`, the proposal card (Enviar / Editar / Descartar) with the confidence pill, the sources and a staleness warning, the helpers `support_pending_draft` and `support_assistant`, `"no_pending_draft"` as an `unavailable_reason`, and the `needs_human` queue tab (`Queue#needs_human`, `#visible_tabs`).
31
+ - Requester engine: `POST /tickets/:id/request_human`, the `support_desk/tickets/_human_door` partial and the `support_human_door(ticket)` helper. The door is offered whenever an assistant is or has been in play on the case — history counts, so it can never vanish because somebody edited an initializer after she answered.
32
+ - Events: `assistant_turn` (the only one a harness subscribes to; it carries the turn), `draft_proposed`, `draft_sent`, `draft_rejected`, `assistant_withheld`, `ticket_escalated`, `human_requested`, `assistant_paused`, `assistant_resumed`. The matching `Event::KINDS` are new too; the five internal ones join `note` and `drop_in` in `Event::INTERNAL_KINDS`, which is what `Event.requester_visible` excludes, and `Event#summary` reads the paragraph an escalation left.
33
+ - Spanish and English copy for all of it — the system lines, the console's proposal card and flashes, the queue's new tab and the requester's door — with the suite asserting both files carry the same keys.
34
+ - Errors: `SupportDesk::NotAnAssistant`, `SupportDesk::AssistantNotAllowed` (carrying the `policy` and the `verb`), `SupportDesk::StaleTurn`.
35
+ - `rake support_desk:release_silent_assistants` (every minute) and `rake support_desk:redispatch_assistant_turns` (every five) — the net under a dead harness and the at-least-once retry of a dropped turn. `rake support_desk:assistant_status` reads and writes nothing.
36
+ - Doctor: `assistants (config)`, `assistant turn subscriber`, `assistant authorship`, `assistant silence`, `assistant seats`, `assistant idle turns`, `drafts`, `ai agents without policy`.
37
+ - `SupportDesk::TestHelpers`: `support_assistant`, `respond_as`, `draft_as`, `assert_pending_draft` / `refute_pending_draft`, `assert_needs_human` / `refute_needs_human`, `assert_held_by_assistant`, `refute_assistant_spoke`, `assert_assistant_policy`, `with_assistant_config`, `with_topic_assistant_cap`.
38
+ - Generators: `rails g support_desk:assistant Rose --disclosure MODE [--desk KEY] [--autonomy LEVEL]` writes the turn job, the service and a job test, and PRINTS the initializer stanza, the subscription and the two scheduler lines. It never edits an initializer, never overwrites a file and copies no locales. `install` and `upgrade` both copy the additive assistants migration.
39
+
40
+ ### Changed — regardless of configuration
41
+
42
+ These land whether or not you declare an assistant:
43
+
44
+ - **A host model declared `acts_as_support_agent kind: :ai` is now refused for every support write, by it or to it** (`NotAnAssistant`). It used to be treated as a human. `kind:` is validated at declaration (`:human` or `:ai`), and `SupportDesk.doctor` warns about such classes. Only the desk's own `SupportDesk::Assistant` has machine authority.
45
+ - **Console picker values are `SupportDesk.actor_key(agent)`**, not bare ids — an assistant and a user can share an integer id. A bare id is still accepted for one release, and only when exactly one pool member matches it.
46
+ - `Queue::TABS` gains `:needs_human`. It is hidden unless the desk has an assistant or the count is non-zero, so no existing queue grows a tab it has no use for.
47
+ - Every registration and every transition writes `assistant_revision`. Additive; a 0.2 process reading those rows is unaffected.
48
+ - The schema adds `support_desk_assistants` and `support_desk_drafts`, and nine columns on `support_desk_tickets` — two defaulted integers (`assistant_revision`, `assistant_turns_count`), the rest nullable.
49
+
50
+ ### Changed — only when an assistant is configured
51
+
52
+ - `Desk#agents` is now an **Array** (humans plus her), and `Desk#humans` is the human-only pool. Page people with `ticket.agents_to_notify` or `desk.humans`: notifying a machine is notifying nobody.
53
+ - `announce_assignments` never announces her — "Rose is taking care of your request" is not a thing to say.
54
+ - **A human reply on a case she holds takes it over under every `reply_policy`**, and supersedes the pending proposal. `:assignee_only` exists so two people don't answer at once, and she is not one.
55
+ - `close!` expires pending proposals; reopening a case she closed leaves it unassigned and caps her at `:draft` for the rest of its life.
56
+ - `Ticket#reply!` and `post_agent_message!` accept `metadata:` (provenance) and `turn:`.
57
+
58
+ ### Upgrading from 0.2
59
+
60
+ Additive and rolling-safe — unlike the 0.2 cutover, no drain is needed.
61
+
62
+ 1. `bundle update support_desk` (0.3.0).
63
+ 2. `rails generate support_desk:upgrade` then `rails db:migrate`. The migration only adds tables and nullable columns.
64
+ 3. **Deploy every process to 0.3.0 before adding `config.assistant`.** A 0.2 worker cannot honour a turn it does not know about.
65
+ 4. `rails g support_desk:assistant Rose --disclosure …`, paste the printed stanza, wire the job to `:assistant_turn`, schedule the two rake tasks, and subscribe your notifiers to `draft_proposed`, `ticket_escalated` and `human_requested`.
66
+ 5. Render `support_desk/tickets/_human_door` in your thread.
67
+ 6. `SupportDesk.doctor` green.
68
+ 7. **Start at `:draft`.** Every word goes through a person until the acceptance rate on reviewed proposals says otherwise, and keep topic caps on money and identity when you raise it.
69
+
70
+ Rolling back: `rose.deactivate!` is the kill switch and needs no deploy. A
71
+ code rollback to 0.2 keeps the schema — the columns are nullable and the two
72
+ tables are ignored — but loses the provenance rendering of her messages, so
73
+ do it only if 0.3 itself is broken.
74
+
75
+ ## [0.2.0] - 2026-09-17
76
+
77
+ **The desk can write first.** Until now a case could only start with somebody
78
+ asking; now it can start with you, through the same seam and the same
79
+ algorithm.
80
+
81
+ ### Added
82
+
83
+ - `agent.open_support_conversation_with!(requester, message, about:, topic:, files:, via:, desk:)` — write first, as the desk: the message is the desk's, signed by the agent, and it lands in the requester's inbox as the desk. The only agent-side verb in the gem, because it is the only action with no ticket yet.
84
+ - `SupportDesk::Ticket.open!(…, by: an_agent)` — the seam under it. `by:` defaults to the requester; an agent there is the desk writing first. A Symbol (`by: :system`) is refused with `NotAnAgent`: automation is deferred (docs 12, Q17).
85
+ - `opened_by` on every case — a polymorphic record, exactly like `closed_by` — with `opened_by_requester?` / `opened_by_support?` and the `opened_by_requester` / `opened_by_support` scopes, which partition the table.
86
+ - `config.opening_line` and `config.opening_line_from_support`: the system line a thread opens with, posted inside the opening transaction. A String (with `%{label}`, `%{desk}`, `%{reply_within}`), an I18n key, a block, or nil. The first defaults to nil; the second to the gem's own copy, because a message from a desk somebody never wrote to has to explain itself.
87
+ - `config.find_requester`: how a console turns something an agent typed into the person they meant. Without it the console accepts only a GlobalID from one of your own pages.
88
+ - `has_support_tickets if:` — who may ask, and who may be written to — with `support_requester?` and `support_desk` on the requester model.
89
+ - Console: `new` (the form) and `open_conversation` (the send) as collection actions, with the "Write to someone" door in the mounted console and the generated one. `SupportDesk::Console::COLLECTION_VERBS` is the table the router reads.
90
+ - Generators: `rails g support_desk:upgrade` copies the additive `opened_by` migration (the same file a fresh install runs), and `rake support_desk:backfill_opened_by` runs its catch-up after draining old processes and before admitting 0.2 traffic.
91
+ - `SupportDesk::NotARequester`, and `SupportDesk.humanize_duration` (moved off `Wizard`, which keeps a delegation).
92
+
93
+ ### Changed
94
+
95
+ Behaviour changes, not refactors. Read them before upgrading:
96
+
97
+ - **System messages are never folded into the clocks.** They never reached the clocks through chats anyway; now a hand-written `register!` ignores them too, and so does a message from somebody who is neither the requester nor the desk. Neither moves `awaiting`, an SLA clock or the last-registered marker.
98
+ - **`first_agent_reply_at` is only set when a requester message came first.** An agent writing into a case with nothing in it is not answering anything, and an out-of-order replay whose requester clock is later is not evidence either.
99
+ - **`:agent_replied` is not emitted for the desk's own opening message** on a case the desk opened. An agent's first message into an old empty case the REQUESTER opened is still a reply, and still emits.
100
+ - **The asking limits count only requester-opened cases.** `open_rate_limit` and `max_open_tickets` limit asking, not being asked, so cases you opened no longer spend somebody's allowance.
101
+ - **Requester eligibility is checked at every message write.** `chat_locked?` is now "the requester can't be written to, or the case is closed on a desk that locks closed cases", re-read from the database rather than taken from a cached association. The thread, the queue row, the notes and the close are all unchanged; only new messages stop, and `actions_for` stops offering `:reply`.
102
+ - **`reply!` registers its message inside its own lock**, in a savepoint that owns the whole operation — so a caller who rescues its failure and commits their own transaction commits none of ours. `open!` does the same for opening and for the reply a reused case turns into.
103
+ - **The opening line is the gem's, and it is posted inside the opening transaction**, pinned one database tick above the message it introduces. Hosts that posted their own line after commit can delete that code.
104
+ - **`Console::ERROR_KEYS` is gone**, replaced by the error's own name (`NotTheAssignee` → `not_the_assignee`) with a generic fallback. Every key the table listed is a name it derives to, so a host that overrode one keeps its wording.
105
+ - `Console::TRANSITIONS` and `ConsoleRoutes::MEMBER_VERBS` are aliases of `Console::MEMBER_VERBS` for one release.
106
+ - `Chats::Error` is rescued into a console flash; `Chats::ConfigurationError` is deliberately not.
107
+
108
+ ### Fixed
109
+
110
+ - Stale revoked/deleted agents are refused using an uncached eligibility read, shared by opening and ordinary ticket transitions.
111
+ - Malformed attachment inputs return the compose form with 422; signed blob failures do not discard the draft. Invalid explicit desks cannot silently change the sender, and host finder errors are not hidden as bad GlobalIDs.
112
+ - Legacy NULL provenance remains requester-originated for quotas and reply metrics. The documented upgrade drains old web/workers before the catch-up and new traffic.
113
+ - Rake tasks are discovered once across the two engines; migration collision guidance preserves existing provenance.
114
+ - Valid draft fields survive malformed sibling fields, and unavailable recipients are explained before offering send.
115
+ - `Ticket.open!` no longer reloads the case it just opened. The clocks are folded in on the same instance, inside the transaction, so the row that commits is already true.
116
+
117
+ ### Upgrading from 0.1
118
+
119
+ **This upgrade requires a drained cutover, not a rolling deployment.** Old
120
+ processes cannot release assignments with the new `opened` reason.
121
+
122
+ 1. Copy the upgrade migration with `rails generate support_desk:upgrade` and migrate before 0.2 serves traffic. The additive, nullable columns are compatible with 0.1.
123
+ 2. Pause incoming support writes and background producers. Stop and drain **all old web requests and workers**, including jobs already running; verify none remain. Keep support traffic paused. Do not start serving 0.2 alongside 0.1.
124
+ 3. With the 0.2 artifact available but traffic still paused, run `rake support_desk:backfill_opened_by`. Verify `SupportDesk::Ticket.where(opened_by_id: nil).count` is zero and inspect `SupportDesk.doctor`. The task is idempotent; repeating it is safe in this release because automation openers are not supported.
125
+ 4. Start only 0.2 web/workers, then resume support traffic. Writing-first may now be used. Add `new` to console routes and regenerate/customize views as needed.
126
+
127
+ NULL-provenance cases are treated as requester-opened even before the backfill,
128
+ so quotas, labels and reply metrics stay correct. This read compatibility does
129
+ **not** make old assignment writers compatible with 0.2; the drain is still required.
130
+
131
+ Rolling back is a host code rollback, not a Gemfile pin: 0.1 does not know the new settings, routes or predicates. Keep the columns and the provenance already written — but note that 0.1's `Assignment#release!` revalidates `reason` and rejects `opened`, so closing, releasing or handing off a case the desk opened will fail under 0.1 until a compatibility patch accepts that reason. Prefer disabling the entry point (drop `new`/`open_conversation` from your routes) over downgrading with active desk-opened cases.
132
+
7
133
  ## [0.1.3] - 2026-09-17
8
134
 
9
135
  ### Fixed