Rishav commited on
Commit ·
f7d383d
1
Parent(s): 9600701
Add packaging manifest and refine rewards
Browse files- docs/agent_eval_prompt.md +43 -0
- pyproject.toml +24 -0
- src/delivery_dispatch/environment.py +64 -12
docs/agent_eval_prompt.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Black-Box Agent Evaluation Prompt
|
| 2 |
+
|
| 3 |
+
Use this prompt when evaluating an external agent against Fleetmind without giving it source-code access.
|
| 4 |
+
|
| 5 |
+
## Goal
|
| 6 |
+
|
| 7 |
+
Maximize cumulative reward in the live environment by interacting only through the HTTP API.
|
| 8 |
+
|
| 9 |
+
## Instructions
|
| 10 |
+
|
| 11 |
+
You are interacting with a live delivery-dispatch environment as a black-box external agent.
|
| 12 |
+
|
| 13 |
+
You may use any reasoning tools available to you, including calculations, helper code, or temporary scripts, but you must not inspect the environment source code or hidden files. Treat the HTTP API as the only interface to the environment.
|
| 14 |
+
|
| 15 |
+
Use only these endpoints:
|
| 16 |
+
- `GET /health`
|
| 17 |
+
- `POST /reset`
|
| 18 |
+
- `GET /state`
|
| 19 |
+
- `POST /step`
|
| 20 |
+
|
| 21 |
+
## Required Workflow
|
| 22 |
+
|
| 23 |
+
1. Call `GET /health` to confirm the service is live.
|
| 24 |
+
2. Start a fresh episode with `POST /reset`.
|
| 25 |
+
3. Play the episode entirely through repeated `POST /step` calls until `done = true`.
|
| 26 |
+
4. Use `GET /state` only if needed for recovery or inspection.
|
| 27 |
+
5. Choose assignments and rejections based only on API observations and returned feedback.
|
| 28 |
+
|
| 29 |
+
## Constraints
|
| 30 |
+
|
| 31 |
+
- Do not inspect local repository files or source code.
|
| 32 |
+
- Do not assume hidden future orders or hidden reward terms beyond what can be inferred from the API.
|
| 33 |
+
- Do not modify the environment implementation.
|
| 34 |
+
- You may write local scratch logic for your own planning, but the environment must be treated as a black box.
|
| 35 |
+
|
| 36 |
+
## Final Report
|
| 37 |
+
|
| 38 |
+
At the end of the episode, report:
|
| 39 |
+
- final cumulative reward
|
| 40 |
+
- the policy or strategy you converged on
|
| 41 |
+
- key assignment and rejection decisions
|
| 42 |
+
- what the API feedback taught you
|
| 43 |
+
- what felt confusing, too easy, too derived, or gameable
|
pyproject.toml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=69", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "fleetmind"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "OpenEnv delivery dispatch environment for sequential agent evaluation."
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.11"
|
| 11 |
+
dependencies = [
|
| 12 |
+
"pydantic==2.12.5",
|
| 13 |
+
"openai==2.30.0",
|
| 14 |
+
"fastapi==0.135.2",
|
| 15 |
+
"uvicorn==0.42.0",
|
| 16 |
+
"pyyaml==6.0.1",
|
| 17 |
+
"requests==2.32.3",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
[tool.setuptools]
|
| 21 |
+
package-dir = {"" = "src"}
|
| 22 |
+
|
| 23 |
+
[tool.setuptools.packages.find]
|
| 24 |
+
where = ["src"]
|
src/delivery_dispatch/environment.py
CHANGED
|
@@ -25,12 +25,12 @@ class DeliveryDispatchEnv:
|
|
| 25 |
"""Deterministic event-driven delivery dispatch simulator."""
|
| 26 |
|
| 27 |
invalid_assignment_penalty = -1.0
|
| 28 |
-
idle_penalty = -0.
|
| 29 |
service_time = 1
|
| 30 |
-
missed_order_penalty_multiplier = 0.
|
| 31 |
feasible_assignment_bonus = 0.5
|
| 32 |
infeasible_assignment_penalty = -1.5
|
| 33 |
-
rejection_penalty_multiplier = 0.
|
| 34 |
service_grace_window = 4
|
| 35 |
early_bonus_per_tick = 0.45
|
| 36 |
early_bonus_cap = 4
|
|
@@ -217,7 +217,7 @@ class DeliveryDispatchEnv:
|
|
| 217 |
self.recent_events.append(f"ignored invalid rejection for {order_id}")
|
| 218 |
continue
|
| 219 |
|
| 220 |
-
rejection_penalty = -
|
| 221 |
step_reward += rejection_penalty
|
| 222 |
reward_breakdown["rejection_penalty"] += rejection_penalty
|
| 223 |
order.status = "rejected"
|
|
@@ -272,10 +272,10 @@ class DeliveryDispatchEnv:
|
|
| 272 |
f"assigned {order.order_id} to {agent.agent_id} until t={agent.busy_until}"
|
| 273 |
)
|
| 274 |
|
| 275 |
-
|
|
|
|
| 276 |
idle_agents = len([agent for agent in self.agents if agent.status == "idle"])
|
| 277 |
-
|
| 278 |
-
idle_penalty = self.idle_penalty * min(idle_agents, pending_orders)
|
| 279 |
step_reward += idle_penalty
|
| 280 |
reward_breakdown["idle_penalty"] += idle_penalty
|
| 281 |
self.recent_events.append("avoidable idle capacity remained")
|
|
@@ -494,7 +494,7 @@ class DeliveryDispatchEnv:
|
|
| 494 |
if self._service_cutoff(order) < self.current_time:
|
| 495 |
order.status = "expired"
|
| 496 |
self.stats["expired_orders"] += 1
|
| 497 |
-
penalty
|
| 498 |
events.append(f"order {order.order_id} expired")
|
| 499 |
if order.reward_value >= self.high_value_threshold:
|
| 500 |
high_value_missed += 1
|
|
@@ -618,11 +618,17 @@ class DeliveryDispatchEnv:
|
|
| 618 |
self.current_time = terminal_time
|
| 619 |
return reward, events, error_summary, terminal_info
|
| 620 |
|
| 621 |
-
def
|
| 622 |
-
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
]
|
| 625 |
-
return
|
| 626 |
|
| 627 |
def _is_done(self) -> bool:
|
| 628 |
scenario = self._require_scenario()
|
|
@@ -693,3 +699,49 @@ class DeliveryDispatchEnv:
|
|
| 693 |
else:
|
| 694 |
break
|
| 695 |
return chosen
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
"""Deterministic event-driven delivery dispatch simulator."""
|
| 26 |
|
| 27 |
invalid_assignment_penalty = -1.0
|
| 28 |
+
idle_penalty = -0.35
|
| 29 |
service_time = 1
|
| 30 |
+
missed_order_penalty_multiplier = 0.75
|
| 31 |
feasible_assignment_bonus = 0.5
|
| 32 |
infeasible_assignment_penalty = -1.5
|
| 33 |
+
rejection_penalty_multiplier = 0.4
|
| 34 |
service_grace_window = 4
|
| 35 |
early_bonus_per_tick = 0.45
|
| 36 |
early_bonus_cap = 4
|
|
|
|
| 217 |
self.recent_events.append(f"ignored invalid rejection for {order_id}")
|
| 218 |
continue
|
| 219 |
|
| 220 |
+
rejection_penalty = -self._rejection_penalty(order)
|
| 221 |
step_reward += rejection_penalty
|
| 222 |
reward_breakdown["rejection_penalty"] += rejection_penalty
|
| 223 |
order.status = "rejected"
|
|
|
|
| 272 |
f"assigned {order.order_id} to {agent.agent_id} until t={agent.busy_until}"
|
| 273 |
)
|
| 274 |
|
| 275 |
+
avoidable_idle_slots = self._avoidable_idle_slots()
|
| 276 |
+
if avoidable_idle_slots > 0:
|
| 277 |
idle_agents = len([agent for agent in self.agents if agent.status == "idle"])
|
| 278 |
+
idle_penalty = self.idle_penalty * min(idle_agents, avoidable_idle_slots)
|
|
|
|
| 279 |
step_reward += idle_penalty
|
| 280 |
reward_breakdown["idle_penalty"] += idle_penalty
|
| 281 |
self.recent_events.append("avoidable idle capacity remained")
|
|
|
|
| 494 |
if self._service_cutoff(order) < self.current_time:
|
| 495 |
order.status = "expired"
|
| 496 |
self.stats["expired_orders"] += 1
|
| 497 |
+
penalty -= self._missed_order_penalty(order, self.current_time)
|
| 498 |
events.append(f"order {order.order_id} expired")
|
| 499 |
if order.reward_value >= self.high_value_threshold:
|
| 500 |
high_value_missed += 1
|
|
|
|
| 618 |
self.current_time = terminal_time
|
| 619 |
return reward, events, error_summary, terminal_info
|
| 620 |
|
| 621 |
+
def _avoidable_idle_slots(self) -> int:
|
| 622 |
+
idle_agents = [agent for agent in self.agents if agent.status == "idle"]
|
| 623 |
+
if not idle_agents:
|
| 624 |
+
return 0
|
| 625 |
+
|
| 626 |
+
worthwhile_orders = [
|
| 627 |
+
order
|
| 628 |
+
for order in self._visible_orders()
|
| 629 |
+
if order.status == "unassigned" and self._is_worth_serving_now(order, idle_agents)
|
| 630 |
]
|
| 631 |
+
return len(worthwhile_orders)
|
| 632 |
|
| 633 |
def _is_done(self) -> bool:
|
| 634 |
scenario = self._require_scenario()
|
|
|
|
| 699 |
else:
|
| 700 |
break
|
| 701 |
return chosen
|
| 702 |
+
|
| 703 |
+
def _best_idle_finish_time(self, order: OrderState, idle_agents: list[AgentState] | None = None) -> int | None:
|
| 704 |
+
idle_agents = idle_agents or [agent for agent in self.agents if agent.status == "idle"]
|
| 705 |
+
if not idle_agents:
|
| 706 |
+
return None
|
| 707 |
+
best_cost = min(self._job_time(agent, order) for agent in idle_agents)
|
| 708 |
+
return self.current_time + best_cost
|
| 709 |
+
|
| 710 |
+
def _priority_multiplier(self, order: OrderState, reference_time: int) -> float:
|
| 711 |
+
urgency = order.deadline - reference_time
|
| 712 |
+
multiplier = 1.0
|
| 713 |
+
if order.reward_value >= self.high_value_threshold:
|
| 714 |
+
multiplier += 0.3
|
| 715 |
+
elif order.reward_value >= 12:
|
| 716 |
+
multiplier += 0.15
|
| 717 |
+
|
| 718 |
+
if urgency <= 3:
|
| 719 |
+
multiplier += 0.3
|
| 720 |
+
elif urgency <= 6:
|
| 721 |
+
multiplier += 0.15
|
| 722 |
+
return multiplier
|
| 723 |
+
|
| 724 |
+
def _missed_order_penalty(self, order: OrderState, reference_time: int) -> float:
|
| 725 |
+
return self.missed_order_penalty_multiplier * order.reward_value * self._priority_multiplier(
|
| 726 |
+
order, reference_time
|
| 727 |
+
)
|
| 728 |
+
|
| 729 |
+
def _rejection_penalty(self, order: OrderState) -> float:
|
| 730 |
+
expiry_penalty = self._missed_order_penalty(order, self.current_time)
|
| 731 |
+
best_finish = self._best_idle_finish_time(order)
|
| 732 |
+
if best_finish is None:
|
| 733 |
+
ratio = 0.55
|
| 734 |
+
elif best_finish > self._service_cutoff(order):
|
| 735 |
+
ratio = 0.5
|
| 736 |
+
elif best_finish > order.deadline:
|
| 737 |
+
ratio = 0.65
|
| 738 |
+
else:
|
| 739 |
+
ratio = 0.8
|
| 740 |
+
return max(1.0, expiry_penalty * ratio, self.rejection_penalty_multiplier * order.reward_value)
|
| 741 |
+
|
| 742 |
+
def _is_worth_serving_now(self, order: OrderState, idle_agents: list[AgentState]) -> bool:
|
| 743 |
+
best_finish = self._best_idle_finish_time(order, idle_agents)
|
| 744 |
+
if best_finish is None or best_finish > self._service_cutoff(order):
|
| 745 |
+
return False
|
| 746 |
+
delivery_value = self._completion_reward(order, best_finish)
|
| 747 |
+
return delivery_value >= max(3.0, self._rejection_penalty(order) * 1.2)
|