Part 8 — Finish, Test, and Publish Your LangGraph Support Desk

Part 8 of 8 · Basic Python is enough · Tested with Python 3.12.4, LangGraph 1.2.16 and SQLite checkpointer 3.1.2.

Run the assembled application across separate commands, inspect the local outbox, and evaluate a fixed set of tickets before publishing the repository.

Project files: lessons/08_final_project/README.md. Run commands from the extracted repository root. The matching article ZIP includes this article, its diagrams, and the complete tested repository.

Assemble the behavior you can now explain

We began with a short Python function. The finished project now classifies a ticket, retrieves a small set of notes, prepares a source-linked draft, checks its reference structure, pauses for review, and queues an approved reply locally.

The default backend is deterministic. An optional local model can replace drafting. Neither backend sends email. That keeps the runnable project approachable while leaving a clear boundary where a real delivery integration would require additional work.

The final code is in src/support_desk/. Earlier lessons/ files remain runnable teaching stages, not competing versions of the deployed application. Use the final package for the commands in this article.

The complete support graph includes classification, lookup, draft checks, one bounded revision, human approval, a local queue, and escalation exits.

Understand the final graph's exits

There are three normal outcomes after processing or review:

Outcome Meaning
needs_human Unsupported topic, missing evidence, or exhausted draft attempts
rejected A reviewer explicitly declined the draft
queued A reviewer approved and the local outbox row was recorded

A pending approval is different from all three completed outcomes. Its saved snapshot has approval in the next list. An unexpected persistent lookup exception can also stop execution; this is a failure, not a successful escalation result.

The graph builder in workflow.py connects these outcomes explicitly. The generated assets/final-graph.mmd file also records the executable graph's Mermaid representation. The editorial PNG simplifies its layout for reading; the source graph and tests determine behavior.

Run one complete ticket

After installing the locked dependencies and the editable package as described in article 2, run these commands from the repository root. Use a new ticket ID if you have already completed this example.

python -m support_desk start --id demo-101 --text "My pip install has a missing module" --stream
python -m support_desk status --id demo-101
python -m support_desk outbox

The progress output identifies completed node updates. The final JSON includes the original question, category, retrieved evidence, reply, citations, event list, and pending review payload. The outbox should still be empty if you have not approved any tickets in this data directory.

Read the draft and evidence. Then approve it:

python -m support_desk approve --id demo-101
python -m support_desk outbox

The saved outcome becomes queued, and the outbox contains one row for demo-101. Approval means “record this local reply for this ticket,” not “send an email to a real learner.” There is no recipient address in the example queue.

The commands use the same checkpoint file and thread ID to continue the saved run. If you run from a different directory, explicitly provide the same data location before the subcommand:

python -m support_desk --data-dir .runtime status --id demo-101

Exercise rejection and escalation

Create a second technical ticket and reject its draft:

python -m support_desk start --id demo-102 --text "My ESP32 serial port is missing"
python -m support_desk reject --id demo-102

Its outcome is rejected, and there should be no outbox row for that ID. Next, submit a billing request:

python -m support_desk start --id demo-103 --text "Please refund my Python workshop"

This takes the billing branch and ends with needs_human. It does not prepare a technical draft, pause at approval, or perform a financial action. The example sentence “My camera image is purple” also escalates because our tiny knowledge collection has no matching note, even though the classifier recognizes a hardware topic.

Try approving the already finished first ticket. The CLI returns an error instead of treating it as a new approval. Try starting the same ID with different text. That also returns an error. These guards keep the tutorial's one-ticket-per-thread contract understandable.

Test behavior that matters

Run:

python -m pytest -q

The suite checks approval before queue writes, rejection, thread isolation, restart persistence, malformed decisions, bounded draft revision, transient and persistent lookup failures, citation mismatches, duplicate queue writes, conflicting queue payloads, model-response parsing, and the local fake-model HTTP contract. It also executes the lesson scripts.

One particularly useful test starts a ticket in a fresh Python process, inspects it in another process, and approves it in a third. That exercises the SQLite boundary rather than merely reusing one in-memory object.

Another test confirms that a real-looking source marker does not prove a claim. Keep that test: it documents a limitation reviewers and future maintainers must understand.

The verification report records the actual test count and environment used for this edition. A CI workflow is included for Windows and Linux on Python 3.12, but its results will exist only after you publish and run it. Local preparation on Windows does not establish Linux verification.

Evaluate with a fixed, labeled dataset

Run:

python tools/evaluate.py

The evaluator reads twelve synthetic tickets from data/evaluation.jsonl. Each has an expected category, expected pending/completed outcome, and an expected document where applicable. It uses the demo backend and never approves the paused drafts.

For the included edition, the expected summary is twelve passing regression cases. A “pass” means that the rules selected the expected category and route and included the expected document ID for those examples. It does not measure real-world support accuracy, semantic answer quality, or any language model's capabilities.

To extend the dataset, add a question you actually want the tutorial system to handle and label it before changing the code. If a keyword rule fails, preserve the example as a regression. A test set written only to mirror the current implementation cannot reveal much about behavior you have not considered.

Prepare a repository someone can use

The package includes article Markdown, standalone HTML pages, HTML fragments, original diagrams, lesson sources, final code, tests, a dependency lock, a license, a quickstart, and publishing notes. The article ZIPs contain the same tested repository plus a selected-article pointer. They are convenient reading bundles, not eight divergent implementations of the final application.

Keep .runtime, virtual environments, local model files, and credentials out of Git. The included .gitignore handles the tutorial's generated databases and caches. Review your own additions before publishing.

The article source links are currently repository-relative. After choosing the GitHub repository and branch or commit, you can convert those links into hosted code references. Use a fixed commit when you want an article to point to exactly the code it describes. The publishing guide explains this handover without inventing a repository URL in advance.

You now have a small system whose routes, pauses, failures, and side effects you can explain. A useful next extension is a reviewed web interface around the same application contract. Authentication, authorization, retention policy, broader retrieval evaluation, and actual message delivery should be designed explicitly if that extension becomes a real service.

Generate and explain the graph built by this code

The diagram below is generated from this lesson's compiled get_graph() nodes and edges. Node positions are chosen for readability; the connections come from the Python graph, rather than being copied from a generic illustration.

The following excerpt is inside the complete builder function. Node functions are defined earlier in the linked source file; run the lesson or final package rather than pasting this excerpt by itself.

builder = StateGraph(TicketState)
builder.add_node("classify", classify)
builder.add_node("lookup", lookup, retry_policy=RetryPolicy(
    max_attempts=3, initial_interval=0.01, jitter=False, retry_on=ConnectionError))
builder.add_node("draft", draft)
builder.add_node("check", check)
builder.add_node("revise", revise)
builder.add_node("approval", approval)
builder.add_node("queue", queue)
builder.add_node("escalate", escalate)
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", after_classify, ["lookup", "escalate"])
builder.add_conditional_edges("lookup", after_lookup, ["draft", "escalate"])
builder.add_edge("draft", "check")
builder.add_conditional_edges("check", after_check, ["approval", "revise", "escalate"])
builder.add_edge("revise", "draft")
builder.add_conditional_edges("approval", after_approval, ["queue", END])
builder.add_edge("queue", END)
builder.add_edge("escalate", END)
return builder.compile(checkpointer=checkpointer)

After installing the project, run this command from its root:

python tools/render_graphs.py --part 8

It writes assets/generated/part-08.svg (the image), .mmd (Mermaid source), and .json (the captured nodes and edges). Rendering is local and does not call a model or a remote image service. If you add nodes, update the layout positions in the renderer as well.

Part 8: the graph generated from this lesson's compiled Python code.

Open the full-size graph.

Read the arrows with the code

Solid lines are fixed transitions. Orange dashed lines mark possible conditional destinations; the routing function chooses the destination for a particular run. A node can contain several Python operations that are not separate graph nodes.


This is an educational project using fictional workshop notes and synthetic tickets. It does not access the real CodeTips Academy website or send email. Repository-hosted links can be added after the owner publishes the code; current source links are relative to this repository.