Skip to content

Getting Started

This guide will help you set up Latent and create your first evaluation flow.

Installation

Using pip

pip install latent           # minimal core (agents, protocol, guardrails base)
pip install "latent[eval]"   # full eval platform: Prefect, MLflow, pandas, scipy, ...
uv add "latent[eval]"

From source

git clone https://github.com/latentsp/latent-py.git
cd latent-py
pip install -e ".[eval]"

Prerequisites

  • Python 3.12 or higher

The base pip install latent is intentionally minimal. The flow/eval platform dependencies — Prefect 3.0+, MLflow 2.10+, pandas, scipy — are not installed by default; they ship in the eval extra (pip install "latent[eval]"). Other capabilities live in their own extras: rag, optimizers, guardrails, chat, tracking (MLflow only). The examples below assume the eval extra is installed.

Setting Up Your First Project

1. Create Project Structure

mkdir my_evaluation
cd my_evaluation
mkdir -p data flows

Your project structure should look like:

my_evaluation/
├── data/               # Data files
├── flows/              # Flow definitions
│   ├── my_flow/
│   │   ├── catalog.yaml      # Data catalog
│   │   ├── parameters.yaml   # Flow parameters
│   │   └── flow.py           # Flow definition
└── global.yaml         # Global configuration (optional)

Building agents, not just flows?

This is the minimal flow-only layout. For the full go-to project structure — the agents/ monorepo that Agent Studio expects, with agent packages, tools, guardrails, and flows — see Project Structure.

2. Initialize Configuration

Generate a configuration file using the CLI:

latent init

This creates config/latent.toml with sensible defaults:

# Environment mode: "production" or "dev"
environment = "production"

[workspace]
# flows_dir = "flows"
# data_dir = "data"
# logs_dir = "logs"
# mlruns_dir = "mlruns"

[mlflow]
enabled = true
litellm_autolog = true
langchain_autolog = true

[logging]
level = "INFO"

Environment Variables

You can also configure Latent via environment variables which take precedence over the TOML config. See Workspace Configuration for details.

3. Create Flow Configuration

Create flows/my_flow/parameters.yaml:

# Flow-specific parameters
batch_size: 32
learning_rate: 0.001
max_epochs: 100
model_name: gpt-4

Create flows/my_flow/catalog.yaml:

# Input datasets (loaded from data/my_flow/input/)
input_data:
  type: pandas.CSV
  path: input.csv
  dataset_type: input

# Output datasets (saved to MLflow artifacts)
processed_data:
  type: pandas.Parquet
  path: processed.parquet
  dataset_type: output

results:
  type: json.JSON
  path: results.json
  dataset_type: output

# Text/Markdown outputs (for reports)
report:
  type: text.Markdown
  path: report.md
  dataset_type: output

Supported Dataset Types

  • pandas.CSV, pandas.Parquet, pandas.JSON - DataFrames
  • json.JSON - Dicts and lists
  • text.Text, text.Markdown - Plain strings (or dict[str, str] for multiple files)
  • pickle.Pickle - Any Python object

Input vs Output

  • Inputs (dataset_type: input): Loaded from data/<flow>/input/
  • Outputs (dataset_type: output): Saved to MLflow artifacts and accessible via data/<flow>/output/latest symlink

4. Write Your First Flow

Create flows/my_flow/flow.py:

"""My first evaluation flow."""

from latent.prefect import flow, task, params, logger
from latent.mlflow import log_metric, log_param
import pandas as pd


@task(
    "load_data",
    input="input_data",
    output="loaded_data"
)
async def load_data_task(input_df: pd.DataFrame) -> pd.DataFrame:
    """Load and validate input data."""
    logger.info(f"Loading {len(input_df)} rows")

    # Validate data
    if input_df.empty:
        raise ValueError("Input data is empty")

    # Log basic stats
    log_metric("input_rows", len(input_df))
    log_metric("input_columns", len(input_df.columns))

    return input_df


@task(
    "process_data",
    input="loaded_data",
    output="processed_data"
)
async def process_data_task(data: pd.DataFrame) -> pd.DataFrame:
    """Process the data."""
    logger.info(f"Processing with batch_size={params.batch_size}")

    # Your processing logic
    processed = data.dropna()
    processed = processed[processed['value'] > 0]

    # Log processing metrics
    log_metric("processed_rows", len(processed))
    log_metric("rows_dropped", len(data) - len(processed))

    return processed


@task(
    "analyze_data",
    input="processed_data",
    output="results"
)
async def analyze_data_task(data: pd.DataFrame) -> dict:
    """Analyze processed data."""
    logger.info("Analyzing data")

    results = {
        "total_rows": len(data),
        "mean_value": float(data['value'].mean()),
        "std_value": float(data['value'].std()),
        "model_used": params.model_name,
    }

    # Log results to MLflow
    for key, value in results.items():
        if isinstance(value, (int, float)):
            log_metric(key, value)
        else:
            log_param(key, value)

    return results


@flow(
    "my_flow",
    output=["processed_data", "results"]
)
async def my_evaluation_flow():
    """
    Main evaluation flow.

    This flow:
    1. Loads input data
    2. Processes and cleans the data
    3. Analyzes the results
    4. Saves outputs automatically
    """
    logger.info(f"Starting evaluation with model={params.model_name}")

    # Tasks are automatically chained based on input/output
    loaded = await load_data_task()
    processed = await process_data_task(loaded)
    results = await analyze_data_task(processed)

    logger.info(f"Evaluation complete: {results}")

    return processed, results


if __name__ == "__main__":
    import asyncio

    asyncio.run(my_evaluation_flow())

Flows and tasks must be async def

Latent is async-only since 5.0.0. @flow- and @task-decorated functions must be async def, task calls must be awaited, and a top-level flow is run with asyncio.run(...) (or via latent run my_flow).

5. Prepare Your Data

Create a sample input file data/input.csv:

id,value,category
1,100,A
2,200,B
3,-50,C
4,300,A
5,150,B

6. Run Your Flow

cd flows/my_flow
python flow.py

What Just Happened?

When you ran the flow, Latent automatically:

  1. ✅ Loaded configuration from parameters.yaml and catalog.yaml
  2. ✅ Set up logging with flow-aware loggers
  3. ✅ Created MLflow experiment named "my_flow"
  4. ✅ Loaded input data from the catalog
  5. ✅ Executed tasks with automatic caching
  6. ✅ Tracked metrics in MLflow
  7. ✅ Saved outputs to timestamped directories

Viewing Results

MLflow UI

View your experiment tracking:

mlflow ui

Then open http://localhost:5000 in your browser.

Logs

Logs are automatically saved to logs/my_flow/.

Next Steps

Now that you have a working flow, explore:

Common Patterns

Loading Data from Previous Flows

Reference outputs from other flows using dot notation:

# Use the dot notation in @flow or @task decorator
@flow(
    "my_flow",
    input=["other_flow.output_dataset"]
)
async def my_flow(output_dataset):
    # output_dataset is automatically loaded from other_flow's outputs
    pass

# Or in a task
@task("analyze", input="upstream_flow.results")
async def analyze_task(results):
    # results loaded from upstream_flow's MLflow artifacts
    pass

Cross-flow references automatically load from the source flow's output/latest symlink, which points to the most recent MLflow artifacts.

Conditional Task Execution

@task("conditional_task")
async def conditional_task(data: pd.DataFrame) -> pd.DataFrame:
    if params.enable_feature:
        logger.info("Feature enabled, processing...")
        return await process_data(data)
    else:
        logger.info("Feature disabled, skipping...")
        return data

Parallel Task Execution

task.map(items, concurrency=N) runs each item concurrently on the event loop (bounded by an asyncio.Semaphore) and awaits to a list of results directly — no futures, no .result():

@flow("parallel_flow")
async def parallel_flow():
    # Process items concurrently, at most 5 in flight at once
    completed = await process_task.map(items, concurrency=5)
    return completed

Multiple File Outputs

Save multiple files from a single task by returning a dict:

@task("generate_reports", output="reports")
async def generate_reports(data: pd.DataFrame) -> dict[str, str]:
    """Generate multiple report files."""
    return {
        "summary.md": f"# Summary\n\nTotal rows: {len(data)}",
        "details.md": f"# Details\n\n{data.describe().to_markdown()}",
        "warnings.md": f"# Warnings\n\nMissing values: {data.isna().sum().sum()}",
    }
    # Automatically saves to MLflow artifacts: outputs/reports/

In your catalog:

reports:
  type: text.Markdown
  path: reports.md  # Creates reports/ directory in artifacts
  dataset_type: output

Troubleshooting

Import Errors

If you see ModuleNotFoundError: No module named 'latent':

# Make sure latent is installed (with the eval extra for flows)
pip install "latent[eval]"

# Or in development
cd latent-py
pip install -e ".[eval]"

MLflow Connection Issues

If MLflow tracking fails:

# Check MLflow server
mlflow ui

# Or set tracking URI in config/latent.toml
# [mlflow]
# tracking_uri = "http://localhost:5000"

# Or via environment variable
export LATENT_MLFLOW_TRACKING_URI=http://localhost:5000

Getting Help