In this post, we'll build a practical AI-powered meal planning agent using HazelJS. This agent helps users create personalized meal plans, discover recipes, and generate shopping lists—all while demonstrating HazelJS's powerful agent orchestration, RAG capabilities, and production-ready patterns.
What We're Building
Our meal planning agent handles the complete meal planning workflow:
- Dietary Intake: Extracts dietary restrictions, budget, cooking time, and preferences
- Recipe Search: Finds recipes using RAG over a recipe database
- Meal Planning: Creates day-by-day meal plans with balanced nutrition
- Shopping Lists: Generates organized grocery lists with cost estimates
- Nutrition Coaching: Orchestrates the entire workflow via supervisor routing
Why HazelJS?
HazelJS provides a production-ready framework for building AI-native applications. For this project, we leverage:
-
Multi-agent orchestration with
@Agent,@Tool, and@Delegatedecorators - RAG (Retrieval-Augmented Generation) for semantic recipe search
- Supervisor routing to delegate tasks to specialized agents
- Production resilience with retries, circuit breakers, and rate limiting
- Built-in observability via the HazelJS Inspector
- Guardrails for safer AI recommendations
Project Architecture
meal-planning-agent/
├── src/
│ ├── agents/ # Agent implementations
│ │ ├── dietary-intake.agent.ts
│ │ ├── recipe-search.agent.ts
│ │ ├── meal-plan.agent.ts
│ │ ├── shopping-list.agent.ts
│ │ └── nutrition-coach.agent.ts
│ ├── providers/ # LLM and embedding providers
│ │ ├── meal-planning-llm.provider.ts
│ │ └── local-embedding.provider.ts
│ ├── data/ # Recipe fixtures
│ │ └── recipe-fixtures.ts
│ ├── meal/ # Controllers and services
│ │ ├── meal.controller.ts
│ │ └── recipe-kb.service.ts
│ ├── app.module.ts
│ ├── health.controller.ts
│ └── index.ts
├── package.json
├── tsconfig.json
└── README.md
Agent Implementation
Dietary Intake Agent
The DietaryIntakeAgent extracts structured information from natural language:
@Agent({
name: 'DietaryIntakeAgent',
description: 'Extracts dietary restrictions, budget, cooking time, cuisine preferences, and nutritional goals.',
systemPrompt: 'You are a dietary intake specialist. Extract the user profile before any meal plan is created.',
maxSteps: 4,
temperature: 0,
})
@Service()
export class DietaryIntakeAgent {
@Tool({
name: 'extractDietaryProfile',
description: 'Extract structured dietary planning information from a user request.',
parameters: [
{ name: 'message', type: 'string', description: 'The raw user request', required: true },
{ name: 'userId', type: 'string', description: 'User id when available', required: false },
],
})
async extractDietaryProfile(input: { message: string; userId?: string }) {
const dietaryRestrictions = this.extractDietaryRestrictions(input.message);
const budget = this.extractBudget(input.message) ?? 150;
const cookingTime = this.extractCookingTime(input.message) ?? 30;
return {
userId: input.userId ?? 'user-demo',
dietaryRestrictions,
budget,
cookingTime,
// ... more fields
};
}
}
Recipe Search Agent with RAG
The RecipeSearchAgent uses RAG to find recipes semantically:
@Agent({
name: 'RecipeSearchAgent',
description: 'Retrieves recipes from the knowledge base based on dietary restrictions, cuisine, and nutritional goals.',
systemPrompt: 'You are a recipe search specialist. Use retrieved context only and cite source ids.',
enableRAG: true,
ragTopK: 3,
maxSteps: 4,
temperature: 0,
})
@Service()
export class RecipeSearchAgent {
constructor(private readonly knowledgeBase: RecipeKnowledgeBaseService) {}
@Tool({
name: 'searchRecipes',
description: 'Search recipes based on dietary preferences, cuisine, and nutritional goals.',
parameters: [
{ name: 'query', type: 'string', description: 'The recipe search query', required: true },
{ name: 'topK', type: 'number', description: 'Number of recipes to retrieve', required: false },
],
})
async searchRecipes(input: { query: string; topK?: number }) {
return this.knowledgeBase.answer(input.query, input.topK ?? 3);
}
}
Nutrition Coach with Supervisor
The NutritionCoachAgent orchestrates the workflow using delegation:
@Agent({
name: 'NutritionCoachAgent',
description: 'Coordinates dietary intake, recipe retrieval, meal planning, and shopping list creation.',
systemPrompt: 'You are the nutrition coach orchestrator. Extract constraints, retrieve recipes, build meal plans, and create shopping lists.',
maxSteps: 8,
temperature: 0,
})
@Service()
export class NutritionCoachAgent {
@Delegate({
agent: 'DietaryIntakeAgent',
description: 'Extract dietary restrictions, budget, cooking time, and preferences from a user request.',
inputField: 'input',
})
async analyzeMealRequest(input: string): Promise<string> {
return '';
}
@Delegate({
agent: 'RecipeSearchAgent',
description: 'Retrieve recipes based on dietary preferences and nutritional goals.',
inputField: 'input',
})
async getRecipes(input: string): Promise<string> {
return '';
}
// ... more delegates
}
RAG Implementation
We use MemoryVectorStore and RAGPipeline for semantic recipe search:
@Service()
export class RecipeKnowledgeBaseService {
private readonly embeddings = new LocalMealEmbeddingProvider();
private readonly vectorStore = new MemoryVectorStore(this.embeddings);
private readonly rag = new RAGPipeline({
vectorStore: this.vectorStore,
embeddingProvider: this.embeddings,
topK: 3,
});
async answer(query: string, topK = 3) {
const sources = await this.rag.retrieve(query, { topK }, RetrievalStrategy.HYBRID);
return {
answer: sources.map((source) => source.content).join('\n\n'),
sources: sources.map((source) => ({
id: source.id,
score: Number(source.score.toFixed(3)),
cuisine: source.metadata?.cuisine,
dietary: source.metadata?.dietary,
// ... more metadata
})),
};
}
}
Production Configuration
The AgentModule is configured with production-ready features:
@HazelModule({
imports: [
ConfigModule.forRoot({ envFilePath: ['.env', '.env.local'], isGlobal: true }),
CacheModule.forRoot({ strategy: 'memory', isGlobal: true }),
InspectorModule.forRoot({ inspectorBasePath: '/__hazel', developmentOnly: true }),
GuardrailsModule.forRoot({
redactPIIByDefault: true,
blockInjectionByDefault: true,
blockToxicityByDefault: true,
}),
AIModule,
RAGModule,
AgentModule.forRoot({
runtime: {
llmProvider: new MealPlanningLocalLLMProvider(),
defaultMaxSteps: 8,
defaultTimeout: 15000,
enableObservability: true,
enableMetrics: true,
enableRetry: true,
enableCircuitBreaker: true,
rateLimitPerMinute: 120,
},
}),
],
controllers: [HealthController, MealController],
providers: [
RecipeKnowledgeBaseService,
DietaryIntakeAgent,
RecipeSearchAgent,
MealPlanAgent,
ShoppingListAgent,
NutritionCoachAgent,
],
})
export class AppModule {}
Running the Project
# Install dependencies
npm install --legacy-peer-deps
# Build the project
npm run build
# Run eval tests
npm run eval
# Start development server
npm run dev
Testing the API
# Health check
curl http://localhost:3000/health
# Dietary intake
curl -s -X POST http://localhost:3000/meal/intake \
-H 'content-type: application/json' \
-d '{"message":"I am vegetarian, have a $150 weekly budget, can cook 30 mins per meal.","userId":"user-1"}'
# Recipe search with RAG
curl -s -X POST http://localhost:3000/meal/recipes \
-H 'content-type: application/json' \
-d '{"message":"Find high-protein vegetarian recipes"}'
# Supervisor orchestration
curl -s -X POST http://localhost:3000/meal/supervisor \
-H 'content-type: application/json' \
-d '{"message":"I am vegetarian, have a $150 weekly budget, can cook 30 mins per meal. Plan my week with high-protein options.","userId":"user-1"}'
HazelJS Inspector
Access the built-in inspector at http://localhost:3000/__hazel for:
- Real-time agent execution traces
- Tool call logs
- Performance metrics
- Circuit breaker status
- Rate limiter status
Key Takeaways
- Multi-agent architecture: Each agent has a focused responsibility, making the system maintainable and testable
- RAG for knowledge retrieval: Semantic search over recipe database provides accurate, context-aware results
- Supervisor routing: The orchestrator agent delegates tasks to specialists, enabling complex workflows
- Production resilience: Built-in retries, circuit breakers, and rate limiting ensure reliability
- Observability: The inspector provides deep insights into agent behavior and performance
- Guardrails: PII redaction and content safety protect users from inappropriate recommendations
What's Next?
For a production deployment, you would:
- Replace the local LLM provider with OpenAI, Anthropic, or Google Generative AI
- Add persistent storage with @hazeljs/memory for user preferences and meal history
- Implement @hazeljs/flow for advanced workflow orchestration
- Add @hazeljs/pubsub for async meal planning notifications
- Connect to real recipe APIs for dynamic content
Conclusion
This meal planning agent demonstrates how HazelJS enables building sophisticated AI applications with production-ready patterns. The combination of multi-agent orchestration, RAG, and resilience features makes it easy to create reliable, observable, and scalable AI-native applications.
Complete project is available here: Meal Planning Agent
See you next time!
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.