Integrating OpenTicketAI with Zammad for Automated Ticket Classification
OpenTicketAI is an on-premise AI ticket classifier that automates categorization, routing, and prioritization of support tickets. To integrate it with Zammad, we implement a ZammadAdapter that extends OpenTicketAI’s TicketSystemAdapter
interface. This adapter uses Zammad’s REST API to fetch tickets from Zammad, run them through OpenTicketAI’s pipeline, and update the ticket (queue, priority, comments) based on the AI predictions. The key components are illustrated in the architecture: OpenTicketAI’s AdapterFactory creates the appropriate adapter (e.g. ZammadAdapter) to communicate via REST with the ticket system. The pipeline fetches tickets, classifies them, and finally the ticket system adapter updates Zammad via its API.
OpenTicketAI’s architecture uses a modular pipeline where each ticket is processed by a series of pipes. The final Ticket System Adapter stage applies updates (queue, priority, notes) to the external system via REST API. In practice, you register your ZammadAdapter
in the dependency-injection configuration so that the BasicTicketFetcher pipe uses it to load tickets, and the GenericTicketUpdater pipe uses it to apply updates.
OpenTicketAI Pipeline Overview
OpenTicketAI runs in a pipeline that transforms ticket data step by step. A simplified flow is:
- Preprocessor – merge/clean
subject
andbody
. - Transformer / Tokenizer – prepare text for AI.
- Queue Classifier – predicts target queue/group.
- Priority Classifier – predicts priority level.
- Postprocessor – applies thresholds, chooses actions.
- Ticket System Adapter – updates the ticket in Zammad via REST API.
Each stage takes a PipelineContext
object (containing ticket_id
and a data
dict) and enriches it. For example, after the classifiers run, the context’s data
might have keys like new_queue
, new_priority
, or an article
(comment) to add. The GenericTicketUpdater pipe then looks for an update_data
entry in the context and calls the adapter to apply those fields to the ticket. This design makes it easy to add new steps (e.g. a pseudonymization pipe) or to customize the update logic. The orchestrator manages these AttributePredictors (fetcher, preparer, AI service, modifier) based on YAML config.
TicketSystemAdapter and ZammadAdapter
OpenTicketAI defines an abstract base class TicketSystemAdapter
that all integrations must extend. It declares core methods like:
async update_ticket(ticket_id: str, data: dict) -> dict | None
: Update a ticket’s fields (e.g. queue, priority, add note). Must handle partial updates and return the updated ticket object.async find_tickets(query: dict) -> list[dict]
: Search for tickets matching a query. The query format is adapter-specific, but this should return a list of matching tickets.async find_first_ticket(query: dict) -> dict | None
: Convenience to return only the first match.
A ZammadAdapter will subclass this and implement these methods using Zammad’s API. It will typically hold configuration (base URL, credentials) injected via a SystemConfig
. For example:
from open_ticket_ai.src.ticket_system_integration.ticket_system_adapter import TicketSystemAdapter
import httpx
class ZammadAdapter(TicketSystemAdapter):
def __init__(self, config):
super().__init__(config)
# Assume config.zammad contains URL and auth info
self.base_url = config.zammad.base_url.rstrip('/')
self.auth = (config.zammad.user, config.zammad.password)
async def find_tickets(self, query: dict) -> list[dict]:
# Use Zammad search API (e.g. full-text search or filters).
async with httpx.AsyncClient(auth=self.auth) as client:
params = {"query": query.get("search", "")}
res = await client.get(f"{self.base_url}/api/v1/tickets/search", params=params)
res.raise_for_status()
return res.json() # list of matching tickets (each as dict)
async def find_first_ticket(self, query: dict) -> dict | None:
tickets = await self.find_tickets(query)
return tickets[0] if tickets else None
async def update_ticket(self, ticket_id: str, data: dict) -> dict | None:
# Send PUT to update the ticket. Data can include 'group', 'priority', etc.
url = f"{self.base_url}/api/v1/tickets/{ticket_id}"
async with httpx.AsyncClient(auth=self.auth) as client:
res = await client.put(url, json=data)
if res.status_code == 200:
return res.json() # updated ticket object
return None
Citation: The base class requires these methods. In this example we use httpx.AsyncClient
(since the methods are async), but you could similarly use requests
in a synchronous context. For instance, fetching all tickets might be as simple as requests.get(f"{base_url}/api/v1/tickets", auth=(user, pwd))
.
Fetching Tickets from Zammad
Zammad’s REST API provides endpoints to list and search tickets. A simple way to fetch recent or matching tickets is via:
- List All (paginated):
GET /api/v1/tickets
returns an array of ticket objects. - Search:
GET /api/v1/tickets/search?query=...
supports full-text or field queries, returning matching tickets in JSON form (andexpand=true
can resolve related fields).
Your find_tickets
implementation can use these. For example, to filter by state or subject:
async with httpx.AsyncClient(auth=self.auth) as client:
res = await client.get(f"{base_url}/api/v1/tickets/search", params={"query": "state:open OR state:new"})
res.raise_for_status()
tickets = res.json() # a list of dicts
Then wrap or return those in the format OpenTicketAI expects (a list of ticket dicts). The BasicTicketFetcher
pipe will call this using the ticket ID from the PipelineContext
to load a ticket before processing.
Updating Zammad Tickets
After classification, we update Zammad using its Update Ticket API. Zammad supports changing fields like group (queue) and priority, and even adding an internal note or article in one call. For example, the following payload (sent via PUT /api/v1/tickets/{id}
) sets a new group and priority and appends an internal article:
{
"group": "Sales",
"state": "open",
"priority": "3 high",
"article": {
"subject": "AI Insight",
"body": "Sentiment analysis: negative tone detected.",
"internal": true
}
}
This would reassign the ticket to the “Sales” group, set it to high priority, and attach a new note (internal comment) with AI insights. In code, our update_ticket
could do:
await client.put(f"{base_url}/api/v1/tickets/{ticket_id}", json={
"group": new_queue,
"priority": f"{priority_level} {priority_label}",
"article": {
"subject": "Auto-classified Ticket",
"body": f"Queue={new_queue}, Priority={priority_label}",
"internal": True
}
})
The response will be the full updated ticket JSON if status 200. If you only need to post a comment or note, include the article
block as above. Alternatively, smaller updates (like just setting a note) can use the ticket “note” field or a separate articles endpoint, but the bundled article
in the PUT is convenient.
Pipeline Integration in OpenTicketAI
To wire this into OpenTicketAI’s pipeline, you add pipes in config.yml
. For example:
- BasicTicketFetcher: configured with
ticket_system: ZammadAdapter
. It callsfind_tickets
/find_first_ticket
and populatescontext.data
with the ticket fields. - Preparer: e.g.
SubjectBodyPreparer
to combine subject/body text. - AI Inference Services: your custom queue/priority classifiers (e.g. a HuggingFace model).
- GenericTicketUpdater: configured with
ticket_system: ZammadAdapter
. It looks forcontext.data["update_data"]
after inference and callsupdate_ticket
.
For example, a custom pipe might do:
class QueuePriorityPredictor(Pipe):
def process(self, context: PipelineContext) -> PipelineContext:
subject = context.data.get("subject", "")
body = context.data.get("body", "")
queue_pred = my_queue_model.predict(subject + body)
prio_pred = my_prio_model.predict(subject + body)
# Prepare update data for Zammad
context.data['update_data'] = {
"group": queue_pred.group_name,
"priority": f"{prio_pred.score} {prio_pred.label}",
"article": {
"subject": "AI Classification",
"body": f"Assigned to {queue_pred.group_name}, Priority={prio_pred.label}",
"internal": True
}
}
return context
This sets up the update_data
that GenericTicketUpdater will use.
Finally, the AdapterFactory (configured via DI) ensures that ticket_system: Zammad
creates an instance of your ZammadAdapter
class, injecting the base URL and auth from config.yml
. The GenericTicketUpdater pipe then calls await adapter.update_ticket(id, update_data)
, applying your AI-driven changes.
Enhancements: Classification, Pseudonymization, and Notes
Beyond basic queue/priority, OpenTicketAI offers features to enrich the Zammad integration:
Queue & Priority Classification: You can train custom models for specific Zammad queues or priority schemes. The predicted values map to Zammad’s groups and priorities (for example, the priority API uses
"priority": "2 normal"
format). By adjusting thresholds in the postprocessor, you can also automatically drop low-confidence predictions or escalate tickets.Pseudonymization Connectors: To protect user privacy, you can insert a custom pipeline pipe before inference that pseudonymizes or masks sensitive data (e.g. names, emails) in the ticket text. This could use regex or external services to replace PII with tokens. The masked text is then classified, and the original ticket is updated, ensuring no sensitive content leaves your system.
Note/Article Creation: You can leverage Zammad’s article API to log AI insights or sentiment. As shown above, include an
article
in the update payload to add comments. Alternatively, you could configure a separate note-creation pipe that, regardless of updating queue/priority, always appends a ticket note with model confidence scores or sentiment analysis. These notes help agents understand why a decision was made.
Each enhancement fits naturally into the pipeline and is automatically applied by the GenericTicketUpdater via the adapter. For example, after running a sentiment analysis pipe, you might do:
context.data['update_data'] = {
"article": {
"subject": "Sentiment Score",
"body": f"Sentiment polarity: {sentiment_score}",
"internal": True,
},
}
Then the adapter will POST it as an article to Zammad.
Benefits for Zammad Ticket Automation
With this integration, Zammad gains on-premise AI-powered automation. Incoming tickets can be auto-assigned to the correct queue and given a preliminary priority, freeing support teams to focus on urgent issues. Because OpenTicketAI runs locally, sensitive ticket data stays in-house (important for compliance). This Zammad AI integration turns manual triage into a streamlined process: you maintain full control and customization (via config and custom models) while leveraging OpenTicketAI’s pipeline.
In summary, implementing a ZammadAdapter involves subclassing TicketSystemAdapter
and wiring it into OpenTicketAI’s pipeline. The adapter uses Zammad’s API for ticket CRUD operations (e.g. GET /tickets
and PUT /tickets/{id}
). Once configured, OpenTicketAI will continuously fetch tickets, run them through your AI model stack, and update Zammad with predicted queue, priority, and any notes. This ticket system AI integration empowers Zammad with automated classification and routing, realizing the vision of an on-premise AI ticket classifier for enterprise support teams.
Sources: Zammad REST API docs; OpenTicketAI developer docs.