Emanation
Haskell · written by handFebruary 2024 – May 2025No longer running
A system for running a network of news websites written by language models. From a few lines of configuration it could register a domain, have a model fill in the site's name, description and look, invent its writers, collect news on the site's topics, write articles from it and publish the result. A sentinel then kept every site in line with its configuration.
25,428 lines of Haskell81 internal libraries16 programs1,316 commits
From configuration to website
Everything ran through one program, ema, against one Postgres database shared by every site. The quickest path was ema site auto: given a domain it bought the domain, set its DNS, created the site, scraped its first sources, made at least one author, wrote an article, rendered the site with Hugo and deployed it. Anything left out of the configuration, such as the title or description, was filled in by the model.
Sources came from Google News and Bing News searches on each site's keywords. The deployment, described with Helm, ran the sites alongside Prometheus, Grafana and Loki for monitoring, a Selenium grid for services without an API, and nightly database backups.
The configuration describes 20 sites. Their topics, as written in it:
- technology news
- linux / nix / nixos / nixpkgs / nix flakes / guix
- llm artificial intelligence
- handheld gaming steam deck
- bitcoin crypto
- technology discounts
- money saving tips
- mortgage news uk
- premier league
- movie news review
- new whisky releases
- tech gadget review
- lawn ornaments news
- gardening pickling
- basketball
- nfl
- video game drama / scandal
- python programming
- headphones review
- snooker blunders
Records the model fills in
The idea Emanation used most is a record whose fields can each be given or left for the model. Record types are declared once and parameterised over how each field is held, so the same type serves as a complete value, a partial one and a request to the model. For each field the code either sets a known value or marks it automatic with a short instruction; Template Haskell derives the JSON the model sees and the parser for its answer, which comes back as an ordinary, fully typed record.
Automatic values can carry a seed. The answer is stored under the seed, a label and the domain, so the same seed gives back the same answer on every run, and changing the seed asks the model again. The same machinery wrote sites, authors (with a prompt for their headshot) and product summaries.
src/ema-lib-ai-auto/Emanation/AI/Auto/Partials.hsdeclareBareB
[d|
data PartialSite = PartialSite
{ _partialSiteName :: Text,
_partialSiteKeywords :: Text,
_partialSiteDescription :: Text
}
deriving (Generic)
|]
deriveAutoB ''PartialSite
partialSite :: SiteT Maybe -> Au PartialSite
partialSite siteM =
let autoM lns prompt = case siteM ^. lns of
Nothing -> auto_ & guided_ prompt
Just a -> set_ a
in PartialSite
{ _partialSiteName = autoM siteName "The name of the website. This should be short and catchy, and will be used as the publication name and in the site logo.",
_partialSiteKeywords = autoM siteKeywords "A short list of space-separated keywords relevant to the site. These will be used as keywords when searching for news articles to write about. Do not include too many, or the searches will be too specific and return no results. Three or four is a maximum.",
_partialSiteDescription = autoM siteDescription "A few sentences of description for the site. This should be punchy and catchy. It will appear on the homepage and in search results for the site"
}A reconcile loop for websites
The sentinel compared what the configuration asked for with what the database held, built a plan from the difference and carried it out, in the manner of a Kubernetes controller. A plan is a list of actions, and plans combine by concatenation:
src/ema-lib-sentinel/Emanation/Sentinel/Plan.hsdata Action
= CreateSite Domain (AutoConfig "_domainConfigSite" (SiteTF Covered))
| RemoveSite Domain
| SetSite Site
| SetSiteAuto Domain (AutoConfig "_domainConfigSite" (SiteTF Covered))
| AddAuthors Domain Int
| SetDnsRecords Domain [SetDnsHostsRequestRecord]
deriving (Show)Conversations as trees
The chat program treated a conversation as a tree rather than a list: every message is an instant in a branching history, and you can step back and take a different future from any point. The history type is a small library of its own.
Inside the chat ran a tiny language for scripting it, about 2,700 lines of Haskell, in which functions match on quoted values and even if and case are defined in the language itself. Its values include conversation nodes and branches, and the chat could speak its replies aloud.
src/lib-data-multiverse/Data/Multiverse.hstype Branch a = Tree (Instant a)
data Instant a = Singularity | Instant MultiCoord acotlib/lib.cot 'not ('nil: 'true)
'! not
'bool (. not not)
'? bool
'cond (c: a: ('true: a) (?c))
'match (pat: `pat: 'true)
'matchf ((pat: _): a: match (quote pat) (quote a))
'if (c: then: else: (cond c (const then)) else)What it published
Articles were written from one or more scraped sources in a style chosen per site and per article. The styles ran from news-like, academic, essay, press release and weekly roundup to opinion, satire, irony, contrarian and positive product review. Several of them told the model to invent opinions, quotes, anecdotes or evidence, and not to say that it was doing so.
The writers were invented too: each had a name, a biography and a generated headshot, and the sites carried advertising. The project's notes ruled out science as a topic. Model and API calls were recorded with their cost in US dollars, and a separate program, ema-cost, reported spending.
The notes put the goal plainly: “delegate everything possible to the model”.
How it was built
Emanation was written by hand, almost all of it between February and October 2024, as 81 small Haskell libraries and 16 programs. It used hosted models from OpenAI and Groq and local ones through llama.cpp, Stability for images, Unsplash for stock photos, Namecheap for domains, Google for analytics and search registration, an advertising network, GitHub, Gradio-hosted models and X.
It also built on the author's own Haskell libraries, including quaalude, a custom prelude, and a client for owt. It is no longer running, and its sites no longer resolve.