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
4 changes: 2 additions & 2 deletions .github/workflows/build-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ on:
- 'properdocs.yml'
- 'pyproject.toml'
- 'uv.lock'
- 'lpgcast/**'
- 'schemacast/**'
# A pull request builds the site strictly but does not deploy it.
pull_request:
paths:
- 'docs/**'
- 'properdocs.yml'
- 'pyproject.toml'
- 'uv.lock'
- 'lpgcast/**'
- 'schemacast/**'
workflow_dispatch:

permissions:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ permissions:

jobs:
pypi-publish:
# Off until the `lpgcast` name is reserved on PyPI: set the repository
# Off until the `schemacast` name is reserved on PyPI: set the repository
# variable PYPI_PUBLISH to `true` to turn publishing on.
if: vars.PYPI_PUBLISH == 'true'
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/lpgcast
url: https://pypi.org/p/schemacast
permissions:
id-token: write

Expand Down
50 changes: 45 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
pull_request:
push:
branches: [main]
schedule:
# Daily: catches a new graflo release that breaks this package.
- cron: '35 5 * * *'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down Expand Up @@ -35,17 +39,17 @@ jobs:
run: |
/tmp/base-install/bin/python -c "
import sys
from lpgcast import LPGCaster, load_schema, read_episodes, write_cast
from schemacast import SchemaCaster, load_schema, read_episodes, write_cast
forbidden = ['langchain', 'langgraph', 'torch', 'numpy', 'pandas', 'rdflib']
leaked = [name for name in forbidden if name in sys.modules]
assert not leaked, f'imported on a plain install: {leaked}'
print('import OK')
"
- name: Console scripts resolve
run: |
/tmp/base-install/bin/lpgcast --help
/tmp/base-install/bin/lpgcast-bench --help
/tmp/base-install/bin/lpgcast sheet --schema examples/tickets/schema.yaml > /dev/null
/tmp/base-install/bin/schemacast --help
/tmp/base-install/bin/schemacast-bench --help
/tmp/base-install/bin/schemacast sheet --schema examples/tickets/schema.yaml > /dev/null

unit:
runs-on: ubuntu-latest
Expand All @@ -67,10 +71,46 @@ jobs:
run: uv sync --no-sources --group dev
- name: Type check
if: matrix.python-version == '3.12'
run: uv run --no-sources ty check lpgcast test
run: uv run --no-sources ty check schemacast test
# No model is called: tests use a scripted endpoint, and the `llm` and
# `neo4j` markers are skipped without --run-llm / --run-neo4j.
- name: Run tests
run: uv run --no-sources pytest
env:
PYTHONUNBUFFERED: '1'

# Both ends of the declared graflo range: the floor it promises to support
# and the newest release it admits. Not on PRs.
graflo-range:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
graflo: [floor, latest]
env:
UV_PYTHON: '3.12'
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v10.2.0
with:
version: "0.12.21"
enable-cache: true
- name: Install dependencies
run: uv sync --no-sources --group dev
- name: Select graflo
run: |
range=$(sed -nE 's/^ *"(graflo>=[^"]+)",?$/\1/p' pyproject.toml)
floor=$(sed -nE 's/^ *"graflo>=([0-9.]+).*/\1/p' pyproject.toml)
test -n "$range" && test -n "$floor"
if [ "${{ matrix.graflo }}" = floor ]; then
uv pip install --python .venv "graflo==$floor"
else
uv pip install --python .venv --upgrade-package graflo "$range"
fi
uv run --no-sync python -c "import importlib.metadata as m; print('graflo', m.version('graflo'))"
- name: Run tests
run: uv run --no-sync pytest
env:
PYTHONUNBUFFERED: '1'
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ __marimo__/
# Streamlit
.streamlit/secrets.toml

# Response cache written by tests run without LPGCAST_CACHE_DIR
# Response cache written by tests run without SCHEMACAST_CACHE_DIR
.test_cache/

# Benchmark reports; results are published elsewhere, never committed
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ repos:
- id: check-added-large-files
args: [--maxkb=2000]
- id: end-of-file-fixer
exclude: (^|/)(\.env|data/)|^lpgcast/plot/fonts/|\.svg$
exclude: (^|/)(\.env|data/)|^schemacast/plot/fonts/|\.svg$
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
exclude: (^|/)(\.env|data/)|^lpgcast/plot/fonts/|\.svg$
exclude: (^|/)(\.env|data/)|^schemacast/plot/fonts/|\.svg$
- id: pretty-format-json
args: [--autofix, --indent, '4', --no-sort-keys]

Expand All @@ -28,7 +28,7 @@ repos:
# Type checking
- id: ty
name: ty check
entry: uv run --frozen ty check lpgcast test
entry: uv run --frozen ty check schemacast test
language: system
types: [python]
pass_filenames: false
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **Renamed from `lpgcast` to `schemacast`**: the distribution, the import package, the CLI
(`schemacast`, `schemacast-bench`), `SchemaCaster` and `SchemaCastSettings`, the
`SCHEMACAST_` environment prefixes and the default cache directory. The `text2lpg` alias is gone.
`serve` reports `schemacast_version`; groundings carry `extractor: schemacast/<version>`.

## [0.2.0] - 2026-10-13

### Added

- **`lpgcast-bench compare A.json B.json`**: run B read against run A episode by episode, with
Expand Down
46 changes: 23 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,48 @@
# LPGCast <img src="https://raw.githubusercontent.com/growgraph/LPGCast/refs/heads/main/docs/assets/logo.png" alt="LPGCast logo" height="32" width="32"/>
# SchemaCast <img src="https://raw.githubusercontent.com/growgraph/SchemaCast/refs/heads/main/docs/assets/logo.png" alt="SchemaCast logo" height="32" width="32"/>

**Short texts in, a labeled property graph out, typed by a GraFlo schema you govern.**

[![tests](https://img.shields.io/github/actions/workflow/status/growgraph/LPGCast/tests.yml?branch=main&label=tests)](https://github.com/growgraph/LPGCast/actions/workflows/tests.yml)
[![pre-commit](https://img.shields.io/github/actions/workflow/status/growgraph/LPGCast/pre-commit.yml?branch=main&label=pre-commit)](https://github.com/growgraph/LPGCast/actions/workflows/pre-commit.yml)
[![PyPI](https://img.shields.io/pypi/v/lpgcast?color=224777)](https://pypi.org/project/lpgcast/)
[![Python](https://img.shields.io/pypi/pyversions/lpgcast?color=224777)](https://pypi.org/project/lpgcast/)
[![Docs](https://img.shields.io/badge/docs-growgraph.github.io-224777)](https://growgraph.github.io/LPGCast/)
[![License](https://img.shields.io/badge/license-Apache--2.0-224777)](https://github.com/growgraph/LPGCast/blob/main/LICENSE)
[![tests](https://img.shields.io/github/actions/workflow/status/growgraph/SchemaCast/tests.yml?branch=main&label=tests)](https://github.com/growgraph/SchemaCast/actions/workflows/tests.yml)
[![pre-commit](https://img.shields.io/github/actions/workflow/status/growgraph/SchemaCast/pre-commit.yml?branch=main&label=pre-commit)](https://github.com/growgraph/SchemaCast/actions/workflows/pre-commit.yml)
[![PyPI](https://img.shields.io/pypi/v/schemacast?color=224777)](https://pypi.org/project/schemacast/)
[![Python](https://img.shields.io/pypi/pyversions/schemacast?color=224777)](https://pypi.org/project/schemacast/)
[![Docs](https://img.shields.io/badge/docs-growgraph.github.io-224777)](https://growgraph.github.io/SchemaCast/)
[![License](https://img.shields.io/badge/license-Apache--2.0-224777)](https://github.com/growgraph/SchemaCast/blob/main/LICENSE)

LPGCast reads messages, tickets and notes, and writes what they state as vertices and edges of a
SchemaCast reads messages, tickets and notes, and writes what they state as vertices and edges of a
[GraFlo](https://github.com/growgraph/graflo) schema. Each text gets one model call, checked
against the schema; what fails is corrected at most twice, then quarantined. Every vertex, edge
and property keeps the quote it was read from.

OntoCast builds and evolves an ontology from documents and writes RDF; LPGCast casts short,
OntoCast builds and evolves an ontology from documents and writes RDF; SchemaCast casts short,
frequent texts into a property-graph schema you already govern.

**Status: alpha.** The API may change between minor releases.

## Quick start

```bash
uv add lpgcast # or: pip install lpgcast
uv add schemacast # or: pip install schemacast
```

Point it at any OpenAI-compatible endpoint:

```bash
# OpenAI (the default model is gpt-5.6-luna)
export LPGCAST_LLM_API_KEY=sk-...
export SCHEMACAST_LLM_API_KEY=sk-...

# or Ollama
export LPGCAST_LLM_BASE_URL=http://localhost:11434/v1
export LPGCAST_LLM_MODEL=qwen3:8b
export LPGCAST_LLM_API_KEY=local
export SCHEMACAST_LLM_BASE_URL=http://localhost:11434/v1
export SCHEMACAST_LLM_MODEL=qwen3:8b
export SCHEMACAST_LLM_API_KEY=local
```

Then, from a clone of the repository, with the example in [`examples/tickets/`](https://github.com/growgraph/LPGCast/tree/main/examples/tickets):
Then, from a clone of the repository, with the example in [`examples/tickets/`](https://github.com/growgraph/SchemaCast/tree/main/examples/tickets):

```bash
lpgcast sheet --schema examples/tickets/schema.yaml # what the model is shown; no call
lpgcast select --schema examples/tickets/schema.yaml --episodes examples/tickets/episodes.jsonl
lpgcast cast --schema examples/tickets/schema.yaml --episodes examples/tickets/episodes.jsonl \
schemacast sheet --schema examples/tickets/schema.yaml # what the model is shown; no call
schemacast select --schema examples/tickets/schema.yaml --episodes examples/tickets/episodes.jsonl
schemacast cast --schema examples/tickets/schema.yaml --episodes examples/tickets/episodes.jsonl \
--out out/tickets [--db examples/connections/neo4j.yaml] [--replay]
```

Expand All @@ -56,11 +56,11 @@ From Python:

```python
import asyncio
from lpgcast import LPGCaster, load_schema, read_episodes
from schemacast import SchemaCaster, load_schema, read_episodes

schema = load_schema("examples/tickets/schema.yaml")
result = asyncio.run(
LPGCaster(schema).cast(read_episodes("examples/tickets/episodes.jsonl"))
SchemaCaster(schema).cast(read_episodes("examples/tickets/episodes.jsonl"))
)
```

Expand All @@ -71,18 +71,18 @@ cast twice changes nothing in a database that upserts.

## Benchmark

`lpgcast-bench` casts a bundled corpus of texts whose graphs are known and scores the result
`schemacast-bench` casts a bundled corpus of texts whose graphs are known and scores the result
exactly, with no model as judge: conformance, hallucination, precision and recall, direction,
spans, mention versus mint, injection, selection and re-cast stability, with tokens and cost.

## Not yet

**Registry mode** is planned: selection across the schemas of a GraFlo schema registry, and an
HTTP service. Today LPGCast works on one schema file.
HTTP service. Today SchemaCast works on one schema file.

## Documentation

[growgraph.github.io/LPGCast](https://growgraph.github.io/LPGCast/): installation, how a cast
[growgraph.github.io/SchemaCast](https://growgraph.github.io/SchemaCast/): installation, how a cast
works, identities and mentions, preparing a schema, replay, writing to a database, the benchmark,
and the reference.

Expand Down
16 changes: 8 additions & 8 deletions docs/_build/gen_cli.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""Generate ``reference/cli.md`` from the click commands at build time.

One section per command, its help and a table of its options, read from the
command objects themselves, so the page cannot drift from ``lpgcast/cli.py``.
command objects themselves, so the page cannot drift from ``schemacast/cli.py``.
"""

import click
import mkdocs_gen_files

from lpgcast.bench.cli import main as bench
from lpgcast.cli import main as lpgcast
from schemacast.bench.cli import main as bench
from schemacast.cli import main as schemacast


def _cell(text: str) -> str:
Expand Down Expand Up @@ -49,17 +49,17 @@ def _section(command: click.Command, invocation: str) -> list[str]:
lines = [
"# Command line",
"",
"`lpgcast` (alias `text2lpg`) casts text; `lpgcast-bench` runs the",
"`schemacast` casts text; `schemacast-bench` runs the",
"[benchmark](../guides/benchmark.md). Model settings come from the",
"[environment](settings.md).",
"",
]
assert isinstance(lpgcast, click.Group)
for name, command in lpgcast.commands.items():
lines += _section(command, f"lpgcast {name}")
assert isinstance(schemacast, click.Group)
for name, command in schemacast.commands.items():
lines += _section(command, f"schemacast {name}")
assert isinstance(bench, click.Group)
for name, command in bench.commands.items():
invocation = "lpgcast-bench" if name == "run" else f"lpgcast-bench {name}"
invocation = "schemacast-bench" if name == "run" else f"schemacast-bench {name}"
lines += _section(command, invocation)

with mkdocs_gen_files.open("reference/cli.md", "w") as f:
Expand Down
6 changes: 3 additions & 3 deletions docs/_build/gen_pages.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Generate the Python API reference at build time.

Walks ``lpgcast/**/*.py`` and writes one ``mkdocstrings`` page per public
Walks ``schemacast/**/*.py`` and writes one ``mkdocstrings`` page per public
module under ``reference/python/``, into the virtual docs directory that
``mkdocs-gen-files`` feeds the build. Private modules (``_*.py``) are skipped.

Expand All @@ -11,14 +11,14 @@
A hand-written overview at ``reference/python/<pkg>/index.md`` replaces the
generated ``reference/python/<pkg>.md`` (both build to the same URL), so a
package whose overview exists on disk is skipped here; it must carry
``::: lpgcast.<pkg>`` itself. The nav is inferred by ``literate-nav``.
``::: schemacast.<pkg>`` itself. The nav is inferred by ``literate-nav``.
"""

from pathlib import Path

import mkdocs_gen_files

PACKAGE = "lpgcast"
PACKAGE = "schemacast"
REFERENCE = Path("docs/reference/python")


Expand Down
14 changes: 7 additions & 7 deletions docs/_build/gen_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@

One table per settings class, one row per field: the environment variable, its
default and its description. Generated, never written to disk, so it cannot
drift from ``lpgcast/config.py``.
drift from ``schemacast/config.py``.
"""

from typing import Any

import mkdocs_gen_files
from pydantic_settings import BaseSettings

from lpgcast.config import CacheSettings, LLMSettings, LPGCastSettings, ServeSettings
from schemacast.config import CacheSettings, LLMSettings, SchemaCastSettings, ServeSettings

SECTIONS: list[tuple[str, type[BaseSettings], str]] = [
("Model", LLMSettings, "LPGCAST_LLM_"),
("Response cache", CacheSettings, "LPGCAST_CACHE_"),
("Cast", LPGCastSettings, "LPGCAST_"),
("Service", ServeSettings, "LPGCAST_SERVE_"),
("Model", LLMSettings, "SCHEMACAST_LLM_"),
("Response cache", CacheSettings, "SCHEMACAST_CACHE_"),
("Cast", SchemaCastSettings, "SCHEMACAST_"),
("Service", ServeSettings, "SCHEMACAST_SERVE_"),
]


Expand All @@ -36,7 +36,7 @@ def _default(value: Any) -> str:
"# Settings",
"",
"Settings are read from environment variables only; no file is loaded. In",
"Python, pass `LPGCastSettings(...)` to `LPGCaster` instead.",
"Python, pass `SchemaCastSettings(...)` to `SchemaCaster` instead.",
"",
]
for title, model, prefix in SECTIONS:
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ set to the context item's `source_id`, and its offsets point into that item's
text. Any other quote found only in context is a `quote_not_found` finding.

Context is shown to the model as data, like the text, and counts towards
`LPGCAST_MAX_EPISODE_TOKENS`. LPGCast does not assemble it: the caller chooses
`SCHEMACAST_MAX_EPISODE_TOKENS`. SchemaCast does not assemble it: the caller chooses
the earlier texts, and should keep them few and recent.
4 changes: 2 additions & 2 deletions docs/concepts/groundings.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ graph, not into it.
| `property` | for a property: its name |
| `char_start`, `char_end`, `quote` | the span of the text, and its characters |
| `in_context` | set when the span is in a [context](context.md) item: its `source_id`; offsets then point into that item's text |
| `extractor`, `model`, `schema_ref` | what produced it: the LPGCast version, the model, and `name@version` of the schema |
| `extractor`, `model`, `schema_ref` | what produced it: the SchemaCast version, the model, and `name@version` of the schema |
| `reference_time` | the episode's time |

The model gives quotes, never offsets; LPGCast finds each quote in the text,
The model gives quotes, never offsets; SchemaCast finds each quote in the text,
exactly first, then ignoring case, accents, runs of whitespace and typographic
quotes and dashes. When a quote occurs more than once, a property takes the
occurrence nearest its vertex's or edge's quote. A quote it cannot find is a
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/identities.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Identities and mentions

LPGCast computes every identity before it builds an edge, the same way graflo
SchemaCast computes every identity before it builds an edge, the same way graflo
does for any other source. How depends on how the vertex type is identified:

| Identity of the type | Where its key comes from |
Expand Down
Loading
Loading