The Smart Support Desk: design first¶
The hybrid path is the last one written and the first one that has to be right, because it consumes
both of the others: the traditional path supplies its CRUD, templates, and verified principal, and the
AI path supplies classification, retrieval, and workflows. This page fixed that design before the
path was written, so a decision about who may send an email was made once rather than discovered in
lesson seven. The path is written now — the ten lessons are the design,
executed — and this page is what they are checked against: the lifecycle, the matrix, and the boundary
below are the three things tests/integration/test_hybrid_support_desk.py asserts.
Nothing here is framework behaviour. Xyberos provides routing, dependency injection, a database provider, an AI facade, and a workflow engine — the policy below is application code, which is the point of the exercise.
Ticket lifecycle¶
| State | Reached from | Who may cause the transition |
|---|---|---|
new |
— | any verified principal in the tenant |
classified |
new |
application code, after a call with a response schema returns a category |
awaiting_approval |
classified |
application code, once a draft reply exists |
approved |
awaiting_approval |
support_agent or support_lead |
replied |
approved |
application code, when it sends the reply |
closed |
replied |
support_agent, support_lead, or the requester |
rejected |
new, classified |
support_lead |
spam |
new |
application code, or support_agent |
Every transition is a method on the ticket service that takes the principal as a required argument, so forgetting to pass it is a type error rather than a silent authorization bypass — the shape ADR 0014 sanctions.
The permission matrix¶
| Action | requester |
support_agent |
support_lead |
|---|---|---|---|
| Open a ticket | yes | yes | yes |
| Read tickets in the tenant | own only | all | all |
| Edit a ticket's text | own, while new |
while new |
while new |
| Run classification | no | yes | yes |
| Read a draft reply | no | yes | yes |
| Approve a reply | no | yes | yes |
| Send a reply | no | via approval only | via approval only |
| Close a ticket | own only | yes | yes |
| Reject or mark spam | no | mark spam only | yes |
| Read the audit log | own ticket | tenant | tenant |
The matrix is enforced in the service layer and asserted by tests. It is not declared to the runtime, because the runtime has no authorization model to declare it to.
The approval boundary¶
A model never performs an action. A generated reply is stored as a SuggestedReply row attached to
the ticket: it has no send method, and nothing in the application sends it implicitly. Sending requires
an approve transition, which records the principal, the timestamp, and the exact body that was
approved, and only then does application code hand the body to the mailer.
This is the rule the whole path exists to teach, so it is tested directly rather than described:
- a suggestion with no approval transition sends nothing;
- an approval by a principal without the role is refused with
403; - a request payload that claims a different tenant changes nothing;
- the audit log records the decision, the principal, and the body that was approved.
AI touch points¶
| Touch point | Label | How it is built |
|---|---|---|
| Classification into a category | Built-in | AIFacade.chat with a response schema, against the closed JSON Schema subset |
| Reply drafting | Built-in | AIFacade.chat, stored as a suggestion and never sent |
| Ticket summarisation | Built-in | AIFacade.chat or structured, one step in the workflow |
| Knowledge retrieval | You build it | AIFacade.embed plus a SQLite table and a similarity query — no index ships |
| Usage, cost, and outcome logging | You build it | ordinary rows written by the workflow's steps |
| Tool use | Not used | The desk calls no tools. The model receives the ticket's text and one retrieved passage, and nothing else: a tool that reads tickets would be a second permission boundary to define, and the path does not need one. The AI path's lesson 11 is where tools belong, and the code for them is the same code with an allowlist in front of it |
Slow model calls run inside the workflow engine with a per-step timeout and a whole-run budget, and the classification step is the only one that gates a transition — retrieval and drafting are advisory. The one call that is not inside the graph is the background triage job, which runs the graph itself.
Lessons, and the example that proves each one¶
Every lesson is written, and each one is a module the suite runs. The first three are the material the traditional path also teaches — CRUD, a verified principal, and an audit trail — applied to this application, which is why the path can spend its model-facing lessons on the boundary rather than on scaffolding.
| Lesson | Example module | State |
|---|---|---|
| 1 Tickets as ordinary CRUD | examples/learn_hybrid/hybrid01_ticket_crud.py |
verified |
| 2 A verified principal, and what it may do | examples/learn_hybrid/hybrid02_principal.py |
verified |
| 3 The audit log | examples/learn_hybrid/hybrid03_audit.py |
verified |
| 4 Classification with structured output | examples/learn_hybrid/hybrid04_classification.py |
verified |
| 5 Knowledge retrieval you build | examples/learn_hybrid/hybrid05_retrieval.py |
verified |
| 6 Draft replies that are never sent | examples/learn_hybrid/hybrid06_draft.py |
verified |
| 7 Human approval before sending | examples/learn_hybrid/hybrid07_approval.py |
verified |
| 8 Coordinating the steps with a workflow | examples/learn_hybrid/hybrid08_workflow.py |
verified |
| 9 Background processing for slow work | examples/learn_hybrid/hybrid09_background.py |
verified |
| 10 Logging usage, cost, and outcomes | examples/learn_hybrid/hybrid10_usage.py |
verified |
The test that holds it together¶
The finished application is exercised by tests/integration/test_hybrid_support_desk.py, which is
where the phase-5 exit criteria live: the four assertions under the approval boundary,
plus one run of the full workflow against FakeModelProvider so the suite never needs a model service.
Shared with the traditional path¶
The hybrid application uses the same project layout, the same DatabaseProvider usage, the same
template approach, and the same HttpError shape as the user-management application in the traditional
path, so the two paths teach one set of conventions rather than two.