Skip to main content
This tutorial shows how to build composable LLM-backed functions using the @generative decorator — functions with typed return values, docstring-driven prompts, and consistent behaviour that you can reuse across a codebase. By the end you will have covered:
  • Defining @generative functions with typed returns
  • Composing multiple generative functions into a pipeline
  • Controlling behaviour via ChatContext and context injection
  • Precondition and postcondition validation patterns
Prerequisites: Tutorial 01 complete, pip install mellea, Ollama running locally with granite4.1:3b downloaded.

Step 1: Your first @generative function

A @generative function uses its name, type annotation, and docstring as the prompt. Call it by passing a MelleaSession as the first argument:
Sample output
Note: Output may vary — LLM responses depend on model and temperature.
The return type annotation shapes the output. With -> str, the model returns free text. For constrained output, use Literal:
Now the output is guaranteed to be one of those three strings.

Step 2: Typed and structured returns

Generative functions support any JSON-serialisable return type — str, int, bool, list, dict, and Pydantic models:
Sample output
Note: With a Pydantic return type, result is a validated FeedbackAnalysis instance. result.sentiment will always be one of "positive", "negative", or "neutral".
The return value is a validated FeedbackAnalysis instance. If the model output doesn’t conform, Mellea retries.

Step 3: Compose generative functions

Because each @generative function is just a Python function, you compose them the same way as any other code:
Sample output
Note: LLM output is non-deterministic. The response will be in French (or whichever target language you specify) and will vary in wording.
Each function is an independent LLM call. The composition logic stays in ordinary Python.
Full example: docs/examples/generative_stubs/generate_with_context.py

Step 4: Steer all functions via context

A key advantage of @generative functions over direct instruct() calls: you can change the behaviour of every function in a session by injecting context once.
Sample output
Note: LLM output is non-deterministic. Output will vary.
m.reset() clears injected context while keeping the session and backend alive.

Step 5: Pre- and postcondition validation

For production pipelines, validate inputs before the LLM call and outputs afterwards using plain Python:
Sample output
Note: LLM output is non-deterministic. The letter will vary in wording but should be 50+ words and free of prohibited compliance language ("guaranteed returns", "no risk", etc.). If either check fails, you will see Validation failed: ... instead.
The precondition check runs before the expensive letter generation. The postcondition check uses a second @generative call as a lightweight verifier.
Full example: docs/examples/generative_stubs/investment_advice.py

What you built

A pattern for replacing ad-hoc instruct() calls with reusable, typed, context-steerable generative functions:
See also: Generative Functions | The Requirements System | Write Custom Verifiers