Skip to main content

A batteries-included framework for machine learning, analytics and LLM agents, built on Django's philosophy.

Project description

mlango

A batteries-included framework for machine learning, analytics and LLM agents, built on Django's philosophy.

Read this in Русский.

CI PyPI Python License

pip install "mlango[sklearn]"

ML projects tend to become a pile of scripts: one to load data, one to train, a notebook that produced the number in the slide deck, a checkpoints/ directory nobody can map back to a commit.

Web development had the same problem, and Django's answer was not a better library but a framework: a project layout, a settings module, declarative classes, migrations, an auto-generated admin, and a manage.py that ties it together.

mlango applies that answer to ML. You declare datasets, models, agents and evaluations; the framework runs them, versions them, records them and shows them to you.

# reviews/datasets.py
from mlango.core import fields
from mlango.data import Dataset, JSONLSource

class Reviews(Dataset):
    """Customer product reviews."""

    id = fields.IntegerField()
    text = fields.TextField()
    label = fields.LabelField(["negative", "positive"])

    class Meta:
        source = JSONLSource("data/reviews.jsonl")
        primary_key = "id"
# reviews/models.py
from mlango.core import fields
from mlango.training import Model
from reviews.datasets import Reviews

class Sentiment(Model):
    """TF-IDF into logistic regression."""

    max_features = fields.IntegerField(default=20_000, tunable=True)
    C = fields.FloatField(default=1.0, min_value=0.0, tunable=True)

    class Meta:
        dataset = Reviews
        trainer = "sklearn"
        task = "classification"
        features = ["text"]

    def build(self):
        from sklearn.feature_extraction.text import TfidfVectorizer
        from sklearn.linear_model import LogisticRegression
        from sklearn.pipeline import make_pipeline
        return make_pipeline(
            TfidfVectorizer(max_features=self.max_features),
            LogisticRegression(C=self.C),
        )
python manage.py train reviews.Sentiment -p C=2.0

That one command resolves your class, opens a tracked run, seeds every RNG, splits the data deterministically, calls your build(), drives the training loop, records metrics, captures the git commit, saves the artifact and registers a promotable model version. You wrote build() and four field declarations.


Install

pip install "mlango[sklearn]"

Extras: sklearn, torch, anthropic, dev, or all.

Five minutes from nothing

mlango startproject myproject
cd myproject
python manage.py migrate
python manage.py train demo.Sentiment
python manage.py runserver

mlango startplugin mlango-lightgbm --kind trainer   # a package others can install

Open http://127.0.0.1:8000/admin/. Unlike a bare scaffold, a fresh mlango project already contains a working example: a dataset, a trained model with real metrics, an agent with a tool, and an eval suite. The admin has something in it the first time you look.

No configuration is required to get there: the metastore is SQLite, artifacts go to a local directory, and agents run on an offline provider that needs no API key.


What you get

Declarative classes with a _meta

Four families, one system. Everything generic in the framework (the admin, migrations, the CLI, the API) is written against _meta. That is why a single admin renders all four.

You declare You get
Dataset A lazy queryset, schema validation, deterministic splits, content-addressed versioning
Model Hyperparameters as validated fields, tracked runs, callbacks, a model registry with stages
Agent A tool-use loop, tools with schemas derived from type hints, memory, full step-by-step tracing
Eval Per-case scoring persisted to the metastore, so a regression is a diff between two runs

A queryset for data

Lazy, composable, and recorded alongside the run that used it:

train, val = Reviews.objects.filter(label="positive").shuffle(seed=0).split(train=0.8, val=0.2).values()

for batch in train.batch(32):
    ...

Lookups follow Django's spelling: filter(stars__gte=4), exclude(text__icontains="spam"), filter(language__in=["en", "de"]). Splits are assigned by hashing each record's key, so adding rows never moves existing ones between train and test. That property is what keeps a held-out set trustworthy six months later.

Migrations for schemas

python manage.py makemigrations
python manage.py migrate

Changing a dataset's fields generates a real, reviewable migration file. Data migrations use RunPython, exactly as you would expect.

An admin you did not build

Every declared object appears automatically, with no registration required. Register only to change how it looks:

@admin.register(Reviews)
class ReviewsAdmin(admin.ObjectAdmin):
    list_display = ("id", "text", "label")
    list_filter = ("label",)
    search_fields = ("text",)

The admin shows data previews with filters and search, run history with metric charts, side-by-side run comparison, dataset and model versions with one-click promotion, and a step-by-step trace viewer for every agent call. It is server-rendered with no build step and no CDN.

The mlango admin: everything a project declares, and everything it has run

Every run keeps its environment, parameters, metrics and artifacts, so a number from six months ago still says where it came from:

A run page: environment, parameters, metrics and artifacts

Agents as declarations

from mlango.agents import Agent, BufferMemory, tool

@tool
def search_docs(query: str, limit: int = 5) -> list[str]:
    """Search the product documentation.

    Args:
        query: What to search for.
        limit: Maximum number of results.
    """
    return retrieve(query, limit)

class Support(Agent):
    """Answers product questions from the docs."""

    class Meta:
        model = "claude-opus-5"
        system = "You are a support engineer. Cite the docs you used."
        tools = [search_docs]
        memory = BufferMemory(k=20)

The JSON schema comes from your type hints and docstring, so a tool is described in exactly one place. The framework owns the loop, retries, tool dispatch, usage accounting and tracing.

Serving from the same declaration

# myproject/routes.py
from mlango.serve import path

urlpatterns = [
    path("predict/", Sentiment.as_endpoint(stage="production")),
    path("chat/", Support.as_endpoint()),
]

manage.py runserver serves the admin and a documented API together; OpenAPI schemas are derived from the declarations, so /api/docs describes your model's inputs without you writing a schema.


The command line

python manage.py check                          # validate the whole project
python manage.py inspectdata data/reviews.csv    # declare a Dataset from a file
python manage.py dataset head reviews.Reviews   # peek at the data
python manage.py dataset materialize reviews.Reviews
python manage.py makemigrations && python manage.py migrate
python manage.py train reviews.Sentiment -p C=2.0 --tag baseline
python manage.py predict reviews.Sentiment "loved every minute"
python manage.py explain reviews.Sentiment                # what the fit relied on
python manage.py drift reviews.Sentiment --since 24h      # has the input moved?
python manage.py diff reviews.Sentiment 3 4               # what did v4 break?
python manage.py sweep reviews.Sentiment -p C=0.25,1,4 --promote-best production
python manage.py runs list
python manage.py runs compare 7c8f1020 c089b7e6
python manage.py evaluate reviews.Accuracy --min-pass-rate 0.9
python manage.py agent support.Support           # interactive session
python manage.py traces show a1b2c3d4            # replay an agent call
python manage.py shell                           # everything pre-imported
python manage.py test                            # against a throwaway metastore
python manage.py runserver

inspectdata is Django's inspectdb for data files: it samples a CSV, JSONL or Parquet file and prints a Dataset with the field types, ranges, label classes and primary key already filled in, so your first declaration is an edit rather than a blank page.

Apps can ship their own commands in <app>/management/commands/, and they appear in manage.py help automatically, including ones that override a built-in.


Configuration

One settings module, every default documented in mlango.conf.global_settings. Backends are swapped by setting, not by rewriting code:

METASTORE = {"URL": "postgresql://user@host/mlango"}   # SQLite by default
STORAGE = {"BACKEND": "myproject.storage.S3Storage"}
TRAINERS = {"lightgbm": "myproject.trainers.LightGBMTrainer"}
PROVIDERS = {"vllm": "myproject.providers.VLLMProvider"}
SERVE_MIDDLEWARE = ["mlango.serve.middleware.ApiKeyMiddleware", ...]

Why a framework and not a library

A library is something you call. A framework calls you. That inversion is the whole point, and it is what buys the conveniences above:

  • Project layout and settings: manage.py, MLANGO_SETTINGS_MODULE
  • An app registry that autodiscovers datasets.py, models.py, agents.py, evals.py, admin.py
  • Migrations: generated, reviewable files for declared schemas
  • An admin generated from declarations
  • A management command system apps can extend and override
  • Signals: run_finished, epoch_finished, tool_called, and more
  • Pluggable backends behind settings

If mlango were a library you would still be writing the run loop, the tracking schema, the admin and the CLI. Being a framework is what removes them.


How it works

Everything follows from one idea: your class body is compiled into metadata, and every generic subsystem reads that metadata instead of knowing about your class.

          your class body                    what reads it
    ┌──────────────────────────┐
    │  class Sentiment(Model): │           ┌──────────────► Admin page
    │      C = FloatField(…)   │           │
    │                          │           ├──────────────► POST /api/predict/
    │      class Meta:         │  ────►  _meta               + OpenAPI schema
    │          dataset = …     │        (Options)  │
    │          trainer  = …    │           ├──────────────► Migration file
    │                          │           │
    │      def build(self): …  │           ├──────────────► manage.py train
    └──────────────────────────┘           │                manage.py sweep
                                           └──────────────► Eval + model registry

Nothing on the right imports Model, Dataset, Agent or Eval. They all read _meta, which is why one admin renders four different families and why adding a fifth would not mean touching the admin.

The layers

  core            fields · metaclass · Options · registry · settings · signals
    │             (imports nothing else in mlango)
    ├── metastore   9 tables: runs, metrics, artifacts, versions, traces, spans…
    ├── storage     artifacts, behind one narrow interface
    │
    ├── data ─────┐
    ├── training ─┤  the four families. They never import each other.
    ├── agents ───┤
    ├── evals ────┘
    │
    └── admin · serve · management     read everything, but only through _meta

The rule that matters most is the middle one. The four families not importing each other is what lets you use the agent half without the ML half, and why a project declaring only datasets never loads a line of agent code.

What manage.py train actually does

  train reviews.Sentiment -p C=2.0
        │
        ├─ load settings, autodiscover every app's declarations
        ├─ resolve the label in the registry
        ├─ build the instance — fields validate C=2.0
        ├─ open a run: seed, device, git commit, host, Python version
        ├─ split the data by hashing each record's key, record the fingerprint
        ├─ call your build(), drive the loop, log metrics each epoch
        ├─ save the artifact and register a promotable version
        └─ close the run

You wrote build() and four field declarations. Everything else happens whether you remembered it or not, which is the whole argument for a framework.

Architecture has the full picture: sequence diagrams, the metastore schema, every extension point and its contract. Philosophy explains the decisions behind it.


Documentation

Full docs, including a tutorial that builds a project end to end: https://drobyshevdev.github.io/mlango/

Contributing

Contributions are welcome. See CONTRIBUTING.md for the development setup, and CODE_OF_CONDUCT.md for community expectations. Good first issues are labelled good first issue.

License

MIT. See LICENSE.

mlango is not affiliated with or endorsed by the Django Software Foundation. It borrows Django's design philosophy, gratefully.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mlango-0.2.0.tar.gz (652.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mlango-0.2.0-py3-none-any.whl (259.4 kB view details)

Uploaded Python 3

File details

Details for the file mlango-0.2.0.tar.gz.

File metadata

  • Download URL: mlango-0.2.0.tar.gz
  • Upload date:
  • Size: 652.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for mlango-0.2.0.tar.gz
Algorithm Hash digest
SHA256 608452469621eafafaaafd1dc9e8423ed218cc057c13575e0166577a48c9b971
MD5 a91023592471267babb658c1366fe014
BLAKE2b-256 f9f738c13a853034671317c2d40fe865a332faa88b2b0ef73d5ca0134b56db29

See more details on using hashes here.

Provenance

The following attestation bundles were made for mlango-0.2.0.tar.gz:

Publisher: release.yml on DrobyshevDev/mlango

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mlango-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: mlango-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 259.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for mlango-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 15409062606933a03a75111d3fa47a0ecb1400cdec5752e087b7a2322a39f2f4
MD5 44ae05ce030a41140ce2945ef00584ab
BLAKE2b-256 c308a8cb951144a4d7f1b3a3415ff600cb31fc5a2bbe38a5d9a456e9f8956efe

See more details on using hashes here.

Provenance

The following attestation bundles were made for mlango-0.2.0-py3-none-any.whl:

Publisher: release.yml on DrobyshevDev/mlango

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page