Try Building a Meal-Planning Agent With Apache Kafka and Flink
Why Event-Driven Multiagents?
At its core, an agent is like a little decision-making robot: It can analyze information, reason based on its environment and take actions.
Agent architecture (Inspired by https://arxiv.org/pdf/2304.03442).

Control logic, programmatic versus agentic.

An example multiagent design pattern called the orchestrator-worker pattern.
Designing an Event-Driven Multiagent Meal Planner
Dinner at my house is a nightly puzzle: two picky toddlers with different preferences, my wife and I trying to eat healthier, and barely enough time to pull it all together before bedtime. For example, my son loves fish but my daughter is allergic, so every meal with fish needs a substitution for her. Meanwhile, they both adore macaroni and cheese, but my wife and I aim for meals with a better balance of protein, carbs and fats. Add in the constraints of a busy life and the problem starts to become stressful to deal with weekly when relying on manual planning. To tackle this, I designed a multiagent system — a team of AI experts, each specializing in a specific task. Here’s how it works:- Child meal-planning agent: This agent focuses on creating nutritious, kid-friendly meals that my toddlers will actually eat.
- Adult meal-planning agent: This agent is an expert in planning meals for couples that are high in protein, low-glycemic and carb-conscious — but still tasty.
- Shared preferences agent: Combines the child and adult meal plans into one cohesive menu, factoring in allergies and shared ingredients.
- Format output agent: This agent takes the finalized meal plan, adds a grocery list and reformats it into a structured grocery list in JSON.
Designing the User Interface
The web application is a standard three-tier architecture built with Next.js for the frontend and MongoDB as the application database. It’s intentionally kept simple and doesn’t include any direct AI logic or knowledge of Kafka. Its primary role is to let users configure their meal-planning settings, such as their kids’ likes and dislikes, and submit requests for new meal plans.
An example set of meal preferences.
Creating the Multiagent Workflow
To coordinate a multiagent system effectively, you need a shared language — a structured way for agents to exchange information, interpret commands and collaborate. In an event-driven architecture, events serve as this shared language, acting as structured updates that keep agents aligned, responsive and adaptable to change. Think of it as the system’s group chat, where agents broadcast updates, share context and perform tasks independently while staying synchronized. Kafka provides the backbone for this communication. Agents produce and consume events from Kafka topics, allowing them to react in real time to changes like new preferences or even new agents. Apache Flink adds the ability to process these streams, enabling complex reasoning and transformations on the fly. If you’re unfamiliar with Flink, it’s an open source stream processing framework built for handling large volumes of data in real time, and is ideal for high-throughput, low-latency applications. Flink is perfect for AI applications. With Confluent’s platform simplifying the management and scaling of this architecture, we can build a reliable, event-driven multiagent workflow that handles real-world complexities gracefully. The following diagram illustrates how I used these tools in Confluent Cloud to enable seamless and scalable collaboration across the agents.
Meal Planner AI architecture diagram.
CREATE TABLE `default`.`dev-us-east-1-cluster`.`meal-planner.output.joined-preferences` (
`request_id` STRING,
`child_preference` STRING,
`adult_preference` STRING
) WITH (
'value.format' = 'json-registry'
);
INSERT INTO `meal-planner.output.joined-preferences`
SELECT c.`request_id`, c.content as child_preference, a.content as adult_preference FROM `meal-planner.output.child-preferences` c
JOIN `meal-planner.output.adult-preferences` a ON c.`request_id` = a.`request_id`;
Creating a Meal Plan for Children and Adults
The child and adult meal-planning agents follow the “ReAct” design pattern. This design pattern combines reasoning and action into a single loop, enabling agents to make decisions and act iteratively based on their current understanding of a task.- Reasoning: The agent analyzes the situation, considers context and determines the next best action using its internal knowledge or external input.
- Action: The agent performs the chosen action, which might involve interacting with the environment, querying data or generating output.
- Feedback loop: The result of the action provides new information, which the agent incorporates into its reasoning for the next iteration.
SYSTEM_PROMPT = """You are an expert at designing nutritious meals that toddlers love.
You will be prompted to generate a weekly dinner plan.
You'll have access to meal preferences. Use these as inspiration to come up with meals but you don't have to explicitly use these items.
You'll have access to recent meals. Factor these in so you aren't repetitive.
You must take into account any hard requirements about meals.
Bias towards meals that can be made in less than 30 minutes. Keep meal preparation simple.
There is no human in the loop, so don't prompt for additional input.
"""
SYSTEM_PROMPT = """You are an expert at designing high protein, low glycemic, low carb dinners for couples.
You will be prompted to generate a weekly dinner plan.
You'll have access to recent meals. Factor these in so you aren't repetitive.
Bias towards meals that can be made in less than 30 minutes. Keep meal preparation simple.
There is no human in the loop, so don't prompt for additional input.
"""
@tool
def get_kid_preferences():
"""Use this to get the likes and dislikes for the kids preferences."""
# Connect to the MongoDB instance
client = MongoClient(os.getenv("MONGODB_URI")) # Replace with your MongoDB URI
# Access the database and collection
db = client['meal_planner'] # Database name
collection = db['meal_preferences'] # Collection name
projection = {"likes": 1, "dislikes": 1, "_id": 0}
result = collection.find_one({}, projection)
return result
@tool
def get_hard_requirements():
"""Use this to get the hard requirements for recommending a meal. These must be enforced."""
# Connect to the MongoDB instance
client = MongoClient(os.getenv("MONGODB_URI")) # Replace with your MongoDB URI
# Access the database and collection
db = client['meal_planner'] # Database name
collection = db['meal_preferences'] # Collection name
projection = {"hardRequirements": 1, "_id": 0}
result = collection.find_one({}, projection)
return result
@tool
def get_recent_meals():
"""Use this to get recent meals."""
# Connect to the MongoDB instance
client = MongoClient(os.getenv("MONGODB_URI")) # Replace with your MongoDB URI\
# Access the database and collection
db = client['meal_planner']
collection = db['weekly_meal_plans']
# Query to get the last two entries
recent_meals = list(collection.find().sort([("$natural", -1)]).limit(2))
return recent_meals
Creating a Shared Meal Plan
To merge the outputs of the child and adult meal-planning agents, I used the “reflection” design pattern, a framework that enables generative AI agents to evaluate and improve their outputs iteratively. This pattern operates through three key steps:- Generate: The agent produces an initial output based on its input or task.
- Reflect: The agent critically evaluates the output, comparing it to task requirements or quality benchmarks. This step may involve self-assessment or using another agent for review.
- Revise: Based on the reflection, the agent refines its output, producing a more accurate or polished result.
Implementation
In the shared meal plan agent, I created two tailored prompts to guide the generation and reflection processes:- Generate content prompt:
generate_content_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a meal planning assistant for families."
"Your job is to combine the recommended meal plan for the children and the adults into a singular meal plan that works for the family."
"Aim to minimize creating multiple dishes. Each meal should be able to work for both the adults and kids."
"Make sure you include the same number of meals in the combined plan as in the original plans."
"Output should contain the name of the meal, any modification or version for the children, any modification or version for the adults, core ingredients, prep time, and basic recipe."
"If the user provides critique, respond with a revised version of your previous attempts.",
),
MessagesPlaceholder(variable_name="messages"),
]
)
- Reflection prompt:
reflection_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a family meal planning expert grading the quality of the recommended meals on taste, variety, and nutritional value."
"Generate critique and recommendations for the user's submission."
"Provide detailed recommendations, including requests for greater variety, tastier meals, or higher nutrional value.",
),
MessagesPlaceholder(variable_name="messages"),
]
)
builder = StateGraph(State)
builder.add_node("generate", generation_node)
builder.add_node("reflect", reflection_node)
builder.add_edge(START, "generate")
def should_continue(state: State):
if len(state["messages"]) > MAX_ITERATIONS:
return END
return "reflect"
builder.add_conditional_edges("generate", should_continue)
builder.add_edge("reflect", "generate")
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)