Skip to main content
Version: Next

Choosing Between Primitives and High-Level APIs

Mellea provides multiple levels of abstraction for building generative applications. Understanding which to use depends on your needs.

The Abstraction Hierarchy

High-level (simplest)

├─ MelleaSession (session management + context + generation)
├─ instruct() / chat() (generation + context management)
├─ act() / aact() (generation + basic context management)

Low-level (most control)
├─ call_tools() / acall_tools() (tool execution only)
└─ generate_from_context() (raw model call)

Each level adds convenience by automating lower-level concerns.

Comparison Table

AspectMelleaSessionact()/instruct()call_tools()
Context managementAutomatic, statefulAutomatic, immutableManual
GenerationBuilt-inYesNo
Tool call generationBuilt-in (via generation)Built-in (via generation)N/A
Tool executionManual (via loop or hooks)Manual (you call call_tools())Manual
Hook supportFullFullFull
Sampling/repairVia sampling strategyVia sampling strategyN/A
ValidationVia requirementsVia requirementsN/A
Lines of code5-1010-2020-50
Learning curveLowMediumHigh
FlexibilityMediumHighVery high

Decision Tree

Start here to pick the right level:

Does your code need to:

1. "Manage a multi-turn conversation"?
YES → Use MelleaSession
NO → Continue

2. "Make a single LLM call with tools"?
YES → Use act() or instruct()
NO → Continue

3. "Execute already-generated tool calls"?
YES → Use call_tools()
NO → Continue

4. "Make a raw LLM call"?
YES → Use backend.generate_from_context()

Detailed Scenarios

Scenario 1: Simple Single-Turn Chat

Task: User asks a question, model answers.

Best choice: MelleaSession or chat()

# With MelleaSession (simplest)
with start_session() as m:
result = m.chat("What is 2+2?")
print(result.value)

# With chat() (explicit, no session state)
from mellea.stdlib.functional import chat
result, ctx = chat("What is 2+2?", ctx, backend)

✅ Why: Handles context automatically, minimal code.


Scenario 2: Multi-Turn Conversation

Task: Build a chatbot that remembers previous messages.

Best choice: MelleaSession

with start_session() as m:
m.chat("My name is Alice")
m.chat("What is my name?") # Remembers "Alice"
m.chat("What is 10 + 5?")

✅ Why: Context accumulates automatically, stateful by design.


Scenario 3: Controlled Tool Execution

Task: Generate tool calls, then inspect and filter before execution.

Best choice: act() or instruct() + call_tools()

# Generate with tools
result, ctx = instruct(
"Calculate 2 + 3",
ctx,
backend,
tool_calls=True,
model_options={ModelOption.TOOLS: [add, multiply]}
)

# Inspect and filter before executing
safe_calls = [tc for tc in result.tool_calls if is_safe(tc)]
if safe_calls:
# Execute only safe tool calls
tool_messages = call_tools(result, backend)
for msg in tool_messages:
ctx = ctx.add(msg)

✅ Why: Generate with higher-level APIs, use call_tools() for execution control.


Scenario 4: Custom ReACT Loop

Task: Implement "Reason → Act → Observe → Repeat" with custom logic.

Best choice: call_tools() + act() in a loop

async def custom_react(goal, backend, tools):
ctx = ChatContext().add(Message("user", goal))

for step in range(max_steps):
# Think
result, ctx = await aact(
system_prompt, ctx, backend, tool_calls=True
)

# Act (manual tool execution gives you control)
tool_messages = await acall_tools(result, backend)

# Observe
for msg in tool_messages:
ctx = ctx.add(msg)
# Can inspect/transform results before adding to context

✅ Why: You control the loop flow, context management, and tool filtering.


Scenario 5: Sampling/Validation Loop

Task: Generate, validate, and repair until requirements are met.

Best choice: act() with a SamplingStrategy

from mellea.stdlib.sampling import RejectionSamplingStrategy

result = act(
instruction,
ctx,
backend,
requirements=[must_be_brief, must_be_json],
strategy=RejectionSamplingStrategy(loop_budget=3),
)

✅ Why: act() handles the validate-repair loop, you just provide requirements.


Scenario 6: Tool Execution with Plugin Hooks

Task: Log, audit, or intercept every tool call.

Best choice: Any level — hooks work everywhere

@hook(HookType.TOOL_PRE_INVOKE)
async def audit(payload, _):
log.info(f"Tool: {payload.model_tool_call.name}")

# Hooks fire regardless of whether you use call_tools(),
# act(), instruct(), or MelleaSession

✅ Why: Hooks are orthogonal to the API level — use them everywhere.


Migration Patterns

From Raw API Calls → act()

Before (manual context management):

result, ctx = backend.generate_from_context(component, ctx=ctx)
ctx = ctx.add(result)

After (automatic context management):

result, ctx = act(component, ctx, backend)

✅ Benefits: Cleaner, handles edge cases, telemetry included.


From act()MelleaSession

Before (passing context manually):

result1, ctx = act(msg1, ctx, backend)
result2, act(msg2, ctx, backend)
result3, ctx = act(msg3, ctx, backend)

After (context managed automatically):

with start_session() as m:
result1 = m.act(msg1, backend)
result2 = m.act(msg2, backend)
result3 = m.act(msg3, backend)

✅ Benefits: No context threading, cleaner, easier to reason about.


From act()call_tools() for Specialized Loops

Before (basic tool generation):

result, ctx = act(prompt, ctx, backend, tool_calls=True)
# Tool calls generated but not executed
# You'd need to manually call call_tools() to execute them

After (explicit execution control with inspection):

result, ctx = act(prompt, ctx, backend, tool_calls=True)

# Inspect tool calls before execution
for tc in result.tool_calls:
print(f"Will execute: {tc.name} with {tc.args}")

# Execute and add to context
tool_msgs = call_tools(result, backend)
for msg in tool_msgs:
if is_approved(msg): # Custom filtering
ctx = ctx.add(msg)

✅ Benefits: Full control over tool execution timing, inspection, and filtering.


Summary: Quick Pick

You want to...Use...
Build a chatbotMelleaSession
Make a single generationact() or instruct()
Validate & repairact() + SamplingStrategy
Custom agentic loopcall_tools() + your loop
Just execute toolscall_tools()
Raw LLM callbackend.generate_from_context()
Observe all tool callsAdd @hook(HookType.TOOL_PRE/POST_INVOKE)

See Also