{"slug": "show-hn-amanuensis-a-local-first-ai-persona-that-won-t-fabricate-facts", "title": "Show HN: Amanuensis – a local-first AI persona that won't fabricate facts", "summary": "A developer released Amanuensis, a local-first AI persona system that runs on a personal GPU machine without cloud LLM calls, designed to prevent fabricated information by requiring human approval before any post is published. The pilot persona, AlexaPavlova, posted as a sarcastic senior Berlin developer on Mastodon and Bluesky, with every post reviewed via Telegram before publication. The project is now an archived experiment, with its MIT-licensed code available for others to fork and run their own AI personas.", "body_md": "*A local-first AI persona that writes under a human's veto. It drafts, you approve, and nothing it can't ground gets published.*\n\nA 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.\n\nEverything runs on a local GPU machine. No cloud LLM calls.\n\n**See it live:** [Mastodon](https://hachyderm.io/@alexa_pavlova) · [Bluesky](https://bsky.app/profile/alexapavlova.bsky.social) — now disclosed as AI, no longer posting.\n\n*Every post is reviewed on a phone before it publishes — approve, regenerate text or image, or cancel.*\n\nStatus: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.\n\nThe 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.\n\nFull write-up of the design and what broke along the way: [write-up](https://dev.to/msalsas/building-an-ai-persona-that-doesnt-lie-the-parts-nobody-bothers-to-build-33ii).\n\n```\npip install -e \".[dev]\"\ncp .env.example .env\n```\n\n**LMStudio**\n\n- Download\n[LMStudio](https://lmstudio.ai/)and load any instruction-tuned model (tested with Mistral-7B-Instruct and similar) - Go to\n**Local Server**→ start the server on port`1234`\n\n- Set\n`LMSTUDIO_BASE_URL=http://localhost:1234`\n\nin`.env`\n\n**SwarmUI**\n\n- Install\n[SwarmUI](https://github.com/mcmonkeyprojects/SwarmUI)and load the image model +`41ex4_p4v10v4`\n\nLoRA - Set\n`SWARMUI_BASE_URL=http://localhost:7801`\n\nin`.env`\n\n- Message\n[@BotFather](https://t.me/botfather)→`/newbot`\n\n→ copy the token into`TELEGRAM_BOT_TOKEN`\n\n- Message\n[@userinfobot](https://t.me/userinfobot)→ copy your numeric ID into`TELEGRAM_CHAT_ID`\n\n- Send any message to your new bot so it can message you back\n\n```\npython main_batch.py --dry-run\n```\n\nThis 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.\n\nAdd your social credentials to `.env`\n\n(Mastodon and/or Bluesky — both optional, see table below), then open **three terminals**:\n\n```\n# Terminal 1 — generate today's posts and send to Telegram for approval\npython main_batch.py\n\n# Terminal 2 — listen for your Telegram approvals\npython main_telegram_listener.py\n\n# Terminal 3 — publish approved posts at their scheduled time\npython main_dispatcher.py\n```\n\nApprove posts in Telegram. The dispatcher picks them up and publishes. Done.\n\nFor long-running setups, run the three persistent processes under\n\n`systemd`\n\nor`supervisord`\n\nso they survive reboots.`main_batch.py`\n\nis a one-shot script — run it via cron or manually each day.\n\nThe 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.\n\n**LoRA:** download from Hugging Face —[msalsas/alexa-lora](https://huggingface.co/msalsas/alexa-lora). Trigger word`41ex4_p4v10v4`\n\n, weight`0.3`\n\n, generated at 768×1024.**Base model:**[Juggernaut XL \"Ragnarok\" by RunDiffusion](https://civitai.com/models/133005?modelVersionId=1759168)— get it separately, not distributed here.- The LoRA was trained on a\n**fully synthetic dataset**(images generated with Juggernaut XL); the character is not based on any real person.\n\nTo 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.\n\n```\nAdapters (HN, Lobsters, BearBlog, AskHN)\n    └── Curator (dedup by URL + title + subreddit, banned-topic filter)\n        └── BatchFactory\n            ├── Brain (LMStudio → post text + image prompt)\n            └── ImageService (SwarmUI → PNG via LoRA)\n                └── Scheduler (UTC time windows with jitter)\n                    └── QueueService (SQLite)\n                        └── TelegramNotifier (photo + approval keyboard)\n                            ├── MastodonPublisher (on APPROVE)\n                            └── BlueskyPublisher  (on APPROVE)\n\nReply pipeline (runs in parallel):\n    ReplyListener (random poll, 30 min – 2 h per post)\n        ├── MastodonCommentFetcher / BlueskyCommentFetcher\n        ├── Brain.evaluate_relevance() → skip or draft reply\n        ├── Brain.generate_reply()\n        └── TelegramNotifier (REPLY_APPROVE / REPLY_CANCEL)\n            ├── MastodonPublisher.publish_reply() (on APPROVE)\n            └── BlueskyPublisher.publish_reply()  (on APPROVE)\n```\n\n- Python 3.10+\n[LMStudio](https://lmstudio.ai/)running locally (OpenAI-compatible API)[SwarmUI](https://github.com/mcmonkeyprojects/SwarmUI)running locally with the`41ex4_p4v10v4`\n\nLoRA loaded- A Telegram bot token + chat ID (for the approval workflow)\n- Mastodon and/or Bluesky credentials (for publishing)\n- An\n[OpenWeatherMap](https://openweathermap.org/api)API key (free tier, for ambient context in prompts)\n\n```\npip install -e \".[dev]\"\ncp .env.example .env\n# Fill in .env with your tokens and service URLs\n```\n\n| Variable | Description |\n|---|---|\n`LMSTUDIO_BASE_URL` |\nLMStudio API base, e.g. `http://localhost:1234` |\n`SWARMUI_BASE_URL` |\nSwarmUI base, e.g. `http://localhost:7801` |\n`TELEGRAM_BOT_TOKEN` |\nBot token from @BotFather |\n`TELEGRAM_CHAT_ID` |\nYour personal chat ID (use @userinfobot to find it) |\n`ACTIVE_PROFILE` |\nProfile slug, default `alexa` |\n`WEATHER_API_KEY` |\nOpenWeatherMap key (free) |\n`ALEXA_MASTODON_ACCESS_TOKEN` |\nMastodon token for the alexa profile |\n`ALEXA_BLUESKY_APP_PASSWORD` |\nBluesky app password for the alexa profile |\n`MASTODON_ACCESS_TOKEN` |\nGlobal fallback Mastodon token (used if no prefixed var found) |\n`MASTODON_INSTANCE_URL` |\nFallback — prefer setting `mastodon_instance_url` in `identity.yaml` |\n`BLUESKY_HANDLE` |\nFallback — prefer setting `bluesky_handle` in `identity.yaml` |\n`BLUESKY_APP_PASSWORD` |\nGlobal fallback Bluesky app password |\n\n`main_batch.py`\n\ngenerates 8 posts + images and sends each to Telegram for approval (run via cron).- You APPROVE / REGEN / CANCEL on your phone.\n`main_dispatcher.py`\n\npublishes approved posts at their scheduled time (persistent loop).`main_reply_listener.py`\n\npolls published posts, drafts replies to incoming comments, and sends them back through Telegram for approval (persistent loop).\n\nThe three persistent loops (`dispatcher`\n\n, `telegram_listener`\n\n, `reply_listener`\n\n) belong under systemd or supervisord.\n\n```\npython main_batch.py --dry-run\n```\n\nFetches real stories, generates text and images via local services, prints everything to stdout. No DB writes, no Telegram, no side effects.\n\n```\npython main_dispatcher.py --dry-run\n```\n\nLogs what would be published for each approved post without making any social API calls.\n\n| Script | Purpose |\n|---|---|\n`main_batch.py` |\nRun once daily — fetches stories, generates posts, saves to memory + queue, notifies Telegram |\n`main_dispatcher.py` |\nPersistent loop — publishes APPROVED posts at their scheduled UTC time |\n`main_telegram_listener.py` |\nPersistent loop — handles APPROVE / REGEN / CANCEL / REPLY_APPROVE / REPLY_CANCEL callbacks |\n`main_reply_listener.py` |\nPersistent loop — polls published posts for new comments, drafts replies, sends for Telegram approval |\n\nEach daily batch generates **8 posts** by default (`profiles/alexa/identity.yaml`\n\n):\n\n| Category | Count | Sources |\n|---|---|---|\n| TECH | 5 | Hacker News, Lobste.rs, BearBlog |\n| PERSONAL | 2 | Ask HN discussion threads |\n| RAW | 1 | Internally generated (no source story) |\n\n```\nmkdir -p profiles/marco/prompts profiles/marco/generated\ncp profiles/alexa/identity.yaml profiles/marco/\ncp profiles/alexa/prompts/*.j2 profiles/marco/prompts/\n# Edit profiles/marco/identity.yaml and the .j2 templates\n\n# Run with the new profile\nACTIVE_PROFILE=marco python main_batch.py --dry-run\n```\n\nThe directory name (slug) must match `ACTIVE_PROFILE`\n\n. It is separate from the `name`\n\nfield in `identity.yaml`\n\n(`\"alexa\"`\n\nvs `\"AlexaPavlova\"`\n\n).\n\n```\nconfig/\n  schemas.py              # RawStory, Post, ProfileConfig (Pydantic v2)\n  settings.py             # Pydantic-settings from .env\ncore/\n  brain.py                # LMStudio calls, text cleaning, truncation\n  curator.py              # Dedup by URL + title + subreddit; banned-topic filter\n  factory.py              # Orchestrates adapters → curator → brain → image\n  scheduler.py            # UTC-aware time windows with random jitter\n  profile_loader.py       # Loads profiles/{slug}/identity.yaml\nadapters/\n  hn_adapter.py           # Hacker News top stories (TECH)\n  lobsters_adapter.py     # Lobste.rs hottest (TECH)\n  bearblog_adapter.py     # BearBlog Discover RSS (TECH)\n  ask_hn_adapter.py       # Ask HN discussion posts via Algolia (PERSONAL)\n  reddit_adapter.py       # Reddit (requires OAuth2 credentials; not used by default)\nservices/\n  memory_service.py       # Per-profile SQLite post history + platform IDs\n  queue_service.py        # Approval queue (SQLite)\n  image_gen.py            # SwarmUI REST client\n  notification.py         # Telegram sendPhoto/sendMessage, approval keyboards, long-poll\n  comment_service.py      # comments.sqlite: comments, pending_replies, poll_state\nsocial/\n  mastodon_publisher.py         # Mastodon REST — publish + publish_reply\n  bluesky_publisher.py          # AT Protocol XRPC — publish + publish_reply\n  mastodon_comment_fetcher.py   # Fetches replies via /api/v1/statuses/{id}/context\n  bluesky_comment_fetcher.py    # Fetches replies via app.bsky.feed.getPostThread\nprofiles/\n  alexa/\n    identity.yaml         # Persona config: slots, sources, banned topics, image model\n    prompts/*.j2          # Jinja2 templates: system prompt + per-mood + per-platform\n    generated/            # Output images (slot_NNN.png)\n    memory.sqlite         # Post history injected as context into each prompt\n    queue.sqlite          # Approval queue\n    comments.sqlite       # Comments, pending replies, poll state\npytest tests/ -v      # 312 tests, all mocked — no live services required\n```\n\nAll HTTP calls (LMStudio, SwarmUI, Telegram, HN, Algolia, etc.) are mocked with `respx`\n\n.", "url": "https://wpnews.pro/news/show-hn-amanuensis-a-local-first-ai-persona-that-won-t-fabricate-facts", "canonical_source": "https://github.com/msalsas/amanuensis", "published_at": "2026-06-05 16:34:28+00:00", "updated_at": "2026-06-05 16:51:17.280065+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-tools", "ai-ethics", "ai-agents"], "entities": ["Amanuensis", "AlexaPavlova", "Mastodon", "Bluesky", "LMStudio", "MIT", "Telegram"], "alternates": {"html": "https://wpnews.pro/news/show-hn-amanuensis-a-local-first-ai-persona-that-won-t-fabricate-facts", "markdown": "https://wpnews.pro/news/show-hn-amanuensis-a-local-first-ai-persona-that-won-t-fabricate-facts.md", "text": "https://wpnews.pro/news/show-hn-amanuensis-a-local-first-ai-persona-that-won-t-fabricate-facts.txt", "jsonld": "https://wpnews.pro/news/show-hn-amanuensis-a-local-first-ai-persona-that-won-t-fabricate-facts.jsonld"}}