Watch
1
0
Fork
You've already forked applecore
0
Gotosocial bot
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-14 19:32:08 -07:00
get_gts_token.py python script init 2026-08-10 00:22:47 -07:00
gts_agent.py update the cat prompt 2026-08-14 19:32:08 -07:00
README.md first commit 2026-08-10 00:21:34 -07:00

GoToSocial LA-Beach-Persona Bot

A small agent that reads mentions and your home timeline, decides (via a local model served through LM Studio) whether to reply/favourite/reblog in a friendly LA-beach-person voice, and can occasionally post a cat fact.

Setup

  1. Create a dedicated bot account on your instance — don't reuse your personal account.

  2. Register an OAuth app and get an access token. You can do this by hand with curl (see the GoToSocial docs), or use the included helper:

    python get_gts_token.py https://social.snarfpc.com
    

    Run this while logged in to your instance as the bot account (not your personal account) in the browser it points you to — it'll register the app, walk you through approving it, and print the access token to plug into GTS_ACCESS_TOKEN below.

  3. Install dependencies:

    pip install requests openai --break-system-packages
    

    (the openai package is just the client library — it talks to LM Studio's local server, not OpenAI's API)

  4. Start LM Studio's local server — open LM Studio, load the model you want the bot to use, go to the "Local Server" tab, and click Start Server. Note the model identifier shown there (or run curl http://localhost:1234/v1/models once it's running).

  5. Set environment variables:

    export GTS_BASE_URL="https://social.snarfpc.com"
    export GTS_ACCESS_TOKEN="your-bot-access-token"
    export LMSTUDIO_BASE_URL="http://localhost:1234/v1"   # default, usually fine as-is
    export LMSTUDIO_MODEL="the-model-id-from-lm-studio"
    

Usage

Always start in dry-run (default — no --live flag) and watch the output for a while before trusting it:

python gts_agent.py check-mentions      # dry-run: prints what it would do
python gts_agent.py scan-timeline       # dry-run
python gts_agent.py cat-fact            # dry-run

Once you're happy with the decisions it's making:

python gts_agent.py check-mentions --live
python gts_agent.py scan-timeline --live
python gts_agent.py cat-fact --live

check-mentions and scan-timeline can be combined with all (cat-fact is deliberately excluded from all — see the note in the script — you'll probably want it on its own, much less frequent schedule).

Tuning the persona

Edit PERSONA_PROMPT and CAT_FACT_PROMPT in gts_agent.py directly. Small wording tweaks go a long way — if replies feel too enthusiastic or too flat, adjust the tone description and re-run in dry-run to compare.

Safety notes

  • MAX_ACTIONS_PER_RUN caps how many replies/reactions happen in a single run (default 5). Lower this while testing.
  • State is tracked in state.json (last-seen notification/post IDs) so re-running the script doesn't reprocess the same content.
  • Nothing here auto-schedules itself yet — each run is manual, by design, until you're confident in it. When you're ready to automate, wrap these commands in cron or a systemd timer.
  • LM Studio's server must be running (and the machine awake) whenever you run the script — unlike a cloud API, there's nothing to call if LM Studio isn't up.

If replies feel too stylized / repetitive / too frequent

Local models often latch onto persona examples as a literal template rather than an occasional flavor — so "sprinkle in some beach slang sometimes" turns into "start every reply with 'Right on!'". The script now handles this two ways:

  • Prompt-level: PERSONA_PROMPT explicitly bans reflexive openers/ catchphrases, gives more skip-heavy examples, and asks for varied, low-key phrasing rather than constant enthusiasm.
  • Code-level (more reliable): the last few reply openers are tracked in state.json and fed back to the model each time as "don't start this the way you started these," and there's now a hard MAX_REPLIES_PER_RUN cap (default 2) separate from the general action cap — even if the model wants to reply to everything, the script won't let it past that count in a single run (it downgrades extra "reply" decisions to "favourite" instead). Prompt instructions alone are a soft constraint with local models; this cap is a hard one.

If replies still feel repetitive after this, it's a sign the model itself has a narrow range of phrasing it defaults to — worth testing a different model or raising temperature further.

If it's replying too often or replies feel off-topic

Smaller local models tend to default to "always produce a reply" rather than correctly recognizing when silence is the right call, and can sometimes invent details that aren't actually in the post it's responding to. The persona prompt now explicitly instructs the model to skip the majority of posts and includes a few worked examples of skip vs. reply vs. favourite — if you're still seeing it over-reply or hallucinate details after this, it's worth trying a larger/more capable model in LM Studio, since this is a model-quality issue more than a prompt issue at that point.

If you're seeing "couldn't parse decision JSON" a lot

This almost always means you're running a reasoning/"thinking" model (Qwen3-thinking variants, DeepSeek-R1, etc.). These models generate an internal <think>...</think> block before their real answer, and that reasoning can burn through the whole max_tokens budget before the model ever reaches the JSON — leaving the response empty or cut off mid-string.

The script now:

  • Gives generous headroom (max_tokens=2000) so reasoning has room to finish
  • Strips <think>...</think> blocks if they leak into the response
  • Tells you why a parse failed (hit the token limit vs. reasoning-only response) so you're not guessing

If you're still seeing frequent parse failures after this, the most reliable fix is switching to a non-thinking model in LM Studio for this task — reasoning models are built for working through hard problems slowly, not for fast, cheap "pick one of four actions" classification, and the thinking overhead mostly just wastes time and tokens here without improving the decision quality.

A note on local models

Smaller local models are often less reliable at following the "respond ONLY with JSON" instruction than a larger hosted model. The script has a fallback parser (_extract_json) that tries to pull a {...} block out of messier output, but if you're seeing a lot of [warn] couldn't parse decision JSON lines, it's worth trying a model that's specifically good at instruction- following/JSON output, or tightening the prompt further (e.g. adding a one-shot example of the exact JSON format).