File size: 8,773 Bytes
8697ae6
 
80d9920
 
 
 
 
 
8697ae6
 
 
 
 
 
 
 
 
 
 
 
80d9920
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""FastAPI application entry point for the HR Productivity Environment."""

import json
from pathlib import Path
from typing import Optional

from fastapi import Query
from fastapi.responses import RedirectResponse
from openenv.core.env_server.http_server import create_app

from hr_env.models import HRAction, HRObservation
from hr_env.server.environment import HRProductivityEnvironment

app = create_app(
    HRProductivityEnvironment,
    HRAction,
    HRObservation,
    env_name="hr-productivity-env",
    max_concurrent_envs=1,
)


@app.get("/")
async def root():
    return RedirectResponse(url="/web")


# ── Hyperparameters endpoint ────────────────────────────────────────


@app.get("/hyperparameters")
async def hyperparameters():
    """Return all environment hyperparameters and configuration constants."""
    from hr_env.server.company import (
        BENEFITS_MULTIPLIER,
        DEPT_CONFIGS,
        RECRUITING_COST_PER_HIRE,
    )
    from hr_env.server.data_gen import (
        DEPT_SIZE_FRACTIONS,
        DEPT_SKILLS,
        ROLES,
        SALARY_BASE,
        SALARY_SIGMA,
    )
    from hr_env.server.events import ALL_EVENTS
    from hr_env.server.phases import PHASE_ACTIONS, PHASE_MIN_ACTIONS, PHASES
    from hr_env.server.scoring import compute_final_score  # noqa: F401

    return {
        "environment": {
            "max_quarters": 6,
            "max_steps": 300,
            "max_concurrent_envs": 1,
            "default_company_size": 300,
            "recommended_size_range": [200, 500],
            "departments": ["Engineering", "Sales", "Operations", "HR", "Finance"],
        },
        "company_financials": {
            "base_revenue_formula": "size * 150,000",
            "hr_budget_formula": "size * 6,000",
            "quarterly_hr_budget": "hr_budget / 4",
            "non_employment_cost_ratio": 0.55,
            "benefits_multiplier": BENEFITS_MULTIPLIER,
            "recruiting_cost_per_hire": RECRUITING_COST_PER_HIRE,
            "training_cost_per_hour_per_employee": 50,
        },
        "cobb_douglas_production": {
            dept: {
                "A": cfg["A"],
                "alpha": cfg["alpha"],
                "beta": cfg["beta"],
                "base_revenue_share": cfg["base_revenue_share"],
            }
            for dept, cfg in DEPT_CONFIGS.items()
        },
        "employee_generation": {
            "salary_base_by_level": SALARY_BASE,
            "salary_lognormal_sigma": SALARY_SIGMA,
            "performance_distribution": {"type": "normal", "mean": 3.2, "std": 0.8, "clip": [1.0, 5.0]},
            "tenure_distribution": {"type": "exponential", "lambda_months": 24},
            "engagement_distribution": {"type": "beta", "alpha": 7, "beta": 3, "scale": 100},
            "level_distribution": {
                "type": "categorical",
                "probabilities": {"L1": 0.35, "L2": 0.30, "L3": 0.20, "L4": 0.10, "L5": 0.05},
            },
            "dept_size_fractions": DEPT_SIZE_FRACTIONS,
            "skills_per_employee": {"min": 2, "max": 4},
        },
        "employee_lifecycle": {
            "promotion_salary_increase": 0.15,
            "promotion_engagement_boost": 10,
            "promotion_flight_risk_reduction": 0.15,
            "quarterly_engagement_decay": -1.5,
            "long_tenure_no_promotion_penalty": -3.0,
            "long_tenure_threshold_quarters": 4,
            "transfer_engagement_penalty": -5,
            "termination_colleague_engagement_penalty": -3,
            "new_hire_engagement_distribution": {"type": "beta", "alpha": 8, "beta": 2, "scale": 100},
        },
        "flight_risk": {
            "model": "logistic",
            "formula": "sigmoid(-1 + 2*pay_factor + 1.5*engagement_factor + 1*tenure_factor + 0.8*market_demand)",
            "turnover_probability": "flight_risk * 0.4 per quarter",
            "bounds": [0.02, 0.95],
        },
        "phase_system": {
            "phases": PHASES,
            "actions_per_phase": PHASE_ACTIONS,
            "minimum_actions_per_phase": PHASE_MIN_ACTIONS,
        },
        "stochastic_events": [
            {
                "name": ev.name,
                "description": ev.description,
                "probability": ev.probability,
            }
            for ev in ALL_EVENTS
        ],
        "scoring": {
            "quarterly_reward_weights": {
                "hcva_improvement": 0.40,
                "hcroi_improvement": 0.20,
                "qips_improvement": 0.20,
                "five_indexes": 0.20,
            },
            "final_score_weights": {
                "hcva_trajectory": 0.25,
                "hcroi_final_vs_baseline": 0.20,
                "employee_value": 0.20,
                "qips_consistency": 0.15,
                "five_indexes_cumulative": 0.10,
                "financial_health": 0.10,
            },
            "calibration_targets": {
                "random_agent": "0.15-0.25",
                "reasonable_heuristic": "~0.50",
                "strong_agent": "0.70+",
            },
        },
        "heuristic_strategy": {
            "description": "Data-driven heuristic used in the demo endpoint",
            "hiring_rate": "12% of current headcount for primary dept, 8% for secondary",
            "training_budget_fraction": 0.30,
            "training_hours_per_session": 20,
            "compensation_adjustment_pct": 3.0,
            "retention_budget_fraction": 0.20,
            "promotion_criteria": {"min_performance": 4.0, "max_level": 4, "max_per_quarter": 3},
            "termination_criteria": {"max_performance": 1.8, "max_per_quarter": 2},
        },
        "roles_by_department": {
            dept: [{"role": role, "level": level} for role, level in roles]
            for dept, roles in ROLES.items()
        },
        "skills_by_department": DEPT_SKILLS,
    }


# ── Scenarios endpoint ──────────────────────────────────────────────


@app.get("/scenarios")
async def scenarios():
    """Return all available task scenarios with their configurations."""
    tasks_dir = Path(__file__).resolve().parent.parent / "tasks"
    task_list = []

    if tasks_dir.exists():
        for f in sorted(tasks_dir.glob("*.json")):
            try:
                task_list.append(json.loads(f.read_text()))
            except (json.JSONDecodeError, OSError):
                continue

    # Also include scenario effect descriptions
    scenario_effects = {
        "high_eng_turnover": {
            "modifications": "Engineering flight_risk += 0.20, engagement -= 10",
            "challenge": "Stabilize retention in a bleeding engineering department",
        },
        "budget_cuts": {
            "modifications": "HR budget halved (50% reduction)",
            "challenge": "Optimize workforce with severely constrained resources",
        },
        "rapid_growth": {
            "modifications": "Revenue +30%, 15% employees deactivated (understaffing)",
            "challenge": "Scale the workforce while maintaining quality and culture",
        },
        "balanced_optimization": {
            "modifications": "All employees: performance -0.3, engagement -8",
            "challenge": "Improve below-average metrics across the board",
        },
    }

    return {
        "scenarios": [
            {
                **task,
                "effects": scenario_effects.get(task.get("params", {}).get("scenario"), {}),
            }
            for task in task_list
        ],
        "default": {
            "seed": 42,
            "size": 300,
            "scenario": None,
            "description": "Standard company with no scenario modifications",
        },
    }


# ── Demo endpoint ───────────────────────────────────────────────────


@app.get("/demo")
async def demo(
    seed: int = Query(42, description="Random seed for reproducibility"),
    size: int = Query(300, ge=50, le=1000, description="Company size (employees)"),
    scenario: Optional[str] = Query(
        None,
        description="Scenario variant",
        enum=["high_eng_turnover", "budget_cuts", "rapid_growth", "balanced_optimization"],
    ),
):
    """Run a full 6-quarter demo with a heuristic strategy and return results.

    Executes data-driven HR decisions through all HCM:21 phases for 6 quarters.
    No LLM API key required β€” uses a built-in heuristic agent.
    """
    from demo import run_demo_json

    return run_demo_json(seed=seed, size=size, scenario=scenario)