- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| get_gts_token.py | ||
| gts_agent.py | ||
| README.md | ||
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
-
Create a dedicated bot account on your instance — don't reuse your personal account.
-
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.comRun 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_TOKENbelow. -
Install dependencies:
pip install requests openai --break-system-packages(the
openaipackage is just the client library — it talks to LM Studio's local server, not OpenAI's API) -
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/modelsonce it's running). -
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_RUNcaps 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_PROMPTexplicitly 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.jsonand fed back to the model each time as "don't start this the way you started these," and there's now a hardMAX_REPLIES_PER_RUNcap (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).