AI News HubLIVE
站内改写5 min read

Show HN: Amanuensis – a local-first AI persona that won't fabricate facts

Amanuensis is a local-first AI persona system for posting on Mastodon and Bluesky. It prevents model hallucination through strict pipelines: factual source summaries, deterministic cleanup, regex pre-checks, LLM grounding checks, and human approval via Telegram. MIT-licensed experimental code.

SourceHacker News AIAuthor: msalsas

Notifications You must be signed in to change notification settings

Fork 0

Star 0

BranchesTags

Open more actions menu

Folders and files

NameName

Last commit message

Last commit date

Latest commit

History

3 Commits

3 Commits

adapters

adapters

config

config

core

core

docs/img

docs/img

profiles/alexa

profiles/alexa

scripts

scripts

services

services

social

social

tests

tests

.env.example

.env.example

.gitignore

.gitignore

LICENSE

LICENSE

README.md

README.md

main_batch.py

main_batch.py

main_dispatcher.py

main_dispatcher.py

main_reply_listener.py

main_reply_listener.py

main_telegram_listener.py

main_telegram_listener.py

manual_post.py

manual_post.py

pyproject.toml

pyproject.toml

Repository files navigation

A local-first AI persona that writes under a human's veto. It drafts, you approve, and nothing it can't ground gets published.

A local-first pipeline for running an AI persona on Mastodon and Bluesky. The pilot persona, AlexaPavlova, posts as a sarcastic senior Berlin dev with dry takes on tech news and open source.

Everything runs on a local GPU machine. No cloud LLM calls.

See it live: Mastodon · Bluesky — now disclosed as AI, no longer posting.

Every post is reviewed on a phone before it publishes — approve, regenerate text or image, or cancel.

Status: this was an experiment, not an active product. The code is MIT-licensed and works end-to-end — fork it, learn from it, run your own persona. Issues and PRs may not get a response.

What's interesting here

The hard part wasn't generating text, it was stopping the model from fabricating technical detail. The short version: factual-only source summaries, deterministic cleanup before any LLM judgment, a regex pre-screen in front of an LLM grounding check, titles-only memory, and a human approving every post over Telegram.

Full write-up of the design and what broke along the way: write-up.

Quick start

  1. Install dependencies

pip install -e ".[dev]" cp .env.example .env

  1. Start local services

LMStudio

Download LMStudio and load any instruction-tuned model (tested with Mistral-7B-Instruct and similar)

Go to Local Server → start the server on port 1234

Set LMSTUDIO_BASE_URL=http://localhost:1234 in .env

SwarmUI

Install SwarmUI and load the image model + 41ex4_p4v10v4 LoRA

Set SWARMUI_BASE_URL=http://localhost:7801 in .env

  1. Set up Telegram

Message @BotFather → /newbot → copy the token into TELEGRAM_BOT_TOKEN

Message @userinfobot → copy your numeric ID into TELEGRAM_CHAT_ID

Send any message to your new bot so it can message you back

  1. Verify with a dry run

python main_batch.py --dry-run

This fetches real stories and generates posts + images using your local services. Nothing is written to any database and nothing is sent to Telegram. If this prints 8 posts, your local stack is working.

  1. Run for real

Add your social credentials to .env (Mastodon and/or Bluesky — both optional, see table below), then open three terminals:

Terminal 1 — generate today's posts and send to Telegram for approval

python main_batch.py

Terminal 2 — listen for your Telegram approvals

python main_telegram_listener.py

Terminal 3 — publish approved posts at their scheduled time

python main_dispatcher.py

Approve posts in Telegram. The dispatcher picks them up and publishes. Done.

For long-running setups, run the three persistent processes under systemd or supervisord so they survive reboots. main_batch.py is a one-shot script — run it via cron or manually each day.

Image model

The persona's images come from a custom LoRA trained on top of Juggernaut XL "Ragnarok" (SDXL), generated through SwarmUI. The LoRA is not included in this repo — it only contains trained deltas, not the base model.

LoRA: download from Hugging Face — msalsas/alexa-lora. Trigger word 41ex4_p4v10v4, weight 0.3, generated at 768×1024.

Base model: Juggernaut XL "Ragnarok" by RunDiffusion — get it separately, not distributed here.

The LoRA was trained on a fully synthetic dataset (images generated with Juggernaut XL); the character is not based on any real person.

To run the alexa profile with images you need both: load Juggernaut XL in SwarmUI and apply this LoRA. See the model card on Hugging Face for the exact prompt format.

Architecture

Adapters (HN, Lobsters, BearBlog, AskHN) └── Curator (dedup by URL + title + subreddit, banned-topic filter) └── BatchFactory ├── Brain (LMStudio → post text + image prompt) └── ImageService (SwarmUI → PNG via LoRA) └── Scheduler (UTC time windows with jitter) └── QueueService (SQLite) └── TelegramNotifier (photo + approval keyboard) ├── MastodonPublisher (on APPROVE) └── BlueskyPublisher (on APPROVE)

Reply pipeline (runs in parallel): ReplyListener (random poll, 30 min – 2 h per post) ├── MastodonCommentFetcher / BlueskyCommentFetcher ├── Brain.evaluate_relevance() → skip or draft reply ├── Brain.generate_reply() └── TelegramNotifier (REPLY_APPROVE / REPLY_CANCEL) ├── MastodonPublisher.publish_reply() (on APPROVE) └── BlueskyPublisher.publish_reply() (on APPROVE)

Requirements

Python 3.10+

LMStudio running locally (OpenAI-compatible API)

SwarmUI running locally with the 41ex4_p4v10v4 LoRA loaded

A Telegram bot token + chat ID (for the approval workflow)

Mastodon and/or Bluesky credentials (for publishing)

An OpenWeatherMap API key (free tier, for ambient context in prompts)

Setup

pip install -e ".[dev]" cp .env.example .env

Fill in .env with your tokens and service URLs

.env reference

Variable Description

LMSTUDIO_BASE_URL LMStudio API base, e.g. http://localhost:1234

SWARMUI_BASE_URL SwarmUI base, e.g. http://localhost:7801

TELEGRAM_BOT_TOKEN Bot token from @BotFather

TELEGRAM_CHAT_ID Your personal chat ID (use @userinfobot to find it)

ACTIVE_PROFILE Profile slug, default alexa

WEATHER_API_KEY OpenWeatherMap key (free)

ALEXA_MASTODON_ACCESS_TOKEN Mastodon token for the alexa profile

ALEXA_BLUESKY_APP_PASSWORD Bluesky app password for the alexa profile

MASTODON_ACCESS_TOKEN Global fallback Mastodon token (used if no prefixed var found)

MASTODON_INSTANCE_URL Fallback — prefer setting mastodon_instance_url in identity.yaml

BLUESKY_HANDLE Fallback — prefer setting bluesky_handle in identity.yaml

BLUESKY_APP_PASSWORD Global fallback Bluesky app password

Daily workflow

main_batch.py generates 8 posts + images and sends each to Telegram for approval (run via cron).

You APPROVE / REGEN / CANCEL on your phone.

main_dispatcher.py publishes approved posts at their scheduled time (persistent loop).

main_reply_listener.py polls published posts, drafts replies to incoming comments, and sends them back through Telegram for approval (persistent loop).

The three persistent loops (dispatcher, telegram_listener, reply_listener) belong under systemd or supervisord.

Preview without writing anything

python main_batch.py --dry-run

Fetches real stories, generates text and images via local services, prints everything to stdout. No DB writes, no Telegram, no side effects.

python main_dispatcher.py --dry-run

Logs what would be published for each approved post without making any social API calls.

Entry points

Script Purpose

main_batch.py Run once daily — fetches stories, generates posts, saves to memory + queue, notifies Telegram

main_dispatcher.py Persistent loop — publishes APPROVED posts at their scheduled UTC time

main_telegram_listener.py Persistent loop — handles APPROVE / REGEN / CANCEL / REPLY_APPROVE / REPLY_CANCEL callbacks

main_reply_listener.py Persistent loop — polls published posts for new comments, drafts replies, sends for Telegram approval

Slot distribution

Each daily batch generates 8 posts by default (profiles/alexa/identity.yaml):

Category Count Sources

TECH 5 Hacker News, Lobste.rs, BearBlog

PERSONAL 2 Ask HN discussion threads

RAW 1 Internally generated (no source story)

Adding a new profile

mkdir -p profiles/marco/prompts profiles/marco/generated cp profiles/alexa/identity.yaml profiles/marco/ cp profiles/alexa/prompts/*.j2 profiles/marco/prompts/

Edit profiles/marco/identity.yaml and the .j2 templates

Run with the new profile

ACTIVE_PROFILE=marco python main_batch.py --dry-run

The directory name (slug) must match ACTIVE_PROFILE. It is separate from the name field in identity.yaml ("alexa" vs "AlexaPavlova").

Project structure

config/ schemas.py # RawStory, Post, ProfileConfig (Pydantic v2) settings.py # Pydantic-settings from .env core/ brain.py # LMStudio calls, text cleaning, truncation curator.py # Dedup by URL + title + subreddit; banned-topic filter factory.py # Orchestrates adapters → curator → brain → image scheduler.py # UTC-aware time windows with random jitter profile_loader.py # Loads profiles/{slug}/identity.yaml adapters/ hn_adapter.py # Hacker News top stories (TECH) lobsters_adapter.py # Lobste.rs hottest (TECH) bearblog_adapter.py # BearBlog Discover RSS (TECH) ask_hn_adapter.py # Ask HN discussion posts via Algolia (PERSONAL) reddit_adapter.py # Reddit (requires OAuth2 credentials; not used by default) services/ memory_service.py # Per-profile SQLite post history + platform IDs queue_service.py # Approval queue (SQLite) image_gen.py # SwarmUI REST client notification.py # Telegram sendPhoto/sendMessage, approval keyboards, long-poll comment_service.py # comments.sqlite: comments, pending_replies, poll_state social/ mastodon_publisher.py # Mastodon REST — publish + publish_reply bluesky_publisher.py # AT Protocol XRPC — publish + publish_reply mastodon_comment_fetcher.py # Fetches replies via /api/v1/statuses/{id}/context bluesky_comment_fetcher.py # Fetches replies via app.bsky.feed.getPostThread profiles/ alexa/ identity.yaml # Persona config: slots, sources, banned topics, image model prompts/*.j2 # Jinja2 templates: system prompt + per-mood + per-platform generated/ # Output images (slot_NNN.png) memory.sqlite # Post history injected as context into each prompt queue.sqlite # Approval queue comments.sqlite # Comments, pending replies, poll state

Tests

pytest tests/ -v # 312 tests, all mocked — no live services required

All HTTP calls (LMStudio, SwarmUI, Telegram, HN, Algolia, etc.) are mocked with respx.

About

A local-first AI persona for Mastodon and Bluesky — drafts by machine, approved by a human, nothing it can't ground gets published.

dev.to/msalsas/building-an-ai-persona-that-doesnt-lie-the-parts-nobody-bothers-to-build-33ii

Topics

python

ai

mastodon

human-in-the-loop

bluesky

stable-diffusion

llm-local-first

Resources

Readme

License

MIT license

Uh oh!

There was an error while loading. Please reload this page.

Activity

Stars

0 stars

Watchers

0 watching

Forks

0 forks

Report repository

Contributors

Uh oh!

There was an error while loading. Please reload this page.

Languages

Python 97.2%

Jinja 2.8%