mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 15:29:36 +00:00
157 lines
5.8 KiB
Python
157 lines
5.8 KiB
Python
"""
|
|
Orchestrator Policies — autonomy modes, approval gates, budget limits.
|
|
|
|
Every customer chooses their autonomy mode. The orchestrator consults the
|
|
policy on every action: should I run? do I need human approval? am I
|
|
within budget?
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
|
|
# ── Autonomy modes — the safety slider ────────────────────────────
|
|
class AutonomyMode:
|
|
"""Five named autonomy modes, ordered by independence."""
|
|
|
|
MANUAL = "manual" # Dealix only suggests; user does everything
|
|
SUGGEST = "suggest_only" # Dealix shows what it would do; user actions
|
|
DRAFT_APPROVE = "draft_and_approve" # Dealix drafts; user approves before send
|
|
SAFE_AUTOPILOT = "safe_autopilot" # Dealix sends within rails; high-risk needs approval
|
|
FULL_AUTOPILOT = "full_autopilot" # Dealix runs end-to-end; user reviews logs
|
|
|
|
|
|
ALL_MODES: tuple[str, ...] = (
|
|
AutonomyMode.MANUAL,
|
|
AutonomyMode.SUGGEST,
|
|
AutonomyMode.DRAFT_APPROVE,
|
|
AutonomyMode.SAFE_AUTOPILOT,
|
|
AutonomyMode.FULL_AUTOPILOT,
|
|
)
|
|
|
|
|
|
# ── Budget limits — protect against runaway spend ─────────────────
|
|
@dataclass
|
|
class BudgetLimit:
|
|
"""Per-day caps. Once hit, agents stop until next reset."""
|
|
|
|
max_messages_per_day: int = 200
|
|
max_llm_tokens_per_day: int = 200_000
|
|
max_external_api_calls_per_day: int = 1_000
|
|
max_cost_sar_per_day: float = 100.0
|
|
|
|
|
|
# ── Policy — what each mode allows ────────────────────────────────
|
|
@dataclass
|
|
class Policy:
|
|
"""Consolidated policy for one customer."""
|
|
|
|
customer_id: str
|
|
autonomy_mode: str = AutonomyMode.DRAFT_APPROVE
|
|
budget: BudgetLimit = field(default_factory=BudgetLimit)
|
|
require_human_for_first_send: bool = True
|
|
require_human_for_high_value_deals_above_sar: float = 100_000
|
|
require_human_for_legal_topics: bool = True
|
|
blocked_sectors: tuple[str, ...] = ()
|
|
blocked_keywords: tuple[str, ...] = () # any draft containing these → human review
|
|
max_consecutive_followups: int = 3
|
|
quiet_hours_riyadh: tuple[int, int] = (21, 8) # 9pm - 8am no messaging
|
|
blocked_dates: tuple[str, ...] = () # ISO dates: religious holidays, etc.
|
|
|
|
|
|
def default_policy(customer_id: str) -> Policy:
|
|
return Policy(customer_id=customer_id)
|
|
|
|
|
|
# ── Action types the orchestrator can request ────────────────────
|
|
ACTION_TYPES: tuple[str, ...] = (
|
|
"discover_leads",
|
|
"enrich_lead",
|
|
"draft_message",
|
|
"send_message",
|
|
"classify_reply",
|
|
"book_meeting",
|
|
"generate_proposal",
|
|
"score_deal",
|
|
"compute_health",
|
|
"generate_qbr",
|
|
"publish_pulse",
|
|
)
|
|
|
|
|
|
# ── Decision: requires_approval? ──────────────────────────────────
|
|
def requires_approval(
|
|
*,
|
|
action_type: str,
|
|
policy: Policy,
|
|
risk_factors: dict[str, Any] | None = None,
|
|
) -> tuple[bool, str | None]:
|
|
"""
|
|
Decide whether an action needs human approval before execution.
|
|
|
|
Returns (needs_approval, reason). reason is the human-readable rationale.
|
|
"""
|
|
risks = risk_factors or {}
|
|
|
|
# Mode-based blanket rules
|
|
if policy.autonomy_mode in (AutonomyMode.MANUAL, AutonomyMode.SUGGEST):
|
|
return True, f"autonomy_mode={policy.autonomy_mode}"
|
|
if policy.autonomy_mode == AutonomyMode.DRAFT_APPROVE and action_type in (
|
|
"send_message",
|
|
"book_meeting",
|
|
"generate_proposal",
|
|
):
|
|
return True, "mode=draft_and_approve_requires_human_for_outbound"
|
|
|
|
# Risk-based escalation (applies even in autopilot)
|
|
if action_type == "send_message":
|
|
if policy.require_human_for_first_send and risks.get("is_first_send_to_account"):
|
|
return True, "first_send_to_account"
|
|
deal_value = float(risks.get("deal_value_sar", 0))
|
|
if deal_value >= policy.require_human_for_high_value_deals_above_sar:
|
|
return True, f"high_value_deal_sar={deal_value:.0f}"
|
|
if risks.get("contains_legal_topic") and policy.require_human_for_legal_topics:
|
|
return True, "contains_legal_topic"
|
|
sector = risks.get("sector")
|
|
if sector in policy.blocked_sectors:
|
|
return True, f"blocked_sector={sector}"
|
|
for kw in policy.blocked_keywords:
|
|
if kw in str(risks.get("draft_text", "")):
|
|
return True, f"blocked_keyword={kw}"
|
|
if risks.get("consecutive_followup_index", 0) >= policy.max_consecutive_followups:
|
|
return True, "max_consecutive_followups_reached"
|
|
|
|
return False, None
|
|
|
|
|
|
def is_in_quiet_hours(*, hour_riyadh: int, policy: Policy) -> bool:
|
|
"""Whether the current Riyadh hour is within the quiet window."""
|
|
start, end = policy.quiet_hours_riyadh
|
|
if start < end:
|
|
return start <= hour_riyadh < end
|
|
# Wraps midnight
|
|
return hour_riyadh >= start or hour_riyadh < end
|
|
|
|
|
|
# ── Budget enforcement ───────────────────────────────────────────
|
|
@dataclass
|
|
class BudgetUsage:
|
|
messages_today: int = 0
|
|
llm_tokens_today: int = 0
|
|
api_calls_today: int = 0
|
|
cost_sar_today: float = 0.0
|
|
|
|
|
|
def within_budget(*, usage: BudgetUsage, budget: BudgetLimit) -> tuple[bool, str | None]:
|
|
if usage.messages_today >= budget.max_messages_per_day:
|
|
return False, "messages_per_day_reached"
|
|
if usage.llm_tokens_today >= budget.max_llm_tokens_per_day:
|
|
return False, "llm_tokens_per_day_reached"
|
|
if usage.api_calls_today >= budget.max_external_api_calls_per_day:
|
|
return False, "api_calls_per_day_reached"
|
|
if usage.cost_sar_today >= budget.max_cost_sar_per_day:
|
|
return False, "cost_sar_per_day_reached"
|
|
return True, None
|