Repository navigation
Expand file tree
/
Copy pathwarehouse.py
More file actions
2442 lines (2034 loc) · 108 KB
/
Copy pathwarehouse.py
File metadata and controls
2442 lines (2034 loc) · 108 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Warehouse Agent Module for Supply Chain Optimization System.
This module implements the Warehouse agent, which serves as an intermediary in the supply
chain between stores and suppliers. The Warehouse manages inventory, accepts orders from
stores, purchases materials from suppliers, and coordinates vehicle delivery logistics.
The module implements dual FIPA Contract Net Protocol roles:
1. **Participant** (with Stores): Responds to store purchase requests
2. **Initiator** (with Suppliers): Initiates purchase requests for restocking
Typical usage example:
```python
from world.graph import Graph
# Initialize graph and warehouse agent
map_graph = Graph()
warehouse = Warehouse(
jid="warehouse1@localhost",
password="warehouse_password",
map=map_graph,
node_id=10,
contact_list=["store1@localhost", "supplier1@localhost", "vehicle1@localhost"],
verbose=True
)
await warehouse.start()
```
"""
import asyncio
import random
import json
import queue
import spade
from spade.agent import Agent
from spade.behaviour import OneShotBehaviour, CyclicBehaviour, PeriodicBehaviour
from spade.message import Message
from spade.template import Template
from spade.presence import PresenceShow
from world.graph import Graph, Edge
from veiculos.veiculos import Order
import config
from logger_utils import MessageLogger, InventoryLogger, OrderLifecycleLogger
class Warehouse(Agent):
"""
Warehouse Agent for Supply Chain Management System.
The Warehouse agent acts as a critical intermediary in the supply chain, managing
inventory, fulfilling store orders, and coordinating with suppliers and vehicles.
It implements dual FIPA Contract Net Protocol roles, acting as both participant
and initiator depending on the interaction context.
FIPA Contract Net Protocol - Dual Role Implementation:
1. **As Participant (with Stores)**:
- **Proposal Phase**: Receives store-buy CFP from stores
- **Proposal Submission**: Sends warehouse-accept (propose) or warehouse-reject (refuse)
- **Result Reception**: Receives store-confirm (accept-proposal) or store-deny (reject-proposal)
- **Execution**: Assigns vehicles for confirmed orders
2. **As Initiator (with Suppliers)**:
- **CFP Phase**: Sends warehouse-buy CFP to suppliers for restocking
- **Proposal Collection**: Receives supplier-accept (propose) or supplier-reject (refuse)
- **Evaluation**: Scores suppliers based on delivery time
- **Award**: Sends warehouse-confirm (accept-proposal) or warehouse-deny (reject-proposal)
Stock Management Strategy:
The warehouse uses a three-tier stock system:
1. **Available Stock** (self.stock): Products available for new orders
2. **Locked Stock** (self.locked_stock): Reserved for pending negotiations
3. **Pending Deliveries** (self.pending_deliveries): Confirmed orders awaiting pickup
ID Encoding System:
Similar to Store agent, uses hierarchical encoding:
- Formula: `agent_type * 100_000_000 + instance_id * 1_000_000 + request_counter`
- Agent type code for Warehouse: 2
- Example: warehouse1's first request = 2_001_000_000
Attributes:
jid (str): Jabber ID of the warehouse agent (e.g., "warehouse1@localhost").
node_id (int): Graph node ID representing the warehouse's physical location.
map (Graph): Reference to the world graph for pathfinding calculations.
contact_list (list[str]): JIDs for stores, suppliers, and vehicles.
verbose (bool): Flag to control debug output verbosity.
id_base (int): Base value for generating unique request IDs.
stock (dict[str, int]): Available inventory (not locked or pending).
locked_stock (dict[str, int]): Stock reserved during store negotiations.
current_capacity (dict[str, int]): Remaining capacity per product.
pending_deliveries (dict[int, Order]): Confirmed orders awaiting vehicle pickup.
vehicle_proposals (dict[int, dict[str, tuple[bool, int]]]): Vehicle delivery proposals.
Format: {order_id: {vehicle_jid: (can_fit, delivery_time)}}
vehicles (list[str]): List of vehicle JIDs discovered from contacts.
suppliers (list[str]): List of supplier JIDs discovered from contacts.
request_counter (int): Monotonic counter for unique request IDs.
current_buy_request (Message): Most recent supplier purchase request.
failed_requests (queue.Queue): Queue of failed supplier requests for retry.
presence_infos (dict[str, str]): Vehicle presence status cache.
current_tick (int): Current simulation time, synchronized by world agent.
Behaviours:
Store Interaction:
- ReceiveBuyRequest: Processes store purchase requests (FIPA participant)
- AcceptBuyRequest: Sends proposal to store (FIPA propose)
- RejectBuyRequest: Sends refusal to store (FIPA refuse)
- ReceiveConfirmationOrDenial: Handles store's award/rejection
- AssignVehicle: Coordinates vehicle assignment for deliveries
- ReceivePresenceInfo: Checks vehicle availability
- ReceiveVehicleProposals: Collects vehicle delivery proposals
- ChooseBestVehicle: Selects optimal vehicle using scoring algorithm
Supplier Interaction:
- BuyMaterial: Initiates purchase requests to suppliers (FIPA initiator)
- CollectSupplierResponses: Coordinator for supplier negotiation
- ReceiveAllSupplierResponses: Worker collecting supplier responses
- SendWarehouseConfirmation: Awards contract to supplier (FIPA accept-proposal)
- SendWarehouseDenial: Rejects supplier proposals (FIPA reject-proposal)
- RetryPreviousBuy: Retries failed supplier requests
- HandleResupply: Monitors stock and triggers restocking
Vehicle & Time Management:
- ReceiveVehicleArrival: Handles vehicle pickup and delivery notifications
- ReceiveTimeDelta: Synchronizes time and applies traffic updates
Message Protocols:
Incoming from Stores:
- store-buy: Purchase request (FIPA CFP)
- store-confirm: Order confirmation (FIPA accept-proposal)
- store-deny: Order rejection (FIPA reject-proposal)
Outgoing to Stores:
- warehouse-accept: Order proposal (FIPA propose)
- warehouse-reject: Order refusal (FIPA refuse)
Outgoing to Suppliers:
- warehouse-buy: Purchase request (FIPA CFP)
- warehouse-confirm: Supplier confirmation (FIPA accept-proposal)
- warehouse-deny: Supplier rejection (FIPA reject-proposal)
Incoming from Suppliers:
- supplier-accept: Material proposal (FIPA propose)
- supplier-reject: Material refusal (FIPA refuse)
Vehicle Communication:
- order-proposal: Request vehicle delivery quote
- vehicle-proposal: Vehicle delivery proposal
- order-confirmation: Confirm vehicle assignment
- vehicle-pickup: Vehicle picks up order
- vehicle-delivery: Vehicle delivers supplies
- presence-info: Request vehicle availability
- presence-response: Vehicle availability status
Example:
```python
warehouse = Warehouse(
jid="warehouse1@localhost",
password="password",
map=world_graph,
node_id=10,
contact_list=["store1@localhost", "supplier1@localhost", "vehicle1@localhost"],
verbose=True
)
await warehouse.start()
```
Note:
The warehouse automatically restocks when inventory falls below
config.WAREHOUSE_RESUPPLY_THRESHOLD, maintaining operational capacity.
"""
# ------------------------------------------
# WAREHOUSE <-> STORE
# ------------------------------------------
class ReceiveBuyRequest(CyclicBehaviour):
"""
Cyclic behaviour that processes incoming purchase requests from stores.
This behaviour implements the proposal phase of the FIPA Contract Net Protocol
where the warehouse acts as a **Participant**. It receives CFP messages from
stores, evaluates inventory availability, and responds with either a proposal
(warehouse-accept) or refusal (warehouse-reject).
FIPA Protocol Phase: **Proposal Phase - Participant Role**
- Role: Participant (Warehouse responding to Store's CFP)
- Receives: store-buy messages (FIPA CFP)
- Evaluates: Stock availability vs requested quantity
- Responds: warehouse-accept (propose) or warehouse-reject (refuse)
Stock Locking Mechanism:
When accepting a request, stock is immediately moved from available to locked:
- Prevents overselling (race condition protection)
- Reserved during negotiation (awaiting confirmation)
- Released if store sends denial or timeout occurs
- Transferred to pending_deliveries if store confirms
Decision Logic:
Accept if: (product in stock) AND (stock[product] >= quantity)
Reject if: Product unavailable OR insufficient quantity
Message Format (store-buy):
Metadata:
- performative: "store-buy"
- request_id: Unique request identifier
Body:
- Format: "{quantity} {product}"
- Example: "50 electronics"
Workflow:
1. **Receive**: Wait up to 20 seconds for store-buy message
2. **Parse**: Extract request_id, quantity, product from message
3. **Evaluate**: Check if product exists and quantity available
4. **Accept Path**:
a. Lock stock: stock[product] -= quantity
b. Update locked_stock: locked_stock[product] += quantity
c. Launch AcceptBuyRequest behaviour (send warehouse-accept)
5. **Reject Path**:
a. Launch RejectBuyRequest behaviour with reason
b. No stock changes
Concurrency Handling:
- Processes multiple requests concurrently (cyclic behaviour)
- Stock locking prevents double-allocation
- Each request launches independent OneShot behaviours
Example Execution:
```
Request Received: "50 electronics" (request_id=1001000000)
Stock Check: electronics: 100 available
Decision: ACCEPT
Actions:
- stock[electronics]: 100 -> 50
- locked_stock[electronics]: 0 -> 50
- Launch AcceptBuyRequest
- Wait for store-confirm or store-deny
```
Note:
This behaviour never terminates (cyclic), continuously listening for
store requests throughout the warehouse's lifecycle.
"""
async def run(self):
agent : Warehouse = self.agent
if agent.verbose:
print("Awaiting buy request...")
msg = await self.receive(timeout=20)
if msg != None:
"""
Messages with metadata ("performative", "store-buy") have body
with the following format:
"product_quantity product_type"
request_id is in metadata["request_id"]
"""
request_id = int(msg.get_metadata("request_id"))
request = msg.body.split(" ")
quant = int(request[0])
product = request[1]
print(f"{agent.jid}> Got request {request_id} for {quant} x{product} from {msg.sender}")
if (product in agent.stock.keys()) and agent.stock[product] >= quant:
accept_behav = agent.AcceptBuyRequest(msg)
if agent.verbose:
print(f"{self.agent.jid}> Locking {quant} of {product}...")
# Log inventory change - stock locking
stock_before = agent.stock[product]
agent.stock[product] -= quant
try:
inventory_logger = InventoryLogger.get_instance()
inventory_logger.log_inventory_change(
agent_jid=str(agent.jid),
agent_type="warehouse",
product=product,
change_type="lock",
quantity=quant,
stock_before=stock_before,
stock_after=agent.stock[product],
timestamp_sim=agent.current_tick
)
except Exception:
pass
if product in agent.locked_stock:
agent.locked_stock[product] += quant
else: agent.locked_stock[product] = quant
if agent.verbose:
print(f"Items locked at {agent.jid}.")
agent.print_stock()
agent.add_behaviour(accept_behav)
else:
# Reject the request - insufficient stock
reject_behav = agent.RejectBuyRequest(msg, reason="insufficient_stock")
agent.add_behaviour(reject_behav)
if agent.verbose:
print(f"{agent.jid}> Could not satisfy request. Rejection sent.")
else:
if agent.verbose:
print(f"{agent.jid}> Did not get any buy requests in 20 seconds.")
class AcceptBuyRequest(OneShotBehaviour):
"""
Behaviour that sends proposal acceptance to store (FIPA propose).
This behaviour implements the proposal submission in the FIPA Contract Net
Protocol where the warehouse, acting as a participant, sends its proposal
to fulfill the store's request. It then sets up to receive the store's
decision (accept-proposal or reject-proposal).
FIPA Protocol Phase: **Proposal Submission - Participant Role**
- Role: Participant (Warehouse proposing to Store)
- Sends: warehouse-accept (FIPA propose)
- Purpose: "I can fulfill your request"
- Next Phase: Await store's award or rejection decision
Attributes:
request_id (int): Unique identifier for this request.
quant (int): Quantity of product being proposed.
product (str): Name of product being proposed.
sender (str): Store JID that initiated the request.
Message Format (warehouse-accept):
Metadata:
- performative: "warehouse-accept" (FIPA propose)
- warehouse_id: This warehouse's JID
- store_id: Requesting store's JID
- node_id: Warehouse's graph location
- request_id: Original request identifier
Body:
- Format: "{quantity} {product}"
- Example: "50 electronics"
Workflow:
1. **Send Proposal**: Construct and send warehouse-accept message
2. **Setup Listener**: Create ReceiveConfirmationOrDenial behaviour
3. **Configure Template**: Filter for this specific store and request
4. **Register Behaviour**: Add listener with template to agent
5. **Non-blocking**: Return immediately (don't wait for response)
Template Configuration:
Filters messages to match:
- warehouse_id: This warehouse
- store_id: Specific store that made request
- request_id: This specific request
Accepts both performatives:
- store-confirm (FIPA accept-proposal)
- store-deny (FIPA reject-proposal)
Example:
```
Input: Store request for "50 electronics"
Proposal Sent: warehouse-accept with body "50 electronics"
Listener Setup: ReceiveConfirmationOrDenial waiting for:
- store-confirm (unlock -> pending delivery) OR
- store-deny (unlock -> available stock)
```
Note:
Stock remains locked until ReceiveConfirmationOrDenial processes
the store's decision or times out.
"""
def __init__(self, msg : Message):
"""
Initialize the AcceptBuyRequest behaviour.
Args:
msg (Message): The store-buy request message to accept.
Contains request_id in metadata and "{quantity} {product}" in body.
"""
super().__init__()
self.request_id = int(msg.get_metadata("request_id"))
request = msg.body.split(" ")
self.quant = int(request[0])
self.product = request[1]
self.sender = msg.sender
async def run(self):
"""
Execute the proposal sending and confirmation listener setup.
This method sends the warehouse-accept (FIPA propose) message to the store
and immediately sets up a listener behaviour to await the store's decision.
The method returns quickly without blocking, allowing the warehouse to
continue processing other requests concurrently.
FIPA Protocol Implementation:
- **Phase**: Proposal Submission
- **Performative**: warehouse-accept (FIPA propose)
- **Content**: "{quantity} {product}"
- **Receiver**: Store that sent the CFP
- **Next Step**: Await store's accept-proposal or reject-proposal
Workflow:
1. **Log Acceptance** (verbose): Print proposal details
2. **Construct Message**: Create warehouse-accept with all metadata
3. **Send Proposal**: Transmit to requesting store
4. **Create Listener**: Initialize ReceiveConfirmationOrDenial behaviour
5. **Setup Template**: Configure filter for this specific negotiation
6. **Register Listener**: Add behaviour with template to agent
7. **Return**: Don't block (listener runs independently)
Template Filtering:
The template ensures the listener only receives messages for THIS
specific negotiation by matching:
- warehouse_id: This warehouse's JID
- store_id: The specific store's JID
- request_id: This specific request's ID
Non-Blocking Design:
The commented-out `await confirm_deny_behav.join()` demonstrates
that we intentionally DON'T wait for confirmation here. This allows
the warehouse to accept multiple store requests concurrently.
Side Effects:
- Sends SPADE message to store
- Adds ReceiveConfirmationOrDenial behaviour to agent
- Prints debug information if verbose enabled
Returns:
None. Completes immediately after setting up the listener.
Example:
```
Input: Request for "50 electronics" from store1
Actions:
1. Send warehouse-accept to store1
2. Setup listener with template matching:
- warehouse_id=warehouse1@localhost
- store_id=store1@localhost
- request_id=1001000000
3. Return (warehouse ready for next request)
Listener waits independently for store's decision
```
"""
agent : Warehouse = self.agent
if agent.verbose:
print(
f"{agent.jid}> Accepted a request from {self.sender}: "
f"id={self.request_id} "
f"quant={self.quant} "
f"product={self.product}"
)
msg = Message(to=self.sender)
msg.set_metadata("performative", "warehouse-accept")
msg.set_metadata("warehouse_id", str(agent.jid))
msg.set_metadata("store_id", str(self.sender))
msg.set_metadata("node_id", str(agent.node_id))
msg.set_metadata("request_id", str(self.request_id))
msg.body = f"{self.quant} {self.product}"
await self.send(msg)
try:
msg_logger = MessageLogger.get_instance()
msg_logger.log_message(
sender=str(self.agent.jid),
receiver=str(msg.to),
message_type="Confirmation",
performative="warehouse-accept",
body=msg.body
)
except Exception:
pass # Don't crash on logging errors
# Wait for either confirmation or denial
confirm_deny_behav = agent.ReceiveConfirmationOrDenial(msg, self.sender)
# Template that matches BOTH store-confirm AND store-deny
template = Template()
template.set_metadata("warehouse_id", str(agent.jid))
template.set_metadata("store_id", str(self.sender))
template.set_metadata("request_id", str(self.request_id))
agent.add_behaviour(confirm_deny_behav, template)
if agent.verbose:
print(f"{agent.jid}> AcceptBuyRequest finished, now waiting for confirmation or denial...")
# Aguardar a confirmação ser recebida antes de terminar
# await confirm_deny_behav.join()
class RejectBuyRequest(OneShotBehaviour):
"""
Behaviour that sends proposal refusal to store (FIPA refuse).
This behaviour implements the refusal response in the FIPA Contract Net Protocol
where the warehouse, acting as a participant, declines to fulfill the store's
request due to insufficient inventory or other constraints.
FIPA Protocol Phase: **Proposal Phase - Refusal**
- Role: Participant (Warehouse declining Store's CFP)
- Sends: warehouse-reject (FIPA refuse)
- Purpose: "I cannot fulfill your request"
- Reason: Typically "insufficient_stock"
- Result: Store will try other warehouses
Attributes:
request_id (int): Unique identifier for this request.
quant (int): Requested quantity that cannot be fulfilled.
product (str): Requested product that is unavailable/insufficient.
sender (str): Store JID that initiated the request.
reason (str): Rejection reason (default: "insufficient_stock").
Message Format (warehouse-reject):
Metadata:
- performative: "warehouse-reject" (FIPA refuse)
- warehouse_id: This warehouse's JID
- store_id: Requesting store's JID
- node_id: Warehouse's graph location
- request_id: Original request identifier
Body:
- Format: "{quantity} {product} {reason}"
- Example: "50 electronics insufficient_stock"
Common Rejection Reasons:
- "insufficient_stock": Not enough product available
- "product_unavailable": Product not carried by this warehouse
- "capacity_exceeded": Order too large for capacity constraints
Example:
```
Request: "100 electronics" (only 50 available)
Decision: REJECT
Message Sent: warehouse-reject
Body: "100 electronics insufficient_stock"
Result: Stock unchanged, store tries other warehouses
```
Note:
Unlike AcceptBuyRequest, this behaviour does NOT set up any listeners
because no further interaction is expected after refusal.
"""
def __init__(self, msg : Message, reason : str = "insufficient_stock"):
"""
Initialize the RejectBuyRequest behaviour.
Args:
msg (Message): The store-buy request message to reject.
reason (str, optional): Reason for rejection. Defaults to "insufficient_stock".
"""
super().__init__()
self.request_id = int(msg.get_metadata("request_id"))
request = msg.body.split(" ")
self.quant = int(request[0])
self.product = request[1]
self.sender = msg.sender
self.reason = reason
async def run(self):
"""
Send warehouse-reject message to requesting store.
FIPA Protocol Implementation:
- Phase: Proposal Refusal
- Performative: warehouse-reject (FIPA refuse)
- Direction: Warehouse → Store
Workflow:
1. **Log Rejection** (verbose): Print rejection details
2. **Construct Message**: Create warehouse-reject with metadata
3. **Include Reason**: Add rejection reason to message body
4. **Send Refusal**: Transmit to requesting store
5. **Complete**: No further interaction (one-shot behaviour)
Side Effects:
- No stock changes (was never locked for insufficient stock)
- No listeners created (conversation ends with refusal)
- Store receives refusal and tries other warehouses
- Prints debug information if verbose enabled
Returns:
None. Completes immediately after sending message.
"""
agent : Warehouse = self.agent
if agent.verbose:
print(
f"{agent.jid}> Rejected request from {self.sender}: "
f"id={self.request_id} "
f"quant={self.quant} "
f"product={self.product} "
f"reason={self.reason}"
)
msg = Message(to=self.sender)
msg.set_metadata("performative", "warehouse-reject")
msg.set_metadata("warehouse_id", str(agent.jid))
msg.set_metadata("store_id", str(self.sender))
msg.set_metadata("node_id", str(agent.node_id))
msg.set_metadata("request_id", str(self.request_id))
msg.body = f"{self.quant} {self.product} {self.reason}"
await self.send(msg)
try:
msg_logger = MessageLogger.get_instance()
msg_logger.log_message(
sender=str(self.agent.jid),
receiver=str(msg.to),
message_type="Confirmation",
performative="warehouse-reject",
body=msg.body
)
except Exception:
pass # Don't crash on logging errors
if agent.verbose:
print(f"{agent.jid}> RejectBuyRequest sent to {self.sender}")
class ReceiveConfirmationOrDenial(OneShotBehaviour):
"""
Behaviour that processes store's award or rejection decision (FIPA result notification).
This behaviour implements the result notification reception in the FIPA Contract Net
Protocol where the warehouse, acting as a participant, receives the store's final
decision about whether its proposal was accepted or rejected.
FIPA Protocol Phase: **Result Notification Reception - Participant Role**
- Role: Participant (Warehouse receiving Store's decision)
- Receives: store-confirm (FIPA accept-proposal) or store-deny (FIPA reject-proposal)
- Actions:
- If confirm: Transfer locked stock to pending deliveries, assign vehicle
- If deny: Release locked stock back to available inventory
- If timeout: Release locked stock (store chose another warehouse)
Stock State Transitions:
**On store-confirm (Award)**:
1. locked_stock[product] -= quantity (release lock)
2. current_capacity[product] += quantity (update capacity tracking)
3. Create Order object from message
4. pending_deliveries[order_id] = order (await vehicle pickup)
5. Launch AssignVehicle behaviour
**On store-deny or timeout (Rejection/Timeout)**:
1. locked_stock[product] -= quantity (release lock)
2. stock[product] += quantity (return to available)
3. No further action needed
Attributes:
accepted_id (int): Request ID for which we sent proposal.
accepted_quantity (int): Quantity that was proposed.
accepted_product (str): Product that was proposed.
sender_jid (str): Store JID that will send decision.
Timeout Mechanism:
- Wait time: 10 seconds
- Interpretation: Store chose another warehouse (implicit rejection)
- Action: Unlock stock automatically
Example Execution:
**Scenario 1: Store Confirms**
```
Proposed: 50 electronics
Message Received: store-confirm "50 electronics"
Actions:
- locked_stock[electronics]: 50 -> 0
- pending_deliveries[1001000000] = Order(...)
- Launch AssignVehicle(1001000000)
- Print: "Order 1001000000 confirmed..."
```
**Scenario 2: Store Denies**
```
Proposed: 50 electronics
Message Received: store-deny "50 electronics"
Actions:
- locked_stock[electronics]: 50 -> 0
- stock[electronics]: 50 -> 100
- Print: "Store chose another warehouse"
```
**Scenario 3: Timeout**
```
Proposed: 50 electronics
Message Received: None (10 second timeout)
Actions:
- locked_stock[electronics]: 50 -> 0
- stock[electronics]: 50 -> 100
- Print: "No confirmation received, unlocking stock"
```
Note:
This behaviour is automatically filtered by template to only receive
messages for the specific warehouse, store, and request_id combination.
"""
def __init__(self, accept_msg : Message, sender_jid):
"""
Initialize the ReceiveConfirmationOrDenial behaviour.
Args:
accept_msg (Message): The warehouse-accept message we sent.
Used to extract proposal details for stock unlocking if needed.
sender_jid (str): JID of the store that will send the decision.
"""
super().__init__()
self.accepted_id = int(accept_msg.get_metadata("request_id"))
bod = accept_msg.body.split(" ")
self.accepted_quantity = int(bod[0])
self.accepted_product = bod[1]
self.sender_jid = str(sender_jid)
async def run(self):
if self.agent.verbose:
print(f"{self.agent.jid}> Waiting for store confirmation or denial...")
msg : Message = await self.receive(timeout=10)
if msg != None:
self.agent : Warehouse
performative = msg.get_metadata("performative")
# Message with body format "quantity product"
request = msg.body.split(" ")
quantity = int(request[0])
product = request[1]
if performative == "store-confirm":
# Store confirmed - update locked stock and add to pending orders
self.agent.locked_stock[product] -= quantity
self.agent.current_capacity[product] += quantity
# Create Order object from message
order = self.agent.message_to_order(msg)
# Add to pending_deliveries dict with order_id as key
self.agent.pending_deliveries[order.orderid] = order
# Essential print - order confirmed
print(f"{self.agent.jid}> Order {order.orderid} confirmed: {quantity} x{product} for {order.sender}")
behav = self.agent.AssignVehicle(order.orderid)
self.agent.add_behaviour(behav)
if self.agent.verbose:
self.agent.print_stock()
elif performative == "store-deny":
# Store denied (chose another warehouse) - unlock the stock
if self.agent.verbose:
print(f"{self.agent.jid}> Denial received! Store chose another warehouse.")
print(f"{self.agent.jid}> Unlocking stock: {product} += {quantity}")
self.agent.locked_stock[product] -= quantity
stock_before = self.agent.stock[product]
self.agent.stock[product] += quantity
# Log inventory change - stock unlock (denied)
try:
inventory_logger = InventoryLogger.get_instance()
inventory_logger.log_inventory_change(
agent_jid=str(self.agent.jid),
agent_type="warehouse",
product=product,
change_type="unlock_denied",
quantity=quantity,
stock_before=stock_before,
stock_after=self.agent.stock[product],
timestamp_sim=self.agent.current_tick
)
except Exception:
pass
if self.agent.verbose:
self.agent.print_stock()
else:
if self.agent.verbose:
print(f"{self.agent.jid}> Timeout: No confirmation or denial received in 10 seconds. Unlocking stock...")
self.agent.locked_stock[self.accepted_product] -= self.accepted_quantity
stock_before = self.agent.stock[self.accepted_product]
self.agent.stock[self.accepted_product] += self.accepted_quantity
# Log inventory change - stock unlock (timeout)
try:
inventory_logger = InventoryLogger.get_instance()
inventory_logger.log_inventory_change(
agent_jid=str(self.agent.jid),
agent_type="warehouse",
product=self.accepted_product,
change_type="unlock_timeout",
quantity=self.accepted_quantity,
stock_before=stock_before,
stock_after=self.agent.stock[self.accepted_product],
timestamp_sim=self.agent.current_tick
)
except Exception:
pass
if self.agent.verbose:
self.agent.print_stock()
class AssignVehicle(OneShotBehaviour):
def __init__(self, request_id):
super().__init__()
self.request_id = request_id
def populate_vehicles_from_contacts(self):
"""Populate vehicles list from presence contacts if empty"""
agent : Warehouse = self.agent
# Get all vehicles from presence contacts
vehicles = [str(jid) for jid in agent.presence.contacts.keys() if "vehicle" in str(jid)]
if vehicles:
agent.vehicles = vehicles
if agent.verbose:
print(f"{agent.jid}> 🔍 Discovered {len(vehicles)} vehicle(s) from presence: {vehicles}")
else:
if agent.verbose:
print(f"{agent.jid}> ⚠️ No vehicles found in presence contacts yet!")
return len(vehicles) > 0
def create_presence_info_message(self, to) -> Message:
self.agent : Warehouse
msg : Message = Message(to=to)
msg.set_metadata("performative", "presence-info")
msg.body = ""
return msg
def create_call_for_proposal_message(self, to) -> Message:
self.agent : Warehouse
msg : Message = Message(to=to)
msg.set_metadata("performative", "order-proposal")
order = self.agent.pending_deliveries[self.request_id]
new_body = {
"orderid" : self.request_id,
"product" : order.product,
"quantity" : order.quantity,
"sender" : order.receiver,
"receiver" : order.sender,
"sender_location" : order.receiver_location,
"receiver_location" : order.sender_location
}
msg.body = json.dumps(new_body)
return msg
async def run(self):
agent : Warehouse = self.agent
# Populate vehicles from contacts
has_vehicles = self.populate_vehicles_from_contacts()
if not has_vehicles:
print(f"{agent.jid}> ❌ ERROR: No vehicles found in contacts!")
print(f"{agent.jid}> Make sure vehicles are started and have subscribed to this supplier.")
return
if agent.verbose:
print(f"{agent.jid}> 📤 Requesting vehicle proposals...")
print(f"{agent.jid}> Vehicles to contact: {agent.vehicles}")
# Enviar mensagens de proposal a todos os veículos
# Não verificamos presença - deixamos cada veículo decidir se pode aceitar
n_available_vehicles = 0
n_sent_messages = 0
away_vehicles = []
for vehicle_jid in agent.vehicles:
msg : Message = self.create_presence_info_message(to=vehicle_jid)
await self.send(msg)
try:
msg_logger = MessageLogger.get_instance()
msg_logger.log_message(
sender=str(self.agent.jid),
receiver=str(msg.to),
message_type="Request",
performative="presence-info",
body=msg.body
)
except Exception:
pass # Don't crash on logging errors
behav = self.agent.ReceivePresenceInfo()
temp : Template = Template()
temp.set_metadata("performative", "presence-response")
temp.set_metadata("vehicle_id", str(vehicle_jid))
self.agent.add_behaviour(behav, temp)
await behav.join()
if agent.verbose:
print(f"{agent.jid}> Presence info from {vehicle_jid}: {agent.presence_infos}")
if agent.presence_infos[vehicle_jid] == "PresenceShow.CHAT":
msg : Message = self.create_call_for_proposal_message(to=vehicle_jid)
await self.send(msg)
try:
msg_logger = MessageLogger.get_instance()
msg_logger.log_message(
sender=str(self.agent.jid),
receiver=str(msg.to),
message_type="Request",
performative="order-proposal",
body=msg.body
)
except Exception:
pass # Don't crash on logging errors
n_available_vehicles += 1
n_sent_messages += 1
print(f"{agent.jid}> ✉️ Sent order proposal to {vehicle_jid}")
elif agent.presence_infos[vehicle_jid] == "PresenceShow.AWAY" and n_available_vehicles == 0:
away_vehicles.append(vehicle_jid)
if agent.verbose:
print(f"{agent.jid}> ⚠️ Vehicle {vehicle_jid} is away.")
if n_available_vehicles == 0:
if agent.verbose:
print(f"{agent.jid}> ⚠️ No AVAILABLE vehicles found. All vehicles are AWAY.")
for vehicle_jid in away_vehicles:
msg : Message = self.create_call_for_proposal_message(to=vehicle_jid)
await self.send(msg)
try:
msg_logger = MessageLogger.get_instance()
msg_logger.log_message(
sender=str(self.agent.jid),
receiver=str(msg.to),
message_type="Request",
performative="order-proposal",
body=msg.body
)
except Exception:
pass # Don't crash on logging errors
n_sent_messages += 1
print(f"{agent.jid}> ✉️ Sent order proposal to {vehicle_jid}")
if agent.verbose:
print(f"{agent.jid}> 📨 Sent proposals to {n_sent_messages} vehicle(s)")
behav = self.agent.ChooseBestVehicle(self.request_id, n_sent_messages)
self.agent.add_behaviour(behav, temp)
# Waits for all vehicle proposals to be received
await behav.join()
class ReceivePresenceInfo(OneShotBehaviour):
"""
Behaviour to receive presence information from vehicle agents.
This behaviour requests and collects availability status from vehicle agents,
which is used to determine which vehicles are available for delivery tasks.
The presence information indicates whether a vehicle is busy, available, or
in another state.
The behaviour waits for a single response from a vehicle agent containing
its current presence status. This information is stored in the warehouse's
presence_infos dictionary for later use in vehicle selection.
Workflow:
1. **Receive Message**: Wait up to 10 seconds for presence response
2. **Parse JSON**: Extract presence_show status from message body
3. **Store Information**: Save presence info indexed by vehicle_id
4. **Handle Timeout**: Print warning if no response received
Side Effects:
- Updates agent.presence_infos dictionary with vehicle status
- Prints presence information if verbose mode enabled
Returns:
None. Completes after receiving one presence message or timing out.
Example:
```
Message Received:
Metadata: vehicle_id = "vehicle1@localhost"
Body: {"presence_show": "available"}
Processing:
- presence_infos["vehicle1@localhost"] = "available"
Output:
- Print: "Received presence info response from vehicle1: available"
```
Note:
The 10-second timeout prevents indefinite blocking if a vehicle agent
is offline or unresponsive.
"""
async def run(self):
agent : Warehouse = self.agent
msg : Message = await self.receive(timeout=10)
if msg:
data = json.loads(msg.body)
presence_info = data["presence_show"]
if agent.verbose:
print(f"{agent.jid}> Received presence info response from {msg.sender}:"
f"{presence_info}")
agent.presence_infos[msg.get_metadata("vehicle_id")] = presence_info
else:
if agent.verbose: