Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 81 additions & 54 deletions agents/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from uuid import uuid4

from actions.executor import simulate_actions
from agents.base import BaseAgent
from core.enums import ActionType, ApprovalStatus, PlanStatus
from core.models import (
Expand All @@ -24,14 +23,15 @@
explain_rejected_actions,
)
from policies.constraints import evaluate_plan_constraints, mode_rationale, evaluate_hard_constraints, evaluate_soft_constraints
from policies.guardrails import approval_required
from policies.scoring import compute_score
from policies.strategic_prompt import (
from policies.guardrails import approval_required
from policies.scoring import compute_score
from policies.strategic_prompt import (
retrieve_relevant_cases,
compute_memory_influence,
derive_strategy_rationale,
build_strategic_prompt,
derive_strategy_rationale,
build_strategic_prompt,
)
from simulation.evaluator import evaluate_candidate_plan

logger = get_logger(__name__)

Expand Down Expand Up @@ -338,35 +338,44 @@ def _evaluate_candidate(
actions=actions,
)
feasible = not violations
if not feasible:
return CandidatePlanEvaluation(
strategy_label=strategy_label,
action_ids=[action.action_id for action in actions],
score=0.0,
if not feasible:
return CandidatePlanEvaluation(
strategy_label=strategy_label,
action_ids=[action.action_id for action in actions],
score=0.0,
score_breakdown={
"service_level": 0.0,
"total_cost": 0.0,
"disruption_risk": 0.0,
"recovery_speed": 0.0,
},
projected_kpis=before_kpis.model_copy(deep=True),
feasible=False,
violations=violations,
mode_rationale=selected_mode_rationale,
approval_required=False,
approval_reason="not evaluated because the candidate plan is infeasible",
rationale=rationale,
llm_used=llm_used,
)
simulated = simulate_actions(state, actions)
score, breakdown = compute_score(
service_level=simulated.kpis.service_level,
total_cost=simulated.kpis.total_cost,
disruption_risk=simulated.kpis.disruption_risk,
recovery_speed=simulated.kpis.recovery_speed,
mode=state.mode,
baseline_cost=before_kpis.total_cost,
)
"recovery_speed": 0.0,
},
projected_kpis=before_kpis.model_copy(deep=True),
worst_case_kpis=before_kpis.model_copy(deep=True),
simulation_horizon_days=0,
projection_steps=[],
projected_state_summary=None,
projection_summary="candidate was not simulated because hard constraints failed",
feasible=False,
violations=violations,
mode_rationale=selected_mode_rationale,
approval_required=False,
approval_reason="not evaluated because the candidate plan is infeasible",
rationale=rationale,
llm_used=llm_used,
)
projection = evaluate_candidate_plan(
state=state,
event=event,
actions=actions,
)
score, breakdown = compute_score(
service_level=projection.worst_case_kpis.service_level,
total_cost=projection.projected_kpis.total_cost,
disruption_risk=projection.worst_case_kpis.disruption_risk,
recovery_speed=projection.projected_kpis.recovery_speed,
mode=state.mode,
baseline_cost=before_kpis.total_cost,
)
transient_plan = Plan(
plan_id=f"plan_eval_{uuid4().hex[:8]}",
mode=state.mode,
Expand All @@ -375,22 +384,32 @@ def _evaluate_candidate(
score=score,
score_breakdown=breakdown,
strategy_label=strategy_label,
generated_by="llm" if llm_used else "deterministic_fallback",
planner_reasoning=rationale,
status=PlanStatus.PROPOSED,
)
needs_approval, reason = approval_required(transient_plan, before_kpis, simulated.kpis, event)
generated_by="llm" if llm_used else "deterministic_fallback",
planner_reasoning=rationale,
status=PlanStatus.PROPOSED,
)
needs_approval, reason = approval_required(
transient_plan,
before_kpis,
projection.projected_kpis,
event,
)
return CandidatePlanEvaluation(
strategy_label=strategy_label,
action_ids=[action.action_id for action in actions],
score=score,
score_breakdown=breakdown,
projected_kpis=simulated.kpis,
feasible=True,
violations=[],
mode_rationale=selected_mode_rationale,
approval_required=needs_approval,
approval_reason=reason if needs_approval else "no approval required: thresholds not triggered",
score_breakdown=breakdown,
projected_kpis=projection.projected_kpis,
worst_case_kpis=projection.worst_case_kpis,
simulation_horizon_days=projection.simulation_horizon_days,
projection_steps=projection.projection_steps,
projected_state_summary=projection.projected_state_summary,
projection_summary=projection.projection_summary,
feasible=True,
violations=[],
mode_rationale=selected_mode_rationale,
approval_required=needs_approval,
approval_reason=reason if needs_approval else "no approval required: thresholds not triggered",
rationale=rationale,
llm_used=llm_used,
)
Expand Down Expand Up @@ -808,17 +827,22 @@ def _safe_hold_evaluation(
mode=state.mode,
baseline_cost=before_kpis.total_cost,
)
return CandidatePlanEvaluation(
strategy_label="safe_hold",
action_ids=[safe_action.action_id],
score=score,
score_breakdown=breakdown,
projected_kpis=before_kpis.model_copy(deep=True),
feasible=True,
violations=[],
mode_rationale=mode_rationale(state, event),
approval_required=False,
approval_reason="no approval required: retaining current state because all candidate plans were infeasible",
return CandidatePlanEvaluation(
strategy_label="safe_hold",
action_ids=[safe_action.action_id],
score=score,
score_breakdown=breakdown,
projected_kpis=before_kpis.model_copy(deep=True),
worst_case_kpis=before_kpis.model_copy(deep=True),
simulation_horizon_days=0,
projection_steps=[],
projected_state_summary=None,
projection_summary="the system retained the current network position because no feasible candidate plan was available",
feasible=True,
violations=[],
mode_rationale=mode_rationale(state, event),
approval_required=False,
approval_reason="no approval required: retaining current state because all candidate plans were infeasible",
rationale=(
f"no candidate plan satisfied hard constraints; retaining the current network state after excluding "
f"{infeasible_count} infeasible candidate plan(s)"
Expand Down Expand Up @@ -998,6 +1022,9 @@ def run(self, state: SystemState, event: Event | None = None) -> AgentProposal:
mode=state.mode,
runner_up=runner_up,
mode_rationale=selected_evaluation.mode_rationale,
projection_summary=selected_evaluation.projection_summary,
simulation_horizon_days=selected_evaluation.simulation_horizon_days,
worst_case_kpis=selected_evaluation.worst_case_kpis,
)

final_plan = Plan(
Expand Down
27 changes: 12 additions & 15 deletions agents/risk.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,21 @@ class RiskAgent(BaseAgent):

custom_system_prompt = (
"Role: {agent_name} specialist agent in an autonomous supply chain control tower. "
"CRITICAL: Write the 'domain_summary' in English summarizing the disruption based on external_api_data"
"Make it sound natural but precise. Do not invent new actions or make up false information."
"CRITICAL: Your ONLY goal is to write a comprehensive 'domain_summary' in English "
"summarizing the disruption based on external_api_data. "
"Make it sound natural but precise. Do not invent new actions or make up false information. "
"Return empty arrays for impacts, tradeoffs, and recommended actions."
)

def run(self, state: SystemState, event: Event | None = None) -> AgentProposal:
proposal = AgentProposal(agent=self.name)
state.mode = select_mode(state, event)
api_payloads = {}
if event is None:
proposal.observations.append(
f"Network operating in {state.mode.value} mode with no active trigger event"
)
proposal.domain_summary = (
f"Risk review completed with no incoming disruption. Current operating mode is {state.mode.value}."
)
else:
proposal.observations.append(
f"{event.type.value.replace('_', ' ')} detected at severity {event.severity:.2f}"
)
proposal.risks.append(
f"Mode switched to {state.mode.value} because disruption severity requires closer monitoring"
)
proposal.domain_summary = (
f"{event.type.value.replace('_', ' ').title()} requires risk review for "
f"{', '.join(event.entity_ids) if event.entity_ids else 'the network'}."
Expand Down Expand Up @@ -80,8 +73,12 @@ def run(self, state: SystemState, event: Event | None = None) -> AgentProposal:
"external_api_data": api_payloads,
},
)
if not proposal.downstream_impacts and event is not None:
proposal.downstream_impacts.append(
"Elevated disruption risk may affect downstream service level and recovery speed."
)

# Clear other fields so only domain_summary is returned
proposal.observations.clear()
proposal.risks.clear()
proposal.downstream_impacts.clear()
proposal.recommended_action_ids.clear()
proposal.tradeoffs.clear()

return proposal
50 changes: 39 additions & 11 deletions app_api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,50 @@ class ActionView(BaseModel):
parameters: dict[str, Any] = Field(default_factory=dict)


class ConstraintViolationView(BaseModel):
code: str
message: str
action_id: str | None = None
severity: str = "hard"


class ConstraintViolationView(BaseModel):
code: str
message: str
action_id: str | None = None
severity: str = "hard"


class ProjectionStepView(BaseModel):
step_index: int
label: str
kpis: KPIView
event_severity: float
inventory_at_risk: int
inventory_out_of_stock: int
backlog_units: int
inbound_units_due: int
summary: str = ""
key_changes: list[str] = Field(default_factory=list)


class ProjectedStateSummaryView(BaseModel):
inventory_at_risk: int
inventory_out_of_stock: int
backlog_units: int
inbound_units_scheduled: int
dominant_constraint: str = ""
event_severity_end: float = 0.0
summary: str = ""


class CandidateEvaluationView(BaseModel):
strategy_label: str
action_ids: list[str] = Field(default_factory=list)
score: float
score_breakdown: dict[str, float] = Field(default_factory=dict)
projected_kpis: KPIView
feasible: bool = True
violations: list[ConstraintViolationView] = Field(default_factory=list)
mode_rationale: str = ""
projected_kpis: KPIView
worst_case_kpis: KPIView | None = None
simulation_horizon_days: int = 0
projection_steps: list[ProjectionStepView] = Field(default_factory=list)
projected_state_summary: ProjectedStateSummaryView | None = None
projection_summary: str = ""
feasible: bool = True
violations: list[ConstraintViolationView] = Field(default_factory=list)
mode_rationale: str = ""
approval_required: bool
approval_reason: str
rationale: str
Expand Down
Loading
Loading