Last reviewed: November 14, 2026 (Asia/Shanghai). Checked baseline: Python 3.12.3, Panel 1.10.0, and HTTPX 0.28.1.

Direct answer

Build a Panel prompt workbench for CometAPI by giving pn.chat.ChatInterface an asynchronous callback that sends the current text to POST /v1/chat/completions. Keep the credential and model in the Python process, validate the returned completion, and return its accepted text to Panel. Use one independent question per request while keeping earlier exchanges visible for comparison.

The important distinction is between the displayed transcript and the request’s messages array. Panel displays submitted questions and callback results. It sends conversation history upstream only if your callback constructs that history. The implementation below always sends one user message. Earlier entries remain available for visual comparison without being added to later requests.

This tutorial uses the classic ChatInterface reference . Panel’s current documentation distinguishes pn.chat.ChatInterface from the newer Material interface exposed through pn.ui. Keep the namespace aligned with the reference used here.

Who this is for

Use this small application when you already work with Panel or want a local prompt-comparison tool alongside Python analysis. It provides a text box, a visible exchange log, and a controlled server-side request path. For a conversational interface that deliberately sends earlier turns, compare the Gradio integration .

The checked baseline uses Python 3.12.3 with Panel 1.10.0 and HTTPX 0.28.1. Local verification used synthetic responses and Panel’s Python callback machinery. No authenticated CometAPI request or browser acceptance test was performed for this article; the live workflow below remains a check for your installation.

Key takeaways

  • Give Panel an async callback and use an async HTTP client inside it.
  • Keep the displayed transcript separate from the single-message request.
  • Accept a complete text outcome before displaying an assistant reply.
  • Label validation and request failures as Status messages.
  • Disable Rerun, Undo, and Stop until their request semantics are intentional.
  • Begin with a loopback listener and a text-only input widget.

Install and choose the request contract

Create an isolated environment, then install the checked dependency versions:

python3 -m venv .venv
. .venv/bin/activate
python -m pip install "panel==1.10.0" "httpx==0.28.1"

Supply COMETAPI_KEY and COMETAPI_MODEL through your local secret configuration before starting the application. Select an exact current model identifier that supports ordinary text Chat Completions and is available to your account. The program checks for missing configuration by variable name without printing either value.

The CometAPI chat reference documents the route, bearer authentication, messages, non-streaming completion object, and finish reasons. This example sends only model, messages, and stream: false. Model-specific reasoning or output options should be reviewed against that reference before you add them. The local character and response-byte ceilings below express application policy; they are not token limits or account spending controls.

Build the request and response boundary

Save the following as panel_workbench.py. The optional transport argument lets an offline check replace the network with an HTTPX mock. The normal application leaves it unset.

import asyncio
import json
import os

import httpx
import panel as pn


ENDPOINT = "https://api.cometapi.com/v1/chat/completions"
MAX_PROMPT_CHARS = 4_000
MAX_RESPONSE_BYTES = 262_144
TIMEOUT = httpx.Timeout(connect=5, read=25, write=10, pool=5)


class ReplyError(Exception):
    pass


def required_setting(name):
    value = os.environ.get(name, "").strip()
    if not value:
        raise RuntimeError(f"Missing setting: {name}")
    return value


def accepted_text(data):
    choices = data.get("choices") if isinstance(data, dict) else None
    if (
        not isinstance(data, dict)
        or data.get("object") != "chat.completion"
        or not isinstance(choices, list)
        or len(choices) != 1
    ):
        raise ReplyError("response_shape")
    choice = choices[0]
    message = choice.get("message") if isinstance(choice, dict) else None
    if (
        not isinstance(choice, dict)
        or choice.get("finish_reason") != "stop"
        or not isinstance(message, dict)
        or message.get("role") != "assistant"
        or message.get("refusal") is not None
        or message.get("tool_calls") not in (None, [])
        or not isinstance(message.get("content"), str)
        or not message["content"].strip()
    ):
        raise ReplyError("response_outcome")
    return message["content"]


async def fetch_answer(prompt, key, model, transport=None):
    async with asyncio.timeout(30):
        async with httpx.AsyncClient(
            timeout=TIMEOUT,
            follow_redirects=False,
            transport=transport,
        ) as client:
            async with client.stream(
                "POST",
                ENDPOINT,
                headers={
                    "Authorization": f"Bearer {key}",
                    "Accept": "application/json",
                },
                json={
                    "model": model,
                    "messages": [{"role": "user", "content": prompt}],
                    "stream": False,
                },
            ) as response:
                if response.status_code != 200:
                    raise ReplyError("http_status")
                media_type = response.headers.get("content-type", "")
                if media_type.split(";", 1)[0].strip().lower() != "application/json":
                    raise ReplyError("media_type")
                body = bytearray()
                async for chunk in response.aiter_bytes():
                    if len(body) + len(chunk) > MAX_RESPONSE_BYTES:
                        raise ReplyError("response_size")
                    body.extend(chunk)
    return accepted_text(json.loads(body))

The parser accepts one assistant text choice that finished with stop. It rejects truncation, refusal, tool calls, and empty output. Those are deliberate acceptance rules for this workbench. A tool-using application needs a richer result type before executing anything requested by a model.

HTTPX’s async guide documents the streaming context and async byte iterator used to read the ordinary JSON response. Here, HTTP streaming controls body consumption; stream: false still requests a non-SSE model response. The code limits accumulated decoded bytes before parsing JSON, and the context closes the response on success or failure. This limits the accepted body, without claiming a strict ceiling on HTTPX’s transient decompression allocations.

HTTPX’s timeout reference distinguishes connect, read, write, and pool waits. The outer timeout adds an approximately 30-second budget around client setup and network reading. JSON parsing follows that block and is bounded by the accepted body size. Cancellation depends on cooperative async work; a timeout does not establish whether the remote generation completed.

For this low-volume local tool, each submission creates and closes its client. That makes ownership explicit and avoids leaving a client tied to a destroyed interface. A busy application should give a reusable client an explicit application or session lifecycle; the Python HTTPX client tutorial explains that separate design.

Connect the Panel callback

Append this function to the same module:

def build_app(answer_fn=None):
    if answer_fn is None:
        key = required_setting("COMETAPI_KEY")
        model = required_setting("COMETAPI_MODEL")

        async def answer_fn(prompt):
            return await fetch_answer(prompt, key, model)

    def status(text):
        return {"user": "Status", "object": text}

    async def respond(contents, user, instance):
        if not isinstance(contents, str) or not contents.strip():
            return status("Enter a text question.")
        if len(contents) > MAX_PROMPT_CHARS:
            return status("Use at most 4,000 characters.")
        try:
            return await answer_fn(contents)
        except (ReplyError, httpx.HTTPError, TimeoutError, ValueError):
            return status("No accepted reply was received. Check the local configuration.")

    def unexpected_error(exception, instance):
        instance.send("The request could not be completed.", respond=False, user="Status")

    return pn.chat.ChatInterface(
        callback=respond,
        callback_user="Assistant",
        callback_exception=unexpected_error,
        widgets=pn.widgets.TextInput(placeholder="Ask one independent question"),
        adaptive=False,
        show_stop=False,
        show_rerun=False,
        show_undo=False,
        show_clear=True,
        renderers=[pn.pane.Str],
        help_text="Each question is independent. Previous messages are display only.",
        height=600,
        sizing_mode="stretch_width",
    )

The ChatFeed reference documents the callback’s content, user, and instance arguments, async callbacks, returned message objects, and exception handling. This callback uses only contents; it never serializes the feed. Successful text receives the Assistant label. Local input rejections and expected request failures return a message with the Status label instead.

The custom exception handler covers unexpected callback failures with fixed display text. It calls send with respond=False, preventing the status message from invoking another generation. Keep error handling equally narrow if you add logging: record a fixed category and correlation value instead of prompts, keys, raw responses, or exception strings.

renderers=[pn.pane.Str] chooses literal text for the exchange bodies. The Str reference says this pane applies no Markdown or HTML formatting and preserves whitespace. A response containing markup therefore stays text in this design. Rich formatting is a separate rendering decision.

Launch the local interface

Create app.py beside the module:

import panel as pn
from panel_workbench import build_app

pn.extension()
build_app().servable(title="Independent prompt workbench")

Start it with an explicit loopback address:

panel serve app.py --address 127.0.0.1 --port 5006 --show

Panel’s command-line reference documents serving scripts, the listener address, port, and browser-opening option. Open the printed local URL if the browser does not launch automatically. Changing the listener to a network interface requires an intentional deployment and access policy.

The input widget accepts text without an upload selector. adaptive=False uses the ordinary callback flow instead of interrupting and restarting it when another message arrives. The application also hides Rerun, which otherwise resends the latest user input, and Undo, which changes the visible log. Stop is hidden because this first version waits for one complete result and exposes no user-facing upstream cancellation contract.

Clear remains available. It clears Panel’s displayed messages; the next request still contains only its own question. Retain the help text so users understand that a visible earlier answer is unavailable to the next model request. The display can accumulate messages during a long session, so clear it periodically when comparing many prompts.

Contract details to verify

BoundaryBehavior in this exampleAcceptance check
ProviderFixed CometAPI Chat Completions endpointInspect the mock request’s exact method and URL.
ContextExactly one user message per requestSubmit two different questions and inspect both mock bodies.
InputNonempty text, at most 4,000 charactersVerify an over-limit input never reaches the answer function.
CompletionOne accepted assistant text choiceReject length, refusal, tool calls, and empty choices.
ErrorsFixed Status messagesConfirm private exception details are absent from the display.
RenderingLiteral text through StrDisplay a synthetic string containing markup.
ControlsIndependent questions with ClearConfirm Rerun, Undo, and Stop are absent.

Happy-path operator workflow

First construct the interface with an injected async answer function that returns a fixed string. Submit a harmless question and confirm one user entry and one Assistant result. Test a markup-containing fixture to check that the selected renderer treats it as literal text.

Next replace the HTTP transport with httpx.MockTransport. Assert the endpoint, bearer-header shape, and exact JSON body using synthetic credentials. Make two submissions and verify that each body contains exactly its own question. These checks establish the context policy independently of a model’s willingness to follow a prompt.

During preparation, 12 transport and response cases passed on the checked baseline, covering two successful independent requests plus HTTP, redirect, media-type, malformed-JSON, size, and completion-outcome failures. Additional checks passed for Panel construction, input rejection, fixed error mapping, the async callback-to-renderer flow, and failure messages carrying the Status label. These results describe Python-level offline checks.

Finally, run the app with your real configuration and submit one short question. Confirm that a complete answer appears, Clear removes the display, and another question still works. Record the dependency versions, configured model, and acceptance result without retaining the credential or conversation text in routine diagnostics.

Failure modes

The app fails before serving. Check whether the process receives both required settings. Restore the named configuration entry through your secret mechanism and restart. Do not paste the key into the input widget.

The display shows Status instead of Assistant. Inspect your local configuration and the selected model’s route support. The message can represent an HTTP rejection, timeout, unexpected media type, malformed JSON, oversized body, or unaccepted completion. It deliberately omits upstream details. Reproduce with a synthetic transport when distinguishing those branches.

The interface waits while other work freezes. Check for synchronous networking or blocking work added inside the async callback. The ChatFeed documentation recommends async callbacks with nonblocking operations; adding async to a function does not make a blocking library cooperative.

A follow-up ignores the earlier answer. That is the stated request policy. Include all necessary context in the new question. If you add conversation history, define which entries are accepted model turns before using serialization; Status and display-only messages should not become assistant history accidentally.

An error causes repeated requests. Check for a status-message send that omitted respond=False, a re-enabled Rerun control, or application retry logic. This example performs one attempt per accepted submission and never automatically resubmits an uncertain request.

Error-path operator workflow

Run the injected callback with an over-limit input and assert that the answer function is not called. Then make the mock transport return an HTTP error, a redirect, HTML, invalid JSON, and a body larger than the ceiling. Confirm that none becomes an accepted assistant answer and that no redirect is followed.

Test valid JSON with finish_reason: "length", a refusal, and a tool-call result. Each should reach the same fixed Status path rather than being presented as completed text. Finally, inject an unexpected exception containing a synthetic private marker and confirm that only the generic status appears.

After those checks, use a deliberately unavailable model in a development run and submit one harmless prompt. Verify the Status label, absence of raw provider details, and lack of automatic retry. Restore the verified model before repeating the happy path.

FAQ

Does Panel automatically send the displayed history?

The callback constructs the request. This one sends a new single-entry messages array each time. Panel also provides serialization methods, but the example does not call them.

Why disable Rerun for a prompt workbench?

Rerun triggers another callback for the latest user input. Hiding it makes the first version’s request triggers easy to inspect. Add an explicit repeat action later when users understand that it starts another provider attempt.

Does Clear delete provider records?

Clear changes the Panel log. This implementation issues no provider deletion request and maintains no separate durable conversation database. Decide retention for any storage or logging you add.

Can this display incremental tokens?

Panel supports async generators, but this implementation accepts a complete JSON result. Streaming requires a separate SSE reader and a policy for incomplete output before changing the callback to yield text.

Sources checked

Seven primary public pages were fetched successfully on November 14, 2026 UTC. They support an evergreen integration task, rather than a release or incident claim. Panel references displayed version 1.10.0; the CometAPI and HTTPX references were checked against the request path and methods used here.

The single-question policy, limits, completion acceptance, and verification procedures are application recommendations derived from those interfaces.

Reader next step

Build the fixture-backed interface first, inspect two outgoing request bodies, and verify the failure labels. Then configure a current model and make one short local exchange. For the account and model setup needed for that check, Start with CometAPI , keep the key in the Python process, and repeat the same acceptance checks before extending the app.