SmartFilter Webhook

Experimental: The feature described in this document is considered experimental and subject to change or removal. Please provide QA Cafe with any feedback you have if using this.

Introduction

SmartFilter lets a user type a natural-language question (“show me the DNS errors”, “TCP resets from 10.0.0.5”) and have Packet Viewer translate it into a Wireshark display filter.

Packet Viewer does not call any language model itself, and it does not ship with a provider, model, or API key. The model call is entirely your responsibility: you provide an HTTP endpoint, and Packet Viewer POSTs the query to it and expects a display filter back. Everything provider-specific — model choice, credentials, structured output — lives behind your URL. This keeps LLM configuration an operations concern, separate from the embedded UI, and means your provider credentials never reach the browser.

When no endpoint is configured, the SmartFilter capability is simply disabled.

Enabling SmartFilter

Point the server at your webhook with the --smartfilter-endpoint flag:

--smartfilter-endpoint https://your-host.example.com/pv-smartfilter

As with every server option, you may set this with an environment variable (PV_SMARTFILTER_ENDPOINT) or in config.yaml (smartfilter-endpoint) instead. See Configuration for the precedence rules.

When the endpoint is unset, requests to the SmartFilter API return 503 and the input reports that the capability is not configured.

Turning it on in the front end

Configuring the endpoint enables the capability on the server; the UI must also opt in. There are two ways to surface SmartFilter:

  • The PacketViewer view — set the enableSmartFilter prop to true. It replaces the standard display filter bar with the SmartFilter input.

    import { PacketViewer } from "@qacafe/pv-react";
    
    <PacketViewer file="example.pcap" enableSmartFilter={true} />;
    
  • The composable SmartFilterInput component — import it from the components entry point and place it inside your own layout, within a PacketViewerProvider.

    import { SmartFilterInput } from "@qacafe/pv-react/components";
    

Both are experimental, and both rely on the webhook configured above; without it the input reports that SmartFilter is not configured.

How a request flows

When a user applies a SmartFilter query, the server runs the following steps before any answer reaches the UI:

  1. Passthrough check. If the query is already a valid Wireshark display filter, it is applied as-is and your webhook is never called.
  2. Webhook call. Otherwise the server POSTs the query to your endpoint and reads back a candidate filter.
  3. Validation. The candidate is checked against Wireshark. A valid filter is returned to the UI and applied.
  4. One retry. If the candidate is invalid, the server calls your endpoint a second time, this time including the rejected filter and Wireshark’s reason (see Honoring previous). This retry happens at most once.

If your endpoint instead declines to produce a filter (for example, the question isn’t answerable as a display filter), it returns an explanation and the UI shows that text as guidance rather than applying a filter.

The request

Packet Viewer sends an HTTP POST with a JSON body and these headers:

Content-Type: application/json
Accept: application/json

Body fields

Field Type Description
query string The user’s natural-language input.
system string Release-versioned instructions from Packet Viewer describing how to turn the query into a display filter. Forward it to your model verbatim, or ignore it.
previous object Present only on a retry. Carries the prior attempt so your endpoint can correct it. Omitted on the first call.

The previous object has two fields:

Field Type Description
filter string The filter your endpoint returned that failed validation.
error string Wireshark’s reason for rejecting it.

About the system prompt

The system string is built into the Packet Viewer backend — you do not author or configure it. It contains the instructions we’ve tuned for turning a natural-language question into a valid Wireshark display filter, and it ships with the product, versioned to each release. Forwarding it to your model as the system prompt is the recommended path: it’s how Packet Viewer steers the output toward correct, valid filters, and you get our improvements automatically as you upgrade.

Expect this prompt to keep evolving. We’ll continue to refine it based on customer feedback and real-world queries, so its exact wording will change from release to release. Treat it as an opaque, pass-through value rather than something to depend on the precise contents of. You are free to ignore it and supply your own instructions, but then the quality of the results is entirely up to your prompt.

Example — first call

{
  "query": "show me DNS responses that failed",
  "system": "You translate natural-language questions into Wireshark display filters. ..."
}

Example — retry

{
  "query": "show me DNS responses that failed",
  "system": "You translate natural-language questions into Wireshark display filters. ...",
  "previous": {
    "filter": "dns.flags.rcode neq 0",
    "error": "\"neq\" was unexpected in this context."
  }
}

The response

Respond with HTTP 200 and a JSON body containing two fields:

Field Type Description
filter string The Wireshark display filter. Set this when you can answer the query.
explanation string A short, user-facing reason. Set this when you cannot produce a filter.

Return one or the other:

  • Produced a filter — set filter, leave explanation empty.
  • Declined — leave filter empty, set explanation. The UI shows the text as guidance and applies no filter.

Both fields are trimmed of surrounding whitespace. Return the filter expression only — no backticks, quoting, or surrounding prose.

Example — filter produced

{
  "filter": "dns.flags.response == 1 && dns.flags.rcode != 0",
  "explanation": ""
}

Example — declined

{
  "filter": "",
  "explanation": "That looks like a request about capture timing, which can't be expressed as a display filter."
}

Timeouts and failures

Each call to your endpoint is bounded by a 30-second timeout. Any of the following is treated as a webhook failure, surfaced to the user as a temporary error (HTTP 502, see below):

  • the endpoint is unreachable or the request times out,
  • it returns a non-200 status, or
  • it returns a body that cannot be decoded as the JSON above.

The underlying detail is logged server-side; the user sees a clean, generic message.

Honoring previous

The retry only helps if your endpoint acts on previous. When it is present, feed the prior filter and its error back to your model as a correction (“this filter was rejected because …; produce a corrected one”). This is how the one-shot correction loop recovers from a near-miss. If you ignore previous, the retry degrades to simply asking the same question again.

What the user sees

The API resolves to one of the following. The UI branches on the HTTP status code, not on message text.

Outcome HTTP What the user sees
Filter applied 200 The filter is applied to the packet list.
Declined (model guidance) 200 Your explanation, shown inline as guidance — not an error.
Not configured 503 A message that SmartFilter is not configured on the server.
Webhook failed 502 “The smart filter service is temporarily unavailable. Please try again.”
Internal error 500 “An unexpected error occurred while generating the filter.”

A 502 is the only case the user is invited to retry; the query stays in the input so they can simply apply it again. 503 means an operator needs to configure the endpoint, and 500 indicates a problem on the Packet Viewer side.

Example endpoint

A minimal webhook that forwards to a chat-completions-style provider. Error handling and your provider’s request shape are omitted for brevity.

import express from "express";

const app = express();
app.use(express.json());

app.post("/pv-smartfilter", async (req, res) => {
  const { query, system, previous } = req.body;

  // Build the prompt. On a retry, previous carries the rejected filter and why.
  let userPrompt = query;
  if (previous) {
    userPrompt =
      `The filter "${previous.filter}" was rejected: ${previous.error}. ` +
      `Produce a corrected display filter for: ${query}`;
  }

  const filter = await callYourModel({ system, prompt: userPrompt });

  if (!filter) {
    return res.json({
      filter: "",
      explanation: "I couldn't turn that into a display filter.",
    });
  }

  res.json({ filter, explanation: "" });
});

app.listen(8080);