📖 Cookbook — 10 Real-World Recipes¶
Copy-paste-ready patterns for common agentomatic use cases.
Every recipe uses the exact API from agentomatic.agents —
no stubs, no TODOs, no placeholders.
Table of Contents¶
| # | Recipe | Description |
|---|---|---|
| 1 | Minimal Echo Agent | Simplest possible agent — single node that echoes input |
| 2 | RAG Agent | Two-node pipeline: retrieve documents, then generate |
| 3 | Conditional Router | Route to different nodes based on intent classification |
| 4 | @agent_node Decorator | Decorator-only graph wiring — no build_graph() needed |
| 5 | Custom Schemas | Pydantic input/output validation with schemas.py |
| 6 | Prompt Versioning | prompts.json + PromptManager for A/B testing |
| 7 | Multi-Turn Chat | Thread-based conversation with memory |
| 8 | Human-in-the-Loop | AgentSuspendedException for approval workflows |
| 9 | ML Plugin | BaseMLPlugin wrapping a scikit-learn model |
| 10 | Evaluate & Optimize | compile() → fit() → evaluate() pipeline |
Recipe 1 — Minimal Echo Agent¶
Simplest possible agent — single node that echoes input.
graph TD
START(["▶ Start"]) --> echo
echo["Echo back input"]
echo --> DONE(["✅ Done"])
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from agentomatic.agents import BaseGraphAgent
@dataclass
class EchoState:
"""Minimal state with query in, response out."""
query: str = ""
response: str = ""
class EchoAgent(BaseGraphAgent[EchoState]):
"""The simplest possible agent — echoes the user's input."""
agent_name = "echo"
agent_description = "Echoes back user input"
agent_framework = "graph_agent"
def build_graph(self):
g = self.new_graph() # (1)!
g.add_node("echo", self.echo) # (2)!
g.set_entry_point("echo") # (3)!
g.set_finish_point("echo") # (4)!
return g.compile() # (5)!
def echo(self, state: EchoState) -> EchoState:
state.response = f"You said: {state.query}"
return state
def input_to_state(self, input_data: dict[str, Any]) -> EchoState:
return EchoState(query=input_data.get("current_query", ""))
def state_to_output(self, state: EchoState) -> dict[str, Any]:
return {"response": state.response}
# --- Usage ---
agent = EchoAgent()
result = agent.transform({"current_query": "hello world"})
print(result) # {"response": "You said: hello world"}
new_graph()returns aGraphBuilder— agentomatic's own builder, not LangGraph.add_node(name, handler)registers a callable node.set_entry_point()marks where execution begins.set_finish_point()marks where execution ends.compile()validates the graph and returns anAgentGraphruntime.
When to use this pattern
Start here for any agent. Once it works, add more nodes, edges, or conditional routing as your logic grows.
Recipe 2 — RAG Agent (Retrieve / Generate)¶
Two-node pipeline: retrieve documents, then generate a grounded response.
graph TD
START(["▶ Start"]) --> retrieve
retrieve["Retrieve documents"]
retrieve --> generate
generate["Generate grounded answer"]
generate --> DONE(["✅ Done"])
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from agentomatic.agents import BaseGraphAgent
@dataclass
class RAGState:
"""State carrying the query, retrieved docs, and final answer."""
query: str = ""
documents: list[str] = field(default_factory=list)
response: str = ""
class RAGAgent(BaseGraphAgent[RAGState]):
"""Retrieve-then-generate agent."""
agent_name = "rag_agent"
agent_description = "Retrieves documents and generates a grounded response"
agent_framework = "graph_agent"
def __init__(self, *, knowledge_base: list[str] | None = None) -> None:
super().__init__()
self.knowledge_base = knowledge_base or [
"Agentomatic uses a graph-based execution model.",
"Agents are defined as Python classes with typed state.",
"The GraphBuilder provides both fluent and LangGraph-style APIs.",
"Evaluation uses the Metric protocol with score().",
]
def build_graph(self):
g = self.new_graph()
g.add_node("retrieve", self.retrieve)
g.add_node("generate", self.generate)
g.set_entry_point("retrieve")
g.add_edge("retrieve", "generate")
g.set_finish_point("generate")
return g.compile()
def retrieve(self, state: RAGState) -> RAGState:
"""Simple keyword retrieval over the knowledge base."""
query_lower = state.query.lower()
state.documents = [
doc for doc in self.knowledge_base
if any(word in doc.lower() for word in query_lower.split())
]
if not state.documents:
state.documents = [self.knowledge_base[0]]
return state
def generate(self, state: RAGState) -> RAGState:
"""Generate an answer grounded in the retrieved documents."""
context = "\n".join(f"- {doc}" for doc in state.documents)
state.response = (
f"Based on {len(state.documents)} document(s):\n"
f"{context}\n\n"
f"Answer: The information above addresses '{state.query}'."
)
return state
def input_to_state(self, input_data: dict[str, Any]) -> RAGState:
return RAGState(query=input_data.get("current_query", ""))
def state_to_output(self, state: RAGState) -> dict[str, Any]:
return {
"response": state.response,
"citations": state.documents,
}
# --- Usage ---
agent = RAGAgent()
result = agent.transform({"current_query": "How does evaluation work?"})
print(result["response"])
Swap in a real vector store
Replace the retrieve() body with calls to ChromaDB, FAISS, or any
embedding-based retriever. The graph structure stays exactly the same.
Recipe 3 — Conditional Router¶
Routes to different nodes based on intent classification using
add_conditional_edge().
graph TD
START(["▶ Start"]) --> classify
classify["Classify intent"]
classify -->|"conditional"| classify_router
classify_router{"route?"}
classify_router -->|"greeting"| handle_greeting
classify_router -->|"question"| handle_question
classify_router -->|"complaint"| handle_complaint
handle_greeting["Handle greeting"] --> DONE(["✅ Done"])
handle_question["Handle question"] --> DONE
handle_complaint["Handle complaint"] --> DONE
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from agentomatic.agents import BaseGraphAgent
@dataclass
class RouterState:
"""State with a routing_decision field for conditional edges."""
query: str = ""
intent: str = ""
response: str = ""
class ConditionalRouterAgent(BaseGraphAgent[RouterState]):
"""Classifies intent and routes to the right handler."""
agent_name = "router"
agent_description = "Routes queries by intent"
agent_framework = "graph_agent"
INTENT_KEYWORDS: dict[str, list[str]] = {
"greeting": ["hello", "hi", "hey", "good morning"],
"question": ["what", "how", "why", "when", "where", "?"],
"complaint": ["bad", "broken", "issue", "problem", "terrible"],
}
def build_graph(self):
g = self.new_graph()
g.add_node("classify", self.classify)
g.add_node("handle_greeting", self.handle_greeting)
g.add_node("handle_question", self.handle_question)
g.add_node("handle_complaint", self.handle_complaint)
g.set_entry_point("classify")
# Conditional edge — route function returns the next node name
g.add_conditional_edge(
"classify",
self.route,
routes={
"greeting": "handle_greeting",
"question": "handle_question",
"complaint": "handle_complaint",
},
)
g.set_finish_point("handle_greeting")
g.set_finish_point("handle_question")
g.set_finish_point("handle_complaint")
return g.compile()
def classify(self, state: RouterState) -> RouterState:
"""Classify the query into an intent category."""
query_lower = state.query.lower()
for intent, keywords in self.INTENT_KEYWORDS.items():
if any(kw in query_lower for kw in keywords):
state.intent = intent
return state
state.intent = "question" # default fallback
return state
def route(self, state: RouterState) -> str:
"""Return the intent — maps to node name via routes dict."""
return state.intent
def handle_greeting(self, state: RouterState) -> RouterState:
state.response = f"Hello! How can I help you today?"
return state
def handle_question(self, state: RouterState) -> RouterState:
state.response = f"Great question! Let me look into: {state.query}"
return state
def handle_complaint(self, state: RouterState) -> RouterState:
state.response = (
f"I'm sorry to hear about this issue. "
f"Let me escalate: {state.query}"
)
return state
def input_to_state(self, input_data: dict[str, Any]) -> RouterState:
return RouterState(query=input_data.get("current_query", ""))
def state_to_output(self, state: RouterState) -> dict[str, Any]:
return {"response": state.response, "intent": state.intent}
# --- Usage ---
agent = ConditionalRouterAgent()
print(agent.transform({"current_query": "hello there"}))
# {"response": "Hello! How can I help you today?", "intent": "greeting"}
print(agent.transform({"current_query": "this is broken!"}))
# {"response": "I'm sorry to hear about this issue. ...", "intent": "complaint"}
How add_conditional_edge works
The condition function receives the current state and returns a string.
The optional routes dict maps that string to a node name.
If no mapping is found, the graph terminates via the END sentinel.
Recipe 4 — @agent_node Decorator¶
No
build_graph()needed — use decorators to wire the graph automatically.
graph TD
START(["▶ Start"]) --> step_one
step_one["Step One — validate"]
step_one --> step_two
step_two["Step Two — enrich"]
step_two --> step_three
step_three["Step Three — format"]
step_three --> DONE(["✅ Done"])
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from agentomatic.agents import BaseGraphAgent, agent_node
@dataclass
class PipelineState:
"""State flowing through a three-step pipeline."""
query: str = ""
validated: bool = False
enriched_data: str = ""
response: str = ""
class DecoratorAgent(BaseGraphAgent[PipelineState]):
"""Three-step pipeline using only @agent_node decorators."""
agent_name = "decorator_agent"
agent_description = "Demonstrates decorator-based graph wiring"
agent_framework = "graph_agent"
@agent_node(entrypoint=True)
def step_one(self, state: PipelineState) -> PipelineState:
"""Validate input."""
state.validated = len(state.query.strip()) > 0
return state
@agent_node(after="step_one")
def step_two(self, state: PipelineState) -> PipelineState:
"""Enrich with additional data."""
if state.validated:
state.enriched_data = f"[enriched] {state.query.upper()}"
else:
state.enriched_data = "[no input to enrich]"
return state
@agent_node(after="step_two", finish=True)
def step_three(self, state: PipelineState) -> PipelineState:
"""Format the final response."""
state.response = f"Result: {state.enriched_data}"
return state
def input_to_state(self, input_data: dict[str, Any]) -> PipelineState:
return PipelineState(query=input_data.get("current_query", ""))
def state_to_output(self, state: PipelineState) -> dict[str, Any]:
return {
"response": state.response,
"validated": state.validated,
}
# --- Usage ---
agent = DecoratorAgent()
result = agent.transform({"current_query": "decorate me"})
print(result)
# {"response": "Result: [enriched] DECORATE ME", "validated": True}
Decorator parameters reference
| Parameter | Type | Description |
|---|---|---|
name |
str | None |
Override node name (defaults to method name) |
after |
str | None |
Predecessor node — creates an edge |
entrypoint |
bool |
Mark as graph entry point |
finish |
bool |
Mark as graph finish node |
description |
str | None |
Human-readable description |
metadata |
dict | None |
Arbitrary metadata dict |
Recipe 5 — Custom Schemas¶
Pydantic input/output validation with a
schemas.pymodule.
graph TD
START(["▶ Start"]) --> validate
validate["Validate & parse input"]
validate --> process
process["Process order"]
process --> DONE(["✅ Done"])
from __future__ import annotations
from pydantic import BaseModel, Field
class OrderInput(BaseModel):
"""Validated input for the order-processing agent."""
product_name: str = Field(
..., min_length=1, max_length=200,
description="Name of the product to order",
)
quantity: int = Field(
..., ge=1, le=1000,
description="Number of units",
)
customer_email: str = Field(
..., pattern=r"^[\w.+-]+@[\w-]+\.[\w.]+$",
description="Customer email address",
)
class OrderOutput(BaseModel):
"""Validated output from the order-processing agent."""
order_id: str = Field(
..., description="Generated order ID",
)
total_price: float = Field(
..., ge=0, description="Total price in USD",
)
confirmation: str = Field(
..., description="Human-readable confirmation",
)
from __future__ import annotations
import uuid
from dataclasses import dataclass
from typing import Any
from agentomatic.agents import BaseGraphAgent
from .schemas import OrderInput, OrderOutput
PRICE_CATALOG: dict[str, float] = {
"widget": 9.99,
"gadget": 24.99,
"gizmo": 49.99,
}
@dataclass
class OrderState:
"""Internal processing state."""
raw_input: dict[str, Any] | None = None
validated: OrderInput | None = None
order_id: str = ""
total_price: float = 0.0
response: str = ""
class OrderAgent(BaseGraphAgent[OrderState]):
agent_name = "order_agent"
agent_description = "Processes product orders with validation"
agent_framework = "graph_agent"
def build_graph(self):
g = self.new_graph()
g.add_node("validate", self.validate_input)
g.add_node("process", self.process_order)
g.set_entry_point("validate")
g.add_edge("validate", "process")
g.set_finish_point("process")
return g.compile()
def validate_input(self, state: OrderState) -> OrderState:
"""Parse and validate input through the Pydantic schema."""
state.validated = OrderInput.model_validate(state.raw_input)
return state
def process_order(self, state: OrderState) -> OrderState:
"""Look up price and create the order."""
inp = state.validated
assert inp is not None
unit_price = PRICE_CATALOG.get(inp.product_name.lower(), 19.99)
state.order_id = f"ORD-{uuid.uuid4().hex[:8].upper()}"
state.total_price = round(unit_price * inp.quantity, 2)
state.response = (
f"Order {state.order_id}: "
f"{inp.quantity}x {inp.product_name} = ${state.total_price}"
)
return state
def input_to_state(self, input_data: dict[str, Any]) -> OrderState:
return OrderState(raw_input=input_data)
def state_to_output(self, state: OrderState) -> dict[str, Any]:
return OrderOutput(
order_id=state.order_id,
total_price=state.total_price,
confirmation=state.response,
).model_dump()
# --- Usage ---
agent = OrderAgent()
result = agent.transform({
"product_name": "Widget",
"quantity": 3,
"customer_email": "alice@example.com",
})
print(result)
# {"order_id": "ORD-A1B2C3D4", "total_price": 29.97, "confirmation": "..."}
Validation errors
If OrderInput.model_validate() fails, a Pydantic ValidationError will
propagate as a RuntimeError from the graph runtime — node 'validate'
failed. Catch it in your calling code or add an error-handling node.
Recipe 6 — Prompt Versioning¶
Use
prompts.jsonandPromptManagerto A/B test prompt versions.
graph TD
START(["▶ Start"]) --> load_prompt
load_prompt["Load versioned prompt"]
load_prompt --> generate
generate["Generate response with prompt"]
generate --> DONE(["✅ Done"])
{
"v1": {
"system": "You are a helpful assistant. Be concise.",
"user_template": "Answer this question: {query}"
},
"v2": {
"system": "You are an expert assistant. Provide detailed, structured answers with examples.",
"user_template": "## Question\n{query}\n\n## Instructions\nProvide a thorough answer with examples."
}
}
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from agentomatic.agents import BaseGraphAgent
from agentomatic.prompts import PromptManager
@dataclass
class PromptState:
"""State with prompt versioning support."""
query: str = ""
prompt_version: str = "v1"
system_prompt: str = ""
user_prompt: str = ""
response: str = ""
class VersionedPromptAgent(BaseGraphAgent[PromptState]):
agent_name = "versioned_prompt"
agent_description = "Demonstrates prompt versioning"
agent_framework = "graph_agent"
def __init__(
self,
*,
prompts_file: str | Path = "prompts.json",
) -> None:
super().__init__()
self.prompt_manager = PromptManager(
agent_name=self.agent_name,
prompts_file=prompts_file,
)
def build_graph(self):
g = self.new_graph()
g.add_node("load_prompt", self.load_prompt)
g.add_node("generate", self.generate)
g.set_entry_point("load_prompt")
g.add_edge("load_prompt", "generate")
g.set_finish_point("generate")
return g.compile()
def load_prompt(self, state: PromptState) -> PromptState:
"""Load the system and user prompts for the requested version."""
version = state.prompt_version
system = self.prompt_manager.get_prompt(
version=version,
prompt_type="system",
)
user = self.prompt_manager.format_prompt(
version=version,
prompt_type="user_template",
query=state.query,
)
state.system_prompt = system or "You are a helpful assistant."
state.user_prompt = user or state.query
return state
def generate(self, state: PromptState) -> PromptState:
"""Generate response (replace with real LLM call)."""
state.response = (
f"[System: {state.system_prompt[:50]}...]\n"
f"[User: {state.user_prompt[:80]}...]\n"
f"Generated answer for: {state.query}"
)
return state
def input_to_state(self, input_data: dict[str, Any]) -> PromptState:
return PromptState(
query=input_data.get("current_query", ""),
prompt_version=input_data.get("prompt_version", "v1"),
)
def state_to_output(self, state: PromptState) -> dict[str, Any]:
return {
"response": state.response,
"prompt_version": state.prompt_version,
}
# --- Usage ---
agent = VersionedPromptAgent(prompts_file="prompts.json")
versions = agent.prompt_manager.list_versions() # ["v1", "v2"]
result_v1 = agent.transform({
"current_query": "What is RAG?",
"prompt_version": "v1",
})
result_v2 = agent.transform({
"current_query": "What is RAG?",
"prompt_version": "v2",
})
PromptManager API quick reference
| Method | Description |
|---|---|
get_prompt(version, prompt_type) |
Get raw template string |
format_prompt(version, prompt_type, **kwargs) |
Format template with variables |
list_versions() |
List all available version keys |
reload(path) |
Hot-reload prompts from disk |
as_langchain_template(version, prompt_type) |
Convert to LangChain PromptTemplate |
as_chat_template(version) |
Convert to LangChain ChatPromptTemplate |
Recipe 7 — Multi-Turn Chat¶
Thread-based conversation with message history.
graph TD
START(["▶ Start"]) --> load_history
load_history["Load thread history"]
load_history --> respond
respond["Generate contextual response"]
respond --> save_history
save_history["Persist to thread"]
save_history --> DONE(["✅ Done"])
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from typing import Any
from agentomatic.agents import BaseGraphAgent
@dataclass
class ChatMessage:
"""A single chat message."""
role: str # "user" or "assistant"
content: str
@dataclass
class ChatState:
"""State for multi-turn chat with thread memory."""
thread_id: str = ""
user_message: str = ""
history: list[ChatMessage] = field(default_factory=list)
response: str = ""
# Simple in-memory thread store (replace with DB in production)
THREAD_STORE: dict[str, list[ChatMessage]] = {}
class MultiTurnChatAgent(BaseGraphAgent[ChatState]):
agent_name = "chat_agent"
agent_description = "Multi-turn chat with thread memory"
agent_framework = "graph_agent"
def build_graph(self):
g = self.new_graph()
g.add_node("load_history", self.load_history)
g.add_node("respond", self.respond)
g.add_node("save_history", self.save_history)
g.set_entry_point("load_history")
g.add_edge("load_history", "respond")
g.add_edge("respond", "save_history")
g.set_finish_point("save_history")
return g.compile()
def load_history(self, state: ChatState) -> ChatState:
"""Load existing conversation from the thread store."""
if not state.thread_id:
state.thread_id = f"thread-{uuid.uuid4().hex[:8]}"
state.history = list(THREAD_STORE.get(state.thread_id, []))
return state
def respond(self, state: ChatState) -> ChatState:
"""Generate a response considering conversation history."""
turn_count = len(state.history) // 2 + 1
history_summary = (
f" (continuing turn {turn_count})"
if state.history
else " (new conversation)"
)
state.response = (
f"I understand you said: '{state.user_message}'"
f"{history_summary}. "
f"Previous messages: {len(state.history)}."
)
return state
def save_history(self, state: ChatState) -> ChatState:
"""Persist the new messages to the thread store."""
if state.thread_id not in THREAD_STORE:
THREAD_STORE[state.thread_id] = []
THREAD_STORE[state.thread_id].append(
ChatMessage(role="user", content=state.user_message)
)
THREAD_STORE[state.thread_id].append(
ChatMessage(role="assistant", content=state.response)
)
return state
def input_to_state(self, input_data: dict[str, Any]) -> ChatState:
return ChatState(
thread_id=input_data.get("thread_id", ""),
user_message=input_data.get("current_query", ""),
)
def state_to_output(self, state: ChatState) -> dict[str, Any]:
return {
"response": state.response,
"thread_id": state.thread_id,
"message_count": len(
THREAD_STORE.get(state.thread_id, [])
),
}
# --- Usage ---
agent = MultiTurnChatAgent()
r1 = agent.transform({"current_query": "Hi, I'm Alice"})
tid = r1["thread_id"] # save for follow-ups
r2 = agent.transform({
"current_query": "What's the weather?",
"thread_id": tid,
})
print(r2)
# {"response": "... (continuing turn 2). Previous messages: 2.", ...}
Production thread storage
Replace the THREAD_STORE dict with a database or use the built-in
thread endpoints (POST /threads, GET /threads/{tid}/messages)
provided by agentomatic's router factory.
Recipe 8 — Human-in-the-Loop¶
Suspend execution for manual approval using
AgentSuspendedException.
graph TD
START(["▶ Start"]) --> draft
draft["Draft response"]
draft --> review
review["Review gate"]
review -->|"approved"| finalize
review -->|"suspended"| PAUSE(["⏸ Awaiting approval"])
finalize["Finalize & send"]
finalize --> DONE(["✅ Done"])
from __future__ import annotations
import uuid
from dataclasses import dataclass
from typing import Any
from agentomatic.agents import BaseGraphAgent
from agentomatic.core import AgentSuspendedException
@dataclass
class ApprovalState:
"""State for a human-in-the-loop approval workflow."""
query: str = ""
draft: str = ""
approved: bool = False
approval_id: str = ""
response: str = ""
class HumanInTheLoopAgent(BaseGraphAgent[ApprovalState]):
agent_name = "hitl_agent"
agent_description = "Requires human approval before sending"
agent_framework = "graph_agent"
def __init__(self, *, auto_approve: bool = False) -> None:
super().__init__()
self.auto_approve = auto_approve
def build_graph(self):
g = self.new_graph()
g.add_node("draft", self.create_draft)
g.add_node("review", self.review_gate)
g.add_node("finalize", self.finalize)
g.set_entry_point("draft")
g.add_edge("draft", "review")
g.add_edge("review", "finalize")
g.set_finish_point("finalize")
return g.compile()
def create_draft(self, state: ApprovalState) -> ApprovalState:
"""Create a draft response for review."""
state.draft = (
f"Dear Customer,\n\n"
f"Regarding your inquiry: {state.query}\n\n"
f"We would like to offer a full refund.\n\n"
f"Best regards,\nSupport Team"
)
state.approval_id = f"approval-{uuid.uuid4().hex[:8]}"
return state
def review_gate(self, state: ApprovalState) -> ApprovalState:
"""Gate that suspends execution if not pre-approved."""
if self.auto_approve or state.approved:
return state
# Suspend execution — the platform stores the state
# and exposes it via GET /threads/{tid}/pending
raise AgentSuspendedException(
approval_id=state.approval_id,
node_name="review",
state_snapshot={
"draft": state.draft,
"query": state.query,
"approval_id": state.approval_id,
},
message=(
f"Draft response requires approval. "
f"Review ID: {state.approval_id}"
),
)
def finalize(self, state: ApprovalState) -> ApprovalState:
"""Finalize the approved response."""
state.response = f"[APPROVED] {state.draft}"
return state
def input_to_state(self, input_data: dict[str, Any]) -> ApprovalState:
return ApprovalState(
query=input_data.get("current_query", ""),
approved=input_data.get("approved", False),
)
def state_to_output(self, state: ApprovalState) -> dict[str, Any]:
return {
"response": state.response,
"approval_id": state.approval_id,
}
# --- Usage (auto-approve for demo) ---
agent = HumanInTheLoopAgent(auto_approve=True)
result = agent.transform({"current_query": "I want a refund"})
print(result["response"]) # "[APPROVED] Dear Customer, ..."
Suspension in production
When AgentSuspendedException is raised, the platform catches it and:
- Stores the
state_snapshotin the thread's pending queue - Returns an HTTP 202 Accepted to the caller
- Exposes
GET /threads/{tid}/pendingfor the reviewer POST /threads/{tid}/approveresumes execution from the suspended nodePOST /threads/{tid}/rejectdiscards the pending state
Recipe 9 — ML Plugin¶
Wrap a scikit-learn model with
BaseMLPluginfor auto-generated REST APIs.
graph TD
START(["▶ Start"]) --> load["load_model()"]
load --> predict["predict(inputs)"]
predict --> DONE(["✅ Done"])
from __future__ import annotations
from pydantic import BaseModel, Field
from agentomatic.plugins.ml import BaseMLPlugin
# --- Pydantic schemas ---
class IrisInput(BaseModel):
"""Input features for iris classification."""
sepal_length: float = Field(..., ge=0, le=10)
sepal_width: float = Field(..., ge=0, le=10)
petal_length: float = Field(..., ge=0, le=10)
petal_width: float = Field(..., ge=0, le=10)
class IrisOutput(BaseModel):
"""Prediction result."""
species: str
confidence: float = Field(..., ge=0, le=1)
class_probabilities: dict[str, float]
# --- Plugin ---
class IrisClassifier(BaseMLPlugin[IrisInput, IrisOutput]):
"""Scikit-learn iris classifier exposed as an agentomatic plugin."""
plugin_name = "iris_classifier"
plugin_description = "Classifies iris flowers by species"
plugin_version = "1.0.0"
def __init__(self) -> None:
super().__init__()
self._model = None
self._label_names = ["setosa", "versicolor", "virginica"]
async def load_model(self) -> None:
"""Train a simple model on the iris dataset."""
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
data = load_iris()
self._model = RandomForestClassifier(
n_estimators=100, random_state=42,
)
self._model.fit(data.data, data.target)
self._is_loaded = True
async def predict(self, inputs: IrisInput) -> IrisOutput:
"""Run inference on the loaded model."""
assert self._model is not None, "Call load_model() first"
features = [[
inputs.sepal_length,
inputs.sepal_width,
inputs.petal_length,
inputs.petal_width,
]]
prediction = self._model.predict(features)[0]
probabilities = self._model.predict_proba(features)[0]
return IrisOutput(
species=self._label_names[prediction],
confidence=float(max(probabilities)),
class_probabilities={
name: round(float(prob), 4)
for name, prob in zip(
self._label_names, probabilities
)
},
)
def model_card(self) -> dict:
"""Return metadata about the model."""
base = super().model_card()
base.update({
"algorithm": "RandomForestClassifier",
"n_estimators": 100,
"features": [
"sepal_length", "sepal_width",
"petal_length", "petal_width",
],
"labels": self._label_names,
})
return base
import asyncio
from plugin import IrisClassifier, IrisInput
async def main():
clf = IrisClassifier()
await clf.load_model()
# Check schemas
print(clf.get_input_schema().model_json_schema())
print(clf.get_output_schema().model_json_schema())
# Predict
result = await clf.predict(IrisInput(
sepal_length=5.1,
sepal_width=3.5,
petal_length=1.4,
petal_width=0.2,
))
print(result.species) # "setosa"
print(result.confidence) # 0.99
# Model card
print(clf.model_card())
asyncio.run(main())
Auto-generated REST endpoints
When registered with agentomatic, the plugin automatically gets:
POST /predict— callspredict()with validated inputGET /model-card— returnsmodel_card()metadataGET /health— checksis_loadedstatus
Recipe 10 — Evaluate / Optimize¶
Full ML-like pipeline:
compile() → fit() → evaluate().
graph TD
subgraph Pipeline
A["compile(dataset, metrics, optimizer)"] --> B["fit(dataset)"]
B --> C["evaluate(test_set, metrics)"]
C --> D["save(path)"]
end
subgraph Agent Graph
E["extract"] --> F["analyze"]
end
A -.->|"configures"| E
B -.->|"optimizes"| E
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from agentomatic.agents import (
AgentDataset,
AgentExample,
BaseGraphAgent,
ExactKeyMatchMetric,
GridSearchOptimizer,
)
@dataclass
class AnalysisState:
"""State for a text analysis agent."""
query: str = ""
response: str = ""
summary: str = ""
sentiment: str = ""
class AnalysisAgent(BaseGraphAgent[AnalysisState]):
"""Agent with tunable parameters for optimization."""
agent_name = "analysis_agent"
agent_description = "Analyzes text with configurable behavior"
agent_framework = "graph_agent"
def __init__(self) -> None:
super().__init__()
# Tunable parameters — GridSearchOptimizer can set these
self.detail_level: str = "brief"
self.include_sentiment: bool = True
def build_graph(self):
g = self.new_graph()
g.add_node("extract", self.extract)
g.add_node("analyze", self.analyze)
g.set_entry_point("extract")
g.add_edge("extract", "analyze")
g.set_finish_point("analyze")
return g.compile()
def extract(self, state: AnalysisState) -> AnalysisState:
"""Extract key information from the query."""
words = state.query.split()
state.summary = (
" ".join(words[:5]) + "..."
if len(words) > 5
else state.query
)
return state
def analyze(self, state: AnalysisState) -> AnalysisState:
"""Analyze text based on current configuration."""
parts = [f"Summary: {state.summary}"]
if self.detail_level == "detailed":
parts.append(
f"Word count: {len(state.query.split())}"
)
if self.include_sentiment:
# Simple heuristic sentiment
positive = {"good", "great", "love", "excellent"}
negative = {"bad", "terrible", "hate", "awful"}
words = set(state.query.lower().split())
if words & positive:
state.sentiment = "positive"
elif words & negative:
state.sentiment = "negative"
else:
state.sentiment = "neutral"
parts.append(f"Sentiment: {state.sentiment}")
state.response = " | ".join(parts)
return state
def input_to_state(
self, input_data: dict[str, Any],
) -> AnalysisState:
return AnalysisState(
query=input_data.get("current_query", ""),
)
def state_to_output(
self, state: AnalysisState,
) -> dict[str, Any]:
return {
"response": state.response,
"summary": state.summary,
"sentiment": state.sentiment,
}
# ============================================================
# Build dataset
# ============================================================
dataset = AgentDataset.from_list(
[
{
"id": "ex_001",
"input": {"current_query": "This product is great and I love it"},
"expected_output": {
"response": "...",
"summary": "...",
"sentiment": "positive",
},
"split": "train",
},
{
"id": "ex_002",
"input": {"current_query": "The service was terrible today"},
"expected_output": {
"response": "...",
"summary": "...",
"sentiment": "negative",
},
"split": "train",
},
{
"id": "ex_003",
"input": {"current_query": "I placed an order yesterday"},
"expected_output": {
"response": "...",
"summary": "...",
"sentiment": "neutral",
},
"split": "test",
},
],
name="sentiment_data",
)
# ============================================================
# Metrics
# ============================================================
metrics = [
ExactKeyMatchMetric(
["response", "summary", "sentiment"],
name="has_all_keys",
),
]
# ============================================================
# compile() → fit() → evaluate() → save()
# ============================================================
agent = AnalysisAgent()
# 1. Compile — configure the optimization strategy
agent.compile(
dataset=dataset,
metrics=metrics,
optimizer=GridSearchOptimizer(
param_grid={
"detail_level": ["brief", "detailed"],
"include_sentiment": [True, False],
},
max_examples=10,
),
)
# 2. Fit — run the optimizer and apply best config
agent.fit(dataset)
# 3. Evaluate on test split
report = agent.evaluate(dataset.test, metrics)
print(report.summary())
# Evaluation Report: analysis_agent
# Dataset: inline
# Examples: 1
# Pass Rate: 100.0%
# Scores:
# has_all_keys: 1.000
# 4. Save compiled state
agent.save("./checkpoints/analysis_agent")
# 5. Inspect traces
for event in agent.get_last_trace():
print(f" {event.node_name}: {event.status} ({event.duration_ms:.1f}ms)")
The ML-like lifecycle
| Method | Purpose |
|---|---|
compile(dataset, metrics, optimizer) |
Set optimization strategy |
fit(dataset) |
Run the optimizer, apply best config |
evaluate(examples, metrics) |
Score predictions on a test set |
save(path) |
Persist config, metadata, evaluation history |
load_compiled(path) |
Restore a previously saved checkpoint |
Loading a saved checkpoint
What's Next?¶
You're ready to build
These 10 recipes cover the most common patterns. Mix and match them — combine RAG with routing, add human-in-the-loop to any agent, or wrap the whole thing in the evaluate/optimize pipeline.
- API Reference — full method signatures for every class
- CLI Guide —
agentomatic init,run,test - Studio Guide — visual debugging dashboard