Skip to main content
Looking to use this in code? See Tutorial 05: MIFYing Legacy Code for a practical walkthrough.
Object-oriented programming organizes related data and the methods that operate on it into classes. Mellea applies the same principle to LLM interactions: an MObject is a Python class whose fields and methods can be exposed to a model in a controlled, structured way. The @mify decorator turns any class into an MObject. You specify exactly which fields and methods are visible to the LLM — nothing else is exposed.

The @mify decorator

import mellea
from mellea.stdlib.components import mify
from mellea.stdlib.components.mify import MifiedProtocol

@mify(fields_include={"table"}, template="{{ table }}")
class SalesDatabase:
    table: str = """| Store      | Sales  |
                    | ---------- | ------ |
                    | Northeast  | $250   |
                    | Southeast  | $80    |
                    | Midwest    | $420   |"""

    def internal_method(self):
        # not exposed to the LLM
        ...

m = mellea.start_session()
db = SalesDatabase()
assert isinstance(db, MifiedProtocol)

answer = m.query(db, "What were sales for the Northeast branch this month?")
print(str(answer))
# Output will vary — LLM responses depend on model and temperature.
fields_include controls which fields appear in the prompt. template is a Jinja2 template that controls how those fields are rendered. The m.query() call sends the rendered object plus the question to the model. @mify is useful whenever you need to expose structured data to a model without leaking internal state.

Methods as tools

When you mify a class, every method that has a docstring is automatically registered as a tool the LLM can call. Use funcs_include or funcs_exclude to control which methods are exposed:
from mellea.stdlib.components import mify

@mify(funcs_include={"from_markdown"})
class DocumentLoader:
    def __init__(self) -> None:
        self.content = ""

    @classmethod
    def from_markdown(cls, text: str) -> "DocumentLoader":
        """Load a document from a Markdown string."""
        doc = DocumentLoader()
        doc.content = text
        return doc

    def internal_helper(self) -> str:
        # no docstring, and not in funcs_include — never exposed
        return "..."
Only from_markdown is registered as a tool. The model can call it during a m.transform() or m.query() operation; internal_helper is invisible. When a class method and an LLM operation would produce the same result, Mellea will note that the direct method call is available:
# Both of these transform the table in the same way.
# Mellea will suggest using the direct method call instead.
table_transposed = m.transform(table, "Transpose the table.")
table_transposed_direct = table.transpose()

Working with documents

Mellea provides mified wrappers around Docling documents for working with PDFs and other rich documents.
from mellea.stdlib.components.docs.richdocument import RichDocument

rd = RichDocument.from_document_file("https://arxiv.org/pdf/1906.04043")
This loads the PDF and parses it into Mellea’s intermediate representation. From there you can extract structured elements:
from mellea.stdlib.components.docs.richdocument import Table

table: Table = rd.get_tables()[0]
print(table.to_markdown())
Table is already an MObject, so you can pass it directly to m.transform() or m.query():
from mellea.backends import ModelOption
from mellea import start_session

m = start_session()

# Try a few seeds to find a run that returns a parsable table
for seed in [x * 12 for x in range(5)]:
    result = m.transform(
        table,
        "Add a column 'Model' that extracts which model was used, or 'None' if none.",
        model_options={ModelOption.SEED: seed},
    )
    if isinstance(result, Table):
        print(result.to_markdown())
        break
The seed loop is a simple retry strategy: LLM output is non-deterministic, so iterating over seeds gives multiple independent samples until one produces a valid table structure.
Note: LLM output is non-deterministic. Your exact results will vary.

When to use MObjects

MObjects are well-suited for:
  • Document querying — wrap a document, expose only the relevant sections, query or transform them with the model
  • Tool registration — expose a controlled set of methods as tools the LLM can invoke during generation
  • Evolving existing codebases — add @mify to an existing class to make it LLM-accessible without rewriting it
For simple one-off generation, m.instruct() is usually sufficient. MObjects add value when you have structured data or methods that the model needs to reason about or call. See also: Context and Sessions | Generative Functions