Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ wheels/
.envrc
CLAUDE.md
AGENTS.md
kolkhoz.log
funes.log
*.log
*.db
*.db-journal
Expand Down
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Pravda is an in-process async library. Kolkhoz owns and runs the
# Pravda is an in-process async library. Funes owns and runs the
# infrastructure it connects to: a remote headed Chrome (Playwright
# run-server), a Postgres database shared by Pravda and Kolkhoz, and an fsspec
# run-server), a Postgres database shared by Pravda and Funes, and an fsspec
# artifact store. Both applications use this asyncpg URL through SQLAlchemy.
# Bring them up with: docker compose up -d
PRAVDA_DATABASE_URL=postgresql+asyncpg://pravda:pravda@localhost:5432/pravda
Expand Down
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Kolkhoz
# Funes

Kolkhoz is an orchestrator that turns raw web pages into structured data about political position holders. It is built on top of Pravda, the evidence layer that captures and stores durable snapshots of web pages.
Funes is an orchestrator that turns raw web pages into structured data about political position holders. It is built on top of Pravda, the evidence layer that captures and stores durable snapshots of web pages.

## Project philosophy

Expand All @@ -9,18 +9,18 @@ Kolkhoz is an orchestrator that turns raw web pages into structured data about p
## Stack

- **Python** 3.13+ managed by **uv**.
- **Pravda** ([github.com/opensanctions/pravda](https://github.com/opensanctions/pravda)), published on PyPI as `opensanctions-pravda` (imported as `pravda`), for web page capture and storage, embedded as an in-process async library. Kolkhoz owns the infrastructure Pravda connects to — a headed Chrome browser (remote Playwright server), an async Postgres database, and an fsspec artifact store — run via `docker compose`. Connection settings are `PRAVDA_DATABASE_URL`, `PRAVDA_BROWSER_WS_URL`, and `PRAVDA_STORAGE_BASE_PATH` (see `.env`). Kolkhoz constructs Pravda's `PravdaConfig` at the CLI boundary, reads artifacts from the shared storage backend over fsspec, and applies Pravda's packaged migrations (`pravda.migrate`) idempotently before the `run` command opens a `Pravda` instance.
- Kolkhoz does not persist extraction results. Each `run` writes only that run's records as JSONL to `OUTPUT_BASE_PATH`; PostgreSQL is used only by Pravda.
- **Pravda** ([github.com/opensanctions/pravda](https://github.com/opensanctions/pravda)), published on PyPI as `opensanctions-pravda` (imported as `pravda`), for web page capture and storage, embedded as an in-process async library. Funes owns the infrastructure Pravda connects to — a headed Chrome browser (remote Playwright server), an async Postgres database, and an fsspec artifact store — run via `docker compose`. Connection settings are `PRAVDA_DATABASE_URL`, `PRAVDA_BROWSER_WS_URL`, and `PRAVDA_STORAGE_BASE_PATH` (see `.env`). Funes constructs Pravda's `PravdaConfig` at the CLI boundary, reads artifacts from the shared storage backend over fsspec, and applies Pravda's packaged migrations (`pravda.migrate`) idempotently before the `run` command opens a `Pravda` instance.
- Funes does not persist extraction results. Each `run` writes only that run's records as JSONL to `OUTPUT_BASE_PATH`; PostgreSQL is used only by Pravda.

## Project structure

```
kolkhoz/ # the package: cli.py (run), capture.py
funes/ # the package: cli.py (run), capture.py
# (Pravda client + capture/artifact helpers), extract.py,
# sources.py, export.py, config.py
evaluate.py # score the extraction pipeline against synthetic fixtures
fixtures/ # one directory per fixture: page.html, expected.json, optional screenshot.png
docker-compose.yml # Kolkhoz-owned browser (Playwright server) + Postgres
docker-compose.yml # Funes-owned browser (Playwright server) + Postgres
```

`input/` (gitignored) holds the input CSVs (one dataset per file). `output/`
Expand Down
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM python:3.13-slim

LABEL org.opencontainers.image.title="Kolkhoz"
LABEL org.opencontainers.image.source="https://github.com/opensanctions/kolkhoz"
LABEL org.opencontainers.image.title="Funes"
LABEL org.opencontainers.image.source="https://github.com/opensanctions/funes"

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
Expand All @@ -24,7 +24,7 @@ WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project --no-dev

COPY kolkhoz ./kolkhoz
COPY funes ./funes
COPY evaluate.py README.md ./
RUN uv sync --frozen --no-dev

Expand All @@ -39,5 +39,5 @@ USER app
# CronJob spec).
ENV INPUT_BASE_PATH=/app/datasets

ENTRYPOINT ["kolkhoz"]
ENTRYPOINT ["funes"]
CMD ["run"]
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# Kolkhoz
# Funes

Kolkhoz turns the internet into lists of politicians. It orchestrates web capture (via the in-process [Pravda](https://github.com/opensanctions/pravda) async library), LLM extraction, and JSONL output to pull political position holders out of web pages.
Funes turns the internet into lists of politicians. It orchestrates web capture (via the in-process [Pravda](https://github.com/opensanctions/pravda) async library), LLM extraction, and JSONL output to pull political position holders out of web pages.

## Status

Early R&D. Currently exploring what a viable automated extraction pipeline looks like.

## What it does

1. Captures snapshots (plaintext + rendered HTML + screenshot) via the in-process Pravda library against a remote browser, Postgres, and an artifact store that Kolkhoz owns and runs
1. Captures snapshots (plaintext + rendered HTML + screenshot) via the in-process Pravda library against a remote browser, Postgres, and an artifact store that Funes owns and runs
2. Feeds snapshots to an LLM to extract structured "human / position" pairs
3. Writes that run's extracted holders as JSONL (one record per person/position observation), shaped for ingest by zavod

Kolkhoz does not persist extraction results in PostgreSQL. Pravda still stores the captured evidence and snapshot metadata there.
Funes does not persist extraction results in PostgreSQL. Pravda still stores the captured evidence and snapshot metadata there.

## Setup

Requires uv. Kolkhoz embeds Pravda as an async library and
Requires uv. Funes embeds Pravda as an async library and
owns the infrastructure Pravda connects to: a headed Chrome browser, a
Postgres database, and an artifact store. Pravda ships on PyPI as
`opensanctions-pravda` (imported as `pravda`); `uv sync` installs it.
Expand All @@ -34,7 +34,7 @@ The `run` command applies Pravda's packaged Alembic migrations idempotently befo

## Usage

All commands run through the `kolkhoz` console script (installed by `uv sync`). Input and output
All commands run through the `funes` console script (installed by `uv sync`). Input and output
locations are fsspec URLs set via `INPUT_BASE_PATH` and `OUTPUT_BASE_PATH`
in `.env` (a local dir or a `gs://`/`s3://` bucket prefix):

Expand All @@ -43,10 +43,10 @@ in `.env` (a local dir or a `gs://`/`s3://` bucket prefix):
# through Pravda, then extract position holders from each snapshot and write
# this run's JSONL to <OUTPUT_BASE_PATH>/<dataset>/<date>.jsonl. Each CSV is
# its own dataset, named after the file's stem.
uv run kolkhoz run
uv run kolkhoz run -d hio_leadership # one dataset only
uv run kolkhoz run -n 20 # random sample of 20 page inputs
uv run kolkhoz run -c 10 # up to 10 concurrent captures
uv run funes run
uv run funes run -d hio_leadership # one dataset only
uv run funes run -n 20 # random sample of 20 page inputs
uv run funes run -c 10 # up to 10 concurrent captures
```

## Evaluation
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Kolkhoz-owned infrastructure for Pravda's in-process async client.
# Funes-owned infrastructure for Pravda's in-process async client.
#
# Pravda connects from Kolkhoz's process to three things Kolkhoz owns and
# Pravda connects from Funes's process to three things Funes owns and
# runs here: a remote headed Chrome (Playwright run-server), Pravda's Postgres
# database, and the artifact store (a local path on disk by default,
# configured via PRAVDA_STORAGE_BASE_PATH).
Expand Down
4 changes: 2 additions & 2 deletions evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
from bs4 import BeautifulSoup
from openai import OpenAI

from kolkhoz.config import load_config
from kolkhoz.extract import extract, flatten_persons, metadata_from_html
from funes.config import load_config
from funes.extract import extract, flatten_persons, metadata_from_html

log = logging.getLogger("evaluate")

Expand Down
2 changes: 1 addition & 1 deletion kolkhoz/__init__.py → funes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Kolkhoz: turn web pages into structured data about political position holders.
"""Funes: turn web pages into structured data about political position holders.

Orchestrates web capture (via Pravda), LLM extraction, and PostgreSQL storage.
"""
12 changes: 6 additions & 6 deletions kolkhoz/capture.py → funes/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
- ``capture_urls`` captures each URL once, concurrently, through one
long-lived Pravda instance (bounded by a semaphore).
- ``read_artifact`` reads a snapshot artifact blob from the shared fsspec
storage backend Kolkhoz and Pravda both use.
storage backend Funes and Pravda both use.
- ``is_blank`` / ``split_image`` are image primitives over a screenshot blob
(shared by capture and the extraction tiling path).

Pravda is an in-process async library. Kolkhoz constructs one ``Pravda``
Pravda is an in-process async library. Funes constructs one ``Pravda``
instance from its environment-backed settings and reuses it across captures;
it never speaks HTTP to Pravda.
"""
Expand All @@ -24,16 +24,16 @@
from PIL import Image
from pravda import Pravda, PravdaConfig, Snapshot

from kolkhoz.config import PravdaSettings
from funes.config import PravdaSettings

log = logging.getLogger("kolkhoz")
log = logging.getLogger("funes")


def pravda_client(settings: PravdaSettings) -> Pravda:
"""Construct a Pravda instance from Kolkhoz's environment-backed settings.
"""Construct a Pravda instance from Funes's environment-backed settings.

The ``PravdaConfig`` is built here, at the application boundary, from the
explicit settings Kolkhoz owns; Kolkhoz holds no Pravda URL of its own.
explicit settings Funes owns; Funes holds no Pravda URL of its own.
"""
config = PravdaConfig(
database_url=settings.database_url,
Expand Down
16 changes: 8 additions & 8 deletions kolkhoz/cli.py → funes/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from openai import OpenAI
from pravda import Snapshot, migrate

from kolkhoz.capture import (
from funes.capture import (
OperationalError,
capture_urls,
is_blank,
Expand All @@ -17,24 +17,24 @@
storage_filesystem,
summarise_errors,
)
from kolkhoz.config import Config, load_config
from kolkhoz.export import holder_to_record, write_outputs
from kolkhoz.extract import (
from funes.config import Config, load_config
from funes.export import holder_to_record, write_outputs
from funes.extract import (
extract,
flatten_persons,
metadata_from_html,
screenshot_reason,
)
from kolkhoz.sources import InputRow, load_inputs
from funes.sources import InputRow, load_inputs

log = logging.getLogger("kolkhoz")
log = logging.getLogger("funes")


@click.group()
def cli() -> None:
root = logging.getLogger()
root.setLevel(logging.INFO)
root.addHandler(logging.FileHandler("kolkhoz.log"))
root.addHandler(logging.FileHandler("funes.log"))
root.addHandler(logging.StreamHandler())


Expand Down Expand Up @@ -99,7 +99,7 @@ async def _run_pipeline(
A URL repeated within a dataset collapses to one page association. The
same URL in several datasets remains several associations, backed by one
Pravda capture. Sampling happens over associations before capture.
Kolkhoz keeps no database state of its own; Pravda alone persists the
Funes keeps no database state of its own; Pravda alone persists the
captured evidence.
"""
associations: list[tuple[str, str, str]] = []
Expand Down
2 changes: 1 addition & 1 deletion kolkhoz/config.py → funes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class PravdaSettings:
"""Environment-backed settings for Pravda's in-process async client.

These three values are handed to Pravda's own ``PravdaConfig`` at the
application boundary. The database URL is also used by Kolkhoz.
application boundary. The database URL is also used by Funes.
"""

database_url: str
Expand Down
4 changes: 2 additions & 2 deletions kolkhoz/export.py → funes/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
from pravda import Snapshot

from kolkhoz.config import PathsConfig
from funes.config import PathsConfig

log = logging.getLogger("kolkhoz")
log = logging.getLogger("funes")

# The flat JSONL schema handed to zavod. One record per (person, position)
# observation extracted from one snapshot. Fixed key order, UTF-8, source
Expand Down
8 changes: 4 additions & 4 deletions kolkhoz/extract.py → funes/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
text/screenshot decision (``screenshot_reason``), and the ``extract()`` call.
``screenshot_parts``
tiles a screenshot blob into model input parts using ``split_image`` from
:mod:`kolkhoz.capture`.
:mod:`funes.capture`.

The OpenAI client is an explicit argument: it is constructed at the CLI
boundary, not held as module-global state.
Expand All @@ -18,10 +18,10 @@
from openai import OpenAI
from pydantic import BaseModel, Field

from kolkhoz.capture import split_image
from kolkhoz.config import ImageConfig, ModelConfig
from funes.capture import split_image
from funes.config import ImageConfig, ModelConfig

log = logging.getLogger("kolkhoz")
log = logging.getLogger("funes")

REASONING_EFFORT = "low"

Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[project]
name = "kolkhoz"
name = "funes"
version = "0.1.0"
description = "Orchestrate web capture and LLM extraction of political position holders into structured data."
readme = "README.md"
Expand All @@ -21,7 +21,7 @@ dev = [
]

[project.scripts]
kolkhoz = "kolkhoz.cli:cli"
funes = "funes.cli:cli"

[build-system]
requires = ["hatchling"]
Expand Down
74 changes: 37 additions & 37 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading