Skip to content

Latest commit

 

History

History
233 lines (187 loc) · 11.8 KB

File metadata and controls

233 lines (187 loc) · 11.8 KB

Deploying personagent

The README covers the happy path. This covers the parts that are load-bearing and were previously written down nowhere — every item here was found by an audit that tried to deploy from a clean clone and hit something the docs did not mention.

The shortest config that works

To get one reply out of python try_chat.py: one setting.

LLM_API_KEY=...

LLM_BASE_URL and LLM_MODEL only matter if you are not using DeepSeek — the names are historical, and any OpenAI-compatible /v1 endpoint works (OpenAI, Zhipu, Moonshot, Together, a local ollama or llama.cpp). There is no persona.txt requirement; a default persona ships in the code.

To run live on QQ: three.

LLM_API_KEY=...
BOT_NAME=...          # what the persona is called, and what it answers to
BOT_QQ=...            # the bot account's number

NAPCAT_API defaults to http://127.0.0.1:3000, HOST/PORT default to 127.0.0.1:8080, and an empty QQ_GROUPS means every group the account is in.

Everything else in .env.example has a working default. Run python tools/healthcheck.py after configuring: it reports missing settings, misspelled ones (a typo is otherwise completely silent — the value is ignored and the default is used), and whether each upstream service answers.

What the host has to provide

Four requirements that nothing else states:

  1. The deployment root must be writable. Startup takes an instance lock at <AGENT_HOME>/.personagent.instance.lock, outside runtime/. A read-only checkout fails to start.
  2. One process per root. That lock is how two copies are kept from corrupting each other's ledgers. To run several personas from one installed copy, give each its own AGENT_HOME.
  3. python main.py runs from the repository root. It starts uvicorn with an import string (main:app), so the working directory has to contain it.
  4. data/ must sit next to the package. If it does not, the root detector falls back to the working directory and every seed lookup silently misses.

Two path rules that follow from AGENT_HOME: AGENT_RUNTIME_DIR must resolve beneath it (startup raises otherwise), and a relative PERSONA_FILE / PERSONA_CARD_FILE is resolved under it too, so a persona kept outside the checkout needs an absolute path.

The OneBot bridge

Deprecated since 0.3.0. The direct ingress described here — the bridge's webhook posting to /webhook/qq — still works and is warned about once at first use, but the supported path is AstrBot with GATEWAY_NATIVE_PLATFORMS=aiocqhttp (next section), which manages the QQ client itself. Nothing below is lost on that path: the bridge's HTTP API (NAPCAT_API) still carries the QQ-only background actions.

This is the largest piece of the deployment and none of it lives in this repository. You need, roughly in order:

  1. A second QQ account. Do not use your own — see DISCLAIMER.md.
  2. The QQ NT desktop client, which the bridge attaches to.
  3. A OneBot v11 implementation: NapCat, LLOneBot or Lagrange. NapCat is what this project is tested against.
  4. A logged-in session on that account, usually by QR, that survives restarts.
  5. Two network directions configured in the bridge, which is the part people get wrong:
    • an HTTP server the agent calls to send messages — this is what NAPCAT_API points at, http://127.0.0.1:3000 by default;
    • an HTTP client / webhook the bridge calls to deliver events, pointed at http://127.0.0.1:8080/webhook/qq (HOST/PORT).
  6. The bot account added to the target group.

The exact configuration shape is the bridge's, not ours, and it has changed between NapCat major versions — read NapCat's own configuration documentation rather than copying a snippet from here. What must be true is the two directions above; how they are spelled is theirs.

Verify the bridge before blaming the agent, in this order:

# 1. the outbound direction: the agent -> the bridge
curl http://127.0.0.1:3000/get_login_info

# 2. the inbound direction: send the bot a message in the group and watch
#    the agent's log for the webhook arriving

If (1) fails, the agent can never send. If (1) works and (2) never logs anything, the bridge's webhook is not configured or is pointed elsewhere. Both failures look identical from the outside — a bot that is running and silent.

More than one platform

Everything above is the QQ path. For Telegram, Discord, Slack and the rest, the agent does not connect to the platform at all — a forwarder does, and POSTs a platform-neutral event to POST /webhook/gateway. The one in this repo is an AstrBot plugin at integrations/astrbot/astrbot_plugin_llm_persona_gateway/; copy it into AstrBot's data/plugins/, and configure the platforms in AstrBot's own UI. The persona, memory, learning and typing simulation stay here.

python quickstart.py connects the two for you: it copies the plugin into AstrBot's data/plugins/, generates GATEWAY_TOKEN and writes it to .env and to the plugin's config, writes the allowlists, and sets GATEWAY_NATIVE_PLATFORMS when QQ is included. --astrbot <data dir> [--qq] does the same without the wizard. What follows is what it writes and why.

Two settings and one decision:

  • GATEWAY_TOKEN — shared with the plugin. Required off-host, and the request carries an HMAC-SHA256 envelope over timestamp.nonce.body with a replay guard, so a token seen in a log is not enough on its own.
  • GATEWAY_OWNER_IDS — platform-prefixed ids (telegram:12345) that get the owner branch in DMs. Gateway identities are namespaced <platform>:<id> so they can never collide with a QQ number.
  • The decision: does QQ go through the forwarder too?

Leave QQ on NapCat and you have two inbound paths but nothing to reconcile. Route it through the forwarder and there is one door and one place to configure platforms — but then set GATEWAY_NATIVE_PLATFORMS to the forwarder's QQ adapter name (aiocqhttp for AstrBot), or every QQ conversation arrives under a namespaced name it has never had before. Memory, history and every learned example are keyed the bare way, and the evidence and candidate ledgers content-address their rows over the conversation id — so the rename changes every id derived from it and cannot be undone by rewriting a field. GATEWAY_NATIVE_PLATFORMS keeps the ids identical to NapCat's.

Naming a platform there grants its forwarder QQ authority, since bare ids are what OWNER_QQ, QQ_GROUPS and PRIVATE_ALLOWED_QQS are compared against. Which is why those whitelists then apply to it, unlike to a namespaced platform — the forwarder's own allowlist is not the only filter any more.

Do not run both doors for QQ at once; the same message would arrive twice.

Keep NapCat's HTTP server on either way. AstrBot talks to NapCat over a reverse WebSocket, which covers inbound and replies — but it is not the channel this agent uses. Everything the agent does on its own initiative goes out through NAPCAT_API directly: proactive messages, the catch-up sweep for @s it missed while offline, resolving an old quoted message, and OCR. Turn the HTTP server off because "AstrBot handles QQ now" and the bot keeps answering while quietly losing all four, with nothing in the log to say so.

That direct channel is also why QQ loses nothing by moving inbound: the outbound path never depended on where the message came from.

Other platforms have no such channel — the agent can only speak inside the request that brought a message, because the reply sink closes when that request returns. The proactive loops skip any conversation whose id is namespaced, for want of anywhere to send.

For proactive turns that is worked around rather than solved, by inverting them: POST an ordinary gateway event with "proactive": true, and the text on it is read as a cue to the persona ("they have been quiet a while, say something if you genuinely have something to say") instead of as the other person's words. If it decides to speak, the reply comes back in the response like any other, and the caller relays it. Anything scheduling those requests works — AstrBot plugins can hold a background task or a cron entry.

The flag is what keeps the cue out of the transcript. Without it the caller's own directive lands in the conversation history as something the reader said, stays for 40 turns, can be quoted back at them, and can be promoted into a memory about them.

Delayed elicitation and the LLM-failure excuse are still lost on those platforms; both are fire-and-forget sends with no request to ride back on.

Exposing the webhook

Keep the default loopback binding when the bridge and the agent share a machine. If they do not, set HOST=0.0.0.0 and both WEBHOOK_SECRET and GATEWAY_TOKEN. Startup refuses a non-loopback HOST without them, including for a QQ-only deployment: /webhook/gateway is mounted whether or not you use it, so binding a public interface without a gateway token would leave it open.

The detailed health endpoint

GET /health is free and unauthenticated. GET /health/details probes the upstream services (cached 60 s) and is gated: with GATEWAY_TOKEN blank it answers loopback only; once a token is configured it answers only requests carrying a matching X-Gateway-Token header, and loopback alone is refused. A degraded critical dependency turns it into HTTP 503.

Costs to know about

The gateway retains transient state for up to 256 namespaced conversations, evicting idle sessions in least-recently-used order. Active requests are pinned and may temporarily exceed this cap; completion reclaims the excess. Native QQ conversations do not enter this cache. Eviction clears recent history and pending reactions, but preserves long-term memories, core notes and learning ledgers. The memory maps are loaded from disk at startup and are not bounded by the transient conversation cap; back up runtime/ to preserve learned state.

  • tools/healthcheck.py spends credits. Its probes POST to /v1/chat/completions on every configured endpoint — primary, private, eval and vision. The GET /health endpoint is the cheap one. The config and ledger-size sections of the CLI cost nothing; the service probes do.
  • tools/prompt_lab.py needs a second vendor. pip install -e ".[judge]" plus ANTHROPIC_API_KEY, because the lab generates through a different model from the one being tuned — that separation is what makes it a measurement.
  • tools/auto_reviewer.py --dry-run is free; it calls no model. --no-write reviews for real and prints instead of writing.

When the bot goes quiet

In rough order of likelihood:

  1. BOT_QQ unset or wrong. The mention detector returns false immediately, so the bot starts cleanly, logs nothing, and never answers. healthcheck warns about this now.
  2. A misspelled setting. Silent by construction — the default is used. healthcheck lists any key .env.example does not know.
  3. A BOM on .env. The first setting's name carries it and never reaches the process, and the file looks correct in every editor.
  4. The bridge's webhook is not reaching you. See the verification above.
  5. BOT_NAME or PERSONA_VERSION changed. Either starts a new character and the promoted material of the old one is refused, with a warning when an entire view is dropped. Editing persona.txt itself does not do this any more: every revision seen under one PERSONA_VERSION shares the learning scope (runtime/persona_lineage.json). Rows learned before that file existed can be folded in with python tools/candidates_admin.py lineage adopt <hash> (the hash is on the candidate's scope in show), then rebuild.