Building a Multi-Agent Supply Chain Simulation with SmolAgents Library from Hugging Face
- Rifx.Online
- Programming , Technology , Data Science
- 11 Jan, 2025
“smolagents — a smol library to build great agents”
Supply chain management is a complex domain where multiple entities need to coordinate effectively to deliver products to end consumers. Modern supply chains are intricate networks involving suppliers, manufacturers, distributors, and retailers. Simulating these networks can help us understand bottlenecks, optimize operations, and improve efficiency.
In this article, we’ll explore how to build a sophisticated supply chain simulation using SmolAgents, a powerful library for creating multi-agent systems, that includes:
- Dynamic supply and demand management
- Real-time performance metrics
- Cost calculations
- Backorder tracking
- Demand forecasting
What are Agents?
Any efficient system using AI will need to provide LLMs some kind of access to the real world: for instance the possibility to call a search tool to get external information, or to act on certain programs in order to solve a task. In other words, LLMs should have agency. Agentic programs are the gateway to the outside world for LLMs.
AI Agents are programs where LLM outputs control the workflow.
Now, let’s start building a supply chain simulation.
Setting Up the Environment
1. Create a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
- Install requirements:
pip install smolagents==0.1.0
- Run the simulation:
python main.py
## System Architecture
This supply chain simulation consists of multiple interconnected components working together to model real\-world supply chain dynamics. Our system consists of four main agents, each represented by a Tool class in SmolAgent:
1. **Supplier Agent**: Manages raw material supply
2. **Manufacturer Agent**: Converts raw materials into finished goods
3. **Distributor Agent**: Handles logistics and distribution
4. **Retail Agent**: Manages customer\-facing operations
![](https://wsrv.nl/?url=https://cdn-images-1.readmedium.com/v2/resize:fit:800/0*fEAT0jK9TfmDiauA)
## Technical Deep Dive
Each agent in our system is implemented as a SmolAgent Tool. Let’s look at the detailed implementation:
```python
class SupplyTool(Tool):
name = "supply_tool"
description = "Supplies raw materials to the manufacturer."
inputs = {
"demand": {"type": "number", "description": "Demand from manufacturer"},
"inventory": {"type": "number", "description": "Supplier's current inventory"}
}
output_type = "number"
def forward(self, demand: int, inventory: int) -> int:
supply = min(inventory, demand)
return supply
Each agent is designed to handle specific responsibilities while maintaining coordination with other agents in the system.
1. Demand Forecasting
We implemented a sophisticated demand forecasting system using exponential smoothing:
class DemandForecast:
def __init__(self, window_size=10):
self.demand_history = deque(maxlen=window_size)
self.window_size = window_size
def update(self, actual_demand):
self.demand_history.append(actual_demand)
def forecast(self):
if len(self.demand_history) < 2:
return 30 # Default demand if not enough history
# Simple exponential smoothing with trend
alpha = 0.3 # Smoothing factor
trend = (sum(np.diff(list(self.demand_history))) / (len(self.demand_history) - 1))
last_demand = self.demand_history[-1]
forecast = last_demand + alpha * trend
return max(int(forecast), 0)
2. Performance Metrics
The simulation tracks key performance indicators (KPIs):
class PerformanceMetrics:
def __init__(self):
self.total_demand = 0
self.fulfilled_demand = 0
self.backorders = 0
self.inventory_history = []
self.total_costs = 0
def update_fill_rate(self, demand, fulfilled):
self.total_demand += demand
self.fulfilled_demand += fulfilled
self.backorders += demand - fulfilled
def update_inventory(self, inventory_levels):
# Calculate total inventory across all stages
total_inventory = sum(inventory_levels.values())
self.inventory_history.append(total_inventory)
def update_costs(self, new_costs):
self.total_costs += new_costs
def calculate_metrics(self):
fill_rate = (self.fulfilled_demand / self.total_demand * 100) if self.total_demand > 0 else 0
avg_inventory = sum(self.inventory_history) / len(self.inventory_history) if self.inventory_history else 1
inventory_turnover = self.fulfilled_demand / avg_inventory if avg_inventory > 0 else 0
return {
"fill_rate": round(fill_rate, 2),
"inventory_turnover": round(inventory_turnover, 2),
"backorders": self.backorders,
"total_costs": round(self.total_costs, 2),
"average_inventory": round(avg_inventory, 2)
}
3. Cost Management
We track various costs throughout the supply chain:
costs = {
"raw_material": 10,
"manufacturing": 15,
"distribution": 5,
"holding": 2,
"backorder": 20
}
4. Managing State and Updates
One of the key challenges in building this simulation was managing state updates across all agents. Here’s how we handle it:
def calculate_daily_costs(state, supply, production, distribution):
daily_costs = (
supply * costs["raw_material"] +
production * costs["manufacturing"] +
distribution * costs["distribution"] +
(state["supplier_inventory"] + state["manufacturer_inventory"] +
state["distributor_inventory"] + state["retail_inventory"]) * costs["holding"] +
state["backorders"] * costs["backorder"]
)
return daily_costs
5. Handling Backorders
The system tracks and manages backorders to ensure customer demand is eventually met:
## Update backorders
new_backorders = total_demand - fulfilled_demand
state["backorders"] = new_backorders
## Update metrics
metrics.update_fill_rate(initial_demand, fulfilled_demand)
6. Dynamic Inventory Management
The simulation includes dynamic inventory adjustments:
## Daily updates
state["manufacturer_capacity"] = 50
state["supplier_inventory"] += random.randint(10, 20)
state["retailer_customer_demand"] = max(30 + random.randint(-5, 5), 0)
The Simulation Loop
The heart of our system is the simulation loop that coordinates all agents:
- Update demand forecast
- Process supplier operations
- Handle manufacturing
- Manage distribution
- Process retail operations
- Update metrics and state
for step in range(5):
print(f"\nStep {step + 1}")
print("Starting state:", state)
initial_demand = state["retailer_customer_demand"]
# 1. Update demand forecast
state["forecast_demand"] = demand_forecast.forecast()
print(f"Forecast demand: {state['forecast_demand']}")
# 2. Supply raw materials
manufacturer_demand = max(state["forecast_demand"] - state["manufacturer_inventory"], 0)
supply = supply_tool.forward(manufacturer_demand, state["supplier_inventory"])
state["supplier_inventory"] -= supply
print_state_changes(state, step, "Supply", {"Raw materials supplied": supply})
time.sleep(0.5)
# 3. Manufacturing
production = manufacture_tool.forward(
raw_material=supply,
capacity=state["manufacturer_capacity"],
demand=manufacturer_demand
)
state["manufacturer_capacity"] -= production
state["manufacturer_inventory"] += production
print_state_changes(state, step, "Production", {"Goods manufactured": production})
# 4. Distribution
distributor_intake = min(state["manufacturer_inventory"], 50 - state["distributor_inventory"])
state["manufacturer_inventory"] -= distributor_intake
state["distributor_inventory"] += distributor_intake
retail_supply = distribute_tool.forward(
inventory=state["distributor_inventory"],
demand=state["retailer_customer_demand"] + state["backorders"]
)
state["distributor_inventory"] -= retail_supply
state["retail_inventory"] += retail_supply
# 5. Retail sales and backorder management
total_demand = state["retailer_customer_demand"] + state["backorders"]
fulfilled_demand = retail_tool.forward(
customer_demand=total_demand,
available_stock=state["retail_inventory"]
)
state["retail_inventory"] -= fulfilled_demand
# Update backorders
new_backorders = total_demand - fulfilled_demand
state["backorders"] = new_backorders
# Update metrics
metrics.update_fill_rate(initial_demand, fulfilled_demand)
metrics.update_inventory({
"supplier": state["supplier_inventory"],
"manufacturer": state["manufacturer_inventory"],
"distributor": state["distributor_inventory"],
"retail": state["retail_inventory"]
})
daily_costs = calculate_daily_costs(state, supply, production, retail_supply)
metrics.update_costs(daily_costs)
# 6. Daily updates
state["manufacturer_capacity"] = 50
state["supplier_inventory"] += random.randint(10, 20)
state["retailer_customer_demand"] = max(30 + random.randint(-5, 5), 0)
demand_forecast.update(state["retailer_customer_demand"])
# Print performance metrics
print("\nPerformance Metrics:")
current_metrics = metrics.calculate_metrics()
for metric, value in current_metrics.items():
print(f"{metric}: {value}")
print("\nEnd state:", state)
Output
Step 1
Starting state: {'supplier_inventory': 100, 'manufacturer_capacity': 50, 'manufacturer_inventory': 0, 'distributor_inventory': 50, 'retailer_customer_demand': 30, 'retail_inventory': 0, 'backorders': 0, 'forecast_demand': 30}
Forecast demand: 30
--- Step 1: Supply ---
Raw materials supplied: 30
--- Step 1: Production ---
Goods manufactured: 30
Performance Metrics:
fill_rate: 100.0
inventory_turnover: 0.25
backorders: 0
total_costs: 1140
average_inventory: 120.0
End state: {'supplier_inventory': 90, 'manufacturer_capacity': 50, 'manufacturer_inventory': 30, 'distributor_inventory': 20, 'retailer_customer_demand': 26, 'retail_inventory': 0, 'backorders': 0, 'forecast_demand': 30}
Step 2
Starting state: {'supplier_inventory': 90, 'manufacturer_capacity': 50, 'manufacturer_inventory': 30, 'distributor_inventory': 20, 'retailer_customer_demand': 26, 'retail_inventory': 0, 'backorders': 0, 'forecast_demand': 30}
Forecast demand: 26
--- Step 2: Supply ---
Raw materials supplied: 0
--- Step 2: Production ---
Goods manufactured: 0
Performance Metrics:
fill_rate: 100.0
inventory_turnover: 0.48
backorders: 0
total_costs: 1498
average_inventory: 117.0
End state: {'supplier_inventory': 100, 'manufacturer_capacity': 50, 'manufacturer_inventory': 0, 'distributor_inventory': 24, 'retailer_customer_demand': 29, 'retail_inventory': 0, 'backorders': 0, 'forecast_demand': 26}
Step 3
Starting state: {'supplier_inventory': 100, 'manufacturer_capacity': 50, 'manufacturer_inventory': 0, 'distributor_inventory': 24, 'retailer_customer_demand': 29, 'retail_inventory': 0, 'backorders': 0, 'forecast_demand': 26}
Forecast demand: 26
--- Step 3: Supply ---
Raw materials supplied: 26
--- Step 3: Production ---
Goods manufactured: 26
Performance Metrics:
fill_rate: 100.0
inventory_turnover: 0.78
backorders: 0
total_costs: 2483
average_inventory: 109.67
End state: {'supplier_inventory': 87, 'manufacturer_capacity': 50, 'manufacturer_inventory': 0, 'distributor_inventory': 21, 'retailer_customer_demand': 28, 'retail_inventory': 0, 'backorders': 0, 'forecast_demand': 26}
Step 4
Starting state: {'supplier_inventory': 87, 'manufacturer_capacity': 50, 'manufacturer_inventory': 0, 'distributor_inventory': 21, 'retailer_customer_demand': 28, 'retail_inventory': 0, 'backorders': 0, 'forecast_demand': 26}
Forecast demand: 27
--- Step 4: Supply ---
Raw materials supplied: 27
--- Step 4: Production ---
Goods manufactured: 27
Performance Metrics:
fill_rate: 100.0
inventory_turnover: 1.11
backorders: 0
total_costs: 3458
average_inventory: 102.25
End state: {'supplier_inventory': 72, 'manufacturer_capacity': 50, 'manufacturer_inventory': 0, 'distributor_inventory': 20, 'retailer_customer_demand': 26, 'retail_inventory': 0, 'backorders': 0, 'forecast_demand': 27}
Step 5
Starting state: {'supplier_inventory': 72, 'manufacturer_capacity': 50, 'manufacturer_inventory': 0, 'distributor_inventory': 20, 'retailer_customer_demand': 26, 'retail_inventory': 0, 'backorders': 0, 'forecast_demand': 27}
Forecast demand: 27
--- Step 5: Supply ---
Raw materials supplied: 27
--- Step 5: Production ---
Goods manufactured: 27
Performance Metrics:
fill_rate: 100.0
inventory_turnover: 1.46
backorders: 0
total_costs: 4395
average_inventory: 95.0
End state: {'supplier_inventory': 65, 'manufacturer_capacity': 50, 'manufacturer_inventory': 0, 'distributor_inventory': 21, 'retailer_customer_demand': 33, 'retail_inventory': 0, 'backorders': 0, 'forecast_demand': 27}
Final Simulation Metrics:
fill_rate: 100.0
inventory_turnover: 1.46
backorders: 0
total_costs: 4395
average_inventory: 95.0
Model used in Multi-Agent Supply Chain Simulation
Current Model: Llama 3.3–70B-Instruct
In our implementation, we use the meta-llama/Llama-3.3-70B-Instruct
model through HuggingFace’s API. This model serves as the central coordinator for our multi-agent system.
Key Characteristics
- Model Size: 70 billion parameters
- Architecture: Decoder-only transformer
- Training: Instruction-tuned on diverse datasets
- Specialization: Strong at following instructions and coordinating complex tasks
- Context Length: 4096 tokens
- Response Style: Structured and instruction-following
Why We Chose This Model
- Instruction Following: Excellent at understanding and executing complex supply chain instructions
- Context Awareness: Can maintain context across multiple agent interactions
- Output Quality: Provides consistent and well-structured responses
- Performance: Good balance between model size and inference speed
Model Selection Criteria
Implementation Considerations
- Model Loading
from smolagents import HfApiModel
def initialize_model(model_type="large"):
model_configs = {
"large": "meta-llama/Llama-3.3-70B-Instruct",
"medium": "meta-llama/Llama-2-13b-chat",
"small": "meta-llama/Llama-2-7b-chat"
}
return HfApiModel(model_id=model_configs[model_type])
2. Custom Model Wrapper
class SupplyChainModel:
def __init__(self, base_model, config=None):
self.model = base_model
self.config = config or {}
def process_agent_action(self, agent_type, state, action):
context = self._build_context(agent_type, state)
response = self.model.generate(context)
return self._parse_response(response)
3. Performance Monitoring
class ModelPerformanceTracker:
def __init__(self):
self.response_times = []
self.accuracy_metrics = []
def log_performance(self, start_time, end_time, predicted, actual):
self.response_times.append(end_time - start_time)
self.accuracy_metrics.append(self._calculate_accuracy(predicted, actual))
Future Model Improvements
- Specialized Training
- Fine-tune on supply chain specific data
- Incorporate industry-specific constraints
- Train on historical supply chain data
2. Hybrid Approaches
- Combine machine learning with rule-based systems
- Integrate traditional optimization algorithms
- Use reinforcement learning for dynamic adaptation
3. Model Optimization
- Quantization for faster inference
- Pruning for smaller model size
- Knowledge distillation for efficient deployment
Future Enhancements
The current supply chain system could be extended with:
1. Machine learning-based demand forecasting2. Real-time optimization algorithms3. Integration with external data sources4. More sophisticated cost models5. Visual analytics and reporting
Best Practices and Lessons Learned while working with SmolAgent
1. Type Handling: Use proper type definitions in SmolAgent tools2. Error Handling: Implement robust error checking3. State Management: Keep state updates atomic and consistent4. Performance Monitoring: Track key metrics continuously5. Code Organization: Maintain clear separation of concerns
Conclusion
Building a multi-agent supply chain simulation with SmolAgent demonstrates the power of agent-based modeling in understanding complex systems. The simulation provides a foundation for testing different strategies and optimizing supply chain operations.
Through this implementation, we’ve shown how to:- Create coordinated multi-agent systems- Implement realistic supply chain dynamics- Track and optimize performance metrics- Handle complex state management- Build extensible and maintainable code
Source Code
I am still making some changes in the code. Please comment if you want me to share the complete code on GitHub.
#Python, #SupplyChain, #MultiAgentSystems #SmolAgent #HuggingFace