devxpy commited on
Commit
e181764
·
verified ·
1 Parent(s): 6c8a204

Upload folder using huggingface_hub

Browse files
Dockerfile CHANGED
@@ -22,7 +22,7 @@ RUN apt-get update && \
22
 
23
  # Build argument to control whether we're building standalone or in-repo
24
  ARG BUILD_MODE=in-repo
25
- ARG ENV_NAME=basic_openenv
26
 
27
  # Copy environment code (always at root of build context)
28
  COPY . /app/env
 
22
 
23
  # Build argument to control whether we're building standalone or in-repo
24
  ARG BUILD_MODE=in-repo
25
+ ARG ENV_NAME=hr_onboarding_env
26
 
27
  # Copy environment code (always at root of build context)
28
  COPY . /app/env
ENVIRONMENT_DEEP_DIVE.md ADDED
@@ -0,0 +1,963 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HR Onboarding & Offboarding Environment — Deep Dive
2
+
3
+ This document explains **everything** about the environment in detail: what it is, how it works internally, what each component does, how the agent interacts with it, how reward is computed, and what makes tasks easy or hard. Read this if you want a complete mental model of the system.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [What Is This Environment?](#1-what-is-this-environment)
10
+ 2. [The Big Picture: How It All Fits Together](#2-the-big-picture-how-it-all-fits-together)
11
+ 3. [World State: The Simulated Company](#3-world-state-the-simulated-company)
12
+ 4. [Tools: What the Agent Can Do](#4-tools-what-the-agent-can-do)
13
+ 5. [Tasks: What the Agent Is Asked To Do](#5-tasks-what-the-agent-is-asked-to-do)
14
+ 6. [Rubrics: How We Score the Agent](#6-rubrics-how-we-score-the-agent)
15
+ 7. [The OpenEnv Interface: How It All Connects](#7-the-openenv-interface-how-it-all-connects)
16
+ 8. [A Full Episode Walkthrough](#8-a-full-episode-walkthrough)
17
+ 9. [Business Rules & Edge Cases](#9-business-rules--edge-cases)
18
+ 10. [File-by-File Reference](#10-file-by-file-reference)
19
+
20
+ ---
21
+
22
+ ## 1. What Is This Environment?
23
+
24
+ This is a **reinforcement learning environment** that simulates the HR department of a fictional company called **AcmeCorp**. The environment is designed to train and evaluate LLM agents on real-world enterprise workflows.
25
+
26
+ ### The Analogy
27
+
28
+ Think of it like a video game:
29
+ - **The world** is AcmeCorp — a company with 200 employees, 8 departments, laptops, software licenses, access badges, etc.
30
+ - **The player** is an LLM agent that acts as an HR automation bot.
31
+ - **The quest** is a task like "Onboard Priya Sharma to Engineering" or "Offboard a departing director."
32
+ - **The moves** are tool calls — the agent can call `hr_create_employee`, `it_assign_asset`, `email_send`, etc.
33
+ - **The score** is computed by a rubric that checks: Did the agent call the right tools? In the right order? With the right parameters?
34
+
35
+ ### Why Does This Exist?
36
+
37
+ We're training LLMs to be better at **multi-step tool calling** in enterprise settings. Most LLM benchmarks test simple Q&A or single-tool-use. This environment tests whether an agent can:
38
+
39
+ 1. **Plan** a sequence of 3-10 tool calls to complete a complex workflow
40
+ 2. **Follow business rules** (RBAC levels, department restrictions, headcount limits)
41
+ 3. **Handle edge cases** (license seats full, manager on leave, contractor-specific policies)
42
+ 4. **Recover from errors** (tool returns an error → agent adapts)
43
+ 5. **Prioritize** (complete all required steps within a limited step budget of 15)
44
+
45
+ ---
46
+
47
+ ## 2. The Big Picture: How It All Fits Together
48
+
49
+ ```
50
+ ┌─────────────────────────────────────────────────────────┐
51
+ │ LLM AGENT │
52
+ │ (GPT, Claude, Qwen, etc.) │
53
+ │ │
54
+ │ Receives: task instruction + tool results │
55
+ │ Produces: tool calls (JSON) │
56
+ └────────────────────────┬────────────────────────────────┘
57
+
58
+ tool call
59
+ {"tool": "hr_create_employee",
60
+ "params": {"name": "Priya Sharma", ...}}
61
+
62
+
63
+ ┌─────────────────────────────────────────────────────────┐
64
+ │ ENVIRONMENT (this repo) │
65
+ │ │
66
+ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
67
+ │ │ Tasks │ │ Tools │ │ Rubrics │ │
68
+ │ │ (77) │ │ (25) │ │ (scoring)│ │
69
+ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
70
+ │ │ │ │ │
71
+ │ │ ┌─────────▼──────────┐ │ │
72
+ │ └───►│ World State │◄──┘ │
73
+ │ │ (500+ entities) │ │
74
+ │ │ - 200 employees │ │
75
+ │ │ - 100 IT assets │ │
76
+ │ │ - 20 access roles │ │
77
+ │ │ - 15 policies │ │
78
+ │ │ - 15 licenses │ │
79
+ │ │ - 15 sec groups │ │
80
+ │ │ - 8 departments │ │
81
+ │ └────────────────────┘ │
82
+ └─────────────────────────────────────────────────────────┘
83
+ ```
84
+
85
+ **Data flow for one episode:**
86
+
87
+ 1. `env.reset()` → Picks a task, resets world state, returns task instruction to agent
88
+ 2. Agent reads instruction → decides which tool to call → sends `HROnboardingAction`
89
+ 3. `env.step(action)` → Executes tool against world state → returns result to agent
90
+ 4. Repeat steps 2-3 up to 15 times
91
+ 5. When episode ends → Rubric evaluator checks the action log → computes reward (0.0 to 1.0)
92
+
93
+ ---
94
+
95
+ ## 3. World State: The Simulated Company
96
+
97
+ The world state (`server/world.py`) is the **single source of truth** for everything in the simulated company. It's an in-memory database that tools read from and write to.
98
+
99
+ ### 3.1 Entities (loaded from `server/data/`)
100
+
101
+ #### Employees (`employees.json` — 200 records)
102
+
103
+ Every employee has:
104
+
105
+ ```json
106
+ {
107
+ "emp_id": "emp_0001",
108
+ "name": "Alice Johnson",
109
+ "email": "alice.johnson@acmecorp.com",
110
+ "department": "Engineering",
111
+ "level": "L4",
112
+ "role": "Engineering Manager",
113
+ "manager_id": "emp_0003",
114
+ "status": "active",
115
+ "date_of_joining": "2019-03-15",
116
+ "date_of_leaving": null,
117
+ "is_contractor": false,
118
+ "phone": "+1-650-555-1234",
119
+ "location": "San Francisco"
120
+ }
121
+ ```
122
+
123
+ Key fields:
124
+ - **`level`**: L1 (Associate) → L2 (Senior) → L3 (Team Lead) → L4 (Manager) → L5 (Director) → L6 (VP). This drives RBAC — certain actions require certain levels.
125
+ - **`status`**: `active` (normal), `pending` (just created, not yet onboarded), `offboarded` (no longer at company)
126
+ - **`manager_id`**: Creates a tree hierarchy. Every employee (except department heads) has a manager.
127
+ - **`is_contractor`**: Contractors have different onboarding rules (no VPN, limited access, requires legal approval).
128
+
129
+ The 200 employees are distributed across 8 departments with realistic org hierarchies (each department has a head at L5/L6, managers at L3/L4, and individual contributors at L1/L2).
130
+
131
+ #### Departments (`departments.json` — 8 departments)
132
+
133
+ ```json
134
+ {
135
+ "dept_id": "dept_001",
136
+ "name": "Engineering",
137
+ "head": "emp_0003",
138
+ "budget": 5000000,
139
+ "headcount_limit": 45,
140
+ "required_tools": ["GitHub", "Jira", "AWS", "Slack", "VSCode"],
141
+ "onboarding_steps": [
142
+ "Submit signed offer letter and NDA",
143
+ "Complete background check verification",
144
+ "Provision email and Slack accounts",
145
+ "Assign laptop and peripherals",
146
+ "Set up development environment access",
147
+ "Schedule orientation with team lead",
148
+ "Add to relevant Slack channels"
149
+ ],
150
+ "offboarding_steps": [
151
+ "Revoke all system access",
152
+ "Return laptop and equipment",
153
+ "Complete knowledge transfer",
154
+ "Conduct exit interview",
155
+ "Process final payroll",
156
+ "Remove from Slack channels and mailing lists"
157
+ ]
158
+ }
159
+ ```
160
+
161
+ Key fields:
162
+ - **`headcount_limit`**: Maximum number of active+pending employees allowed. If a department is at its limit, `hr_create_employee` will return an error. Two departments (Data Science = 25, Marketing = 30) are intentionally at or near their limits to create edge cases.
163
+ - **`onboarding_steps` / `offboarding_steps`**: Department-specific checklists. When you create an onboarding request, these become the steps that must be completed.
164
+ - **`required_tools`**: Which software tools the department uses (used for context, not enforced).
165
+
166
+ #### IT Assets (`it_assets.json` — 100 assets)
167
+
168
+ ```json
169
+ {
170
+ "asset_id": "asset_001",
171
+ "type": "laptop",
172
+ "brand": "Apple",
173
+ "model": "MacBook Pro 16\" M3 Max",
174
+ "specs": "16-inch Liquid Retina XDR, M3 Max, 64GB RAM, 2TB SSD",
175
+ "status": "assigned",
176
+ "assigned_to": "emp_0001",
177
+ "purchase_date": "2024-01-15"
178
+ }
179
+ ```
180
+
181
+ Breakdown: 50 laptops, 25 monitors, 15 phones, 10 headsets. About half are assigned, half are available. The agent needs to check available assets before assigning one during onboarding.
182
+
183
+ #### Access Roles (`access_roles.json` — 20 roles)
184
+
185
+ ```json
186
+ {
187
+ "role_id": "role_001",
188
+ "name": "basic_employee",
189
+ "permissions": ["email_access", "slack_access", "intranet_access"],
190
+ "department": "all",
191
+ "level_requirement": "L1"
192
+ }
193
+ ```
194
+
195
+ Each role has:
196
+ - **`department`**: Which department can use this role. `"all"` means any department. `"Engineering"` means only Engineering employees.
197
+ - **`level_requirement`**: Minimum level needed. `"L1"` means anyone. `"L4"` means only managers and above.
198
+
199
+ Example roles:
200
+ - `basic_employee` (all departments, L1+) — email, slack, intranet
201
+ - `engineering_developer` (Engineering only, L1+) — github, aws_dev, ci_cd
202
+ - `security_admin` (Security only, L4+) — siem, vault, firewall_mgmt
203
+ - `executive_access` (all departments, L5+) — board_docs, exec_dashboard
204
+
205
+ If an L1 employee tries to get `security_admin` (L4+ required), the tool returns an error. If a Marketing employee tries to get `engineering_developer` (Engineering only), the tool returns an error. These are the RBAC constraints the agent must learn.
206
+
207
+ #### Policies (`policies.json` — 15 policies)
208
+
209
+ ```json
210
+ {
211
+ "policy_id": "pol_001",
212
+ "title": "Standard Employee Onboarding Policy",
213
+ "department": "all",
214
+ "content": "All new employees must complete the following steps within their first 30 days...",
215
+ "last_updated": "2024-06-15",
216
+ "key_rules": [
217
+ "Employee record must be created before any provisioning",
218
+ "Manager approval required for all onboarding requests",
219
+ "IT assets must be checked for availability before assignment"
220
+ ]
221
+ }
222
+ ```
223
+
224
+ Policies cover: onboarding, offboarding, badge access, contractor hiring, termination procedures, software licensing, data handling, remote work, etc. The `policy_lookup` tool lets the agent read these before acting.
225
+
226
+ #### Software Licenses (dynamically initialized — 15 licenses)
227
+
228
+ Not stored in a JSON file; initialized in `world.py` at runtime. Each license tracks:
229
+ - `total_seats` and `used_seats`
230
+ - `department_restriction` (which department can use it, or `null` for all)
231
+
232
+ **Two licenses are intentionally full** (used_seats = total_seats):
233
+ - **Netsuite** (15/15 seats) — Finance tool
234
+ - **LinkedIn Sales Navigator** (25/25 seats) — Sales tool
235
+
236
+ This creates edge cases: if a task asks the agent to assign a Netsuite license to a new Finance hire, the agent should discover it's full and handle that situation.
237
+
238
+ #### Security Groups (dynamically initialized — 15 groups)
239
+
240
+ Groups like `all_employees`, `engineering_team`, `vpn_users`, `server_room_access`, `contractors`, etc. Each has a list of accessible resources.
241
+
242
+ #### Templates (`templates.json` — 12 templates)
243
+
244
+ Email and Slack message templates for welcome messages, farewell emails, IT setup notifications, etc. These provide context for communication tasks.
245
+
246
+ ### 3.2 Dynamic Collections (created during episodes)
247
+
248
+ These start empty and get populated as the agent takes actions:
249
+ - **`onboarding_requests`**: Created by `onboarding_create_request`
250
+ - **`offboarding_requests`**: Created by `offboarding_create_request`
251
+ - **`approvals`**: Created by `approval_request`
252
+ - **`emails`**: Created by `email_send`
253
+ - **`slack_messages`**: Created by `slack_send_message`
254
+ - **`meetings`**: Created by `meeting_schedule`
255
+ - **`badges`**: Created by `access_create_badge`
256
+
257
+ ### 3.3 World State Reset
258
+
259
+ At the start of each episode (`env.reset()`), the world state is deep-copied back to its initial state. This means:
260
+ - All 200 employees are back to their original status
261
+ - All assets are back to their original assignment
262
+ - All dynamic collections (requests, emails, meetings, etc.) are cleared
263
+ - The action log is cleared
264
+
265
+ This ensures each episode is independent — the agent starts from a clean slate every time.
266
+
267
+ ### 3.4 Indexes
268
+
269
+ For performance, the world state builds lookup indexes:
270
+ - `_emp_by_id`: O(1) employee lookup by emp_id
271
+ - `_emp_by_email`: O(1) lookup by email
272
+ - `_emp_by_dept`: O(1) lookup by department (returns list)
273
+ - `_dept_by_id` / `_dept_by_name`: O(1) department lookup
274
+ - `_asset_by_id`: O(1) asset lookup
275
+ - `_role_by_id`: O(1) access role lookup
276
+ - `_policy_by_id`: O(1) policy lookup
277
+
278
+ These are rebuilt after every reset and after certain mutations (like `reassign_reports`).
279
+
280
+ ---
281
+
282
+ ## 4. Tools: What the Agent Can Do
283
+
284
+ The agent interacts with the world through **25 tools** defined in `server/tools.py`. Each tool is a function that takes parameters, operates on the world state, and returns a result dict.
285
+
286
+ ### 4.1 Architecture
287
+
288
+ ```
289
+ Agent sends: {"tool_name": "hr_create_employee", "arguments": {...}}
290
+
291
+
292
+ ┌─────────────────┐
293
+ │ ToolRegistry │
294
+ │ │
295
+ │ execute(name, │──── if unknown tool ──→ {"success": false, "error": "Unknown tool"}
296
+ │ params) │
297
+ │ │
298
+ │ routes to: │
299
+ │ _hr_create_..()│──→ calls world.create_employee(params)
300
+ └────────┬────────┘
301
+
302
+
303
+ ┌─────────────────┐
304
+ │ WorldState │
305
+ │ │──→ validates inputs
306
+ │ create_employee│──→ checks headcount limits
307
+ │ │──→ generates emp_id
308
+ │ │──→ adds to state
309
+ │ │──→ updates indexes
310
+ └────────┬────────┘
311
+
312
+
313
+ Result: {"success": true, "employee": {...}}
314
+ + logged to action_log for rubric evaluation
315
+ ```
316
+
317
+ Every tool call is **logged** to the action log with:
318
+ - `tool`: name of the tool called
319
+ - `params`: parameters passed
320
+ - `result`: what the tool returned
321
+ - `timestamp`: when it was called
322
+
323
+ This log is what the rubric evaluator uses to score the agent.
324
+
325
+ ### 4.2 Tool Categories
326
+
327
+ #### HR System Tools (5 tools)
328
+
329
+ | Tool | What It Does | Modifies State? |
330
+ |------|-------------|----------------|
331
+ | `hr_create_employee` | Creates a new employee record. Validates department exists, checks headcount limit, generates emp_id, sets status to "pending". | YES — adds employee |
332
+ | `hr_read_employee` | Looks up one employee by emp_id or email. | No — read only |
333
+ | `hr_update_employee` | Updates any employee field (except emp_id). Used to change status, department, manager, etc. | YES — modifies employee |
334
+ | `hr_search_employees` | Searches employees by filters (department, level, status, location, role, name). Returns all matches. | No — read only |
335
+ | `hr_get_org_chart` | Returns the org hierarchy for a department as a tree structure (who reports to whom). | No — read only |
336
+
337
+ #### Onboarding/Offboarding Tools (6 tools)
338
+
339
+ | Tool | What It Does | Modifies State? |
340
+ |------|-------------|----------------|
341
+ | `onboarding_create_request` | Creates an onboarding request for a "pending" employee. Generates a checklist of department-specific steps. | YES — creates request |
342
+ | `onboarding_get_status` | Checks progress of an onboarding request (which steps are done/pending). | No — read only |
343
+ | `onboarding_complete_step` | Marks a specific onboarding step as completed. If all steps are done, sets request to "completed" and employee status to "active". | YES — updates request & employee |
344
+ | `offboarding_create_request` | Creates an offboarding request. Different steps for resignation vs termination. | YES — creates request |
345
+ | `offboarding_get_status` | Checks offboarding progress. | No — read only |
346
+ | `offboarding_complete_step` | Marks an offboarding step as completed. If all done, sets employee to "offboarded". | YES — updates request & employee |
347
+
348
+ **Important**: Termination offboarding has different steps than resignation: `["access_revocation", "asset_return", "final_payroll", "legal_review"]` — notably, no farewell communications or exit interview.
349
+
350
+ #### IT Provisioning Tools (5 tools)
351
+
352
+ | Tool | What It Does | Modifies State? |
353
+ |------|-------------|----------------|
354
+ | `it_assign_asset` | Assigns a specific asset (by asset_id) to an employee. Asset must be "available". | YES — marks asset as assigned |
355
+ | `it_get_available_assets` | Lists all unassigned assets, optionally filtered by type (laptop, monitor, phone, headset). | No — read only |
356
+ | `it_create_account` | Creates IT accounts (email, Slack, VPN, GitHub, etc.) for an employee. | YES — adds accounts to employee |
357
+ | `it_revoke_access` | Revokes all IT accounts for an employee (sets status to "revoked"). Used in offboarding. | YES — modifies accounts |
358
+ | `it_get_software_licenses` | Checks license seat availability. Shows total_seats, used_seats, and department_restriction. | No — read only |
359
+
360
+ #### Access Control Tools (4 tools)
361
+
362
+ | Tool | What It Does | Modifies State? |
363
+ |------|-------------|----------------|
364
+ | `access_assign_role` | Assigns an RBAC role to an employee. **Checks level requirements and department restrictions.** | YES — adds role to employee |
365
+ | `access_create_badge` | Creates a physical access badge with zone permissions. **Server room access requires L4+ security approval.** | YES — creates badge |
366
+ | `access_revoke_role` | Removes a specific role from an employee. | YES — removes role |
367
+ | `access_get_security_groups` | Lists all 15 security groups and their resources. | No — read only |
368
+
369
+ #### Communication Tools (3 tools)
370
+
371
+ | Tool | What It Does | Modifies State? |
372
+ |------|-------------|----------------|
373
+ | `email_send` | Sends an email. Requires from_address, to_address, subject, body. | YES — logs email |
374
+ | `slack_send_message` | Posts a Slack message. Requires channel, sender, text. | YES — logs message |
375
+ | `meeting_schedule` | Schedules a meeting. Requires title, attendees (list of emp_ids), datetime, meeting_type. | YES — logs meeting |
376
+
377
+ #### Policy & Approval Tools (2 tools)
378
+
379
+ | Tool | What It Does | Modifies State? |
380
+ |------|-------------|----------------|
381
+ | `policy_lookup` | Searches policies by topic, department, or policy_id. Returns policy content and key_rules. | No — read only |
382
+ | `approval_request` | Submits an approval. **Checks approver level** (L3+ for manager approval, L4+ for security approval). | YES — creates approval |
383
+
384
+ ### 4.3 Error Handling
385
+
386
+ Every tool returns `{"success": true, ...}` or `{"success": false, "error": "..."}`. Common errors:
387
+
388
+ - `"Employee emp_XXXX not found"` — invalid emp_id
389
+ - `"Department 'X' has reached its headcount limit (N)"` — can't create more employees
390
+ - `"Asset asset_XXX is not available"` — already assigned to someone
391
+ - `"Role X not found"` — invalid role_id
392
+ - `"Employee level L1 does not meet minimum L4 for role security_admin"` — RBAC violation
393
+ - `"Role engineering_developer is restricted to Engineering department"` — department restriction
394
+ - `"No available seats for Netsuite (all 15 seats in use)"` — license full
395
+ - `"Approver must be L4+ for security approval"` — approver too junior
396
+ - `"Server room access requires L4+ security approval"` — missing prerequisite approval
397
+
398
+ The agent must learn to handle these errors gracefully — check availability before assigning, verify role requirements before assigning access, etc.
399
+
400
+ ---
401
+
402
+ ## 5. Tasks: What the Agent Is Asked To Do
403
+
404
+ Tasks are defined in `server/tasks.py`. The `TaskGenerator` class creates 77 tasks using the world state data (actual employee names, IDs, departments).
405
+
406
+ ### 5.1 Task Structure
407
+
408
+ Every task has:
409
+
410
+ ```python
411
+ Task(
412
+ task_id="task_0015",
413
+ instruction="Onboard new hire Priya Sharma to Engineering as L2 Software Engineer. Create their employee record and initiate the onboarding request.",
414
+ difficulty="medium",
415
+ category="onboarding",
416
+ expected_tools=["hr_create_employee", "onboarding_create_request"],
417
+ rubric_criteria=[
418
+ {"name": "created_employee", "description": "Created employee record", "check": "tool_used:hr_create_employee"},
419
+ {"name": "correct_name", "description": "Used correct name", "check": "param_value:hr_create_employee.name=Priya Sharma"},
420
+ {"name": "correct_dept", "description": "Assigned to correct department", "check": "param_value:hr_create_employee.department=Engineering"},
421
+ {"name": "correct_level", "description": "Set correct level", "check": "param_value:hr_create_employee.level=L2"},
422
+ {"name": "correct_role", "description": "Set correct role", "check": "param_value:hr_create_employee.role=Software Engineer"},
423
+ {"name": "initiated_onboarding", "description": "Created onboarding request", "check": "tool_used:onboarding_create_request"},
424
+ {"name": "sequencing", "description": "Created employee before onboarding request", "check": "tool_order:hr_create_employee<onboarding_create_request"},
425
+ ],
426
+ setup_fn=None, # or a function that pre-configures world state
427
+ context={"name": "Priya Sharma", "department": "Engineering", "level": "L2", "role": "Software Engineer"},
428
+ )
429
+ ```
430
+
431
+ ### 5.2 Task Categories & Counts
432
+
433
+ #### Simple Lookup Tasks (14 tasks)
434
+
435
+ These require 1-2 tool calls. Testing basic tool selection and parameter passing.
436
+
437
+ - **Employee lookups** (3): "Look up the employee record for X (ID: emp_XXXX)."
438
+ - Expected: `hr_read_employee` with correct emp_id
439
+ - Rubric: 2 criteria (correct tool + correct parameter)
440
+
441
+ - **Department search** (2): "List all employees in the Y department."
442
+ - Expected: `hr_search_employees` with department filter
443
+ - Rubric: 2 criteria
444
+
445
+ - **Org chart** (1): "Show me the organizational chart for the Z department."
446
+ - Expected: `hr_get_org_chart`
447
+ - Rubric: 2 criteria
448
+
449
+ - **Asset availability** (1): "What laptops are currently available for assignment?"
450
+ - Expected: `it_get_available_assets`
451
+ - Rubric: 1 criterion
452
+
453
+ - **License check** (1): "Check how many Jira license seats are available."
454
+ - Expected: `it_get_software_licenses`
455
+ - Rubric: 1 criterion
456
+
457
+ - **Policy lookup** (1): "What is the company's policy on onboarding new employees?"
458
+ - Expected: `policy_lookup`
459
+ - Rubric: 1 criterion
460
+
461
+ - **Security groups** (1): "List all security groups and their accessible resources."
462
+ - Expected: `access_get_security_groups`
463
+ - Rubric: 1 criterion
464
+
465
+ - **Onboarding status** (3): "Check the onboarding status for employee X (emp_XXXX)."
466
+ - Expected: `onboarding_get_status`
467
+ - Rubric: 2 criteria
468
+ - **Setup function**: Pre-creates an onboarding request so there's something to look up
469
+
470
+ - **Resource availability** (1): "Check if there are available laptops and Jira licenses for a new Engineering hire."
471
+ - Expected: `it_get_available_assets` + `it_get_software_licenses`
472
+ - Rubric: 2 criteria
473
+
474
+ #### Medium Onboarding Tasks (10 tasks)
475
+
476
+ These require 2-4 tool calls. Testing multi-step workflows.
477
+
478
+ - "Onboard new hire X to Y as LZ Role. Create their employee record and initiate the onboarding request."
479
+ - Expected: `hr_create_employee` → `onboarding_create_request`
480
+ - Rubric: 7 criteria (create, correct name/dept/level/role, onboarding, sequencing)
481
+ - 10 different hire combinations across different departments
482
+
483
+ #### Complex Onboarding Tasks (10 tasks)
484
+
485
+ These require 5-10 tool calls. Full end-to-end workflows.
486
+
487
+ **Full onboarding (5 tasks)**: "Fully onboard X as LY Role in Z. Create employee record, initiate onboarding, assign a laptop, create IT accounts, set up access roles, send welcome email, and schedule orientation meeting."
488
+ - Expected: `hr_create_employee` → `onboarding_create_request` → `it_get_available_assets` → `it_assign_asset` → `it_create_account` → `access_assign_role` → `email_send` or `slack_send_message` → `meeting_schedule`
489
+ - Rubric: 10 criteria (all the above + sequencing + completeness)
490
+ - Context includes manager emp_id
491
+
492
+ **Complex onboarding with approvals (5 tasks)**: "Onboard X as LY Role in Z. Create record, initiate onboarding, complete at least 3 onboarding steps, assign access roles, and get required approvals."
493
+ - Expected: `hr_create_employee` → `onboarding_create_request` → `onboarding_complete_step` (×3+) → `access_assign_role` → `approval_request`
494
+ - Rubric: 6-7 criteria
495
+
496
+ #### Medium Offboarding Tasks (12 tasks)
497
+
498
+ - "Initiate offboarding for X who is resigning. Create the offboarding request and revoke their system access."
499
+ - Expected: `offboarding_create_request` → `it_revoke_access`
500
+ - Rubric: 3-4 criteria
501
+ - **Setup function**: Sets the employee's `date_of_leaving` to create realistic context
502
+
503
+ #### Complex Offboarding Tasks (8 tasks)
504
+
505
+ **Full offboarding (4 tasks)**: "Fully offboard X, a LY Role in Z who is resigning. Create offboarding request, revoke all access roles, reclaim their laptop, revoke IT access, send farewell email, schedule exit interview."
506
+ - Expected: many tools in sequence
507
+ - Rubric: 8-10 criteria
508
+ - **Setup function**: Assigns assets, roles, and badges to the employee so there's something to revoke/reclaim
509
+
510
+ **Complex offboarding with handover (4 tasks)**: "Process the complete offboarding for X from Y. Create the offboarding request, revoke access, reclaim assets, send farewell, complete at least 3 offboarding steps."
511
+ - Rubric: 6-7 criteria
512
+
513
+ #### Edge Case Tasks (12 tasks)
514
+
515
+ These test business rule awareness and error handling.
516
+
517
+ - **Headcount limit** (2): "Onboard a new L1 to Marketing/Finance." (Department is at limit)
518
+ - Agent should get an error from `hr_create_employee` and the rubric checks that the error message contains "headcount_limit"
519
+
520
+ - **License full** (2): "Assign a Netsuite/LinkedIn Sales Navigator license to a new hire."
521
+ - Agent should discover no seats available
522
+
523
+ - **Manager on leave** (1): "Onboard to Security but the manager is on leave — find the skip-level manager."
524
+ - **Setup function**: Sets the designated manager's status to "on_leave"
525
+ - Agent needs to use `hr_read_employee` to check, realize the manager is unavailable, then look up the org chart or the manager's manager
526
+
527
+ - **Contractor onboarding** (1): "Onboard contractor Amit Verma to Engineering. Contractors need legal approval."
528
+ - Agent should set `is_contractor: true` and submit a `legal_approval`
529
+
530
+ - **Asset return during offboarding** (1): "Offboard Marta Wagner who has company assets that need to be returned."
531
+ - **Setup function**: Assigns assets to this employee
532
+ - Agent should use `it_get_available_assets` or similar to find assigned assets, then reclaim them
533
+
534
+ - **Offer rescinded** (1): "The offer for Wei Xu has been rescinded. They are currently mid-onboarding."
535
+ - **Setup function**: Creates an in-progress onboarding request
536
+ - Agent should offboard someone who hasn't fully onboarded yet
537
+
538
+ - **Termination** (1): "Mark Taylor is being terminated effective immediately."
539
+ - Agent should use `reason: "termination"` (different offboarding steps, no farewell email)
540
+ - Rubric checks `tool_not_used:email_send` (no farewell for terminations)
541
+
542
+ - **Level mismatch** (1): "Assign the security_admin access role to a new L1 Security Associate."
543
+ - security_admin requires L4+ → should fail
544
+ - Rubric checks that the error contains the level requirement
545
+
546
+ - **Department restriction** (1): "A Marketing employee needs access to the Engineering GitHub repository."
547
+ - engineering_developer role is Engineering-only → should fail
548
+
549
+ - **Policy-dependent task** (1): "Before onboarding a new Security team member, look up the badge access policy and check what approvals are needed."
550
+ - Agent should call `policy_lookup` before acting
551
+
552
+ #### Cross-Workflow Tasks (10 tasks)
553
+
554
+ **Department transfers** (3): "X is transferring from A to B. Process the department transfer."
555
+ - Agent needs to offboard from old department + onboard to new one
556
+ - Expected: `hr_update_employee` (change department) + `offboarding_create_request` + `onboarding_create_request`
557
+
558
+ **Rehires** (2): "Rehire X who was previously offboarded."
559
+ - **Setup function**: Sets employee status to "offboarded"
560
+ - Agent should update status back to "pending" and create new onboarding request
561
+
562
+ **Bulk status queries** (3): "Generate a status report for all employees in X department. List each employee, their status, and current onboarding/offboarding status."
563
+ - Tests multiple tool calls: `hr_search_employees` + multiple `onboarding_get_status`
564
+
565
+ **Manager departure** (2): "Manager X in Engineering is leaving. They have N direct reports. Process their offboarding and reassign their reports."
566
+ - Agent needs to: find direct reports → find skip-level manager → offboard departing manager → reassign reports
567
+ - **Setup function**: Ensures the manager has direct reports
568
+
569
+ ### 5.3 Setup Functions
570
+
571
+ Many tasks have a `setup_fn` — a function that modifies the world state before the task starts. This creates the preconditions the task assumes.
572
+
573
+ Examples:
574
+ - Onboarding status tasks: Creates an onboarding request so there's something to look up
575
+ - Offboarding tasks: Assigns assets/roles/badges to the employee, sets their leaving date
576
+ - Edge case tasks: Sets a manager's status to "on_leave", or an employee's status to "offboarded" for rehire
577
+ - Manager departure: Ensures the manager has direct reports in the org hierarchy
578
+
579
+ The agent never sees the setup function — it only sees the task instruction and tool results.
580
+
581
+ ---
582
+
583
+ ## 6. Rubrics: How We Score the Agent
584
+
585
+ The rubric system (`server/rubrics.py`) evaluates the agent's action log against a set of criteria for each task.
586
+
587
+ ### 6.1 How Scoring Works
588
+
589
+ ```
590
+ Agent's action log:
591
+ 1. hr_create_employee({"name": "Priya Sharma", "department": "Engineering", ...})
592
+ 2. onboarding_create_request({"employee_id": "emp_0201"})
593
+
594
+ Task rubric:
595
+ ✓ tool_used:hr_create_employee → PASS (tool was called)
596
+ ✓ param_value:hr_create_employee.name=Priya Sharma → PASS (correct name)
597
+ ✓ param_value:hr_create_employee.department=Engineering → PASS
598
+ ✓ param_value:hr_create_employee.level=L2 → PASS
599
+ ✓ param_value:hr_create_employee.role=Software Engineer → PASS
600
+ ✓ tool_used:onboarding_create_request → PASS
601
+ ✓ tool_order:hr_create_employee<onboarding_create_request → PASS (correct order)
602
+
603
+ Score = 7/7 = 1.0 (100%)
604
+ Passed = True (all criteria met)
605
+ ```
606
+
607
+ ### 6.2 Rubric Check Types (8 types)
608
+
609
+ | Check Type | Format | What It Checks |
610
+ |-----------|--------|---------------|
611
+ | `tool_used` | `tool_used:hr_create_employee` | Was this tool called at least once? |
612
+ | `tool_not_used` | `tool_not_used:email_send` | Was this tool **NOT** called? (e.g., no farewell email for terminations) |
613
+ | `tool_used_any` | `tool_used_any:email_send,slack_send_message` | Was at **least one** of these tools called? |
614
+ | `param_value` | `param_value:hr_create_employee.name=Priya Sharma` | Was the tool called with this **exact** parameter value? |
615
+ | `param_contains` | `param_contains:policy_lookup.topic=onboard` | Does the parameter **contain** this substring? (case-insensitive) |
616
+ | `tool_order` | `tool_order:hr_create_employee<onboarding_create_request` | Was tool A called **before** tool B? |
617
+ | `tool_count` | `tool_count:onboarding_complete_step>=3` | Was the tool called at **least N times**? |
618
+ | `result_contains` | `result_contains:headcount_limit` | Does any tool result contain this substring? (for edge cases where we expect errors) |
619
+
620
+ ### 6.3 How Checks Work Internally
621
+
622
+ The `RubricEvaluator` parses each criterion's `check` string:
623
+
624
+ ```python
625
+ "tool_order:hr_create_employee<onboarding_create_request"
626
+
627
+ check_type = "tool_order"
628
+ check_args = "hr_create_employee<onboarding_create_request"
629
+
630
+ _check_tool_order("hr_create_employee<onboarding_create_request", action_log)
631
+
632
+ Find first occurrence of hr_create_employee → index 0
633
+ Find first occurrence of onboarding_create_request → index 1
634
+ Is 0 < 1? → True → PASS
635
+ ```
636
+
637
+ For `param_value`, it checks both direct parameters and nested `updates` dict (for `hr_update_employee`):
638
+
639
+ ```python
640
+ "param_value:hr_update_employee.status=active"
641
+
642
+ For each action where tool == "hr_update_employee":
643
+ Check params.get("status") == "active"
644
+ OR check params.get("updates", {}).get("status") == "active"
645
+ ```
646
+
647
+ ### 6.4 Reward Computation
648
+
649
+ ```
650
+ reward = passed_criteria_count / total_criteria_count
651
+ ```
652
+
653
+ - A score of 1.0 means all criteria passed
654
+ - A score of 0.5 means half the criteria passed
655
+ - The task is considered "passed" only if ALL criteria are satisfied (score == 1.0)
656
+
657
+ In the training script, additional modifiers are applied:
658
+ - **Step penalty**: -0.01 per step taken (encourages efficiency)
659
+ - **Completion bonus**: +0.2 if all criteria passed
660
+
661
+ ---
662
+
663
+ ## 7. The OpenEnv Interface: How It All Connects
664
+
665
+ ### 7.1 What Is OpenEnv?
666
+
667
+ OpenEnv is Meta + HuggingFace's standard for packaging RL environments for LLM agents. It provides:
668
+ - A base `Environment` class (server-side) with `reset()`, `step()`, `state`
669
+ - An `EnvClient` class (client-side) that connects over HTTP/WebSocket
670
+ - A `create_app()` function that wraps the environment in a FastAPI server
671
+ - Pydantic `Action` and `Observation` base classes for type safety
672
+
673
+ ### 7.2 Our Implementation
674
+
675
+ ```
676
+ ┌──────────────────────────────────────────┐
677
+ │ models.py │
678
+ │ HROnboardingAction(Action): │
679
+ │ - tool_name: str │
680
+ │ - arguments: Dict[str, Any] │
681
+ │ │
682
+ │ HROnboardingObservation(Observation): │
683
+ │ - task_id: str │
684
+ │ - instruction: str │
685
+ │ - tool_name: str │
686
+ │ - tool_result: Dict[str, Any] │
687
+ │ - step: int │
688
+ │ - max_steps: int │
689
+ │ - available_tools: List[str] ���
690
+ │ - done: bool (from Observation) │
691
+ │ - reward: float (from Observation) │
692
+ │ - metadata: dict (from Observation) │
693
+ └──────────────────────────────────────────┘
694
+
695
+ ┌──────────────────────────────────────────┐
696
+ │ server/hr_onboarding_environment.py │
697
+ │ │
698
+ │ class HROnboardingEnvironment(Environment):
699
+ │ │
700
+ │ reset() → HROnboardingObservation │
701
+ │ 1. Reset world state │
702
+ │ 2. Pick next task │
703
+ │ 3. Run setup_fn if any │
704
+ │ 4. Return observation with: │
705
+ │ - task instruction │
706
+ │ - available tool names │
707
+ │ - difficulty & category metadata │
708
+ │ │
709
+ │ step(action) → HROnboardingObservation │
710
+ │ 1. Increment step counter │
711
+ │ 2. Execute tool via ToolRegistry │
712
+ │ 3. Check if max_steps reached │
713
+ │ 4. If done: evaluate rubric → reward │
714
+ │ 5. Return observation with: │
715
+ │ - tool result │
716
+ │ - reward (0.0 until final step) │
717
+ │ - done flag │
718
+ │ - eval breakdown in metadata │
719
+ │ │
720
+ │ state → State(episode_id, step_count) │
721
+ └──────────────────────────────────────────┘
722
+
723
+ ┌──────────────────────────────────────────┐
724
+ │ server/app.py │
725
+ │ │
726
+ │ app = create_app( │
727
+ │ HROnboardingEnvironment, │
728
+ │ HROnboardingAction, │
729
+ │ HROnboardingObservation, │
730
+ │ env_name="hr_onboarding_env", │
731
+ │ max_concurrent_envs=4, │
732
+ │ ) │
733
+ │ │
734
+ │ Endpoints: │
735
+ │ POST /reset → reset + return obs │
736
+ │ POST /step → execute action │
737
+ │ GET /state → current state │
738
+ │ GET /schema → Action/Obs schemas │
739
+ │ GET /health → {"status": "healthy"} │
740
+ │ WS /ws → persistent session │
741
+ └──────────────────────────────────────────┘
742
+
743
+ ┌──────────────────────────────────────────┐
744
+ │ client.py │
745
+ │ │
746
+ │ class HROnboardingEnv(EnvClient): │
747
+ │ Connects to server via HTTP/WebSocket │
748
+ │ Provides Python API: .reset(), .step() │
749
+ └──────────────────────────────────────────┘
750
+ ```
751
+
752
+ ### 7.3 Episode Lifecycle
753
+
754
+ 1. **Client calls `reset()`** → Server creates new episode, picks task, returns observation
755
+ 2. **Client calls `step(action)`** (up to 15 times) → Server executes tool, returns result
756
+ 3. **On step 15 (or earlier if agent signals done)** → Server evaluates rubric, sets `done=True`, returns final reward
757
+ 4. **If client calls `step()` after done** → Server returns `{"error": "Episode already finished"}`
758
+
759
+ ### 7.4 Important: Reward is Only on Final Step
760
+
761
+ During intermediate steps (1 to 14), `reward` is always `0.0`. The actual rubric evaluation only happens when `done=True` (step 15 or agent signals done). This is by design — the agent doesn't get feedback until the episode ends, which makes it a proper RL problem (delayed reward).
762
+
763
+ ---
764
+
765
+ ## 8. A Full Episode Walkthrough
766
+
767
+ Let's trace a **complex onboarding task** step by step.
768
+
769
+ ### Task
770
+
771
+ > "Fully onboard John Lee as L3 Team Lead - ML in Data Science. Their manager will be Rohan Reddy (emp_0128). Create the employee record, initiate onboarding, assign a laptop, create IT accounts (email, Slack, VPN), set up appropriate access roles for their level, send a welcome email to the team channel, and schedule an orientation meeting with their manager."
772
+
773
+ ### What the Agent Should Do
774
+
775
+ ```
776
+ Step 1: hr_create_employee
777
+ → name: "John Lee", department: "Data Science", level: "L3",
778
+ role: "Team Lead - ML", manager_id: "emp_0128"
779
+ → Result: {success: true, employee: {emp_id: "emp_0201", ...}}
780
+
781
+ Step 2: onboarding_create_request
782
+ → employee_id: "emp_0201"
783
+ → Result: {success: true, request: {request_id: "onb_0001", steps: {...}}}
784
+
785
+ Step 3: it_get_available_assets
786
+ → asset_type: "laptop"
787
+ → Result: {success: true, count: 24, assets: [{asset_id: "asset_003", ...}, ...]}
788
+
789
+ Step 4: it_assign_asset
790
+ → asset_id: "asset_003", employee_id: "emp_0201"
791
+ → Result: {success: true}
792
+
793
+ Step 5: it_create_account
794
+ → employee_id: "emp_0201", account_types: ["email", "slack", "vpn"]
795
+ → Result: {success: true, accounts_created: [...]}
796
+
797
+ Step 6: access_assign_role
798
+ → employee_id: "emp_0201", role_id: "role_004" (data_scientist, requires L1+, Data Science)
799
+ → Result: {success: true, role: "data_scientist", permissions: [...]}
800
+
801
+ Step 7: slack_send_message
802
+ → channel: "#data-science", sender: "hr-bot", text: "Welcome John Lee to the team! ..."
803
+ → Result: {success: true}
804
+
805
+ Step 8: meeting_schedule
806
+ → title: "Orientation: John Lee", attendees: ["emp_0201", "emp_0128"],
807
+ datetime: "2026-03-10T10:00:00", meeting_type: "orientation"
808
+ → Result: {success: true}
809
+
810
+ Agent signals done.
811
+ ```
812
+
813
+ ### Rubric Evaluation
814
+
815
+ ```
816
+ [PASS] created_employee: tool_used:hr_create_employee ✓
817
+ [PASS] initiated_onboarding: tool_used:onboarding_create_request ✓
818
+ [PASS] assigned_laptop: tool_used:it_assign_asset ✓
819
+ [PASS] created_accounts: tool_used:it_create_account ✓
820
+ [PASS] assigned_access: tool_used:access_assign_role ✓
821
+ [PASS] sent_welcome: tool_used_any:email_send,slack_send_message ✓
822
+ [PASS] scheduled_orientation: tool_used:meeting_schedule ✓
823
+ [PASS] sequencing_create_first: tool_order:hr_create_employee<onboarding_create_request ✓
824
+ [PASS] sequencing_asset_check: tool_order:it_get_available_assets<it_assign_asset ✓
825
+ [PASS] completeness: tool_count:onboarding_complete_step>=3 ✗
826
+
827
+ Score: 9/10 = 0.9 (90%)
828
+ ```
829
+
830
+ Note: The agent scored 9/10 because it didn't complete any onboarding steps (the `onboarding_complete_step` tool was not called at all). A perfect agent would also call `onboarding_complete_step` 3+ times to mark steps like "Provision email and Slack accounts", "Assign laptop and peripherals", etc. as done.
831
+
832
+ ### What Happens When Things Go Wrong
833
+
834
+ If the agent calls `hr_create_employee` with `department: "Data Science"` and the department is at its headcount limit:
835
+
836
+ ```
837
+ Step 1: hr_create_employee → {success: false, error: "Department 'Data Science' has reached its headcount limit (25)"}
838
+ ```
839
+
840
+ A good agent should recognize this error and try a different approach (or report the issue). A bad agent will keep retrying the same call.
841
+
842
+ ---
843
+
844
+ ## 9. Business Rules & Edge Cases
845
+
846
+ ### 9.1 RBAC (Role-Based Access Control)
847
+
848
+ The level hierarchy governs who can do what:
849
+
850
+ ```
851
+ L1 (Associate) → Basic access roles only
852
+ L2 (Senior) → Same as L1 + can mentor
853
+ L3 (Team Lead) → Can approve onboarding (manager_approval)
854
+ L4 (Manager) → Can approve security (security_approval), server room badge access
855
+ L5 (Director) → All approvals + executive access
856
+ L6 (VP) → Same as L5
857
+ ```
858
+
859
+ Access roles have two constraints:
860
+ 1. **Level requirement**: Employee must be at or above the role's minimum level
861
+ 2. **Department restriction**: Employee must be in the role's allowed department (or role allows "all")
862
+
863
+ ### 9.2 Headcount Limits
864
+
865
+ Each department has a `headcount_limit`. When the number of active+pending employees reaches this limit, `hr_create_employee` fails. The agent should recognize this and either:
866
+ - Report the limitation
867
+ - Check headcount first with `hr_search_employees`
868
+ - Look up the relevant policy
869
+
870
+ ### 9.3 License Seat Limits
871
+
872
+ Two licenses are intentionally full:
873
+ - **Netsuite** (15/15) — used by Finance
874
+ - **LinkedIn Sales Navigator** (25/25) — used by Sales
875
+
876
+ Agents should call `it_get_software_licenses` to check availability before trying to assign.
877
+
878
+ ### 9.4 Contractor Rules
879
+
880
+ When `is_contractor: true`:
881
+ - **Legal approval required** in addition to manager approval
882
+ - **No VPN access** by default
883
+ - **Limited access roles** (contractors group, not full department group)
884
+
885
+ ### 9.5 Termination vs Resignation
886
+
887
+ Different offboarding steps:
888
+ - **Resignation**: access_revocation, asset_return, knowledge_transfer, exit_interview, final_payroll, farewell_communications
889
+ - **Termination**: access_revocation, asset_return, final_payroll, legal_review (NO farewell email, NO exit interview)
890
+
891
+ The rubric for termination tasks checks `tool_not_used:email_send` — the agent should NOT send a farewell email.
892
+
893
+ ### 9.6 Server Room Badge Access
894
+
895
+ Creating a badge with `access_zones: ["server_room"]` requires:
896
+ 1. Employee must be L4+ OR
897
+ 2. A `security_approval` must exist for the relevant onboarding request
898
+
899
+ ### 9.7 Manager On Leave
900
+
901
+ Some tasks set a manager's status to "on_leave". The agent should:
902
+ 1. Try to look up the manager and see they're on leave
903
+ 2. Use `hr_get_org_chart` or `hr_read_employee` on the manager to find the skip-level manager
904
+ 3. Use the skip-level manager for approvals and orientation scheduling
905
+
906
+ ---
907
+
908
+ ## 10. File-by-File Reference
909
+
910
+ ```
911
+ rl_hack/
912
+ ├── __init__.py # Exports: HROnboardingEnv, HROnboardingAction, HROnboardingObservation
913
+ ├── models.py # Pydantic models: Action (tool_name + arguments) and Observation (task_id + instruction + tool_result + step + reward + done)
914
+ ├── client.py # EnvClient subclass: connects to server via HTTP/WebSocket, provides .reset() and .step()
915
+ ├── openenv.yaml # OpenEnv manifest: tells HF Spaces this is a FastAPI environment on port 7860
916
+ ├── pyproject.toml # Python package config: name, version, dependencies (openenv-core)
917
+ ├── test_with_llm.py # Test script: runs GPT-4o-mini against a task, prints rubric evaluation
918
+ ├── .env # API keys (gitignored)
919
+ ├── README.md # User-facing docs with quick start, tool table, task overview
920
+ ├── ENVIRONMENT_DEEP_DIVE.md # This document
921
+
922
+ └── server/
923
+ ├── __init__.py # Exports HROnboardingEnvironment
924
+ ├── app.py # FastAPI app created via create_app(), serves on port 7860
925
+ ├── hr_onboarding_environment.py # Core environment class: reset(), step(), state. Orchestrates world, tools, tasks, rubrics.
926
+ ├── world.py # WorldState: loads data, manages 500+ entities, enforces business rules, provides mutation methods
927
+ ├── tools.py # 25 tool definitions (TOOL_DEFINITIONS list) + ToolRegistry class that maps names to functions
928
+ ├── tasks.py # TaskGenerator: creates 77 tasks with instructions, rubric criteria, and setup functions
929
+ ├── rubrics.py # RubricEvaluator: 8 check types, evaluates action log against criteria, computes score
930
+ ├── Dockerfile # Multi-stage Docker build using openenv-base image
931
+ ├── requirements.txt # Server dependencies: openenv, fastapi, uvicorn
932
+ └── data/
933
+ ├── employees.json # 200 employee records with full org hierarchy
934
+ ├── departments.json # 8 departments with headcount limits, required tools, onboarding/offboarding steps
935
+ ├── it_assets.json # 100 IT assets (50 laptops, 25 monitors, 15 phones, 10 headsets)
936
+ ├── access_roles.json # 20 RBAC roles with level/department restrictions
937
+ ├── policies.json # 15 company policies (onboarding, offboarding, badges, contractors, etc.)
938
+ └── templates.json # 12 email/Slack message templates
939
+ ```
940
+
941
+ ---
942
+
943
+ ## Appendix: Quick Numbers
944
+
945
+ | Metric | Value |
946
+ |--------|-------|
947
+ | Total entities | ~500+ |
948
+ | Employees | 200 |
949
+ | Departments | 8 |
950
+ | IT Assets | 100 |
951
+ | Access Roles | 20 |
952
+ | Software Licenses | 15 (2 intentionally full) |
953
+ | Security Groups | 15 |
954
+ | Policies | 15 |
955
+ | Message Templates | 12 |
956
+ | Tools | 25 |
957
+ | Tasks | 77 |
958
+ | Max steps per episode | 15 |
959
+ | Simple tasks | 14 |
960
+ | Medium tasks | 22 |
961
+ | Complex tasks | 29 |
962
+ | Edge case tasks | 12 |
963
+ | Rubric check types | 8 |
README.md CHANGED
@@ -1,255 +1,351 @@
1
  ---
2
- title: Basic Openenv Environment Server
3
- emoji: 🚀
4
- colorFrom: pink
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
8
  app_port: 7860
9
- base_path: /web
10
  tags:
11
  - openenv
12
  ---
13
 
14
- # Basic Openenv Environment
15
 
16
- A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
17
 
18
- ## Quick Start
19
 
20
- The simplest way to use the Basic Openenv environment is through the `BasicOpenenvEnv` class:
21
 
22
  ```python
23
- from basic_openenv import BasicOpenenvAction, BasicOpenenvEnv
24
-
25
- try:
26
- # Create environment from Docker image
27
- basic_openenvenv = BasicOpenenvEnv.from_docker_image("basic_openenv-env:latest")
28
-
29
- # Reset
30
- result = basic_openenvenv.reset()
31
- print(f"Reset: {result.observation.echoed_message}")
32
-
33
- # Send multiple messages
34
- messages = ["Hello, World!", "Testing echo", "Final message"]
35
 
36
- for msg in messages:
37
- result = basic_openenvenv.step(BasicOpenenvAction(message=msg))
38
- print(f"Sent: '{msg}'")
39
- print(f" → Echoed: '{result.observation.echoed_message}'")
40
- print(f" → Length: {result.observation.message_length}")
41
- print(f" → Reward: {result.reward}")
42
-
43
- finally:
44
- # Always clean up
45
- basic_openenvenv.close()
 
 
46
  ```
47
 
48
- That's it! The `BasicOpenenvEnv.from_docker_image()` method handles:
49
- - Starting the Docker container
50
- - Waiting for the server to be ready
51
- - Connecting to the environment
52
- - Container cleanup when you call `close()`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- ## Building the Docker Image
55
 
56
- Before using the environment, you need to build the Docker image:
 
 
 
57
 
58
- ```bash
59
- # From project root
60
- docker build -t basic_openenv-env:latest -f server/Dockerfile .
61
- ```
62
 
63
- ## Deploying to Hugging Face Spaces
64
 
65
- You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
66
 
67
- ```bash
68
- # From the environment directory (where openenv.yaml is located)
69
- openenv push
 
 
 
70
 
71
- # Or specify options
72
- openenv push --namespace my-org --private
73
- ```
74
 
75
- The `openenv push` command will:
76
- 1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
77
- 2. Prepare a custom build for Hugging Face Docker space (enables web interface)
78
- 3. Upload to Hugging Face (ensuring you're logged in)
 
 
79
 
80
- ### Prerequisites
81
 
82
- - Authenticate with Hugging Face: The command will prompt for login if not already authenticated
 
 
 
 
 
 
 
83
 
84
- ### Options
85
 
86
- - `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
87
- - `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
88
- - `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
89
- - `--private`: Deploy the space as private (default: public)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- ### Examples
92
 
93
- ```bash
94
- # Push to your personal namespace (defaults to username/env-name from openenv.yaml)
95
- openenv push
 
 
 
 
 
 
96
 
97
- # Push to a specific repository
98
- openenv push --repo-id my-org/my-env
99
 
100
- # Push with a custom base image
101
- openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest
102
 
103
- # Push as a private space
104
- openenv push --private
105
 
106
- # Combine options
107
- openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
 
 
108
  ```
109
 
110
- After deployment, your space will be available at:
111
- `https://huggingface.co/spaces/<repo-id>`
112
-
113
- The deployed space includes:
114
- - **Web Interface** at `/web` - Interactive UI for exploring the environment
115
- - **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
116
- - **Health Check** at `/health` - Container health monitoring
117
- - **WebSocket** at `/ws` - Persistent session endpoint for low-latency interactions
118
-
119
- ## Environment Details
120
 
121
- ### Action
122
- **BasicOpenenvAction**: Contains a single field
123
- - `message` (str) - The message to echo back
124
 
125
- ### Observation
126
- **BasicOpenenvObservation**: Contains the echo response and metadata
127
- - `echoed_message` (str) - The message echoed back
128
- - `message_length` (int) - Length of the message
129
- - `reward` (float) - Reward based on message length (length × 0.1)
130
- - `done` (bool) - Always False for echo environment
131
- - `metadata` (dict) - Additional info like step count
132
 
133
- ### Reward
134
- The reward is calculated as: `message_length × 0.1`
135
- - "Hi" → reward: 0.2
136
- - "Hello, World!" → reward: 1.3
137
- - Empty message → reward: 0.0
138
 
139
- ## Advanced Usage
 
140
 
141
- ### Connecting to an Existing Server
 
142
 
143
- If you already have a Basic Openenv environment server running, you can connect directly:
144
 
145
- ```python
146
- from basic_openenv import BasicOpenenvEnv
 
147
 
148
- # Connect to existing server
149
- basic_openenvenv = BasicOpenenvEnv(base_url="<ENV_HTTP_URL_HERE>")
150
 
151
- # Use as normal
152
- result = basic_openenvenv.reset()
153
- result = basic_openenvenv.step(BasicOpenenvAction(message="Hello!"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  ```
155
 
156
- Note: When connecting to an existing server, `basic_openenvenv.close()` will NOT stop the server.
157
 
158
- ### Using the Context Manager
159
 
160
- The client supports context manager usage for automatic connection management:
161
 
162
- ```python
163
- from basic_openenv import BasicOpenenvAction, BasicOpenenvEnv
 
 
164
 
165
- # Connect with context manager (auto-connects and closes)
166
- with BasicOpenenvEnv(base_url="http://localhost:8000") as env:
167
- result = env.reset()
168
- print(f"Reset: {result.observation.echoed_message}")
169
- # Multiple steps with low latency
170
- for msg in ["Hello", "World", "!"]:
171
- result = env.step(BasicOpenenvAction(message=msg))
172
- print(f"Echoed: {result.observation.echoed_message}")
173
- ```
174
 
175
- The client uses WebSocket connections for:
176
- - **Lower latency**: No HTTP connection overhead per request
177
- - **Persistent session**: Server maintains your environment state
178
- - **Efficient for episodes**: Better for many sequential steps
179
 
180
- ### Concurrent WebSocket Sessions
 
181
 
182
- The server supports multiple concurrent WebSocket connections. To enable this,
183
- modify `server/app.py` to use factory mode:
184
 
185
- ```python
186
- # In server/app.py - use factory mode for concurrent sessions
187
- app = create_app(
188
- BasicOpenenvEnvironment, # Pass class, not instance
189
- BasicOpenenvAction,
190
- BasicOpenenvObservation,
191
- max_concurrent_envs=4, # Allow 4 concurrent sessions
192
- )
193
  ```
194
 
195
- Then multiple clients can connect simultaneously:
 
 
 
 
 
 
196
 
197
- ```python
198
- from basic_openenv import BasicOpenenvAction, BasicOpenenvEnv
199
- from concurrent.futures import ThreadPoolExecutor
200
-
201
- def run_episode(client_id: int):
202
- with BasicOpenenvEnv(base_url="http://localhost:8000") as env:
203
- result = env.reset()
204
- for i in range(10):
205
- result = env.step(BasicOpenenvAction(message=f"Client {client_id}, step {i}"))
206
- return client_id, result.observation.message_length
207
-
208
- # Run 4 episodes concurrently
209
- with ThreadPoolExecutor(max_workers=4) as executor:
210
- results = list(executor.map(run_episode, range(4)))
 
 
 
 
 
 
 
 
 
211
  ```
212
 
213
- ## Development & Testing
214
 
215
- ### Direct Environment Testing
 
 
 
 
 
 
 
 
216
 
217
- Test the environment logic directly without starting the HTTP server:
218
 
219
  ```bash
220
- # From the server directory
221
- python3 server/basic_openenv_environment.py
 
 
 
 
 
 
222
  ```
223
 
224
- This verifies that:
225
- - Environment resets correctly
226
- - Step executes actions properly
227
- - State tracking works
228
- - Rewards are calculated correctly
229
 
230
- ### Running Locally
231
 
232
- Run the server locally for development:
 
 
 
233
 
234
- ```bash
235
- uvicorn server.app:app --reload
236
- ```
237
 
238
- ## Project Structure
239
 
240
- ```
241
- basic_openenv/
242
- ├── .dockerignore # Docker build exclusions
243
- ├── __init__.py # Module exports
244
- ├── README.md # This file
245
- ├── openenv.yaml # OpenEnv manifest
246
- ├── pyproject.toml # Project metadata and dependencies
247
- ├── uv.lock # Locked dependencies (generated)
248
- ├── client.py # BasicOpenenvEnv client
249
- ├── models.py # Action and Observation models
250
- └── server/
251
- ├── __init__.py # Server module exports
252
- ├── basic_openenv_environment.py # Core environment logic
253
- ├── app.py # FastAPI application (HTTP + WebSocket endpoints)
254
- └── Dockerfile # Container image definition
255
- ```
 
1
  ---
2
+ title: HR Onboarding & Offboarding Environment
3
+ emoji: 🏢
4
+ colorFrom: green
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
8
  app_port: 7860
9
+ base_path: /playground
10
  tags:
11
  - openenv
12
  ---
13
 
14
+ # HR Onboarding & Offboarding Environment
15
 
16
+ An OpenEnv-compatible RL environment that simulates enterprise HR onboarding and offboarding workflows. The agent interacts with a realistic HR system (200+ employees, 8 departments, RBAC, approval chains) by calling tools to complete multi-step tasks.
17
 
18
+ Built for the Scaler AI hackathon (Statement 3.1).
19
 
20
+ ## Quick Start
21
 
22
  ```python
23
+ from rl_hack import HROnboardingAction, HROnboardingEnv
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ # Connect to the environment
26
+ with HROnboardingEnv(base_url="http://localhost:7860") as env:
27
+ result = env.reset()
28
+ print(result.observation) # Task instruction + available tools
29
+
30
+ # Agent calls tools to complete the task
31
+ result = env.step(HROnboardingAction(
32
+ tool_name="hr_create_employee",
33
+ arguments={"name": "Priya Sharma", "department": "Engineering", "level": "L2", "role": "Software Engineer"}
34
+ ))
35
+ print(result.observation) # Tool result
36
+ print(result.reward) # Rubric-based reward
37
  ```
38
 
39
+ ## Tools / Actions (25 MCP Tools)
40
+
41
+ The agent interacts with the environment by calling these tools. Each tool modifies the world state and returns a result.
42
+
43
+ ### HR System (5 tools)
44
+
45
+ | # | Tool | Description | Key Parameters |
46
+ |---|------|-------------|----------------|
47
+ | 1 | `hr_create_employee` | Create a new employee record | `name`, `department`, `level`, `role`, `manager_id`, `is_contractor` |
48
+ | 2 | `hr_read_employee` | Look up employee by ID or email | `emp_id` or `email` |
49
+ | 3 | `hr_update_employee` | Update employee fields (status, department, etc.) | `emp_id`, `updates` (dict) |
50
+ | 4 | `hr_search_employees` | Search/filter employees by criteria | `department`, `level`, `status`, `location`, `role` |
51
+ | 5 | `hr_get_org_chart` | Get reporting hierarchy for a department | `department` |
52
+
53
+ ### Onboarding / Offboarding (6 tools)
54
+
55
+ | # | Tool | Description | Key Parameters |
56
+ |---|------|-------------|----------------|
57
+ | 6 | `onboarding_create_request` | Initiate onboarding for a new hire | `employee_id` |
58
+ | 7 | `onboarding_get_status` | Check onboarding progress | `request_id` or `employee_id` |
59
+ | 8 | `onboarding_complete_step` | Mark an onboarding step as done | `request_id`, `step` |
60
+ | 9 | `offboarding_create_request` | Initiate offboarding for departing employee | `employee_id`, `reason`, `exit_date` |
61
+ | 10 | `offboarding_get_status` | Check offboarding progress | `request_id` or `employee_id` |
62
+ | 11 | `offboarding_complete_step` | Mark an offboarding step as done | `request_id`, `step` |
63
+
64
+ ### IT Provisioning (5 tools)
65
+
66
+ | # | Tool | Description | Key Parameters |
67
+ |---|------|-------------|----------------|
68
+ | 12 | `it_assign_asset` | Assign laptop/monitor/phone to employee | `asset_id`, `employee_id` |
69
+ | 13 | `it_get_available_assets` | List unassigned assets by type | `asset_type` (laptop, monitor, phone, headset) |
70
+ | 14 | `it_create_account` | Create email/Slack/VPN/GitHub accounts | `employee_id`, `account_types` |
71
+ | 15 | `it_revoke_access` | Revoke all IT access (for offboarding) | `employee_id` |
72
+ | 16 | `it_get_software_licenses` | Check license seat availability | `software_name` |
73
+
74
+ ### Access Control (4 tools)
75
+
76
+ | # | Tool | Description | Key Parameters |
77
+ |---|------|-------------|----------------|
78
+ | 17 | `access_assign_role` | Assign RBAC role (checks level/dept restrictions) | `employee_id`, `role_id` |
79
+ | 18 | `access_create_badge` | Create physical access badge | `employee_id`, `access_zones` |
80
+ | 19 | `access_revoke_role` | Revoke a specific access role | `employee_id`, `role_id` |
81
+ | 20 | `access_get_security_groups` | List all security groups and resources | _(none)_ |
82
+
83
+ ### Communication (3 tools)
84
+
85
+ | # | Tool | Description | Key Parameters |
86
+ |---|------|-------------|----------------|
87
+ | 21 | `email_send` | Send email (welcome, farewell, notifications) | `from_address`, `to_address`, `subject`, `body` |
88
+ | 22 | `slack_send_message` | Post in Slack channel or DM | `channel`, `sender`, `text` |
89
+ | 23 | `meeting_schedule` | Schedule orientation, 1-on-1, exit interview | `title`, `attendees`, `datetime`, `meeting_type` |
90
 
91
+ ### Policy & Approval (2 tools)
92
 
93
+ | # | Tool | Description | Key Parameters |
94
+ |---|------|-------------|----------------|
95
+ | 24 | `policy_lookup` | Look up company policies by topic/department | `topic`, `department`, `policy_id` |
96
+ | 25 | `approval_request` | Submit approval (manager/IT/security/legal) | `request_id`, `approver_id`, `approval_type` |
97
 
98
+ ## Tasks (77 tasks across 4 categories)
 
 
 
99
 
100
+ Each episode presents one task. The agent must call the right tools in the right order.
101
 
102
+ ### Task Categories
103
 
104
+ | Category | Count | Example |
105
+ |----------|-------|---------|
106
+ | **Lookup** (simple) | 11 | "List all employees in the Engineering department" |
107
+ | **Onboarding** | 32 | "Fully onboard John Lee as L3 Team Lead in Data Science — create record, assign laptop, provision accounts, set up access, send welcome email, schedule orientation" |
108
+ | **Offboarding** | 24 | "Offboard departing director — revoke all access, reclaim assets, reassign reports, send farewell, schedule exit interview" |
109
+ | **Cross-workflow** | 10 | "Employee transferring from Engineering to Product — offboard from old dept, onboard to new" |
110
 
111
+ ### Difficulty Levels
 
 
112
 
113
+ | Difficulty | Count | Tools per task | Description |
114
+ |------------|-------|---------------|-------------|
115
+ | Simple | 19 | 1-2 | Single lookups or status checks |
116
+ | Medium | 21 | 2-4 | Create + initiate workflows |
117
+ | Complex | 25 | 5-10 | Full end-to-end workflows with approvals |
118
+ | Edge case | 12 | 2-5 | Business rule violations, policy constraints |
119
 
120
+ ### Edge Cases (designed to test policy compliance)
121
 
122
+ - Department at **headcount limit** create employee should fail
123
+ - Software license **seats full** (Netsuite, LinkedIn Sales Navigator)
124
+ - Manager **on leave** — must find skip-level manager for approvals
125
+ - **Contractor** onboarding — different rules (no VPN, limited access, legal approval required)
126
+ - **Termination** vs resignation — different offboarding steps, no farewell email
127
+ - **Offer rescinded** — offboard someone mid-onboarding
128
+ - **Level mismatch** — L1 employee can't get L4+ access roles
129
+ - **Department restriction** — Marketing employee can't get Engineering GitHub role
130
 
131
+ ## World State (500+ entities)
132
 
133
+ | Entity | Count | Description |
134
+ |--------|-------|-------------|
135
+ | Employees | 200 | Full org hierarchy across 8 departments (L1-L6) |
136
+ | Departments | 8 | Engineering, Product, Marketing, Sales, Finance, HR, Data Science, Security |
137
+ | IT Assets | 100 | Laptops (50), monitors (25), phones (15), headsets (10) |
138
+ | Access Roles | 20 | RBAC roles with level/department restrictions |
139
+ | Software Licenses | 15 | Jira, GitHub, AWS, Slack, Salesforce, etc. (2 intentionally full) |
140
+ | Policies | 15 | Onboarding, offboarding, badge access, contractor, termination, etc. |
141
+ | Security Groups | 15 | engineering_team, vpn_users, server_room_access, etc. |
142
+ | Message Templates | 12 | Welcome/farewell emails, Slack messages, notifications |
143
+
144
+ ### RBAC Rules
145
+
146
+ - **L1** Associate → **L2** Senior → **L3** Team Lead → **L4** Manager → **L5** Director → **L6** VP
147
+ - L3+ can approve onboarding
148
+ - L4+ required for security approvals and server room badge access
149
+ - Contractors require legal approval
150
+ - Access roles have minimum level requirements and department restrictions
151
+
152
+ ## Reward / Rubric
153
+
154
+ Each task has a rubric with verifiable criteria. Reward = proportion of criteria satisfied.
155
+
156
+ ### Rubric Check Types
157
+
158
+ | Check | Example | What it verifies |
159
+ |-------|---------|-----------------|
160
+ | `tool_used` | `tool_used:hr_create_employee` | Tool was called at least once |
161
+ | `tool_not_used` | `tool_not_used:slack_send_message` | Tool was NOT called (e.g. no farewell for terminations) |
162
+ | `tool_used_any` | `tool_used_any:email_send,slack_send_message` | At least one of the tools was used |
163
+ | `param_value` | `param_value:hr_create_employee.name=Priya Sharma` | Tool called with specific parameter value |
164
+ | `param_contains` | `param_contains:policy_lookup.topic=onboard` | Parameter contains substring |
165
+ | `tool_order` | `tool_order:hr_create_employee<onboarding_create_request` | Tool A called before Tool B |
166
+ | `tool_count` | `tool_count:onboarding_complete_step>=3` | Tool called at least N times |
167
+ | `result_contains` | `result_contains:headcount_limit` | Any tool result contains substring |
168
+
169
+ ### Example Rubric (medium task)
170
 
171
+ Task: "Onboard Priya Sharma to Engineering as L2 Software Engineer"
172
 
173
+ | Criterion | Check |
174
+ |-----------|-------|
175
+ | Created employee record | `tool_used:hr_create_employee` |
176
+ | Correct name | `param_value:hr_create_employee.name=Priya Sharma` |
177
+ | Correct department | `param_value:hr_create_employee.department=Engineering` |
178
+ | Correct level | `param_value:hr_create_employee.level=L2` |
179
+ | Correct role | `param_value:hr_create_employee.role=Software Engineer` |
180
+ | Initiated onboarding | `tool_used:onboarding_create_request` |
181
+ | Correct sequencing | `tool_order:hr_create_employee<onboarding_create_request` |
182
 
183
+ **Score**: 7/7 = 1.0 (pass) or partial (e.g. 5/7 = 0.71)
 
184
 
185
+ ## Environment API
 
186
 
187
+ ### OpenEnv Interface (MCPEnvironment)
 
188
 
189
+ ```
190
+ reset() → Observation # Pick task, reset world state, return instruction
191
+ step() → Observation # Agent calls a tool, get result + reward
192
+ state → State # Current step count, episode ID
193
  ```
194
 
195
+ ### Episode Flow
 
 
 
 
 
 
 
 
 
196
 
197
+ ```
198
+ 1. env.reset()
199
+ Task: "Fully onboard John Lee as L3 Team Lead..."
200
 
201
+ 2. Agent calls: hr_create_employee(name="John Lee", department="Data Science", level="L3", ...)
202
+ env.step() {"success": true, "emp_id": "emp_0201"}
 
 
 
 
 
203
 
204
+ 3. Agent calls: onboarding_create_request(employee_id="emp_0201")
205
+ env.step() {"success": true, "request_id": "onb_0001", "steps": {...}}
 
 
 
206
 
207
+ 4. Agent calls: it_get_available_assets(asset_type="laptop")
208
+ → env.step() → {"success": true, "assets": [...]}
209
 
210
+ 5. Agent calls: it_assign_asset(asset_id="asset_003", employee_id="emp_0201")
211
+ → env.step() → {"success": true}
212
 
213
+ ... more tool calls ...
214
 
215
+ N. Episode ends (max 15 steps or agent signals done)
216
+ Reward: 8/10 criteria satisfied = 0.8
217
+ ```
218
 
219
+ ## Project Structure
 
220
 
221
+ ```
222
+ rl_hack/
223
+ ├── README.md # This file
224
+ ├── openenv.yaml # OpenEnv manifest
225
+ ├── pyproject.toml # Project metadata
226
+ ├── __init__.py # Module exports
227
+ ├── client.py # HROnboardingEnv client
228
+ ├── models.py # Action/Observation Pydantic models
229
+ ├── test_with_llm.py # Test script (GPT agent)
230
+ ├── .env # API keys (gitignored)
231
+ └── server/
232
+ ├── __init__.py
233
+ ├── app.py # FastAPI application
234
+ ├── hr_onboarding_environment.py # Core environment (Environment subclass)
235
+ ├── world.py # World state (entities, RBAC, mutations)
236
+ ├── tools.py # Tool registry (25 tools)
237
+ ├── tasks.py # Task definitions + generation (77 tasks)
238
+ ├── rubrics.py # Rubric evaluator (reward computation)
239
+ ├── data/
240
+ │ ├── employees.json # 200 employee records
241
+ │ ├── departments.json # 8 departments with policies
242
+ │ ├── policies.json # 15 business rule documents
243
+ │ ├── it_assets.json # 100 IT assets
244
+ │ ├── access_roles.json # 20 RBAC roles
245
+ │ └── templates.json # 12 message templates
246
+ ├── Dockerfile # Container image
247
+ └── requirements.txt # Server dependencies
248
  ```
249
 
250
+ ## Testing with an LLM Agent
251
 
252
+ You can test the environment locally using GPT (or any OpenAI-compatible model) as the agent.
253
 
254
+ ### Setup
255
 
256
+ 1. Create a `.env` file in the repo root:
257
+ ```
258
+ OPENAI_API_KEY="sk-proj-..."
259
+ ```
260
 
261
+ 2. Install dependencies:
262
+ ```bash
263
+ pip install openai python-dotenv openenv-core
264
+ ```
 
 
 
 
 
265
 
266
+ ### Run
 
 
 
267
 
268
+ ```bash
269
+ cd rl_hack
270
 
271
+ # Test on default task (simple lookup)
272
+ uv run python -m test_with_llm.py
273
 
274
+ # Test a specific task by index (0-76)
275
+ uv run python -m test_with_llm 14 # medium onboarding task
276
+ uv run python -m test_with_llm 24 # complex full onboarding
277
+ uv run python -m test_with_llm 55 # edge case (headcount limit)
 
 
 
 
278
  ```
279
 
280
+ The script will:
281
+ - Reset the environment and pick a task
282
+ - Use GPT-4o-mini to generate tool calls
283
+ - Execute each tool call against the environment
284
+ - Print the rubric evaluation with pass/fail per criterion
285
+
286
+ ### Example Output
287
 
288
+ ```
289
+ Task ID: task_0015
290
+ Difficulty: medium
291
+ Instruction: Onboard new hire Priya Sharma to Engineering as L2 Software Engineer...
292
+
293
+ --- Step 1/15 ---
294
+ LLM: {"tool": "hr_create_employee", "params": {"name": "Priya Sharma", ...}}
295
+ Tool: hr_create_employee
296
+ Result: {"success": true, "employee": {"emp_id": "emp_0201", ...}}
297
+
298
+ --- Step 2/15 ---
299
+ LLM: {"tool": "onboarding_create_request", "params": {"employee_id": "emp_0201"}}
300
+ Tool: onboarding_create_request
301
+ Result: {"success": true, ...}
302
+
303
+ FINAL EVALUATION
304
+ Score: 100% (7/7 criteria)
305
+ Passed: True
306
+ [PASS] created_employee
307
+ [PASS] correct_name
308
+ [PASS] correct_dept
309
+ [PASS] initiated_onboarding
310
+ [PASS] sequencing
311
  ```
312
 
313
+ ### Task Index Reference
314
 
315
+ | Index | Difficulty | Category | Description |
316
+ |-------|-----------|----------|-------------|
317
+ | 0-13 | Simple | Lookup/Onboarding | Single lookups, status checks |
318
+ | 14-23 | Medium | Onboarding | Create employee + initiate workflow |
319
+ | 24-34 | Complex | Onboarding | Full end-to-end with IT, access, comms |
320
+ | 35-46 | Medium | Offboarding | Initiate offboarding + revoke access |
321
+ | 47-54 | Complex | Offboarding | Full offboarding with asset reclaim |
322
+ | 55-66 | Edge case | Various | Headcount limits, license caps, RBAC |
323
+ | 67-76 | Complex | Cross-workflow | Transfers, rehires, manager departures |
324
 
325
+ ## Building & Running
326
 
327
  ```bash
328
+ # Build Docker image
329
+ docker build -t hr-onboarding-env:latest -f server/Dockerfile .
330
+
331
+ # Run locally (as OpenEnv HTTP server)
332
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 7860
333
+
334
+ # Deploy to HF Spaces
335
+ openenv push
336
  ```
337
 
338
+ ## Training
 
 
 
 
339
 
340
+ We use Unsloth + GRPO to train an LLM agent on this environment:
341
 
342
+ - **Model**: Qwen 2.5-7B-Instruct (4-bit quantized)
343
+ - **Algorithm**: GRPO (Group Relative Policy Optimization)
344
+ - **Rollouts**: 8 per prompt
345
+ - **Train/eval split**: 80/20 (62 train, 15 eval tasks)
346
 
347
+ See `training/` directory in the parent repo for training scripts.
 
 
348
 
349
+ ## Live Demo
350
 
351
+ Try the environment on Hugging Face Spaces: https://huggingface.co/spaces/devxpy/rl_hack
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
__init__.py CHANGED
@@ -1,16 +1,10 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
 
7
- """Basic Openenv Environment."""
8
-
9
- from .client import BasicOpenenvEnv
10
- from .models import BasicOpenenvAction, BasicOpenenvObservation
11
 
12
  __all__ = [
13
- "BasicOpenenvAction",
14
- "BasicOpenenvObservation",
15
- "BasicOpenenvEnv",
16
  ]
 
1
+ """HR Onboarding/Offboarding Environment."""
 
 
 
 
2
 
3
+ from .client import HROnboardingEnv
4
+ from .models import HROnboardingAction, HROnboardingObservation
 
 
5
 
6
  __all__ = [
7
+ "HROnboardingAction",
8
+ "HROnboardingObservation",
9
+ "HROnboardingEnv",
10
  ]
client.py CHANGED
@@ -1,10 +1,4 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """Basic Openenv Environment Client."""
8
 
9
  from typing import Dict
10
 
@@ -12,66 +6,59 @@ from openenv.core.client_types import StepResult
12
  from openenv.core.env_server.types import State
13
  from openenv.core import EnvClient
14
 
15
- from .models import BasicOpenenvAction, BasicOpenenvObservation
16
 
17
 
18
- class BasicOpenenvEnv(
19
- EnvClient[BasicOpenenvAction, BasicOpenenvObservation]
20
  ):
21
  """
22
- Client for the Basic Openenv Environment.
23
 
24
- This client maintains a persistent WebSocket connection to the environment server,
25
- enabling efficient multi-step interactions with lower latency.
26
- Each client instance has its own dedicated environment session on the server.
27
 
28
  Example:
29
- >>> # Connect to a running server
30
- >>> with BasicOpenenvEnv(base_url="http://localhost:8000") as client:
31
  ... result = client.reset()
32
- ... print(result.observation.echoed_message)
33
  ...
34
- ... result = client.step(BasicOpenenvAction(message="Hello!"))
35
- ... print(result.observation.echoed_message)
 
 
 
36
 
37
  Example with Docker:
38
- >>> # Automatically start container and connect
39
- >>> client = BasicOpenenvEnv.from_docker_image("basic_openenv-env:latest")
40
  >>> try:
41
  ... result = client.reset()
42
- ... result = client.step(BasicOpenenvAction(message="Test"))
 
 
 
43
  ... finally:
44
  ... client.close()
45
  """
46
 
47
- def _step_payload(self, action: BasicOpenenvAction) -> Dict:
48
- """
49
- Convert BasicOpenenvAction to JSON payload for step message.
50
-
51
- Args:
52
- action: BasicOpenenvAction instance
53
-
54
- Returns:
55
- Dictionary representation suitable for JSON encoding
56
- """
57
  return {
58
- "message": action.message,
 
59
  }
60
 
61
- def _parse_result(self, payload: Dict) -> StepResult[BasicOpenenvObservation]:
62
- """
63
- Parse server response into StepResult[BasicOpenenvObservation].
64
-
65
- Args:
66
- payload: JSON response data from server
67
-
68
- Returns:
69
- StepResult with BasicOpenenvObservation
70
- """
71
  obs_data = payload.get("observation", {})
72
- observation = BasicOpenenvObservation(
73
- echoed_message=obs_data.get("echoed_message", ""),
74
- message_length=obs_data.get("message_length", 0),
 
 
 
 
 
75
  done=payload.get("done", False),
76
  reward=payload.get("reward"),
77
  metadata=obs_data.get("metadata", {}),
@@ -84,15 +71,7 @@ class BasicOpenenvEnv(
84
  )
85
 
86
  def _parse_state(self, payload: Dict) -> State:
87
- """
88
- Parse server response into State object.
89
-
90
- Args:
91
- payload: JSON response from state request
92
-
93
- Returns:
94
- State object with episode_id and step_count
95
- """
96
  return State(
97
  episode_id=payload.get("episode_id"),
98
  step_count=payload.get("step_count", 0),
 
1
+ """HR Onboarding/Offboarding Environment Client."""
 
 
 
 
 
 
2
 
3
  from typing import Dict
4
 
 
6
  from openenv.core.env_server.types import State
7
  from openenv.core import EnvClient
8
 
9
+ from .models import HROnboardingAction, HROnboardingObservation
10
 
11
 
12
+ class HROnboardingEnv(
13
+ EnvClient[HROnboardingAction, HROnboardingObservation]
14
  ):
15
  """
16
+ Client for the HR Onboarding/Offboarding Environment.
17
 
18
+ Maintains a persistent WebSocket connection to the environment server.
19
+ Each client instance has its own dedicated environment session.
 
20
 
21
  Example:
22
+ >>> with HROnboardingEnv(base_url="http://localhost:7860") as client:
 
23
  ... result = client.reset()
24
+ ... print(result.observation.instruction)
25
  ...
26
+ ... result = client.step(HROnboardingAction(
27
+ ... tool_name="hr_read_employee",
28
+ ... arguments={"emp_id": "emp_0001"}
29
+ ... ))
30
+ ... print(result.observation.tool_result)
31
 
32
  Example with Docker:
33
+ >>> client = HROnboardingEnv.from_docker_image("hr-onboarding-env:latest")
 
34
  >>> try:
35
  ... result = client.reset()
36
+ ... result = client.step(HROnboardingAction(
37
+ ... tool_name="hr_search_employees",
38
+ ... arguments={"department": "Engineering"}
39
+ ... ))
40
  ... finally:
41
  ... client.close()
42
  """
43
 
44
+ def _step_payload(self, action: HROnboardingAction) -> Dict:
45
+ """Convert HROnboardingAction to JSON payload for step message."""
 
 
 
 
 
 
 
 
46
  return {
47
+ "tool_name": action.tool_name,
48
+ "arguments": action.arguments,
49
  }
50
 
51
+ def _parse_result(self, payload: Dict) -> StepResult[HROnboardingObservation]:
52
+ """Parse server response into StepResult[HROnboardingObservation]."""
 
 
 
 
 
 
 
 
53
  obs_data = payload.get("observation", {})
54
+ observation = HROnboardingObservation(
55
+ task_id=obs_data.get("task_id", ""),
56
+ instruction=obs_data.get("instruction", ""),
57
+ tool_name=obs_data.get("tool_name", ""),
58
+ tool_result=obs_data.get("tool_result", {}),
59
+ step=obs_data.get("step", 0),
60
+ max_steps=obs_data.get("max_steps", 15),
61
+ available_tools=obs_data.get("available_tools", []),
62
  done=payload.get("done", False),
63
  reward=payload.get("reward"),
64
  metadata=obs_data.get("metadata", {}),
 
71
  )
72
 
73
  def _parse_state(self, payload: Dict) -> State:
74
+ """Parse server response into State object."""
 
 
 
 
 
 
 
 
75
  return State(
76
  episode_id=payload.get("episode_id"),
77
  step_count=payload.get("step_count", 0),
models.py CHANGED
@@ -1,28 +1,45 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
  """
8
- Data models for the Basic Openenv Environment.
9
 
10
- The basic_openenv environment is a simple test environment that echoes back messages.
 
 
11
  """
12
 
 
 
13
  from pydantic import Field
14
 
15
  from openenv.core.env_server.types import Action, Observation
16
 
17
 
18
- class BasicOpenenvAction(Action):
19
- """Action for the Basic Openenv environment - just a message to echo."""
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- message: str = Field(..., description="Message to echo back")
22
 
 
 
23
 
24
- class BasicOpenenvObservation(Observation):
25
- """Observation from the Basic Openenv environment - the echoed message."""
26
 
27
- echoed_message: str = Field(default="", description="The echoed message")
28
- message_length: int = Field(default=0, description="Length of the echoed message")
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Data models for the HR Onboarding/Offboarding Environment.
3
 
4
+ Defines the Action and Observation types used by the environment.
5
+ The agent sends HROnboardingAction (a tool call) and receives
6
+ HROnboardingObservation (the tool result + task context).
7
  """
8
 
9
+ from typing import Any, Dict, List, Optional
10
+
11
  from pydantic import Field
12
 
13
  from openenv.core.env_server.types import Action, Observation
14
 
15
 
16
+ class HROnboardingAction(Action):
17
+ """Action for the HR environment a tool call with name and arguments.
18
+
19
+ The agent picks one of 25 available tools and provides arguments.
20
+
21
+ Example:
22
+ HROnboardingAction(
23
+ tool_name="hr_create_employee",
24
+ arguments={"name": "Priya Sharma", "department": "Engineering",
25
+ "level": "L2", "role": "Software Engineer"}
26
+ )
27
+ """
28
+
29
+ tool_name: str = Field(..., description="Name of the tool to call (e.g. hr_create_employee, it_assign_asset)")
30
+ arguments: Dict[str, Any] = Field(default_factory=dict, description="Arguments to pass to the tool")
31
 
 
32
 
33
+ class HROnboardingObservation(Observation):
34
+ """Observation returned after each step in the HR environment.
35
 
36
+ Contains the tool execution result, task context, and episode progress.
37
+ """
38
 
39
+ task_id: str = Field(default="", description="Current task identifier")
40
+ instruction: str = Field(default="", description="Task instruction for the agent")
41
+ tool_name: str = Field(default="", description="Name of the tool that was called")
42
+ tool_result: Dict[str, Any] = Field(default_factory=dict, description="Result returned by the tool")
43
+ step: int = Field(default=0, description="Current step number")
44
+ max_steps: int = Field(default=15, description="Maximum steps allowed")
45
+ available_tools: List[str] = Field(default_factory=list, description="List of available tool names")
openenv.yaml CHANGED
@@ -1,5 +1,5 @@
1
  spec_version: 1
2
- name: basic_openenv
3
  type: space
4
  runtime: fastapi
5
  app: server.app:app
 
1
  spec_version: 1
2
+ name: hr_onboarding_env
3
  type: space
4
  runtime: fastapi
5
  app: server.app:app
pyproject.toml CHANGED
@@ -1,31 +1,14 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
  [build-system]
8
  requires = ["setuptools>=45", "wheel"]
9
  build-backend = "setuptools.build_meta"
10
 
11
  [project]
12
- name = "openenv-basic_openenv"
13
  version = "0.1.0"
14
- description = "Basic Openenv environment for OpenEnv"
15
  requires-python = ">=3.10"
16
  dependencies = [
17
- # Core OpenEnv runtime (provides FastAPI server + HTTP client types)
18
- # install from github
19
- # "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
20
  "openenv-core[core]>=0.2.0",
21
- # Environment-specific dependencies
22
- # Add all dependencies needed for your environment here
23
- # Examples:
24
- # "numpy>=1.19.0",
25
- # "torch>=2.0.0",
26
- # "gymnasium>=0.29.0",
27
- # "openspiel>=1.0.0",
28
- # "smolagents>=1.22.0,<2",
29
  ]
30
 
31
  [project.optional-dependencies]
@@ -35,11 +18,9 @@ dev = [
35
  ]
36
 
37
  [project.scripts]
38
- # Server entry point - enables running via: uv run --project . server
39
- # or: python -m basic_openenv.server.app
40
- server = "basic_openenv.server.app:main"
41
 
42
  [tool.setuptools]
43
  include-package-data = true
44
- packages = ["basic_openenv", "basic_openenv.server"]
45
- package-dir = { "basic_openenv" = ".", "basic_openenv.server" = "server" }
 
 
 
 
 
 
 
1
  [build-system]
2
  requires = ["setuptools>=45", "wheel"]
3
  build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
+ name = "openenv-hr_onboarding_env"
7
  version = "0.1.0"
8
+ description = "HR Onboarding/Offboarding environment for OpenEnv — simulates enterprise HR workflows with 25 tools, 77 tasks, RBAC, and rubric-based rewards"
9
  requires-python = ">=3.10"
10
  dependencies = [
 
 
 
11
  "openenv-core[core]>=0.2.0",
 
 
 
 
 
 
 
 
12
  ]
13
 
14
  [project.optional-dependencies]
 
18
  ]
19
 
20
  [project.scripts]
21
+ server = "hr_onboarding_env.server.app:main"
 
 
22
 
23
  [tool.setuptools]
24
  include-package-data = true
25
+ packages = ["hr_onboarding_env", "hr_onboarding_env.server"]
26
+ package-dir = { "hr_onboarding_env" = ".", "hr_onboarding_env.server" = "server" }
server/__init__.py CHANGED
@@ -1,11 +1,5 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
 
7
- """Basic Openenv environment server components."""
8
 
9
- from .basic_openenv_environment import BasicOpenenvEnvironment
10
-
11
- __all__ = ["BasicOpenenvEnvironment"]
 
1
+ """HR Onboarding/Offboarding environment server components."""
 
 
 
 
2
 
3
+ from .hr_onboarding_environment import HROnboardingEnvironment
4
 
5
+ __all__ = ["HROnboardingEnvironment"]
 
 
server/app.py CHANGED
@@ -1,80 +1,157 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
  """
8
- FastAPI application for the Basic Openenv Environment.
9
-
10
- This module creates an HTTP server that exposes the BasicOpenenvEnvironment
11
- over HTTP and WebSocket endpoints, compatible with EnvClient.
12
-
13
- Endpoints:
14
- - POST /reset: Reset the environment
15
- - POST /step: Execute an action
16
- - GET /state: Get current environment state
17
- - GET /schema: Get action/observation schemas
18
- - WS /ws: WebSocket endpoint for persistent sessions
19
-
20
- Usage:
21
- # Development (with auto-reload):
22
- uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
23
-
24
- # Production:
25
- uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
26
 
27
- # Or run directly:
28
- python -m server.app
29
  """
30
 
31
  try:
32
  from openenv.core.env_server.http_server import create_app
33
  except Exception as e: # pragma: no cover
34
  raise ImportError(
35
- "openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
36
  ) from e
37
 
38
- # Import from local models.py (PYTHONPATH includes /app/env in Docker)
39
- from models import BasicOpenenvAction, BasicOpenenvObservation
40
- from .basic_openenv_environment import BasicOpenenvEnvironment
41
- from fastapi.responses import RedirectResponse
 
 
 
 
 
42
  import os
 
43
 
44
 
45
  # Required for OpenEnv to mount the HF-style web UI at /web.
46
  os.environ.setdefault("ENABLE_WEB_INTERFACE", "true")
47
 
48
 
49
- # Create the app with web interface and README integration
50
  app = create_app(
51
- BasicOpenenvEnvironment,
52
- BasicOpenenvAction,
53
- BasicOpenenvObservation,
54
- env_name="basic_openenv",
55
- max_concurrent_envs=1, # increase this number to allow more concurrent WebSocket sessions
56
  )
57
 
 
 
 
 
 
 
 
 
 
58
 
59
  @app.get("/", include_in_schema=False)
60
  def root_redirect():
61
- """Match HF Space UX: open app at /web UI."""
62
- return RedirectResponse(url="/web")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
 
65
  def main():
66
- """
67
- Entry point for direct execution via uv run or python -m.
68
-
69
- This function enables running the server without Docker:
70
- uv run --project . server
71
- uv run --project . server --port 8001
72
- python -m basic_openenv.server.app
73
-
74
- For production deployments, consider using uvicorn directly with
75
- multiple workers:
76
- uvicorn basic_openenv.server.app:app --workers 4
77
- """
78
  import argparse
79
  import uvicorn
80
 
 
 
 
 
 
 
 
1
  """
2
+ FastAPI application for the HR Onboarding/Offboarding Environment.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ Serves both the OpenEnv API endpoints and an interactive web playground UI.
 
5
  """
6
 
7
  try:
8
  from openenv.core.env_server.http_server import create_app
9
  except Exception as e: # pragma: no cover
10
  raise ImportError(
11
+ "openenv is required. Install with: uv sync"
12
  ) from e
13
 
14
+ from models import HROnboardingAction, HROnboardingObservation
15
+ from .hr_onboarding_environment import HROnboardingEnvironment
16
+ from .tools import TOOL_DEFINITIONS
17
+ from .rubrics import RubricEvaluator
18
+
19
+ from fastapi import Request
20
+ from fastapi.responses import HTMLResponse, RedirectResponse
21
+ from fastapi.staticfiles import StaticFiles
22
+ from pathlib import Path
23
  import os
24
+ import json
25
 
26
 
27
  # Required for OpenEnv to mount the HF-style web UI at /web.
28
  os.environ.setdefault("ENABLE_WEB_INTERFACE", "true")
29
 
30
 
31
+ # Create the OpenEnv app
32
  app = create_app(
33
+ HROnboardingEnvironment,
34
+ HROnboardingAction,
35
+ HROnboardingObservation,
36
+ env_name="hr_onboarding_env",
37
+ max_concurrent_envs=4,
38
  )
39
 
40
+ # Mount static files
41
+ STATIC_DIR = Path(__file__).parent / "static"
42
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
43
+
44
+ # Shared environment instance for the playground
45
+ _playground_env = HROnboardingEnvironment(seed=42, max_steps=15)
46
+
47
+
48
+ # --- Playground API endpoints ---
49
 
50
  @app.get("/", include_in_schema=False)
51
  def root_redirect():
52
+ """Serve the interactive playground UI."""
53
+ return RedirectResponse(url="/playground")
54
+
55
+
56
+ @app.get("/playground", include_in_schema=False)
57
+ def playground():
58
+ """Serve the interactive playground HTML."""
59
+ html_path = STATIC_DIR / "index.html"
60
+ return HTMLResponse(html_path.read_text())
61
+
62
+
63
+ @app.get("/api/tasks")
64
+ def get_tasks():
65
+ """Return all tasks with metadata for the task picker."""
66
+ env = _playground_env
67
+ tasks = []
68
+ # Save current state
69
+ orig_idx = env._task_idx
70
+
71
+ for i in range(len(env._tasks)):
72
+ task = env._tasks[i]
73
+ tasks.append({
74
+ "index": i,
75
+ "task_id": task.task_id,
76
+ "instruction": task.instruction,
77
+ "difficulty": task.difficulty,
78
+ "category": task.category,
79
+ "expected_tools": task.expected_tools,
80
+ "num_criteria": len(task.rubric_criteria),
81
+ })
82
+
83
+ env._task_idx = orig_idx
84
+ return tasks
85
+
86
+
87
+ @app.get("/api/tool_definitions")
88
+ def get_tool_definitions():
89
+ """Return all tool definitions with descriptions and parameters."""
90
+ return TOOL_DEFINITIONS
91
+
92
+
93
+ @app.post("/api/reset")
94
+ async def playground_reset(request: Request):
95
+ """Reset the environment to a specific task."""
96
+ body = await request.json()
97
+ task_idx = body.get("task_idx", 0)
98
+
99
+ env = _playground_env
100
+ # Set task index so reset() picks the right task
101
+ env._task_idx = task_idx
102
+ obs = env.reset()
103
+
104
+ return {
105
+ "task_id": obs.task_id,
106
+ "instruction": obs.instruction,
107
+ "step": obs.step,
108
+ "max_steps": obs.max_steps,
109
+ "available_tools": obs.available_tools,
110
+ "done": obs.done,
111
+ "reward": obs.reward,
112
+ "metadata": obs.metadata,
113
+ }
114
+
115
+
116
+ @app.post("/api/step")
117
+ async def playground_step(request: Request):
118
+ """Execute one tool call step."""
119
+ body = await request.json()
120
+ tool_name = body.get("tool_name", "")
121
+ arguments = body.get("arguments", {})
122
+
123
+ env = _playground_env
124
+ action = HROnboardingAction(tool_name=tool_name, arguments=arguments)
125
+ obs = env.step(action)
126
+
127
+ return {
128
+ "task_id": obs.task_id,
129
+ "instruction": obs.instruction,
130
+ "tool_name": obs.tool_name,
131
+ "tool_result": obs.tool_result,
132
+ "step": obs.step,
133
+ "max_steps": obs.max_steps,
134
+ "done": obs.done,
135
+ "reward": obs.reward,
136
+ "metadata": obs.metadata,
137
+ }
138
+
139
+
140
+ @app.post("/api/evaluate")
141
+ async def playground_evaluate():
142
+ """Force evaluation of current episode."""
143
+ env = _playground_env
144
+ evaluator = RubricEvaluator()
145
+
146
+ if env._current_task:
147
+ result = evaluator.evaluate(env._current_task, env.world.action_log)
148
+ return result
149
+
150
+ return {"score": 0, "passed": False, "criteria_results": [], "passed_count": 0, "total_criteria": 0}
151
 
152
 
153
  def main():
154
+ """Entry point for direct execution."""
 
 
 
 
 
 
 
 
 
 
 
155
  import argparse
156
  import uvicorn
157
 
server/data/access_roles.json ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "role_id": "role_001",
4
+ "name": "engineering_developer",
5
+ "permissions": ["code_repo_read", "code_repo_write", "ci_cd_trigger", "staging_deploy", "jira_access", "confluence_read", "slack_engineering"],
6
+ "department": "Engineering",
7
+ "level_requirement": "L1",
8
+ "description": "Standard developer access to code repositories, CI/CD pipelines, staging environments, and engineering collaboration tools."
9
+ },
10
+ {
11
+ "role_id": "role_002",
12
+ "name": "engineering_admin",
13
+ "permissions": ["code_repo_admin", "ci_cd_admin", "staging_deploy", "production_deploy", "infra_access", "secrets_management", "jira_admin", "confluence_write", "slack_engineering", "pagerduty_admin"],
14
+ "department": "Engineering",
15
+ "level_requirement": "L4",
16
+ "description": "Administrative engineering access including production deployments, infrastructure management, and secrets management."
17
+ },
18
+ {
19
+ "role_id": "role_003",
20
+ "name": "engineering_lead",
21
+ "permissions": ["code_repo_read", "code_repo_write", "code_repo_approve", "ci_cd_trigger", "staging_deploy", "production_deploy", "jira_admin", "confluence_write", "slack_engineering", "pagerduty_oncall"],
22
+ "department": "Engineering",
23
+ "level_requirement": "L3",
24
+ "description": "Engineering lead access with code review approval rights, production deployment capabilities, and project management tools."
25
+ },
26
+ {
27
+ "role_id": "role_004",
28
+ "name": "product_viewer",
29
+ "permissions": ["jira_access", "confluence_read", "analytics_dashboard_read", "slack_product", "figma_view"],
30
+ "department": "Product",
31
+ "level_requirement": "L1",
32
+ "description": "Read-only access to product management tools, analytics dashboards, and design files."
33
+ },
34
+ {
35
+ "role_id": "role_005",
36
+ "name": "product_manager",
37
+ "permissions": ["jira_admin", "confluence_write", "analytics_dashboard_read", "analytics_dashboard_write", "slack_product", "figma_view", "figma_comment", "feature_flags_manage", "a_b_testing_admin"],
38
+ "department": "Product",
39
+ "level_requirement": "L2",
40
+ "description": "Full product management access including analytics, feature flag management, A/B testing, and project tracking administration."
41
+ },
42
+ {
43
+ "role_id": "role_006",
44
+ "name": "finance_analyst",
45
+ "permissions": ["erp_read", "financial_reports_read", "expense_system_read", "budget_dashboard_read", "slack_finance", "confluence_read"],
46
+ "department": "Finance",
47
+ "level_requirement": "L1",
48
+ "description": "Read access to financial systems, ERP data, expense reports, and budget dashboards."
49
+ },
50
+ {
51
+ "role_id": "role_007",
52
+ "name": "finance_manager",
53
+ "permissions": ["erp_read", "erp_write", "financial_reports_read", "financial_reports_write", "expense_system_admin", "budget_dashboard_admin", "payroll_read", "slack_finance", "confluence_write", "vendor_management"],
54
+ "department": "Finance",
55
+ "level_requirement": "L3",
56
+ "description": "Full finance management access including ERP write, payroll viewing, expense administration, and vendor management."
57
+ },
58
+ {
59
+ "role_id": "role_008",
60
+ "name": "hr_coordinator",
61
+ "permissions": ["hris_read", "hris_write_basic", "recruiting_ats_read", "benefits_portal_read", "slack_hr", "confluence_read", "onboarding_system_read"],
62
+ "department": "Human Resources",
63
+ "level_requirement": "L1",
64
+ "description": "Basic HR operations access for coordinating onboarding, maintaining employee records, and viewing recruiting pipelines."
65
+ },
66
+ {
67
+ "role_id": "role_009",
68
+ "name": "hr_manager",
69
+ "permissions": ["hris_read", "hris_write", "hris_admin", "recruiting_ats_admin", "benefits_portal_admin", "payroll_read", "payroll_write", "compensation_data_read", "slack_hr", "confluence_write", "onboarding_system_admin", "offboarding_system_admin", "performance_review_admin"],
70
+ "department": "Human Resources",
71
+ "level_requirement": "L3",
72
+ "description": "Full HR management access including HRIS administration, recruiting, payroll, benefits, performance reviews, and onboarding/offboarding systems."
73
+ },
74
+ {
75
+ "role_id": "role_010",
76
+ "name": "security_analyst",
77
+ "permissions": ["siem_read", "vulnerability_scanner_read", "access_logs_read", "dlp_dashboard_read", "slack_security", "confluence_read", "incident_management_read"],
78
+ "department": "Security",
79
+ "level_requirement": "L2",
80
+ "description": "Security monitoring access for reviewing SIEM alerts, vulnerability scans, access logs, and DLP incidents."
81
+ },
82
+ {
83
+ "role_id": "role_011",
84
+ "name": "security_admin",
85
+ "permissions": ["siem_admin", "vulnerability_scanner_admin", "access_logs_read", "access_management_admin", "dlp_admin", "firewall_admin", "slack_security", "confluence_write", "incident_management_admin", "secrets_management", "identity_provider_admin"],
86
+ "department": "Security",
87
+ "level_requirement": "L4",
88
+ "description": "Full security administration including SIEM, access management, DLP, firewall rules, identity provider configuration, and incident response."
89
+ },
90
+ {
91
+ "role_id": "role_012",
92
+ "name": "data_science_analyst",
93
+ "permissions": ["data_warehouse_read", "jupyter_notebooks", "ml_platform_read", "analytics_dashboard_read", "slack_data", "confluence_read", "s3_data_buckets_read"],
94
+ "department": "Data Science",
95
+ "level_requirement": "L1",
96
+ "description": "Data analysis access including data warehouse queries, Jupyter notebooks, ML platform viewing, and analytics dashboards."
97
+ },
98
+ {
99
+ "role_id": "role_013",
100
+ "name": "data_science_lead",
101
+ "permissions": ["data_warehouse_read", "data_warehouse_write", "jupyter_notebooks", "ml_platform_admin", "analytics_dashboard_write", "gpu_cluster_access", "slack_data", "confluence_write", "s3_data_buckets_read", "s3_data_buckets_write", "model_registry_admin"],
102
+ "department": "Data Science",
103
+ "level_requirement": "L3",
104
+ "description": "Advanced data science access including data warehouse writes, ML platform administration, GPU cluster usage, and model registry management."
105
+ },
106
+ {
107
+ "role_id": "role_014",
108
+ "name": "sales_crm_user",
109
+ "permissions": ["crm_read", "crm_write", "sales_dashboard_read", "email_sequences", "slack_sales", "confluence_read", "contract_management_read"],
110
+ "department": "Sales",
111
+ "level_requirement": "L1",
112
+ "description": "Standard sales access to CRM, sales dashboards, email outreach tools, and contract viewing."
113
+ },
114
+ {
115
+ "role_id": "role_015",
116
+ "name": "sales_manager",
117
+ "permissions": ["crm_admin", "sales_dashboard_admin", "email_sequences", "commission_reports_read", "slack_sales", "confluence_write", "contract_management_write", "quota_management", "territory_management"],
118
+ "department": "Sales",
119
+ "level_requirement": "L3",
120
+ "description": "Sales management access including CRM administration, commission reports, quota setting, and territory management."
121
+ },
122
+ {
123
+ "role_id": "role_016",
124
+ "name": "marketing_specialist",
125
+ "permissions": ["marketing_automation_read", "marketing_automation_write", "social_media_management", "analytics_dashboard_read", "slack_marketing", "confluence_read", "cms_write", "design_tools_access"],
126
+ "department": "Marketing",
127
+ "level_requirement": "L1",
128
+ "description": "Marketing operations access including automation platforms, social media management, CMS, and analytics."
129
+ },
130
+ {
131
+ "role_id": "role_017",
132
+ "name": "customer_support_agent",
133
+ "permissions": ["ticketing_system_read", "ticketing_system_write", "knowledge_base_read", "crm_read", "slack_support", "confluence_read", "phone_system_access"],
134
+ "department": "Customer Support",
135
+ "level_requirement": "L1",
136
+ "description": "Customer support agent access to ticketing system, knowledge base, CRM viewing, and phone system."
137
+ },
138
+ {
139
+ "role_id": "role_018",
140
+ "name": "design_contributor",
141
+ "permissions": ["figma_edit", "design_system_read", "design_system_write", "slack_design", "confluence_read", "jira_access", "asset_library_access"],
142
+ "department": "Design",
143
+ "level_requirement": "L1",
144
+ "description": "Design team access including Figma editing, design system contributions, and asset library management."
145
+ },
146
+ {
147
+ "role_id": "role_019",
148
+ "name": "legal_counsel",
149
+ "permissions": ["contract_management_read", "contract_management_write", "legal_document_vault", "compliance_dashboard_read", "e_discovery_access", "slack_legal", "confluence_write", "vendor_management"],
150
+ "department": "Legal",
151
+ "level_requirement": "L2",
152
+ "description": "Legal team access to contract management, document vault, compliance monitoring, e-discovery tools, and vendor management."
153
+ },
154
+ {
155
+ "role_id": "role_020",
156
+ "name": "general_employee",
157
+ "permissions": ["email_access", "slack_general", "confluence_read", "hris_self_service", "benefits_portal_self_service", "expense_system_submit", "learning_platform_access"],
158
+ "department": "all",
159
+ "level_requirement": "L1",
160
+ "description": "Baseline access granted to all employees including email, Slack, self-service HR portal, benefits, expense submission, and learning platform."
161
+ }
162
+ ]
server/data/departments.json ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "departments": [
3
+ {
4
+ "dept_id": "dept_001",
5
+ "name": "Engineering",
6
+ "head_id": "emp_0001",
7
+ "budget": 12500000,
8
+ "headcount_limit": 60,
9
+ "current_headcount": 54,
10
+ "required_tools": ["Jira", "GitHub", "AWS", "Slack", "VSCode License", "Docker", "Confluence"],
11
+ "onboarding_steps": [
12
+ "Submit signed offer letter and NDA",
13
+ "Complete background check verification",
14
+ "Provision laptop with approved dev image",
15
+ "Create GitHub organization account and assign teams",
16
+ "Grant AWS IAM role based on team assignment",
17
+ "Set up Jira board access and Confluence spaces",
18
+ "Assign engineering buddy for first 30 days",
19
+ "Schedule intro meetings with team lead and skip-level manager",
20
+ "Enroll in mandatory secure coding training",
21
+ "Complete first-week architecture overview sessions",
22
+ "Set up local development environment with team repo access",
23
+ "Review on-call rotation expectations and PagerDuty setup"
24
+ ],
25
+ "offboarding_steps": [
26
+ "Revoke GitHub organization access and transfer owned repos",
27
+ "Remove AWS IAM credentials and rotate shared secrets",
28
+ "Deactivate Jira and Confluence accounts",
29
+ "Collect company-issued laptop and peripherals",
30
+ "Remove from PagerDuty on-call rotations",
31
+ "Transfer ownership of critical services and documentation",
32
+ "Conduct knowledge transfer sessions with remaining team",
33
+ "Complete exit interview with engineering manager",
34
+ "Revoke VPN and SSH key access",
35
+ "Remove from all Slack engineering channels"
36
+ ]
37
+ },
38
+ {
39
+ "dept_id": "dept_002",
40
+ "name": "Product",
41
+ "head_id": "emp_0002",
42
+ "budget": 6200000,
43
+ "headcount_limit": 30,
44
+ "current_headcount": 26,
45
+ "required_tools": ["Jira", "Figma", "Slack", "Confluence", "Amplitude", "Productboard"],
46
+ "onboarding_steps": [
47
+ "Submit signed offer letter and NDA",
48
+ "Complete background check verification",
49
+ "Provision laptop with standard business image",
50
+ "Grant Jira and Confluence access with product spaces",
51
+ "Set up Figma viewer account",
52
+ "Provide Amplitude and Productboard credentials",
53
+ "Assign product mentor for first 60 days",
54
+ "Schedule intro meetings with cross-functional stakeholders",
55
+ "Complete product domain training modules",
56
+ "Review current product roadmap and OKRs",
57
+ "Shadow customer calls for first two weeks"
58
+ ],
59
+ "offboarding_steps": [
60
+ "Transfer product ownership and roadmap documents",
61
+ "Reassign Jira epics and stories to remaining PMs",
62
+ "Revoke Amplitude and Productboard access",
63
+ "Hand over stakeholder relationships and meeting cadences",
64
+ "Complete exit interview with VP Product",
65
+ "Collect company-issued equipment",
66
+ "Remove from Slack product channels",
67
+ "Archive personal Confluence spaces",
68
+ "Revoke Figma access"
69
+ ]
70
+ },
71
+ {
72
+ "dept_id": "dept_003",
73
+ "name": "Marketing",
74
+ "head_id": "emp_0003",
75
+ "budget": 4800000,
76
+ "headcount_limit": 25,
77
+ "current_headcount": 25,
78
+ "required_tools": ["Hubspot", "Canva", "Slack", "Google Analytics", "Hootsuite", "Marketo", "WordPress"],
79
+ "onboarding_steps": [
80
+ "Submit signed offer letter and NDA",
81
+ "Complete background check verification",
82
+ "Provision laptop with standard business image",
83
+ "Set up Hubspot CRM access with appropriate role",
84
+ "Grant Canva team account and brand kit access",
85
+ "Provide Google Analytics and Hootsuite credentials",
86
+ "Configure Marketo user with campaign permissions",
87
+ "Assign marketing mentor for first 30 days",
88
+ "Complete brand guidelines and tone-of-voice training",
89
+ "Review current campaign calendar and quarterly goals",
90
+ "Schedule intro meetings with agency partners"
91
+ ],
92
+ "offboarding_steps": [
93
+ "Transfer campaign ownership and scheduled content",
94
+ "Revoke Hubspot, Marketo, and Hootsuite access",
95
+ "Reassign social media account credentials",
96
+ "Hand over agency and vendor relationships",
97
+ "Complete exit interview with marketing director",
98
+ "Collect company-issued equipment",
99
+ "Remove from Slack marketing channels",
100
+ "Transfer Google Analytics property access",
101
+ "Revoke WordPress admin privileges",
102
+ "Archive brand assets created by departing employee"
103
+ ]
104
+ },
105
+ {
106
+ "dept_id": "dept_004",
107
+ "name": "Sales",
108
+ "head_id": "emp_0004",
109
+ "budget": 7500000,
110
+ "headcount_limit": 30,
111
+ "current_headcount": 27,
112
+ "required_tools": ["Salesforce", "Slack", "Gong", "Outreach", "LinkedIn Sales Navigator", "Zoom"],
113
+ "onboarding_steps": [
114
+ "Submit signed offer letter and NDA",
115
+ "Complete background check verification",
116
+ "Provision laptop with standard business image",
117
+ "Set up Salesforce CRM account with territory assignment",
118
+ "Grant Gong and Outreach licenses",
119
+ "Provision LinkedIn Sales Navigator seat",
120
+ "Assign sales mentor and shadow partner for first 30 days",
121
+ "Complete product knowledge certification",
122
+ "Review territory plan and quota targets",
123
+ "Attend sales methodology boot camp",
124
+ "Shadow five customer calls before solo engagement"
125
+ ],
126
+ "offboarding_steps": [
127
+ "Reassign Salesforce accounts and open opportunities",
128
+ "Transfer pipeline and forecast data to manager",
129
+ "Revoke Gong, Outreach, and LinkedIn Sales Navigator access",
130
+ "Notify active customers of account transition",
131
+ "Complete exit interview with sales director",
132
+ "Collect company-issued equipment and demo devices",
133
+ "Remove from Slack sales channels",
134
+ "Revoke Salesforce credentials and API tokens",
135
+ "Hand over pending contract negotiations",
136
+ "Deactivate Zoom webinar host privileges"
137
+ ]
138
+ },
139
+ {
140
+ "dept_id": "dept_005",
141
+ "name": "Finance",
142
+ "head_id": "emp_0005",
143
+ "budget": 3200000,
144
+ "headcount_limit": 20,
145
+ "current_headcount": 20,
146
+ "required_tools": ["NetSuite", "Slack", "Expensify", "Tableau", "Excel Advanced", "Stripe Dashboard"],
147
+ "onboarding_steps": [
148
+ "Submit signed offer letter and NDA",
149
+ "Complete enhanced background and credit check",
150
+ "Provision laptop with finance-secured image",
151
+ "Set up NetSuite account with role-based access controls",
152
+ "Grant Expensify admin or reviewer access",
153
+ "Provide Tableau credentials with finance dashboards",
154
+ "Complete SOX compliance training",
155
+ "Review internal controls documentation",
156
+ "Assign finance buddy for first 30 days",
157
+ "Schedule intro meetings with external auditors",
158
+ "Complete data handling and PII training"
159
+ ],
160
+ "offboarding_steps": [
161
+ "Revoke NetSuite access and rotate shared credentials",
162
+ "Transfer Expensify approval chains",
163
+ "Remove Tableau dashboard ownership",
164
+ "Complete handover of open reconciliations and reports",
165
+ "Revoke Stripe Dashboard access",
166
+ "Conduct exit interview with CFO or finance director",
167
+ "Collect company-issued equipment",
168
+ "Remove from Slack finance channels",
169
+ "Notify external auditors of personnel change",
170
+ "Archive financial models and working papers"
171
+ ]
172
+ },
173
+ {
174
+ "dept_id": "dept_006",
175
+ "name": "HR",
176
+ "head_id": "emp_0006",
177
+ "budget": 2100000,
178
+ "headcount_limit": 15,
179
+ "current_headcount": 13,
180
+ "required_tools": ["Workday", "Slack", "Greenhouse", "BambooHR", "DocuSign", "Culture Amp"],
181
+ "onboarding_steps": [
182
+ "Submit signed offer letter and NDA",
183
+ "Complete background check verification",
184
+ "Provision laptop with HR-secured image",
185
+ "Set up Workday account with HRIS admin permissions",
186
+ "Grant Greenhouse ATS access with recruiter role",
187
+ "Provide BambooHR and Culture Amp credentials",
188
+ "Complete HIPAA and employee data privacy training",
189
+ "Review employee handbook and current policies",
190
+ "Assign HR mentor for first 30 days",
191
+ "Schedule intro meetings with department heads",
192
+ "Complete conflict resolution and interviewer training"
193
+ ],
194
+ "offboarding_steps": [
195
+ "Revoke Workday HRIS admin access immediately",
196
+ "Transfer Greenhouse pipeline and candidate ownership",
197
+ "Remove BambooHR and Culture Amp access",
198
+ "Hand over active employee relations cases",
199
+ "Complete exit interview with CHRO",
200
+ "Collect company-issued equipment",
201
+ "Remove from Slack HR channels",
202
+ "Revoke DocuSign sending privileges",
203
+ "Transfer benefits administration contacts",
204
+ "Ensure compliance documentation is up to date"
205
+ ]
206
+ },
207
+ {
208
+ "dept_id": "dept_007",
209
+ "name": "Data Science",
210
+ "head_id": "emp_0007",
211
+ "budget": 5800000,
212
+ "headcount_limit": 25,
213
+ "current_headcount": 22,
214
+ "required_tools": ["Jupyter", "Databricks", "AWS SageMaker", "Slack", "GitHub", "Snowflake", "Tableau"],
215
+ "onboarding_steps": [
216
+ "Submit signed offer letter and NDA",
217
+ "Complete background check verification",
218
+ "Provision laptop with data science workstation image",
219
+ "Set up Databricks workspace and cluster access",
220
+ "Grant AWS SageMaker and S3 bucket permissions",
221
+ "Provide Snowflake account with appropriate warehouse access",
222
+ "Configure GitHub repo access for ML pipelines",
223
+ "Assign data science mentor for first 30 days",
224
+ "Complete data governance and ethics training",
225
+ "Review model registry and deployment pipeline docs",
226
+ "Set up Jupyter environment with approved packages",
227
+ "Schedule intro meetings with data engineering partners"
228
+ ],
229
+ "offboarding_steps": [
230
+ "Transfer model ownership and experiment tracking data",
231
+ "Revoke Databricks and SageMaker access",
232
+ "Remove Snowflake credentials and query history",
233
+ "Hand over active model deployments and monitoring",
234
+ "Revoke GitHub ML pipeline repo access",
235
+ "Complete exit interview with data science director",
236
+ "Collect company-issued equipment and GPU hardware",
237
+ "Remove from Slack data science channels",
238
+ "Archive Jupyter notebooks and transfer to team repo",
239
+ "Revoke Tableau published data source ownership"
240
+ ]
241
+ },
242
+ {
243
+ "dept_id": "dept_008",
244
+ "name": "Security",
245
+ "head_id": "emp_0008",
246
+ "budget": 4100000,
247
+ "headcount_limit": 15,
248
+ "current_headcount": 13,
249
+ "required_tools": ["Splunk", "CrowdStrike", "Slack", "Okta Admin", "Burp Suite", "Snyk", "Vault"],
250
+ "onboarding_steps": [
251
+ "Submit signed offer letter and enhanced NDA with security addendum",
252
+ "Complete comprehensive background check including criminal and financial history",
253
+ "Provision hardened laptop with security team image",
254
+ "Set up Splunk account with SIEM dashboard access",
255
+ "Grant CrowdStrike console access",
256
+ "Provide Okta admin credentials with scoped permissions",
257
+ "Configure Vault access with least-privilege policy",
258
+ "Assign security mentor for first 60 days",
259
+ "Complete advanced security clearance training",
260
+ "Review incident response playbooks and escalation paths",
261
+ "Set up Burp Suite and Snyk licenses",
262
+ "Complete red team / blue team orientation exercise"
263
+ ],
264
+ "offboarding_steps": [
265
+ "Revoke Okta admin access immediately upon notice",
266
+ "Remove Splunk and CrowdStrike console access",
267
+ "Rotate all shared secrets and credentials known to employee",
268
+ "Revoke Vault access and audit token usage",
269
+ "Transfer incident response case ownership",
270
+ "Complete exit interview with CISO",
271
+ "Collect company-issued equipment including security hardware tokens",
272
+ "Remove from Slack security channels including incident response",
273
+ "Revoke VPN and privileged network access",
274
+ "Conduct security-specific exit debrief on confidentiality obligations",
275
+ "Update all security runbooks to remove personal references"
276
+ ]
277
+ }
278
+ ]
279
+ }
server/data/employees.json ADDED
@@ -0,0 +1,3002 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "emp_id": "emp_0001",
4
+ "name": "Rajesh Kumar",
5
+ "email": "rajesh.kumar@acmecorp.com",
6
+ "department": "Engineering",
7
+ "level": "L6",
8
+ "role": "VP of Engineering",
9
+ "manager_id": null,
10
+ "status": "active",
11
+ "date_of_joining": "2018-03-15",
12
+ "date_of_leaving": null,
13
+ "is_contractor": false,
14
+ "phone": "+1-415-332-7891",
15
+ "location": "San Francisco"
16
+ },
17
+ {
18
+ "emp_id": "emp_0002",
19
+ "name": "James Wilson",
20
+ "email": "james.wilson@acmecorp.com",
21
+ "department": "Engineering",
22
+ "level": "L5",
23
+ "role": "Director of Engineering",
24
+ "manager_id": "emp_0001",
25
+ "status": "active",
26
+ "date_of_joining": "2018-06-01",
27
+ "date_of_leaving": null,
28
+ "is_contractor": false,
29
+ "phone": "+1-415-221-4532",
30
+ "location": "San Francisco"
31
+ },
32
+ {
33
+ "emp_id": "emp_0003",
34
+ "name": "Wei Zhang",
35
+ "email": "wei.zhang@acmecorp.com",
36
+ "department": "Engineering",
37
+ "level": "L5",
38
+ "role": "Senior Engineering Manager",
39
+ "manager_id": "emp_0001",
40
+ "status": "active",
41
+ "date_of_joining": "2018-09-10",
42
+ "date_of_leaving": null,
43
+ "is_contractor": false,
44
+ "phone": "+1-650-443-8821",
45
+ "location": "San Francisco"
46
+ },
47
+ {
48
+ "emp_id": "emp_0004",
49
+ "name": "Priya Sharma",
50
+ "email": "priya.sharma@acmecorp.com",
51
+ "department": "Engineering",
52
+ "level": "L4",
53
+ "role": "Engineering Manager",
54
+ "manager_id": "emp_0002",
55
+ "status": "active",
56
+ "date_of_joining": "2019-01-14",
57
+ "date_of_leaving": null,
58
+ "is_contractor": false,
59
+ "phone": "+1-408-556-3312",
60
+ "location": "Bangalore"
61
+ },
62
+ {
63
+ "emp_id": "emp_0005",
64
+ "name": "Michael Brown",
65
+ "email": "michael.brown@acmecorp.com",
66
+ "department": "Engineering",
67
+ "level": "L4",
68
+ "role": "Staff Engineer",
69
+ "manager_id": "emp_0003",
70
+ "status": "active",
71
+ "date_of_joining": "2019-03-22",
72
+ "date_of_leaving": null,
73
+ "is_contractor": false,
74
+ "phone": "+1-512-778-4421",
75
+ "location": "Austin"
76
+ },
77
+ {
78
+ "emp_id": "emp_0006",
79
+ "name": "Hans Mueller",
80
+ "email": "hans.mueller@acmecorp.com",
81
+ "department": "Engineering",
82
+ "level": "L4",
83
+ "role": "Principal Engineer",
84
+ "manager_id": "emp_0002",
85
+ "status": "active",
86
+ "date_of_joining": "2019-05-18",
87
+ "date_of_leaving": null,
88
+ "is_contractor": false,
89
+ "phone": "+1-415-889-2234",
90
+ "location": "San Francisco"
91
+ },
92
+ {
93
+ "emp_id": "emp_0007",
94
+ "name": "Ananya Reddy",
95
+ "email": "ananya.reddy@acmecorp.com",
96
+ "department": "Engineering",
97
+ "level": "L3",
98
+ "role": "Senior Software Engineer",
99
+ "manager_id": "emp_0004",
100
+ "status": "active",
101
+ "date_of_joining": "2019-07-08",
102
+ "date_of_leaving": null,
103
+ "is_contractor": false,
104
+ "phone": "+1-408-332-5567",
105
+ "location": "Bangalore"
106
+ },
107
+ {
108
+ "emp_id": "emp_0008",
109
+ "name": "Jing Liu",
110
+ "email": "jing.liu@acmecorp.com",
111
+ "department": "Engineering",
112
+ "level": "L3",
113
+ "role": "Tech Lead",
114
+ "manager_id": "emp_0005",
115
+ "status": "active",
116
+ "date_of_joining": "2019-11-04",
117
+ "date_of_leaving": null,
118
+ "is_contractor": false,
119
+ "phone": "+1-650-114-7789",
120
+ "location": "San Francisco"
121
+ },
122
+ {
123
+ "emp_id": "emp_0009",
124
+ "name": "Sarah Johnson",
125
+ "email": "sarah.johnson@acmecorp.com",
126
+ "department": "Engineering",
127
+ "level": "L3",
128
+ "role": "Senior Developer",
129
+ "manager_id": "emp_0006",
130
+ "status": "active",
131
+ "date_of_joining": "2020-02-17",
132
+ "date_of_leaving": null,
133
+ "is_contractor": false,
134
+ "phone": "+1-212-556-3341",
135
+ "location": "New York"
136
+ },
137
+ {
138
+ "emp_id": "emp_0010",
139
+ "name": "Lars Johansson",
140
+ "email": "lars.johansson@acmecorp.com",
141
+ "department": "Engineering",
142
+ "level": "L3",
143
+ "role": "Senior Software Engineer",
144
+ "manager_id": "emp_0004",
145
+ "status": "active",
146
+ "date_of_joining": "2020-04-23",
147
+ "date_of_leaving": null,
148
+ "is_contractor": false,
149
+ "phone": "+1-415-667-8812",
150
+ "location": "San Francisco"
151
+ },
152
+ {
153
+ "emp_id": "emp_0011",
154
+ "name": "Rohan Patel",
155
+ "email": "rohan.patel@acmecorp.com",
156
+ "department": "Engineering",
157
+ "level": "L3",
158
+ "role": "Tech Lead",
159
+ "manager_id": "emp_0005",
160
+ "status": "pending",
161
+ "date_of_joining": "2025-09-15",
162
+ "date_of_leaving": null,
163
+ "is_contractor": false,
164
+ "phone": "+1-408-221-9934",
165
+ "location": "Bangalore"
166
+ },
167
+ {
168
+ "emp_id": "emp_0012",
169
+ "name": "David Garcia",
170
+ "email": "david.garcia@acmecorp.com",
171
+ "department": "Engineering",
172
+ "level": "L2",
173
+ "role": "Software Engineer II",
174
+ "manager_id": "emp_0007",
175
+ "status": "active",
176
+ "date_of_joining": "2020-06-15",
177
+ "date_of_leaving": null,
178
+ "is_contractor": false,
179
+ "phone": "+1-512-334-2218",
180
+ "location": "Austin"
181
+ },
182
+ {
183
+ "emp_id": "emp_0013",
184
+ "name": "Ming Wang",
185
+ "email": "ming.wang@acmecorp.com",
186
+ "department": "Engineering",
187
+ "level": "L2",
188
+ "role": "Developer",
189
+ "manager_id": "emp_0008",
190
+ "status": "active",
191
+ "date_of_joining": "2020-08-20",
192
+ "date_of_leaving": null,
193
+ "is_contractor": false,
194
+ "phone": "+1-650-887-1123",
195
+ "location": "San Francisco"
196
+ },
197
+ {
198
+ "emp_id": "emp_0014",
199
+ "name": "Elena Rossi",
200
+ "email": "elena.rossi@acmecorp.com",
201
+ "department": "Engineering",
202
+ "level": "L2",
203
+ "role": "Engineer",
204
+ "manager_id": "emp_0009",
205
+ "status": "active",
206
+ "date_of_joining": "2021-01-11",
207
+ "date_of_leaving": null,
208
+ "is_contractor": false,
209
+ "phone": "+1-212-445-6678",
210
+ "location": "New York"
211
+ },
212
+ {
213
+ "emp_id": "emp_0015",
214
+ "name": "Karthik Iyer",
215
+ "email": "karthik.iyer@acmecorp.com",
216
+ "department": "Engineering",
217
+ "level": "L2",
218
+ "role": "Software Engineer II",
219
+ "manager_id": "emp_0010",
220
+ "status": "active",
221
+ "date_of_joining": "2021-03-05",
222
+ "date_of_leaving": null,
223
+ "is_contractor": false,
224
+ "phone": "+1-408-778-3345",
225
+ "location": "Bangalore"
226
+ },
227
+ {
228
+ "emp_id": "emp_0016",
229
+ "name": "Jennifer Davis",
230
+ "email": "jennifer.davis@acmecorp.com",
231
+ "department": "Engineering",
232
+ "level": "L2",
233
+ "role": "Developer",
234
+ "manager_id": "emp_0007",
235
+ "status": "active",
236
+ "date_of_joining": "2021-05-19",
237
+ "date_of_leaving": null,
238
+ "is_contractor": true,
239
+ "phone": "+1-415-992-4456",
240
+ "location": "San Francisco"
241
+ },
242
+ {
243
+ "emp_id": "emp_0017",
244
+ "name": "Stefan Weber",
245
+ "email": "stefan.weber@acmecorp.com",
246
+ "department": "Engineering",
247
+ "level": "L2",
248
+ "role": "Engineer",
249
+ "manager_id": "emp_0008",
250
+ "status": "offboarded",
251
+ "date_of_joining": "2020-02-01",
252
+ "date_of_leaving": "2022-11-15",
253
+ "is_contractor": false,
254
+ "phone": "+1-650-113-7782",
255
+ "location": "London"
256
+ },
257
+ {
258
+ "emp_id": "emp_0018",
259
+ "name": "Aditya Gupta",
260
+ "email": "aditya.gupta@acmecorp.com",
261
+ "department": "Engineering",
262
+ "level": "L1",
263
+ "role": "Software Engineer I",
264
+ "manager_id": "emp_0012",
265
+ "status": "active",
266
+ "date_of_joining": "2022-01-10",
267
+ "date_of_leaving": null,
268
+ "is_contractor": false,
269
+ "phone": "+1-408-556-8890",
270
+ "location": "Bangalore"
271
+ },
272
+ {
273
+ "emp_id": "emp_0019",
274
+ "name": "Mary Thompson",
275
+ "email": "mary.thompson@acmecorp.com",
276
+ "department": "Engineering",
277
+ "level": "L1",
278
+ "role": "Junior Developer",
279
+ "manager_id": "emp_0013",
280
+ "status": "active",
281
+ "date_of_joining": "2022-03-28",
282
+ "date_of_leaving": null,
283
+ "is_contractor": false,
284
+ "phone": "+1-212-334-5567",
285
+ "location": "New York"
286
+ },
287
+ {
288
+ "emp_id": "emp_0020",
289
+ "name": "Tao Chen",
290
+ "email": "tao.chen@acmecorp.com",
291
+ "department": "Engineering",
292
+ "level": "L1",
293
+ "role": "Associate Engineer",
294
+ "manager_id": "emp_0014",
295
+ "status": "active",
296
+ "date_of_joining": "2022-06-13",
297
+ "date_of_leaving": null,
298
+ "is_contractor": false,
299
+ "phone": "+1-650-221-3345",
300
+ "location": "San Francisco"
301
+ },
302
+ {
303
+ "emp_id": "emp_0021",
304
+ "name": "Pierre Moreau",
305
+ "email": "pierre.moreau@acmecorp.com",
306
+ "department": "Engineering",
307
+ "level": "L1",
308
+ "role": "Software Engineer I",
309
+ "manager_id": "emp_0015",
310
+ "status": "active",
311
+ "date_of_joining": "2022-09-05",
312
+ "date_of_leaving": null,
313
+ "is_contractor": false,
314
+ "phone": "+1-415-778-2234",
315
+ "location": "London"
316
+ },
317
+ {
318
+ "emp_id": "emp_0022",
319
+ "name": "Shreya Nair",
320
+ "email": "shreya.nair@acmecorp.com",
321
+ "department": "Engineering",
322
+ "level": "L1",
323
+ "role": "Junior Developer",
324
+ "manager_id": "emp_0012",
325
+ "status": "active",
326
+ "date_of_joining": "2023-01-16",
327
+ "date_of_leaving": null,
328
+ "is_contractor": false,
329
+ "phone": "+1-408-889-6671",
330
+ "location": "Bangalore"
331
+ },
332
+ {
333
+ "emp_id": "emp_0023",
334
+ "name": "Robert Martinez",
335
+ "email": "robert.martinez@acmecorp.com",
336
+ "department": "Engineering",
337
+ "level": "L1",
338
+ "role": "Software Engineer I",
339
+ "manager_id": "emp_0013",
340
+ "status": "active",
341
+ "date_of_joining": "2023-04-03",
342
+ "date_of_leaving": null,
343
+ "is_contractor": false,
344
+ "phone": "+1-512-445-7783",
345
+ "location": "Austin"
346
+ },
347
+ {
348
+ "emp_id": "emp_0024",
349
+ "name": "Feng Li",
350
+ "email": "feng.li@acmecorp.com",
351
+ "department": "Engineering",
352
+ "level": "L1",
353
+ "role": "Associate Engineer",
354
+ "manager_id": "emp_0016",
355
+ "status": "pending",
356
+ "date_of_joining": "2025-11-01",
357
+ "date_of_leaving": null,
358
+ "is_contractor": false,
359
+ "phone": "+1-650-556-9912",
360
+ "location": "San Francisco"
361
+ },
362
+ {
363
+ "emp_id": "emp_0025",
364
+ "name": "Ingrid Larsson",
365
+ "email": "ingrid.larsson@acmecorp.com",
366
+ "department": "Engineering",
367
+ "level": "L1",
368
+ "role": "Software Engineer I",
369
+ "manager_id": "emp_0014",
370
+ "status": "active",
371
+ "date_of_joining": "2023-07-21",
372
+ "date_of_leaving": null,
373
+ "is_contractor": false,
374
+ "phone": "+1-212-667-1145",
375
+ "location": "London"
376
+ },
377
+ {
378
+ "emp_id": "emp_0026",
379
+ "name": "Vivaan Singh",
380
+ "email": "vivaan.singh@acmecorp.com",
381
+ "department": "Engineering",
382
+ "level": "L1",
383
+ "role": "Junior Developer",
384
+ "manager_id": "emp_0015",
385
+ "status": "active",
386
+ "date_of_joining": "2023-10-09",
387
+ "date_of_leaving": null,
388
+ "is_contractor": true,
389
+ "phone": "+1-408-113-4456",
390
+ "location": "Bangalore"
391
+ },
392
+ {
393
+ "emp_id": "emp_0027",
394
+ "name": "Karen Robinson",
395
+ "email": "karen.robinson@acmecorp.com",
396
+ "department": "Engineering",
397
+ "level": "L1",
398
+ "role": "Software Engineer I",
399
+ "manager_id": "emp_0016",
400
+ "status": "offboarded",
401
+ "date_of_joining": "2021-08-15",
402
+ "date_of_leaving": "2023-05-20",
403
+ "is_contractor": false,
404
+ "phone": "+1-512-889-2238",
405
+ "location": "Austin"
406
+ },
407
+ {
408
+ "emp_id": "emp_0028",
409
+ "name": "Hao Wu",
410
+ "email": "hao.wu@acmecorp.com",
411
+ "department": "Engineering",
412
+ "level": "L1",
413
+ "role": "Associate Engineer",
414
+ "manager_id": "emp_0012",
415
+ "status": "active",
416
+ "date_of_joining": "2024-02-12",
417
+ "date_of_leaving": null,
418
+ "is_contractor": false,
419
+ "phone": "+1-650-334-8801",
420
+ "location": "San Francisco"
421
+ },
422
+ {
423
+ "emp_id": "emp_0029",
424
+ "name": "Sofia Petrov",
425
+ "email": "sofia.petrov@acmecorp.com",
426
+ "department": "Product",
427
+ "level": "L6",
428
+ "role": "VP of Product",
429
+ "manager_id": null,
430
+ "status": "active",
431
+ "date_of_joining": "2018-04-02",
432
+ "date_of_leaving": null,
433
+ "is_contractor": false,
434
+ "phone": "+1-415-221-5543",
435
+ "location": "San Francisco"
436
+ },
437
+ {
438
+ "emp_id": "emp_0030",
439
+ "name": "Amit Joshi",
440
+ "email": "amit.joshi@acmecorp.com",
441
+ "department": "Product",
442
+ "level": "L5",
443
+ "role": "Director of Product",
444
+ "manager_id": "emp_0029",
445
+ "status": "active",
446
+ "date_of_joining": "2018-08-20",
447
+ "date_of_leaving": null,
448
+ "is_contractor": false,
449
+ "phone": "+1-408-667-3319",
450
+ "location": "Bangalore"
451
+ },
452
+ {
453
+ "emp_id": "emp_0031",
454
+ "name": "Elizabeth Taylor",
455
+ "email": "elizabeth.taylor@acmecorp.com",
456
+ "department": "Product",
457
+ "level": "L5",
458
+ "role": "Director of Product",
459
+ "manager_id": "emp_0029",
460
+ "status": "active",
461
+ "date_of_joining": "2019-02-11",
462
+ "date_of_leaving": null,
463
+ "is_contractor": false,
464
+ "phone": "+1-212-443-7718",
465
+ "location": "New York"
466
+ },
467
+ {
468
+ "emp_id": "emp_0032",
469
+ "name": "Lei Huang",
470
+ "email": "lei.huang@acmecorp.com",
471
+ "department": "Product",
472
+ "level": "L4",
473
+ "role": "Group Product Manager",
474
+ "manager_id": "emp_0030",
475
+ "status": "active",
476
+ "date_of_joining": "2019-06-03",
477
+ "date_of_leaving": null,
478
+ "is_contractor": false,
479
+ "phone": "+1-650-889-4421",
480
+ "location": "San Francisco"
481
+ },
482
+ {
483
+ "emp_id": "emp_0033",
484
+ "name": "Marco Ferrari",
485
+ "email": "marco.ferrari@acmecorp.com",
486
+ "department": "Product",
487
+ "level": "L4",
488
+ "role": "Principal Product Manager",
489
+ "manager_id": "emp_0031",
490
+ "status": "active",
491
+ "date_of_joining": "2019-09-17",
492
+ "date_of_leaving": null,
493
+ "is_contractor": false,
494
+ "phone": "+1-212-556-8832",
495
+ "location": "London"
496
+ },
497
+ {
498
+ "emp_id": "emp_0034",
499
+ "name": "Kavya Desai",
500
+ "email": "kavya.desai@acmecorp.com",
501
+ "department": "Product",
502
+ "level": "L3",
503
+ "role": "Senior Product Manager",
504
+ "manager_id": "emp_0032",
505
+ "status": "active",
506
+ "date_of_joining": "2020-01-20",
507
+ "date_of_leaving": null,
508
+ "is_contractor": false,
509
+ "phone": "+1-408-334-6654",
510
+ "location": "Bangalore"
511
+ },
512
+ {
513
+ "emp_id": "emp_0035",
514
+ "name": "Thomas White",
515
+ "email": "thomas.white@acmecorp.com",
516
+ "department": "Product",
517
+ "level": "L3",
518
+ "role": "Lead Product Designer",
519
+ "manager_id": "emp_0033",
520
+ "status": "active",
521
+ "date_of_joining": "2020-05-11",
522
+ "date_of_leaving": null,
523
+ "is_contractor": false,
524
+ "phone": "+1-415-778-9945",
525
+ "location": "San Francisco"
526
+ },
527
+ {
528
+ "emp_id": "emp_0036",
529
+ "name": "Xin Yang",
530
+ "email": "xin.yang@acmecorp.com",
531
+ "department": "Product",
532
+ "level": "L3",
533
+ "role": "Senior Product Manager",
534
+ "manager_id": "emp_0032",
535
+ "status": "active",
536
+ "date_of_joining": "2020-08-24",
537
+ "date_of_leaving": null,
538
+ "is_contractor": false,
539
+ "phone": "+1-650-445-1167",
540
+ "location": "San Francisco"
541
+ },
542
+ {
543
+ "emp_id": "emp_0037",
544
+ "name": "Sven Berg",
545
+ "email": "sven.berg@acmecorp.com",
546
+ "department": "Product",
547
+ "level": "L2",
548
+ "role": "Product Manager",
549
+ "manager_id": "emp_0034",
550
+ "status": "active",
551
+ "date_of_joining": "2021-01-18",
552
+ "date_of_leaving": null,
553
+ "is_contractor": false,
554
+ "phone": "+1-212-221-3378",
555
+ "location": "London"
556
+ },
557
+ {
558
+ "emp_id": "emp_0038",
559
+ "name": "Diya Mehta",
560
+ "email": "diya.mehta@acmecorp.com",
561
+ "department": "Product",
562
+ "level": "L2",
563
+ "role": "Product Designer",
564
+ "manager_id": "emp_0035",
565
+ "status": "active",
566
+ "date_of_joining": "2021-04-05",
567
+ "date_of_leaving": null,
568
+ "is_contractor": false,
569
+ "phone": "+1-408-556-2241",
570
+ "location": "Bangalore"
571
+ },
572
+ {
573
+ "emp_id": "emp_0039",
574
+ "name": "Richard Harris",
575
+ "email": "richard.harris@acmecorp.com",
576
+ "department": "Product",
577
+ "level": "L2",
578
+ "role": "Product Manager",
579
+ "manager_id": "emp_0036",
580
+ "status": "active",
581
+ "date_of_joining": "2021-07-12",
582
+ "date_of_leaving": null,
583
+ "is_contractor": false,
584
+ "phone": "+1-415-889-5567",
585
+ "location": "San Francisco"
586
+ },
587
+ {
588
+ "emp_id": "emp_0040",
589
+ "name": "Hui Zhou",
590
+ "email": "hui.zhou@acmecorp.com",
591
+ "department": "Product",
592
+ "level": "L2",
593
+ "role": "Product Designer",
594
+ "manager_id": "emp_0034",
595
+ "status": "pending",
596
+ "date_of_joining": "2025-10-15",
597
+ "date_of_leaving": null,
598
+ "is_contractor": false,
599
+ "phone": "+1-650-113-8893",
600
+ "location": "San Francisco"
601
+ },
602
+ {
603
+ "emp_id": "emp_0041",
604
+ "name": "Nikhil Verma",
605
+ "email": "nikhil.verma@acmecorp.com",
606
+ "department": "Product",
607
+ "level": "L1",
608
+ "role": "Associate Product Manager",
609
+ "manager_id": "emp_0037",
610
+ "status": "active",
611
+ "date_of_joining": "2022-02-14",
612
+ "date_of_leaving": null,
613
+ "is_contractor": false,
614
+ "phone": "+1-408-778-4432",
615
+ "location": "Bangalore"
616
+ },
617
+ {
618
+ "emp_id": "emp_0042",
619
+ "name": "Patricia Anderson",
620
+ "email": "patricia.anderson@acmecorp.com",
621
+ "department": "Product",
622
+ "level": "L1",
623
+ "role": "Product Analyst",
624
+ "manager_id": "emp_0038",
625
+ "status": "active",
626
+ "date_of_joining": "2022-05-09",
627
+ "date_of_leaving": null,
628
+ "is_contractor": false,
629
+ "phone": "+1-212-334-7789",
630
+ "location": "New York"
631
+ },
632
+ {
633
+ "emp_id": "emp_0043",
634
+ "name": "Yan Ma",
635
+ "email": "yan.ma@acmecorp.com",
636
+ "department": "Product",
637
+ "level": "L1",
638
+ "role": "Associate Product Manager",
639
+ "manager_id": "emp_0039",
640
+ "status": "active",
641
+ "date_of_joining": "2022-08-22",
642
+ "date_of_leaving": null,
643
+ "is_contractor": false,
644
+ "phone": "+1-650-667-2256",
645
+ "location": "San Francisco"
646
+ },
647
+ {
648
+ "emp_id": "emp_0044",
649
+ "name": "Klaus Fischer",
650
+ "email": "klaus.fischer@acmecorp.com",
651
+ "department": "Product",
652
+ "level": "L1",
653
+ "role": "Product Analyst",
654
+ "manager_id": "emp_0037",
655
+ "status": "active",
656
+ "date_of_joining": "2023-01-30",
657
+ "date_of_leaving": null,
658
+ "is_contractor": true,
659
+ "phone": "+1-415-992-1123",
660
+ "location": "London"
661
+ },
662
+ {
663
+ "emp_id": "emp_0045",
664
+ "name": "Meera Rao",
665
+ "email": "meera.rao@acmecorp.com",
666
+ "department": "Product",
667
+ "level": "L1",
668
+ "role": "Associate Product Manager",
669
+ "manager_id": "emp_0038",
670
+ "status": "offboarded",
671
+ "date_of_joining": "2021-06-01",
672
+ "date_of_leaving": "2023-09-15",
673
+ "is_contractor": false,
674
+ "phone": "+1-408-221-5578",
675
+ "location": "Bangalore"
676
+ },
677
+ {
678
+ "emp_id": "emp_0046",
679
+ "name": "Daniel Smith",
680
+ "email": "daniel.smith@acmecorp.com",
681
+ "department": "Product",
682
+ "level": "L1",
683
+ "role": "Product Analyst",
684
+ "manager_id": "emp_0039",
685
+ "status": "active",
686
+ "date_of_joining": "2023-06-19",
687
+ "date_of_leaving": null,
688
+ "is_contractor": false,
689
+ "phone": "+1-512-556-3349",
690
+ "location": "Austin"
691
+ },
692
+ {
693
+ "emp_id": "emp_0047",
694
+ "name": "Rui Xu",
695
+ "email": "rui.xu@acmecorp.com",
696
+ "department": "Product",
697
+ "level": "L1",
698
+ "role": "Associate Product Manager",
699
+ "manager_id": "emp_0037",
700
+ "status": "active",
701
+ "date_of_joining": "2024-01-08",
702
+ "date_of_leaving": null,
703
+ "is_contractor": false,
704
+ "phone": "+1-650-889-6612",
705
+ "location": "San Francisco"
706
+ },
707
+ {
708
+ "emp_id": "emp_0048",
709
+ "name": "Anna Schmidt",
710
+ "email": "anna.schmidt@acmecorp.com",
711
+ "department": "Marketing",
712
+ "level": "L6",
713
+ "role": "VP of Marketing",
714
+ "manager_id": null,
715
+ "status": "active",
716
+ "date_of_joining": "2018-05-14",
717
+ "date_of_leaving": null,
718
+ "is_contractor": false,
719
+ "phone": "+1-212-443-9934",
720
+ "location": "New York"
721
+ },
722
+ {
723
+ "emp_id": "emp_0049",
724
+ "name": "Vikram Chopra",
725
+ "email": "vikram.chopra@acmecorp.com",
726
+ "department": "Marketing",
727
+ "level": "L5",
728
+ "role": "Senior Director of Marketing",
729
+ "manager_id": "emp_0048",
730
+ "status": "active",
731
+ "date_of_joining": "2018-10-22",
732
+ "date_of_leaving": null,
733
+ "is_contractor": false,
734
+ "phone": "+1-408-334-1156",
735
+ "location": "Bangalore"
736
+ },
737
+ {
738
+ "emp_id": "emp_0050",
739
+ "name": "Jessica Martin",
740
+ "email": "jessica.martin@acmecorp.com",
741
+ "department": "Marketing",
742
+ "level": "L4",
743
+ "role": "Marketing Director",
744
+ "manager_id": "emp_0049",
745
+ "status": "active",
746
+ "date_of_joining": "2019-04-08",
747
+ "date_of_leaving": null,
748
+ "is_contractor": false,
749
+ "phone": "+1-212-778-2267",
750
+ "location": "New York"
751
+ },
752
+ {
753
+ "emp_id": "emp_0051",
754
+ "name": "Qiang Sun",
755
+ "email": "qiang.sun@acmecorp.com",
756
+ "department": "Marketing",
757
+ "level": "L4",
758
+ "role": "Head of Growth",
759
+ "manager_id": "emp_0049",
760
+ "status": "active",
761
+ "date_of_joining": "2019-08-19",
762
+ "date_of_leaving": null,
763
+ "is_contractor": false,
764
+ "phone": "+1-650-445-3378",
765
+ "location": "San Francisco"
766
+ },
767
+ {
768
+ "emp_id": "emp_0052",
769
+ "name": "Erik Wagner",
770
+ "email": "erik.wagner@acmecorp.com",
771
+ "department": "Marketing",
772
+ "level": "L3",
773
+ "role": "Senior Marketing Manager",
774
+ "manager_id": "emp_0050",
775
+ "status": "active",
776
+ "date_of_joining": "2020-01-13",
777
+ "date_of_leaving": null,
778
+ "is_contractor": false,
779
+ "phone": "+1-415-113-4489",
780
+ "location": "London"
781
+ },
782
+ {
783
+ "emp_id": "emp_0053",
784
+ "name": "Neha Bhat",
785
+ "email": "neha.bhat@acmecorp.com",
786
+ "department": "Marketing",
787
+ "level": "L3",
788
+ "role": "Growth Lead",
789
+ "manager_id": "emp_0051",
790
+ "status": "active",
791
+ "date_of_joining": "2020-05-25",
792
+ "date_of_leaving": null,
793
+ "is_contractor": false,
794
+ "phone": "+1-408-889-7712",
795
+ "location": "Bangalore"
796
+ },
797
+ {
798
+ "emp_id": "emp_0054",
799
+ "name": "Matthew Rodriguez",
800
+ "email": "matthew.rodriguez@acmecorp.com",
801
+ "department": "Marketing",
802
+ "level": "L3",
803
+ "role": "Brand Manager",
804
+ "manager_id": "emp_0050",
805
+ "status": "active",
806
+ "date_of_joining": "2020-09-07",
807
+ "date_of_leaving": null,
808
+ "is_contractor": false,
809
+ "phone": "+1-212-221-8856",
810
+ "location": "New York"
811
+ },
812
+ {
813
+ "emp_id": "emp_0055",
814
+ "name": "Shan Guo",
815
+ "email": "shan.guo@acmecorp.com",
816
+ "department": "Marketing",
817
+ "level": "L2",
818
+ "role": "Marketing Specialist",
819
+ "manager_id": "emp_0052",
820
+ "status": "active",
821
+ "date_of_joining": "2021-02-01",
822
+ "date_of_leaving": null,
823
+ "is_contractor": false,
824
+ "phone": "+1-650-556-1134",
825
+ "location": "San Francisco"
826
+ },
827
+ {
828
+ "emp_id": "emp_0056",
829
+ "name": "Katarina Volkov",
830
+ "email": "katarina.volkov@acmecorp.com",
831
+ "department": "Marketing",
832
+ "level": "L2",
833
+ "role": "Content Strategist",
834
+ "manager_id": "emp_0053",
835
+ "status": "active",
836
+ "date_of_joining": "2021-05-17",
837
+ "date_of_leaving": null,
838
+ "is_contractor": false,
839
+ "phone": "+1-415-667-2278",
840
+ "location": "London"
841
+ },
842
+ {
843
+ "emp_id": "emp_0057",
844
+ "name": "Pranav Mishra",
845
+ "email": "pranav.mishra@acmecorp.com",
846
+ "department": "Marketing",
847
+ "level": "L2",
848
+ "role": "Brand Specialist",
849
+ "manager_id": "emp_0054",
850
+ "status": "active",
851
+ "date_of_joining": "2021-08-30",
852
+ "date_of_leaving": null,
853
+ "is_contractor": false,
854
+ "phone": "+1-408-992-3367",
855
+ "location": "Bangalore"
856
+ },
857
+ {
858
+ "emp_id": "emp_0058",
859
+ "name": "Linda Jackson",
860
+ "email": "linda.jackson@acmecorp.com",
861
+ "department": "Marketing",
862
+ "level": "L2",
863
+ "role": "Marketing Specialist",
864
+ "manager_id": "emp_0052",
865
+ "status": "pending",
866
+ "date_of_joining": "2025-08-01",
867
+ "date_of_leaving": null,
868
+ "is_contractor": false,
869
+ "phone": "+1-212-334-4490",
870
+ "location": "New York"
871
+ },
872
+ {
873
+ "emp_id": "emp_0059",
874
+ "name": "Zhi Lin",
875
+ "email": "zhi.lin@acmecorp.com",
876
+ "department": "Marketing",
877
+ "level": "L1",
878
+ "role": "Marketing Associate",
879
+ "manager_id": "emp_0055",
880
+ "status": "active",
881
+ "date_of_joining": "2022-03-14",
882
+ "date_of_leaving": null,
883
+ "is_contractor": false,
884
+ "phone": "+1-650-778-5543",
885
+ "location": "San Francisco"
886
+ },
887
+ {
888
+ "emp_id": "emp_0060",
889
+ "name": "Olaf Schulz",
890
+ "email": "olaf.schulz@acmecorp.com",
891
+ "department": "Marketing",
892
+ "level": "L1",
893
+ "role": "Content Writer",
894
+ "manager_id": "emp_0056",
895
+ "status": "active",
896
+ "date_of_joining": "2022-06-27",
897
+ "date_of_leaving": null,
898
+ "is_contractor": false,
899
+ "phone": "+1-415-889-6623",
900
+ "location": "London"
901
+ },
902
+ {
903
+ "emp_id": "emp_0061",
904
+ "name": "Riya Pillai",
905
+ "email": "riya.pillai@acmecorp.com",
906
+ "department": "Marketing",
907
+ "level": "L1",
908
+ "role": "Marketing Coordinator",
909
+ "manager_id": "emp_0057",
910
+ "status": "active",
911
+ "date_of_joining": "2022-10-10",
912
+ "date_of_leaving": null,
913
+ "is_contractor": false,
914
+ "phone": "+1-408-113-9978",
915
+ "location": "Bangalore"
916
+ },
917
+ {
918
+ "emp_id": "emp_0062",
919
+ "name": "Anthony Thomas",
920
+ "email": "anthony.thomas@acmecorp.com",
921
+ "department": "Marketing",
922
+ "level": "L1",
923
+ "role": "Marketing Associate",
924
+ "manager_id": "emp_0055",
925
+ "status": "active",
926
+ "date_of_joining": "2023-02-20",
927
+ "date_of_leaving": null,
928
+ "is_contractor": false,
929
+ "phone": "+1-512-445-1134",
930
+ "location": "Austin"
931
+ },
932
+ {
933
+ "emp_id": "emp_0063",
934
+ "name": "Chen Luo",
935
+ "email": "chen.luo@acmecorp.com",
936
+ "department": "Marketing",
937
+ "level": "L1",
938
+ "role": "Content Writer",
939
+ "manager_id": "emp_0056",
940
+ "status": "active",
941
+ "date_of_joining": "2023-05-08",
942
+ "date_of_leaving": null,
943
+ "is_contractor": true,
944
+ "phone": "+1-650-992-2245",
945
+ "location": "San Francisco"
946
+ },
947
+ {
948
+ "emp_id": "emp_0064",
949
+ "name": "Marie Dubois",
950
+ "email": "marie.dubois@acmecorp.com",
951
+ "department": "Marketing",
952
+ "level": "L1",
953
+ "role": "Marketing Coordinator",
954
+ "manager_id": "emp_0057",
955
+ "status": "offboarded",
956
+ "date_of_joining": "2021-04-12",
957
+ "date_of_leaving": "2023-08-30",
958
+ "is_contractor": false,
959
+ "phone": "+1-212-556-7712",
960
+ "location": "London"
961
+ },
962
+ {
963
+ "emp_id": "emp_0065",
964
+ "name": "Arjun Chauhan",
965
+ "email": "arjun.chauhan@acmecorp.com",
966
+ "department": "Marketing",
967
+ "level": "L1",
968
+ "role": "Marketing Associate",
969
+ "manager_id": "emp_0055",
970
+ "status": "active",
971
+ "date_of_joining": "2024-03-11",
972
+ "date_of_leaving": null,
973
+ "is_contractor": false,
974
+ "phone": "+1-408-221-6634",
975
+ "location": "Bangalore"
976
+ },
977
+ {
978
+ "emp_id": "emp_0066",
979
+ "name": "Barbara Miller",
980
+ "email": "barbara.miller@acmecorp.com",
981
+ "department": "Sales",
982
+ "level": "L6",
983
+ "role": "VP of Sales",
984
+ "manager_id": null,
985
+ "status": "active",
986
+ "date_of_joining": "2018-02-19",
987
+ "date_of_leaving": null,
988
+ "is_contractor": false,
989
+ "phone": "+1-212-889-3345",
990
+ "location": "New York"
991
+ },
992
+ {
993
+ "emp_id": "emp_0067",
994
+ "name": "Suresh Thakur",
995
+ "email": "suresh.thakur@acmecorp.com",
996
+ "department": "Sales",
997
+ "level": "L5",
998
+ "role": "Senior Director of Sales",
999
+ "manager_id": "emp_0066",
1000
+ "status": "active",
1001
+ "date_of_joining": "2018-07-16",
1002
+ "date_of_leaving": null,
1003
+ "is_contractor": false,
1004
+ "phone": "+1-408-667-8823",
1005
+ "location": "Bangalore"
1006
+ },
1007
+ {
1008
+ "emp_id": "emp_0068",
1009
+ "name": "Jun Zheng",
1010
+ "email": "jun.zheng@acmecorp.com",
1011
+ "department": "Sales",
1012
+ "level": "L4",
1013
+ "role": "Regional Sales Director",
1014
+ "manager_id": "emp_0067",
1015
+ "status": "active",
1016
+ "date_of_joining": "2019-01-28",
1017
+ "date_of_leaving": null,
1018
+ "is_contractor": false,
1019
+ "phone": "+1-650-334-4456",
1020
+ "location": "San Francisco"
1021
+ },
1022
+ {
1023
+ "emp_id": "emp_0069",
1024
+ "name": "Henrik Becker",
1025
+ "email": "henrik.becker@acmecorp.com",
1026
+ "department": "Sales",
1027
+ "level": "L4",
1028
+ "role": "Head of Enterprise Sales",
1029
+ "manager_id": "emp_0067",
1030
+ "status": "active",
1031
+ "date_of_joining": "2019-05-13",
1032
+ "date_of_leaving": null,
1033
+ "is_contractor": false,
1034
+ "phone": "+1-415-445-5567",
1035
+ "location": "London"
1036
+ },
1037
+ {
1038
+ "emp_id": "emp_0070",
1039
+ "name": "Pooja Menon",
1040
+ "email": "pooja.menon@acmecorp.com",
1041
+ "department": "Sales",
1042
+ "level": "L3",
1043
+ "role": "Senior Account Executive",
1044
+ "manager_id": "emp_0068",
1045
+ "status": "active",
1046
+ "date_of_joining": "2019-10-07",
1047
+ "date_of_leaving": null,
1048
+ "is_contractor": false,
1049
+ "phone": "+1-408-778-6678",
1050
+ "location": "Bangalore"
1051
+ },
1052
+ {
1053
+ "emp_id": "emp_0071",
1054
+ "name": "Steven Williams",
1055
+ "email": "steven.williams@acmecorp.com",
1056
+ "department": "Sales",
1057
+ "level": "L3",
1058
+ "role": "Sales Manager",
1059
+ "manager_id": "emp_0069",
1060
+ "status": "active",
1061
+ "date_of_joining": "2020-03-16",
1062
+ "date_of_leaving": null,
1063
+ "is_contractor": false,
1064
+ "phone": "+1-212-113-7789",
1065
+ "location": "New York"
1066
+ },
1067
+ {
1068
+ "emp_id": "emp_0072",
1069
+ "name": "Peng Han",
1070
+ "email": "peng.han@acmecorp.com",
1071
+ "department": "Sales",
1072
+ "level": "L3",
1073
+ "role": "Enterprise Sales Rep",
1074
+ "manager_id": "emp_0068",
1075
+ "status": "active",
1076
+ "date_of_joining": "2020-07-27",
1077
+ "date_of_leaving": null,
1078
+ "is_contractor": false,
1079
+ "phone": "+1-650-889-8812",
1080
+ "location": "San Francisco"
1081
+ },
1082
+ {
1083
+ "emp_id": "emp_0073",
1084
+ "name": "Astrid Koch",
1085
+ "email": "astrid.koch@acmecorp.com",
1086
+ "department": "Sales",
1087
+ "level": "L3",
1088
+ "role": "Sales Manager",
1089
+ "manager_id": "emp_0069",
1090
+ "status": "pending",
1091
+ "date_of_joining": "2025-07-01",
1092
+ "date_of_leaving": null,
1093
+ "is_contractor": false,
1094
+ "phone": "+1-415-221-9923",
1095
+ "location": "London"
1096
+ },
1097
+ {
1098
+ "emp_id": "emp_0074",
1099
+ "name": "Deepak Rao",
1100
+ "email": "deepak.rao@acmecorp.com",
1101
+ "department": "Sales",
1102
+ "level": "L2",
1103
+ "role": "Account Executive II",
1104
+ "manager_id": "emp_0070",
1105
+ "status": "active",
1106
+ "date_of_joining": "2020-11-09",
1107
+ "date_of_leaving": null,
1108
+ "is_contractor": false,
1109
+ "phone": "+1-408-556-1145",
1110
+ "location": "Bangalore"
1111
+ },
1112
+ {
1113
+ "emp_id": "emp_0075",
1114
+ "name": "Brian Jones",
1115
+ "email": "brian.jones@acmecorp.com",
1116
+ "department": "Sales",
1117
+ "level": "L2",
1118
+ "role": "Sales Engineer",
1119
+ "manager_id": "emp_0071",
1120
+ "status": "active",
1121
+ "date_of_joining": "2021-02-22",
1122
+ "date_of_leaving": null,
1123
+ "is_contractor": false,
1124
+ "phone": "+1-512-334-2256",
1125
+ "location": "Austin"
1126
+ },
1127
+ {
1128
+ "emp_id": "emp_0076",
1129
+ "name": "Mei Xie",
1130
+ "email": "mei.xie@acmecorp.com",
1131
+ "department": "Sales",
1132
+ "level": "L2",
1133
+ "role": "Business Development Rep",
1134
+ "manager_id": "emp_0072",
1135
+ "status": "active",
1136
+ "date_of_joining": "2021-06-14",
1137
+ "date_of_leaving": null,
1138
+ "is_contractor": false,
1139
+ "phone": "+1-650-667-3367",
1140
+ "location": "San Francisco"
1141
+ },
1142
+ {
1143
+ "emp_id": "emp_0077",
1144
+ "name": "Luca Hoffman",
1145
+ "email": "luca.hoffman@acmecorp.com",
1146
+ "department": "Sales",
1147
+ "level": "L2",
1148
+ "role": "Account Executive II",
1149
+ "manager_id": "emp_0070",
1150
+ "status": "active",
1151
+ "date_of_joining": "2021-09-27",
1152
+ "date_of_leaving": null,
1153
+ "is_contractor": false,
1154
+ "phone": "+1-415-778-4478",
1155
+ "location": "London"
1156
+ },
1157
+ {
1158
+ "emp_id": "emp_0078",
1159
+ "name": "Tanvi Kumar",
1160
+ "email": "tanvi.kumar@acmecorp.com",
1161
+ "department": "Sales",
1162
+ "level": "L2",
1163
+ "role": "Sales Engineer",
1164
+ "manager_id": "emp_0071",
1165
+ "status": "active",
1166
+ "date_of_joining": "2022-01-10",
1167
+ "date_of_leaving": null,
1168
+ "is_contractor": true,
1169
+ "phone": "+1-408-889-5589",
1170
+ "location": "Bangalore"
1171
+ },
1172
+ {
1173
+ "emp_id": "emp_0079",
1174
+ "name": "Kevin Harris",
1175
+ "email": "kevin.harris@acmecorp.com",
1176
+ "department": "Sales",
1177
+ "level": "L2",
1178
+ "role": "Business Development Rep",
1179
+ "manager_id": "emp_0072",
1180
+ "status": "offboarded",
1181
+ "date_of_joining": "2020-04-15",
1182
+ "date_of_leaving": "2022-08-20",
1183
+ "is_contractor": false,
1184
+ "phone": "+1-212-992-6623",
1185
+ "location": "New York"
1186
+ },
1187
+ {
1188
+ "emp_id": "emp_0080",
1189
+ "name": "Yun Zhu",
1190
+ "email": "yun.zhu@acmecorp.com",
1191
+ "department": "Sales",
1192
+ "level": "L1",
1193
+ "role": "Sales Development Rep",
1194
+ "manager_id": "emp_0074",
1195
+ "status": "active",
1196
+ "date_of_joining": "2022-04-18",
1197
+ "date_of_leaving": null,
1198
+ "is_contractor": false,
1199
+ "phone": "+1-650-445-7734",
1200
+ "location": "San Francisco"
1201
+ },
1202
+ {
1203
+ "emp_id": "emp_0081",
1204
+ "name": "Sergio Laurent",
1205
+ "email": "sergio.laurent@acmecorp.com",
1206
+ "department": "Sales",
1207
+ "level": "L1",
1208
+ "role": "Account Executive I",
1209
+ "manager_id": "emp_0075",
1210
+ "status": "active",
1211
+ "date_of_joining": "2022-07-25",
1212
+ "date_of_leaving": null,
1213
+ "is_contractor": false,
1214
+ "phone": "+1-415-556-8845",
1215
+ "location": "London"
1216
+ },
1217
+ {
1218
+ "emp_id": "emp_0082",
1219
+ "name": "Ishaan Patel",
1220
+ "email": "ishaan.patel@acmecorp.com",
1221
+ "department": "Sales",
1222
+ "level": "L1",
1223
+ "role": "Inside Sales Rep",
1224
+ "manager_id": "emp_0076",
1225
+ "status": "active",
1226
+ "date_of_joining": "2022-11-07",
1227
+ "date_of_leaving": null,
1228
+ "is_contractor": false,
1229
+ "phone": "+1-408-113-9956",
1230
+ "location": "Bangalore"
1231
+ },
1232
+ {
1233
+ "emp_id": "emp_0083",
1234
+ "name": "Mark Taylor",
1235
+ "email": "mark.taylor@acmecorp.com",
1236
+ "department": "Sales",
1237
+ "level": "L1",
1238
+ "role": "Sales Development Rep",
1239
+ "manager_id": "emp_0077",
1240
+ "status": "active",
1241
+ "date_of_joining": "2023-02-13",
1242
+ "date_of_leaving": null,
1243
+ "is_contractor": false,
1244
+ "phone": "+1-212-667-1167",
1245
+ "location": "New York"
1246
+ },
1247
+ {
1248
+ "emp_id": "emp_0084",
1249
+ "name": "Ling Wu",
1250
+ "email": "ling.wu@acmecorp.com",
1251
+ "department": "Sales",
1252
+ "level": "L1",
1253
+ "role": "Account Executive I",
1254
+ "manager_id": "emp_0074",
1255
+ "status": "active",
1256
+ "date_of_joining": "2023-05-29",
1257
+ "date_of_leaving": null,
1258
+ "is_contractor": false,
1259
+ "phone": "+1-650-221-2278",
1260
+ "location": "San Francisco"
1261
+ },
1262
+ {
1263
+ "emp_id": "emp_0085",
1264
+ "name": "Eva Richter",
1265
+ "email": "eva.richter@acmecorp.com",
1266
+ "department": "Sales",
1267
+ "level": "L1",
1268
+ "role": "Inside Sales Rep",
1269
+ "manager_id": "emp_0075",
1270
+ "status": "active",
1271
+ "date_of_joining": "2023-09-11",
1272
+ "date_of_leaving": null,
1273
+ "is_contractor": false,
1274
+ "phone": "+1-415-334-3389",
1275
+ "location": "London"
1276
+ },
1277
+ {
1278
+ "emp_id": "emp_0086",
1279
+ "name": "Sai Sharma",
1280
+ "email": "sai.sharma@acmecorp.com",
1281
+ "department": "Sales",
1282
+ "level": "L1",
1283
+ "role": "Sales Development Rep",
1284
+ "manager_id": "emp_0076",
1285
+ "status": "pending",
1286
+ "date_of_joining": "2025-10-01",
1287
+ "date_of_leaving": null,
1288
+ "is_contractor": false,
1289
+ "phone": "+1-408-445-4490",
1290
+ "location": "Bangalore"
1291
+ },
1292
+ {
1293
+ "emp_id": "emp_0087",
1294
+ "name": "Timothy Garcia",
1295
+ "email": "timothy.garcia@acmecorp.com",
1296
+ "department": "Sales",
1297
+ "level": "L1",
1298
+ "role": "Account Executive I",
1299
+ "manager_id": "emp_0077",
1300
+ "status": "active",
1301
+ "date_of_joining": "2024-01-22",
1302
+ "date_of_leaving": null,
1303
+ "is_contractor": false,
1304
+ "phone": "+1-512-556-5501",
1305
+ "location": "Austin"
1306
+ },
1307
+ {
1308
+ "emp_id": "emp_0088",
1309
+ "name": "Xin Chen",
1310
+ "email": "xin.chen@acmecorp.com",
1311
+ "department": "Sales",
1312
+ "level": "L1",
1313
+ "role": "Inside Sales Rep",
1314
+ "manager_id": "emp_0074",
1315
+ "status": "active",
1316
+ "date_of_joining": "2024-05-06",
1317
+ "date_of_leaving": null,
1318
+ "is_contractor": false,
1319
+ "phone": "+1-650-778-6612",
1320
+ "location": "San Francisco"
1321
+ },
1322
+ {
1323
+ "emp_id": "emp_0089",
1324
+ "name": "Nikolai Petrov",
1325
+ "email": "nikolai.petrov@acmecorp.com",
1326
+ "department": "Finance",
1327
+ "level": "L6",
1328
+ "role": "VP of Finance",
1329
+ "manager_id": null,
1330
+ "status": "active",
1331
+ "date_of_joining": "2018-01-15",
1332
+ "date_of_leaving": null,
1333
+ "is_contractor": false,
1334
+ "phone": "+1-212-889-7723",
1335
+ "location": "New York"
1336
+ },
1337
+ {
1338
+ "emp_id": "emp_0090",
1339
+ "name": "Rahul Gupta",
1340
+ "email": "rahul.gupta@acmecorp.com",
1341
+ "department": "Finance",
1342
+ "level": "L5",
1343
+ "role": "Senior Director of Finance",
1344
+ "manager_id": "emp_0089",
1345
+ "status": "active",
1346
+ "date_of_joining": "2018-06-25",
1347
+ "date_of_leaving": null,
1348
+ "is_contractor": false,
1349
+ "phone": "+1-408-113-2234",
1350
+ "location": "Bangalore"
1351
+ },
1352
+ {
1353
+ "emp_id": "emp_0091",
1354
+ "name": "Susan Davis",
1355
+ "email": "susan.davis@acmecorp.com",
1356
+ "department": "Finance",
1357
+ "level": "L4",
1358
+ "role": "Finance Director",
1359
+ "manager_id": "emp_0090",
1360
+ "status": "active",
1361
+ "date_of_joining": "2019-02-04",
1362
+ "date_of_leaving": null,
1363
+ "is_contractor": false,
1364
+ "phone": "+1-212-221-3345",
1365
+ "location": "New York"
1366
+ },
1367
+ {
1368
+ "emp_id": "emp_0092",
1369
+ "name": "Hui Wang",
1370
+ "email": "hui.wang@acmecorp.com",
1371
+ "department": "Finance",
1372
+ "level": "L4",
1373
+ "role": "Head of FP&A",
1374
+ "manager_id": "emp_0090",
1375
+ "status": "active",
1376
+ "date_of_joining": "2019-07-15",
1377
+ "date_of_leaving": null,
1378
+ "is_contractor": false,
1379
+ "phone": "+1-650-334-8834",
1380
+ "location": "San Francisco"
1381
+ },
1382
+ {
1383
+ "emp_id": "emp_0093",
1384
+ "name": "Clara Weber",
1385
+ "email": "clara.weber@acmecorp.com",
1386
+ "department": "Finance",
1387
+ "level": "L3",
1388
+ "role": "Finance Manager",
1389
+ "manager_id": "emp_0091",
1390
+ "status": "active",
1391
+ "date_of_joining": "2020-01-06",
1392
+ "date_of_leaving": null,
1393
+ "is_contractor": false,
1394
+ "phone": "+1-415-445-9945",
1395
+ "location": "London"
1396
+ },
1397
+ {
1398
+ "emp_id": "emp_0094",
1399
+ "name": "Aarav Verma",
1400
+ "email": "aarav.verma@acmecorp.com",
1401
+ "department": "Finance",
1402
+ "level": "L3",
1403
+ "role": "Senior Financial Analyst",
1404
+ "manager_id": "emp_0092",
1405
+ "status": "active",
1406
+ "date_of_joining": "2020-05-18",
1407
+ "date_of_leaving": null,
1408
+ "is_contractor": false,
1409
+ "phone": "+1-408-556-1156",
1410
+ "location": "Bangalore"
1411
+ },
1412
+ {
1413
+ "emp_id": "emp_0095",
1414
+ "name": "Joseph Robinson",
1415
+ "email": "joseph.robinson@acmecorp.com",
1416
+ "department": "Finance",
1417
+ "level": "L3",
1418
+ "role": "Controller",
1419
+ "manager_id": "emp_0091",
1420
+ "status": "active",
1421
+ "date_of_joining": "2020-09-28",
1422
+ "date_of_leaving": null,
1423
+ "is_contractor": false,
1424
+ "phone": "+1-212-667-2267",
1425
+ "location": "New York"
1426
+ },
1427
+ {
1428
+ "emp_id": "emp_0096",
1429
+ "name": "Tao Li",
1430
+ "email": "tao.li@acmecorp.com",
1431
+ "department": "Finance",
1432
+ "level": "L2",
1433
+ "role": "Financial Analyst II",
1434
+ "manager_id": "emp_0093",
1435
+ "status": "active",
1436
+ "date_of_joining": "2021-01-25",
1437
+ "date_of_leaving": null,
1438
+ "is_contractor": false,
1439
+ "phone": "+1-650-778-3378",
1440
+ "location": "San Francisco"
1441
+ },
1442
+ {
1443
+ "emp_id": "emp_0097",
1444
+ "name": "Jean Moreau",
1445
+ "email": "jean.moreau@acmecorp.com",
1446
+ "department": "Finance",
1447
+ "level": "L2",
1448
+ "role": "Senior Accountant",
1449
+ "manager_id": "emp_0094",
1450
+ "status": "active",
1451
+ "date_of_joining": "2021-05-10",
1452
+ "date_of_leaving": null,
1453
+ "is_contractor": false,
1454
+ "phone": "+1-415-889-4489",
1455
+ "location": "London"
1456
+ },
1457
+ {
1458
+ "emp_id": "emp_0098",
1459
+ "name": "Vihaan Iyer",
1460
+ "email": "vihaan.iyer@acmecorp.com",
1461
+ "department": "Finance",
1462
+ "level": "L2",
1463
+ "role": "Tax Analyst",
1464
+ "manager_id": "emp_0095",
1465
+ "status": "active",
1466
+ "date_of_joining": "2021-09-13",
1467
+ "date_of_leaving": null,
1468
+ "is_contractor": false,
1469
+ "phone": "+1-408-992-5590",
1470
+ "location": "Bangalore"
1471
+ },
1472
+ {
1473
+ "emp_id": "emp_0099",
1474
+ "name": "Andrew White",
1475
+ "email": "andrew.white@acmecorp.com",
1476
+ "department": "Finance",
1477
+ "level": "L2",
1478
+ "role": "Financial Analyst II",
1479
+ "manager_id": "emp_0093",
1480
+ "status": "active",
1481
+ "date_of_joining": "2022-01-03",
1482
+ "date_of_leaving": null,
1483
+ "is_contractor": false,
1484
+ "phone": "+1-212-113-6601",
1485
+ "location": "New York"
1486
+ },
1487
+ {
1488
+ "emp_id": "emp_0100",
1489
+ "name": "Yun Liu",
1490
+ "email": "yun.liu@acmecorp.com",
1491
+ "department": "Finance",
1492
+ "level": "L1",
1493
+ "role": "Financial Analyst I",
1494
+ "manager_id": "emp_0096",
1495
+ "status": "active",
1496
+ "date_of_joining": "2022-04-11",
1497
+ "date_of_leaving": null,
1498
+ "is_contractor": false,
1499
+ "phone": "+1-650-221-7712",
1500
+ "location": "San Francisco"
1501
+ },
1502
+ {
1503
+ "emp_id": "emp_0101",
1504
+ "name": "Pablo Rossi",
1505
+ "email": "pablo.rossi@acmecorp.com",
1506
+ "department": "Finance",
1507
+ "level": "L1",
1508
+ "role": "Accounting Associate",
1509
+ "manager_id": "emp_0097",
1510
+ "status": "active",
1511
+ "date_of_joining": "2022-07-25",
1512
+ "date_of_leaving": null,
1513
+ "is_contractor": false,
1514
+ "phone": "+1-415-334-8823",
1515
+ "location": "London"
1516
+ },
1517
+ {
1518
+ "emp_id": "emp_0102",
1519
+ "name": "Shaurya Mehta",
1520
+ "email": "shaurya.mehta@acmecorp.com",
1521
+ "department": "Finance",
1522
+ "level": "L1",
1523
+ "role": "Bookkeeper",
1524
+ "manager_id": "emp_0098",
1525
+ "status": "active",
1526
+ "date_of_joining": "2022-11-14",
1527
+ "date_of_leaving": null,
1528
+ "is_contractor": false,
1529
+ "phone": "+1-408-445-9934",
1530
+ "location": "Bangalore"
1531
+ },
1532
+ {
1533
+ "emp_id": "emp_0103",
1534
+ "name": "Charles Martin",
1535
+ "email": "charles.martin@acmecorp.com",
1536
+ "department": "Finance",
1537
+ "level": "L1",
1538
+ "role": "Financial Analyst I",
1539
+ "manager_id": "emp_0099",
1540
+ "status": "active",
1541
+ "date_of_joining": "2023-03-06",
1542
+ "date_of_leaving": null,
1543
+ "is_contractor": false,
1544
+ "phone": "+1-212-556-1145",
1545
+ "location": "New York"
1546
+ },
1547
+ {
1548
+ "emp_id": "emp_0104",
1549
+ "name": "Feng Yang",
1550
+ "email": "feng.yang@acmecorp.com",
1551
+ "department": "Finance",
1552
+ "level": "L1",
1553
+ "role": "Accounting Associate",
1554
+ "manager_id": "emp_0096",
1555
+ "status": "offboarded",
1556
+ "date_of_joining": "2021-03-15",
1557
+ "date_of_leaving": "2023-07-20",
1558
+ "is_contractor": false,
1559
+ "phone": "+1-650-667-2256",
1560
+ "location": "San Francisco"
1561
+ },
1562
+ {
1563
+ "emp_id": "emp_0105",
1564
+ "name": "Ivan Volkov",
1565
+ "email": "ivan.volkov@acmecorp.com",
1566
+ "department": "Finance",
1567
+ "level": "L1",
1568
+ "role": "Bookkeeper",
1569
+ "manager_id": "emp_0097",
1570
+ "status": "active",
1571
+ "date_of_joining": "2023-08-28",
1572
+ "date_of_leaving": null,
1573
+ "is_contractor": false,
1574
+ "phone": "+1-415-778-3334",
1575
+ "location": "London"
1576
+ },
1577
+ {
1578
+ "emp_id": "emp_0106",
1579
+ "name": "Reyansh Singh",
1580
+ "email": "reyansh.singh@acmecorp.com",
1581
+ "department": "Finance",
1582
+ "level": "L1",
1583
+ "role": "Financial Analyst I",
1584
+ "manager_id": "emp_0098",
1585
+ "status": "active",
1586
+ "date_of_joining": "2024-01-15",
1587
+ "date_of_leaving": null,
1588
+ "is_contractor": true,
1589
+ "phone": "+1-408-889-4445",
1590
+ "location": "Bangalore"
1591
+ },
1592
+ {
1593
+ "emp_id": "emp_0107",
1594
+ "name": "William Thompson",
1595
+ "email": "william.thompson@acmecorp.com",
1596
+ "department": "HR",
1597
+ "level": "L6",
1598
+ "role": "VP of HR",
1599
+ "manager_id": null,
1600
+ "status": "active",
1601
+ "date_of_joining": "2018-03-05",
1602
+ "date_of_leaving": null,
1603
+ "is_contractor": false,
1604
+ "phone": "+1-415-113-5556",
1605
+ "location": "San Francisco"
1606
+ },
1607
+ {
1608
+ "emp_id": "emp_0108",
1609
+ "name": "Diya Nair",
1610
+ "email": "diya.nair@acmecorp.com",
1611
+ "department": "HR",
1612
+ "level": "L5",
1613
+ "role": "Senior Director of HR",
1614
+ "manager_id": "emp_0107",
1615
+ "status": "active",
1616
+ "date_of_joining": "2018-08-13",
1617
+ "date_of_leaving": null,
1618
+ "is_contractor": false,
1619
+ "phone": "+1-408-221-6667",
1620
+ "location": "Bangalore"
1621
+ },
1622
+ {
1623
+ "emp_id": "emp_0109",
1624
+ "name": "Lei Zhou",
1625
+ "email": "lei.zhou@acmecorp.com",
1626
+ "department": "HR",
1627
+ "level": "L4",
1628
+ "role": "HR Director",
1629
+ "manager_id": "emp_0108",
1630
+ "status": "active",
1631
+ "date_of_joining": "2019-03-11",
1632
+ "date_of_leaving": null,
1633
+ "is_contractor": false,
1634
+ "phone": "+1-650-334-7778",
1635
+ "location": "San Francisco"
1636
+ },
1637
+ {
1638
+ "emp_id": "emp_0110",
1639
+ "name": "Marta Wagner",
1640
+ "email": "marta.wagner@acmecorp.com",
1641
+ "department": "HR",
1642
+ "level": "L4",
1643
+ "role": "Head of Talent Acquisition",
1644
+ "manager_id": "emp_0108",
1645
+ "status": "active",
1646
+ "date_of_joining": "2019-07-22",
1647
+ "date_of_leaving": null,
1648
+ "is_contractor": false,
1649
+ "phone": "+1-212-445-8889",
1650
+ "location": "London"
1651
+ },
1652
+ {
1653
+ "emp_id": "emp_0111",
1654
+ "name": "Amit Desai",
1655
+ "email": "amit.desai@acmecorp.com",
1656
+ "department": "HR",
1657
+ "level": "L3",
1658
+ "role": "Senior HR Manager",
1659
+ "manager_id": "emp_0109",
1660
+ "status": "active",
1661
+ "date_of_joining": "2020-02-03",
1662
+ "date_of_leaving": null,
1663
+ "is_contractor": false,
1664
+ "phone": "+1-408-556-9990",
1665
+ "location": "Bangalore"
1666
+ },
1667
+ {
1668
+ "emp_id": "emp_0112",
1669
+ "name": "Jason Anderson",
1670
+ "email": "jason.anderson@acmecorp.com",
1671
+ "department": "HR",
1672
+ "level": "L3",
1673
+ "role": "HR Business Partner",
1674
+ "manager_id": "emp_0110",
1675
+ "status": "active",
1676
+ "date_of_joining": "2020-06-15",
1677
+ "date_of_leaving": null,
1678
+ "is_contractor": false,
1679
+ "phone": "+1-512-667-1101",
1680
+ "location": "Austin"
1681
+ },
1682
+ {
1683
+ "emp_id": "emp_0113",
1684
+ "name": "Min Hu",
1685
+ "email": "min.hu@acmecorp.com",
1686
+ "department": "HR",
1687
+ "level": "L3",
1688
+ "role": "Senior Recruiter",
1689
+ "manager_id": "emp_0109",
1690
+ "status": "active",
1691
+ "date_of_joining": "2020-10-26",
1692
+ "date_of_leaving": null,
1693
+ "is_contractor": false,
1694
+ "phone": "+1-650-778-2212",
1695
+ "location": "San Francisco"
1696
+ },
1697
+ {
1698
+ "emp_id": "emp_0114",
1699
+ "name": "Friedrich Schulz",
1700
+ "email": "friedrich.schulz@acmecorp.com",
1701
+ "department": "HR",
1702
+ "level": "L2",
1703
+ "role": "HR Specialist",
1704
+ "manager_id": "emp_0111",
1705
+ "status": "active",
1706
+ "date_of_joining": "2021-03-08",
1707
+ "date_of_leaving": null,
1708
+ "is_contractor": false,
1709
+ "phone": "+1-415-889-3323",
1710
+ "location": "London"
1711
+ },
1712
+ {
1713
+ "emp_id": "emp_0115",
1714
+ "name": "Kavya Thakur",
1715
+ "email": "kavya.thakur@acmecorp.com",
1716
+ "department": "HR",
1717
+ "level": "L2",
1718
+ "role": "Recruiter",
1719
+ "manager_id": "emp_0112",
1720
+ "status": "active",
1721
+ "date_of_joining": "2021-06-21",
1722
+ "date_of_leaving": null,
1723
+ "is_contractor": false,
1724
+ "phone": "+1-408-992-4434",
1725
+ "location": "Bangalore"
1726
+ },
1727
+ {
1728
+ "emp_id": "emp_0116",
1729
+ "name": "John Brown",
1730
+ "email": "john.brown@acmecorp.com",
1731
+ "department": "HR",
1732
+ "level": "L2",
1733
+ "role": "Compensation Analyst",
1734
+ "manager_id": "emp_0113",
1735
+ "status": "active",
1736
+ "date_of_joining": "2021-10-04",
1737
+ "date_of_leaving": null,
1738
+ "is_contractor": false,
1739
+ "phone": "+1-212-113-5545",
1740
+ "location": "New York"
1741
+ },
1742
+ {
1743
+ "emp_id": "emp_0117",
1744
+ "name": "Wei Xu",
1745
+ "email": "wei.xu@acmecorp.com",
1746
+ "department": "HR",
1747
+ "level": "L2",
1748
+ "role": "HR Specialist",
1749
+ "manager_id": "emp_0111",
1750
+ "status": "pending",
1751
+ "date_of_joining": "2025-09-01",
1752
+ "date_of_leaving": null,
1753
+ "is_contractor": false,
1754
+ "phone": "+1-650-445-6656",
1755
+ "location": "San Francisco"
1756
+ },
1757
+ {
1758
+ "emp_id": "emp_0118",
1759
+ "name": "Carlos Schmidt",
1760
+ "email": "carlos.schmidt@acmecorp.com",
1761
+ "department": "HR",
1762
+ "level": "L1",
1763
+ "role": "HR Coordinator",
1764
+ "manager_id": "emp_0114",
1765
+ "status": "active",
1766
+ "date_of_joining": "2022-02-07",
1767
+ "date_of_leaving": null,
1768
+ "is_contractor": false,
1769
+ "phone": "+1-415-556-7767",
1770
+ "location": "London"
1771
+ },
1772
+ {
1773
+ "emp_id": "emp_0119",
1774
+ "name": "Krishna Mishra",
1775
+ "email": "krishna.mishra@acmecorp.com",
1776
+ "department": "HR",
1777
+ "level": "L1",
1778
+ "role": "Recruiting Coordinator",
1779
+ "manager_id": "emp_0115",
1780
+ "status": "active",
1781
+ "date_of_joining": "2022-05-23",
1782
+ "date_of_leaving": null,
1783
+ "is_contractor": false,
1784
+ "phone": "+1-408-667-8878",
1785
+ "location": "Bangalore"
1786
+ },
1787
+ {
1788
+ "emp_id": "emp_0120",
1789
+ "name": "Sarah Martinez",
1790
+ "email": "sarah.martinez@acmecorp.com",
1791
+ "department": "HR",
1792
+ "level": "L1",
1793
+ "role": "HR Associate",
1794
+ "manager_id": "emp_0116",
1795
+ "status": "active",
1796
+ "date_of_joining": "2022-09-05",
1797
+ "date_of_leaving": null,
1798
+ "is_contractor": false,
1799
+ "phone": "+1-212-778-9989",
1800
+ "location": "New York"
1801
+ },
1802
+ {
1803
+ "emp_id": "emp_0121",
1804
+ "name": "Hao Sun",
1805
+ "email": "hao.sun@acmecorp.com",
1806
+ "department": "HR",
1807
+ "level": "L1",
1808
+ "role": "HR Coordinator",
1809
+ "manager_id": "emp_0114",
1810
+ "status": "active",
1811
+ "date_of_joining": "2023-01-16",
1812
+ "date_of_leaving": null,
1813
+ "is_contractor": false,
1814
+ "phone": "+1-650-889-1190",
1815
+ "location": "San Francisco"
1816
+ },
1817
+ {
1818
+ "emp_id": "emp_0122",
1819
+ "name": "Alexei Koch",
1820
+ "email": "alexei.koch@acmecorp.com",
1821
+ "department": "HR",
1822
+ "level": "L1",
1823
+ "role": "Recruiting Coordinator",
1824
+ "manager_id": "emp_0115",
1825
+ "status": "offboarded",
1826
+ "date_of_joining": "2021-02-10",
1827
+ "date_of_leaving": "2023-04-15",
1828
+ "is_contractor": false,
1829
+ "phone": "+1-415-334-2201",
1830
+ "location": "London"
1831
+ },
1832
+ {
1833
+ "emp_id": "emp_0123",
1834
+ "name": "Tanvi Sharma",
1835
+ "email": "tanvi.sharma@acmecorp.com",
1836
+ "department": "HR",
1837
+ "level": "L1",
1838
+ "role": "HR Associate",
1839
+ "manager_id": "emp_0116",
1840
+ "status": "active",
1841
+ "date_of_joining": "2023-06-26",
1842
+ "date_of_leaving": null,
1843
+ "is_contractor": false,
1844
+ "phone": "+1-408-445-3312",
1845
+ "location": "Bangalore"
1846
+ },
1847
+ {
1848
+ "emp_id": "emp_0124",
1849
+ "name": "Andrew Johnson",
1850
+ "email": "andrew.johnson@acmecorp.com",
1851
+ "department": "HR",
1852
+ "level": "L1",
1853
+ "role": "HR Coordinator",
1854
+ "manager_id": "emp_0114",
1855
+ "status": "active",
1856
+ "date_of_joining": "2024-02-05",
1857
+ "date_of_leaving": null,
1858
+ "is_contractor": true,
1859
+ "phone": "+1-512-556-4423",
1860
+ "location": "Austin"
1861
+ },
1862
+ {
1863
+ "emp_id": "emp_0125",
1864
+ "name": "Jing Ma",
1865
+ "email": "jing.ma@acmecorp.com",
1866
+ "department": "Data Science",
1867
+ "level": "L5",
1868
+ "role": "Director of Data Science",
1869
+ "manager_id": null,
1870
+ "status": "active",
1871
+ "date_of_joining": "2018-09-03",
1872
+ "date_of_leaving": null,
1873
+ "is_contractor": false,
1874
+ "phone": "+1-650-113-4434",
1875
+ "location": "San Francisco"
1876
+ },
1877
+ {
1878
+ "emp_id": "emp_0126",
1879
+ "name": "Vikram Singh",
1880
+ "email": "vikram.singh@acmecorp.com",
1881
+ "department": "Data Science",
1882
+ "level": "L5",
1883
+ "role": "Director of Data Science",
1884
+ "manager_id": null,
1885
+ "status": "active",
1886
+ "date_of_joining": "2019-01-21",
1887
+ "date_of_leaving": null,
1888
+ "is_contractor": false,
1889
+ "phone": "+1-408-221-5545",
1890
+ "location": "Bangalore"
1891
+ },
1892
+ {
1893
+ "emp_id": "emp_0127",
1894
+ "name": "Dmitri Larsson",
1895
+ "email": "dmitri.larsson@acmecorp.com",
1896
+ "department": "Data Science",
1897
+ "level": "L4",
1898
+ "role": "Staff Data Scientist",
1899
+ "manager_id": "emp_0125",
1900
+ "status": "active",
1901
+ "date_of_joining": "2019-06-10",
1902
+ "date_of_leaving": null,
1903
+ "is_contractor": false,
1904
+ "phone": "+1-415-334-6656",
1905
+ "location": "London"
1906
+ },
1907
+ {
1908
+ "emp_id": "emp_0128",
1909
+ "name": "Rohan Reddy",
1910
+ "email": "rohan.reddy@acmecorp.com",
1911
+ "department": "Data Science",
1912
+ "level": "L4",
1913
+ "role": "Principal ML Engineer",
1914
+ "manager_id": "emp_0126",
1915
+ "status": "active",
1916
+ "date_of_joining": "2019-10-28",
1917
+ "date_of_leaving": null,
1918
+ "is_contractor": false,
1919
+ "phone": "+1-408-445-7767",
1920
+ "location": "Bangalore"
1921
+ },
1922
+ {
1923
+ "emp_id": "emp_0129",
1924
+ "name": "Michael Jones",
1925
+ "email": "michael.jones@acmecorp.com",
1926
+ "department": "Data Science",
1927
+ "level": "L3",
1928
+ "role": "Senior Data Scientist",
1929
+ "manager_id": "emp_0127",
1930
+ "status": "active",
1931
+ "date_of_joining": "2020-03-09",
1932
+ "date_of_leaving": null,
1933
+ "is_contractor": false,
1934
+ "phone": "+1-212-556-8878",
1935
+ "location": "New York"
1936
+ },
1937
+ {
1938
+ "emp_id": "emp_0130",
1939
+ "name": "Rui Zhang",
1940
+ "email": "rui.zhang@acmecorp.com",
1941
+ "department": "Data Science",
1942
+ "level": "L3",
1943
+ "role": "Senior ML Engineer",
1944
+ "manager_id": "emp_0128",
1945
+ "status": "active",
1946
+ "date_of_joining": "2020-07-20",
1947
+ "date_of_leaving": null,
1948
+ "is_contractor": false,
1949
+ "phone": "+1-650-667-9989",
1950
+ "location": "San Francisco"
1951
+ },
1952
+ {
1953
+ "emp_id": "emp_0131",
1954
+ "name": "Stefan Hoffman",
1955
+ "email": "stefan.hoffman@acmecorp.com",
1956
+ "department": "Data Science",
1957
+ "level": "L3",
1958
+ "role": "Lead Analyst",
1959
+ "manager_id": "emp_0127",
1960
+ "status": "active",
1961
+ "date_of_joining": "2020-11-30",
1962
+ "date_of_leaving": null,
1963
+ "is_contractor": false,
1964
+ "phone": "+1-415-778-1190",
1965
+ "location": "London"
1966
+ },
1967
+ {
1968
+ "emp_id": "emp_0132",
1969
+ "name": "Neha Patel",
1970
+ "email": "neha.patel@acmecorp.com",
1971
+ "department": "Data Science",
1972
+ "level": "L3",
1973
+ "role": "Senior Data Scientist",
1974
+ "manager_id": "emp_0128",
1975
+ "status": "pending",
1976
+ "date_of_joining": "2025-11-15",
1977
+ "date_of_leaving": null,
1978
+ "is_contractor": false,
1979
+ "phone": "+1-408-889-2201",
1980
+ "location": "Bangalore"
1981
+ },
1982
+ {
1983
+ "emp_id": "emp_0133",
1984
+ "name": "Robert Garcia",
1985
+ "email": "robert.garcia@acmecorp.com",
1986
+ "department": "Data Science",
1987
+ "level": "L2",
1988
+ "role": "Data Scientist",
1989
+ "manager_id": "emp_0129",
1990
+ "status": "active",
1991
+ "date_of_joining": "2021-03-15",
1992
+ "date_of_leaving": null,
1993
+ "is_contractor": false,
1994
+ "phone": "+1-512-992-3312",
1995
+ "location": "Austin"
1996
+ },
1997
+ {
1998
+ "emp_id": "emp_0134",
1999
+ "name": "Ming Huang",
2000
+ "email": "ming.huang@acmecorp.com",
2001
+ "department": "Data Science",
2002
+ "level": "L2",
2003
+ "role": "ML Engineer II",
2004
+ "manager_id": "emp_0130",
2005
+ "status": "active",
2006
+ "date_of_joining": "2021-06-28",
2007
+ "date_of_leaving": null,
2008
+ "is_contractor": false,
2009
+ "phone": "+1-650-113-4423",
2010
+ "location": "San Francisco"
2011
+ },
2012
+ {
2013
+ "emp_id": "emp_0135",
2014
+ "name": "Ingrid Schmidt",
2015
+ "email": "ingrid.schmidt@acmecorp.com",
2016
+ "department": "Data Science",
2017
+ "level": "L2",
2018
+ "role": "Analytics Engineer",
2019
+ "manager_id": "emp_0131",
2020
+ "status": "active",
2021
+ "date_of_joining": "2021-10-11",
2022
+ "date_of_leaving": null,
2023
+ "is_contractor": false,
2024
+ "phone": "+1-415-221-5534",
2025
+ "location": "London"
2026
+ },
2027
+ {
2028
+ "emp_id": "emp_0136",
2029
+ "name": "Pranav Joshi",
2030
+ "email": "pranav.joshi@acmecorp.com",
2031
+ "department": "Data Science",
2032
+ "level": "L2",
2033
+ "role": "Data Scientist",
2034
+ "manager_id": "emp_0129",
2035
+ "status": "active",
2036
+ "date_of_joining": "2022-02-14",
2037
+ "date_of_leaving": null,
2038
+ "is_contractor": false,
2039
+ "phone": "+1-408-334-6645",
2040
+ "location": "Bangalore"
2041
+ },
2042
+ {
2043
+ "emp_id": "emp_0137",
2044
+ "name": "Karen Smith",
2045
+ "email": "karen.smith@acmecorp.com",
2046
+ "department": "Data Science",
2047
+ "level": "L2",
2048
+ "role": "ML Engineer II",
2049
+ "manager_id": "emp_0130",
2050
+ "status": "offboarded",
2051
+ "date_of_joining": "2020-09-01",
2052
+ "date_of_leaving": "2023-02-28",
2053
+ "is_contractor": false,
2054
+ "phone": "+1-212-445-7756",
2055
+ "location": "New York"
2056
+ },
2057
+ {
2058
+ "emp_id": "emp_0138",
2059
+ "name": "Qiang Guo",
2060
+ "email": "qiang.guo@acmecorp.com",
2061
+ "department": "Data Science",
2062
+ "level": "L2",
2063
+ "role": "Analytics Engineer",
2064
+ "manager_id": "emp_0131",
2065
+ "status": "active",
2066
+ "date_of_joining": "2022-06-20",
2067
+ "date_of_leaving": null,
2068
+ "is_contractor": true,
2069
+ "phone": "+1-650-556-8867",
2070
+ "location": "San Francisco"
2071
+ },
2072
+ {
2073
+ "emp_id": "emp_0139",
2074
+ "name": "Eva Fischer",
2075
+ "email": "eva.fischer@acmecorp.com",
2076
+ "department": "Data Science",
2077
+ "level": "L1",
2078
+ "role": "Data Analyst",
2079
+ "manager_id": "emp_0133",
2080
+ "status": "active",
2081
+ "date_of_joining": "2022-09-05",
2082
+ "date_of_leaving": null,
2083
+ "is_contractor": false,
2084
+ "phone": "+1-415-667-9978",
2085
+ "location": "London"
2086
+ },
2087
+ {
2088
+ "emp_id": "emp_0140",
2089
+ "name": "Aditya Chopra",
2090
+ "email": "aditya.chopra@acmecorp.com",
2091
+ "department": "Data Science",
2092
+ "level": "L1",
2093
+ "role": "Junior Data Scientist",
2094
+ "manager_id": "emp_0134",
2095
+ "status": "active",
2096
+ "date_of_joining": "2022-12-12",
2097
+ "date_of_leaving": null,
2098
+ "is_contractor": false,
2099
+ "phone": "+1-408-778-1189",
2100
+ "location": "Bangalore"
2101
+ },
2102
+ {
2103
+ "emp_id": "emp_0141",
2104
+ "name": "David Williams",
2105
+ "email": "david.williams@acmecorp.com",
2106
+ "department": "Data Science",
2107
+ "level": "L1",
2108
+ "role": "ML Engineer I",
2109
+ "manager_id": "emp_0135",
2110
+ "status": "active",
2111
+ "date_of_joining": "2023-03-27",
2112
+ "date_of_leaving": null,
2113
+ "is_contractor": false,
2114
+ "phone": "+1-512-889-2290",
2115
+ "location": "Austin"
2116
+ },
2117
+ {
2118
+ "emp_id": "emp_0142",
2119
+ "name": "Shan Lin",
2120
+ "email": "shan.lin@acmecorp.com",
2121
+ "department": "Data Science",
2122
+ "level": "L1",
2123
+ "role": "Data Analyst",
2124
+ "manager_id": "emp_0136",
2125
+ "status": "active",
2126
+ "date_of_joining": "2023-07-10",
2127
+ "date_of_leaving": null,
2128
+ "is_contractor": false,
2129
+ "phone": "+1-650-992-3301",
2130
+ "location": "San Francisco"
2131
+ },
2132
+ {
2133
+ "emp_id": "emp_0143",
2134
+ "name": "Marie Fischer",
2135
+ "email": "marie.fischer@acmecorp.com",
2136
+ "department": "Data Science",
2137
+ "level": "L1",
2138
+ "role": "Junior Data Scientist",
2139
+ "manager_id": "emp_0133",
2140
+ "status": "active",
2141
+ "date_of_joining": "2023-10-23",
2142
+ "date_of_leaving": null,
2143
+ "is_contractor": false,
2144
+ "phone": "+1-415-113-4412",
2145
+ "location": "London"
2146
+ },
2147
+ {
2148
+ "emp_id": "emp_0144",
2149
+ "name": "Vihaan Rao",
2150
+ "email": "vihaan.rao@acmecorp.com",
2151
+ "department": "Data Science",
2152
+ "level": "L1",
2153
+ "role": "ML Engineer I",
2154
+ "manager_id": "emp_0134",
2155
+ "status": "pending",
2156
+ "date_of_joining": "2025-10-15",
2157
+ "date_of_leaving": null,
2158
+ "is_contractor": false,
2159
+ "phone": "+1-408-221-5523",
2160
+ "location": "Bangalore"
2161
+ },
2162
+ {
2163
+ "emp_id": "emp_0145",
2164
+ "name": "Timothy Johnson",
2165
+ "email": "timothy.johnson@acmecorp.com",
2166
+ "department": "Data Science",
2167
+ "level": "L1",
2168
+ "role": "Data Analyst",
2169
+ "manager_id": "emp_0135",
2170
+ "status": "active",
2171
+ "date_of_joining": "2024-02-19",
2172
+ "date_of_leaving": null,
2173
+ "is_contractor": false,
2174
+ "phone": "+1-212-334-6634",
2175
+ "location": "New York"
2176
+ },
2177
+ {
2178
+ "emp_id": "emp_0146",
2179
+ "name": "Peng Zheng",
2180
+ "email": "peng.zheng@acmecorp.com",
2181
+ "department": "Data Science",
2182
+ "level": "L1",
2183
+ "role": "Junior Data Scientist",
2184
+ "manager_id": "emp_0136",
2185
+ "status": "active",
2186
+ "date_of_joining": "2024-06-03",
2187
+ "date_of_leaving": null,
2188
+ "is_contractor": false,
2189
+ "phone": "+1-650-445-7745",
2190
+ "location": "San Francisco"
2191
+ },
2192
+ {
2193
+ "emp_id": "emp_0147",
2194
+ "name": "Henrik Richter",
2195
+ "email": "henrik.richter@acmecorp.com",
2196
+ "department": "Data Science",
2197
+ "level": "L1",
2198
+ "role": "ML Engineer I",
2199
+ "manager_id": "emp_0138",
2200
+ "status": "active",
2201
+ "date_of_joining": "2024-09-16",
2202
+ "date_of_leaving": null,
2203
+ "is_contractor": false,
2204
+ "phone": "+1-415-556-8856",
2205
+ "location": "London"
2206
+ },
2207
+ {
2208
+ "emp_id": "emp_0148",
2209
+ "name": "Shreya Gupta",
2210
+ "email": "shreya.gupta@acmecorp.com",
2211
+ "department": "Data Science",
2212
+ "level": "L1",
2213
+ "role": "Data Analyst",
2214
+ "manager_id": "emp_0133",
2215
+ "status": "active",
2216
+ "date_of_joining": "2025-01-06",
2217
+ "date_of_leaving": null,
2218
+ "is_contractor": false,
2219
+ "phone": "+1-408-667-9967",
2220
+ "location": "Bangalore"
2221
+ },
2222
+ {
2223
+ "emp_id": "emp_0149",
2224
+ "name": "Sven Becker",
2225
+ "email": "sven.becker@acmecorp.com",
2226
+ "department": "Security",
2227
+ "level": "L5",
2228
+ "role": "Senior Director of Security",
2229
+ "manager_id": null,
2230
+ "status": "active",
2231
+ "date_of_joining": "2018-05-21",
2232
+ "date_of_leaving": null,
2233
+ "is_contractor": false,
2234
+ "phone": "+1-415-334-1178",
2235
+ "location": "San Francisco"
2236
+ },
2237
+ {
2238
+ "emp_id": "emp_0150",
2239
+ "name": "Deepak Menon",
2240
+ "email": "deepak.menon@acmecorp.com",
2241
+ "department": "Security",
2242
+ "level": "L5",
2243
+ "role": "Senior Director of Security",
2244
+ "manager_id": null,
2245
+ "status": "active",
2246
+ "date_of_joining": "2018-11-12",
2247
+ "date_of_leaving": null,
2248
+ "is_contractor": false,
2249
+ "phone": "+1-408-445-2289",
2250
+ "location": "Bangalore"
2251
+ },
2252
+ {
2253
+ "emp_id": "emp_0151",
2254
+ "name": "Elizabeth Garcia",
2255
+ "email": "elizabeth.garcia@acmecorp.com",
2256
+ "department": "Security",
2257
+ "level": "L4",
2258
+ "role": "Security Director",
2259
+ "manager_id": "emp_0149",
2260
+ "status": "active",
2261
+ "date_of_joining": "2019-04-15",
2262
+ "date_of_leaving": null,
2263
+ "is_contractor": false,
2264
+ "phone": "+1-212-556-3390",
2265
+ "location": "New York"
2266
+ },
2267
+ {
2268
+ "emp_id": "emp_0152",
2269
+ "name": "Jun Xu",
2270
+ "email": "jun.xu@acmecorp.com",
2271
+ "department": "Security",
2272
+ "level": "L4",
2273
+ "role": "Head of Infrastructure Security",
2274
+ "manager_id": "emp_0150",
2275
+ "status": "active",
2276
+ "date_of_joining": "2019-09-02",
2277
+ "date_of_leaving": null,
2278
+ "is_contractor": false,
2279
+ "phone": "+1-650-667-4401",
2280
+ "location": "San Francisco"
2281
+ },
2282
+ {
2283
+ "emp_id": "emp_0153",
2284
+ "name": "Pierre Laurent",
2285
+ "email": "pierre.laurent@acmecorp.com",
2286
+ "department": "Security",
2287
+ "level": "L3",
2288
+ "role": "Senior Security Engineer",
2289
+ "manager_id": "emp_0151",
2290
+ "status": "active",
2291
+ "date_of_joining": "2020-01-20",
2292
+ "date_of_leaving": null,
2293
+ "is_contractor": false,
2294
+ "phone": "+1-415-778-5512",
2295
+ "location": "London"
2296
+ },
2297
+ {
2298
+ "emp_id": "emp_0154",
2299
+ "name": "Arjun Pillai",
2300
+ "email": "arjun.pillai@acmecorp.com",
2301
+ "department": "Security",
2302
+ "level": "L3",
2303
+ "role": "Security Architect",
2304
+ "manager_id": "emp_0152",
2305
+ "status": "active",
2306
+ "date_of_joining": "2020-06-08",
2307
+ "date_of_leaving": null,
2308
+ "is_contractor": false,
2309
+ "phone": "+1-408-889-6623",
2310
+ "location": "Bangalore"
2311
+ },
2312
+ {
2313
+ "emp_id": "emp_0155",
2314
+ "name": "Jennifer Williams",
2315
+ "email": "jennifer.williams@acmecorp.com",
2316
+ "department": "Security",
2317
+ "level": "L3",
2318
+ "role": "AppSec Lead",
2319
+ "manager_id": "emp_0151",
2320
+ "status": "active",
2321
+ "date_of_joining": "2020-10-19",
2322
+ "date_of_leaving": null,
2323
+ "is_contractor": false,
2324
+ "phone": "+1-212-992-7734",
2325
+ "location": "New York"
2326
+ },
2327
+ {
2328
+ "emp_id": "emp_0156",
2329
+ "name": "Chen Han",
2330
+ "email": "chen.han@acmecorp.com",
2331
+ "department": "Security",
2332
+ "level": "L2",
2333
+ "role": "Security Engineer II",
2334
+ "manager_id": "emp_0153",
2335
+ "status": "active",
2336
+ "date_of_joining": "2021-02-15",
2337
+ "date_of_leaving": null,
2338
+ "is_contractor": false,
2339
+ "phone": "+1-650-113-8845",
2340
+ "location": "San Francisco"
2341
+ },
2342
+ {
2343
+ "emp_id": "emp_0157",
2344
+ "name": "Olaf Berg",
2345
+ "email": "olaf.berg@acmecorp.com",
2346
+ "department": "Security",
2347
+ "level": "L2",
2348
+ "role": "Penetration Tester",
2349
+ "manager_id": "emp_0154",
2350
+ "status": "active",
2351
+ "date_of_joining": "2021-05-31",
2352
+ "date_of_leaving": null,
2353
+ "is_contractor": false,
2354
+ "phone": "+1-415-221-9956",
2355
+ "location": "London"
2356
+ },
2357
+ {
2358
+ "emp_id": "emp_0158",
2359
+ "name": "Meera Chauhan",
2360
+ "email": "meera.chauhan@acmecorp.com",
2361
+ "department": "Security",
2362
+ "level": "L2",
2363
+ "role": "Compliance Analyst",
2364
+ "manager_id": "emp_0155",
2365
+ "status": "active",
2366
+ "date_of_joining": "2021-09-13",
2367
+ "date_of_leaving": null,
2368
+ "is_contractor": false,
2369
+ "phone": "+1-408-334-1167",
2370
+ "location": "Bangalore"
2371
+ },
2372
+ {
2373
+ "emp_id": "emp_0159",
2374
+ "name": "Kevin Davis",
2375
+ "email": "kevin.davis@acmecorp.com",
2376
+ "department": "Security",
2377
+ "level": "L2",
2378
+ "role": "Security Engineer II",
2379
+ "manager_id": "emp_0153",
2380
+ "status": "active",
2381
+ "date_of_joining": "2022-01-24",
2382
+ "date_of_leaving": null,
2383
+ "is_contractor": false,
2384
+ "phone": "+1-512-445-2278",
2385
+ "location": "Austin"
2386
+ },
2387
+ {
2388
+ "emp_id": "emp_0160",
2389
+ "name": "Yan Chen",
2390
+ "email": "yan.chen@acmecorp.com",
2391
+ "department": "Security",
2392
+ "level": "L2",
2393
+ "role": "Penetration Tester",
2394
+ "manager_id": "emp_0154",
2395
+ "status": "offboarded",
2396
+ "date_of_joining": "2020-07-01",
2397
+ "date_of_leaving": "2022-12-15",
2398
+ "is_contractor": false,
2399
+ "phone": "+1-650-556-3389",
2400
+ "location": "San Francisco"
2401
+ },
2402
+ {
2403
+ "emp_id": "emp_0161",
2404
+ "name": "Marco Dubois",
2405
+ "email": "marco.dubois@acmecorp.com",
2406
+ "department": "Security",
2407
+ "level": "L2",
2408
+ "role": "Compliance Analyst",
2409
+ "manager_id": "emp_0155",
2410
+ "status": "active",
2411
+ "date_of_joining": "2022-05-09",
2412
+ "date_of_leaving": null,
2413
+ "is_contractor": false,
2414
+ "phone": "+1-415-667-4490",
2415
+ "location": "London"
2416
+ },
2417
+ {
2418
+ "emp_id": "emp_0162",
2419
+ "name": "Sai Iyer",
2420
+ "email": "sai.iyer@acmecorp.com",
2421
+ "department": "Security",
2422
+ "level": "L1",
2423
+ "role": "Security Analyst I",
2424
+ "manager_id": "emp_0156",
2425
+ "status": "active",
2426
+ "date_of_joining": "2022-08-22",
2427
+ "date_of_leaving": null,
2428
+ "is_contractor": false,
2429
+ "phone": "+1-408-778-5501",
2430
+ "location": "Bangalore"
2431
+ },
2432
+ {
2433
+ "emp_id": "emp_0163",
2434
+ "name": "Thomas Robinson",
2435
+ "email": "thomas.robinson@acmecorp.com",
2436
+ "department": "Security",
2437
+ "level": "L1",
2438
+ "role": "SOC Analyst",
2439
+ "manager_id": "emp_0157",
2440
+ "status": "active",
2441
+ "date_of_joining": "2022-12-05",
2442
+ "date_of_leaving": null,
2443
+ "is_contractor": false,
2444
+ "phone": "+1-212-889-6612",
2445
+ "location": "New York"
2446
+ },
2447
+ {
2448
+ "emp_id": "emp_0164",
2449
+ "name": "Zhi Wu",
2450
+ "email": "zhi.wu@acmecorp.com",
2451
+ "department": "Security",
2452
+ "level": "L1",
2453
+ "role": "Security Engineer I",
2454
+ "manager_id": "emp_0158",
2455
+ "status": "active",
2456
+ "date_of_joining": "2023-03-20",
2457
+ "date_of_leaving": null,
2458
+ "is_contractor": false,
2459
+ "phone": "+1-650-992-7723",
2460
+ "location": "San Francisco"
2461
+ },
2462
+ {
2463
+ "emp_id": "emp_0165",
2464
+ "name": "Sofia Wagner",
2465
+ "email": "sofia.wagner@acmecorp.com",
2466
+ "department": "Security",
2467
+ "level": "L1",
2468
+ "role": "Security Analyst I",
2469
+ "manager_id": "emp_0159",
2470
+ "status": "active",
2471
+ "date_of_joining": "2023-07-03",
2472
+ "date_of_leaving": null,
2473
+ "is_contractor": true,
2474
+ "phone": "+1-415-113-8834",
2475
+ "location": "London"
2476
+ },
2477
+ {
2478
+ "emp_id": "emp_0166",
2479
+ "name": "Rahul Thakur",
2480
+ "email": "rahul.thakur@acmecorp.com",
2481
+ "department": "Security",
2482
+ "level": "L1",
2483
+ "role": "SOC Analyst",
2484
+ "manager_id": "emp_0156",
2485
+ "status": "active",
2486
+ "date_of_joining": "2023-10-16",
2487
+ "date_of_leaving": null,
2488
+ "is_contractor": false,
2489
+ "phone": "+1-408-221-9945",
2490
+ "location": "Bangalore"
2491
+ },
2492
+ {
2493
+ "emp_id": "emp_0167",
2494
+ "name": "Matthew Miller",
2495
+ "email": "matthew.miller@acmecorp.com",
2496
+ "department": "Security",
2497
+ "level": "L1",
2498
+ "role": "Security Engineer I",
2499
+ "manager_id": "emp_0157",
2500
+ "status": "pending",
2501
+ "date_of_joining": "2025-08-15",
2502
+ "date_of_leaving": null,
2503
+ "is_contractor": false,
2504
+ "phone": "+1-512-334-1156",
2505
+ "location": "Austin"
2506
+ },
2507
+ {
2508
+ "emp_id": "emp_0168",
2509
+ "name": "Xin Liu",
2510
+ "email": "xin.liu@acmecorp.com",
2511
+ "department": "Security",
2512
+ "level": "L1",
2513
+ "role": "Security Analyst I",
2514
+ "manager_id": "emp_0158",
2515
+ "status": "active",
2516
+ "date_of_joining": "2024-02-26",
2517
+ "date_of_leaving": null,
2518
+ "is_contractor": false,
2519
+ "phone": "+1-650-445-2267",
2520
+ "location": "San Francisco"
2521
+ },
2522
+ {
2523
+ "emp_id": "emp_0169",
2524
+ "name": "Clara Petrov",
2525
+ "email": "clara.petrov@acmecorp.com",
2526
+ "department": "Security",
2527
+ "level": "L1",
2528
+ "role": "SOC Analyst",
2529
+ "manager_id": "emp_0159",
2530
+ "status": "active",
2531
+ "date_of_joining": "2024-06-10",
2532
+ "date_of_leaving": null,
2533
+ "is_contractor": false,
2534
+ "phone": "+1-415-556-3378",
2535
+ "location": "London"
2536
+ },
2537
+ {
2538
+ "emp_id": "emp_0170",
2539
+ "name": "Nikhil Kumar",
2540
+ "email": "nikhil.kumar@acmecorp.com",
2541
+ "department": "Security",
2542
+ "level": "L1",
2543
+ "role": "Security Engineer I",
2544
+ "manager_id": "emp_0161",
2545
+ "status": "active",
2546
+ "date_of_joining": "2024-09-23",
2547
+ "date_of_leaving": null,
2548
+ "is_contractor": false,
2549
+ "phone": "+1-408-667-4489",
2550
+ "location": "Bangalore"
2551
+ },
2552
+ {
2553
+ "emp_id": "emp_0171",
2554
+ "name": "Aarav Patel",
2555
+ "email": "aarav.patel@acmecorp.com",
2556
+ "department": "Engineering",
2557
+ "level": "L1",
2558
+ "role": "Junior Developer",
2559
+ "manager_id": "emp_0012",
2560
+ "status": "active",
2561
+ "date_of_joining": "2024-04-15",
2562
+ "date_of_leaving": null,
2563
+ "is_contractor": false,
2564
+ "phone": "+1-408-332-1178",
2565
+ "location": "Bangalore"
2566
+ },
2567
+ {
2568
+ "emp_id": "emp_0172",
2569
+ "name": "Patricia Brown",
2570
+ "email": "patricia.brown@acmecorp.com",
2571
+ "department": "Engineering",
2572
+ "level": "L2",
2573
+ "role": "Software Engineer II",
2574
+ "manager_id": "emp_0009",
2575
+ "status": "active",
2576
+ "date_of_joining": "2022-03-07",
2577
+ "date_of_leaving": null,
2578
+ "is_contractor": false,
2579
+ "phone": "+1-212-889-4456",
2580
+ "location": "New York"
2581
+ },
2582
+ {
2583
+ "emp_id": "emp_0173",
2584
+ "name": "Hui Lin",
2585
+ "email": "hui.lin@acmecorp.com",
2586
+ "department": "Engineering",
2587
+ "level": "L1",
2588
+ "role": "Associate Engineer",
2589
+ "manager_id": "emp_0013",
2590
+ "status": "active",
2591
+ "date_of_joining": "2024-07-22",
2592
+ "date_of_leaving": null,
2593
+ "is_contractor": false,
2594
+ "phone": "+1-650-334-2289",
2595
+ "location": "San Francisco"
2596
+ },
2597
+ {
2598
+ "emp_id": "emp_0174",
2599
+ "name": "Erik Schulz",
2600
+ "email": "erik.schulz@acmecorp.com",
2601
+ "department": "Product",
2602
+ "level": "L2",
2603
+ "role": "Product Manager",
2604
+ "manager_id": "emp_0035",
2605
+ "status": "active",
2606
+ "date_of_joining": "2022-09-12",
2607
+ "date_of_leaving": null,
2608
+ "is_contractor": false,
2609
+ "phone": "+1-415-445-3390",
2610
+ "location": "London"
2611
+ },
2612
+ {
2613
+ "emp_id": "emp_0175",
2614
+ "name": "Pooja Kumar",
2615
+ "email": "pooja.kumar@acmecorp.com",
2616
+ "department": "Product",
2617
+ "level": "L1",
2618
+ "role": "Product Analyst",
2619
+ "manager_id": "emp_0039",
2620
+ "status": "active",
2621
+ "date_of_joining": "2024-03-18",
2622
+ "date_of_leaving": null,
2623
+ "is_contractor": false,
2624
+ "phone": "+1-408-556-4401",
2625
+ "location": "Bangalore"
2626
+ },
2627
+ {
2628
+ "emp_id": "emp_0176",
2629
+ "name": "Brian Martinez",
2630
+ "email": "brian.martinez@acmecorp.com",
2631
+ "department": "Marketing",
2632
+ "level": "L2",
2633
+ "role": "Content Strategist",
2634
+ "manager_id": "emp_0053",
2635
+ "status": "active",
2636
+ "date_of_joining": "2022-11-28",
2637
+ "date_of_leaving": null,
2638
+ "is_contractor": false,
2639
+ "phone": "+1-212-667-5512",
2640
+ "location": "New York"
2641
+ },
2642
+ {
2643
+ "emp_id": "emp_0177",
2644
+ "name": "Mei Zhou",
2645
+ "email": "mei.zhou@acmecorp.com",
2646
+ "department": "Marketing",
2647
+ "level": "L1",
2648
+ "role": "Marketing Associate",
2649
+ "manager_id": "emp_0057",
2650
+ "status": "active",
2651
+ "date_of_joining": "2024-05-13",
2652
+ "date_of_leaving": null,
2653
+ "is_contractor": false,
2654
+ "phone": "+1-650-778-6623",
2655
+ "location": "San Francisco"
2656
+ },
2657
+ {
2658
+ "emp_id": "emp_0178",
2659
+ "name": "Katarina Moreau",
2660
+ "email": "katarina.moreau@acmecorp.com",
2661
+ "department": "Sales",
2662
+ "level": "L2",
2663
+ "role": "Account Executive II",
2664
+ "manager_id": "emp_0071",
2665
+ "status": "active",
2666
+ "date_of_joining": "2022-08-15",
2667
+ "date_of_leaving": null,
2668
+ "is_contractor": false,
2669
+ "phone": "+1-415-889-7734",
2670
+ "location": "London"
2671
+ },
2672
+ {
2673
+ "emp_id": "emp_0179",
2674
+ "name": "Vivaan Desai",
2675
+ "email": "vivaan.desai@acmecorp.com",
2676
+ "department": "Sales",
2677
+ "level": "L1",
2678
+ "role": "Sales Development Rep",
2679
+ "manager_id": "emp_0075",
2680
+ "status": "active",
2681
+ "date_of_joining": "2024-08-19",
2682
+ "date_of_leaving": null,
2683
+ "is_contractor": false,
2684
+ "phone": "+1-408-992-8845",
2685
+ "location": "Bangalore"
2686
+ },
2687
+ {
2688
+ "emp_id": "emp_0180",
2689
+ "name": "Joseph Thomas",
2690
+ "email": "joseph.thomas@acmecorp.com",
2691
+ "department": "Finance",
2692
+ "level": "L2",
2693
+ "role": "Senior Accountant",
2694
+ "manager_id": "emp_0094",
2695
+ "status": "active",
2696
+ "date_of_joining": "2022-07-11",
2697
+ "date_of_leaving": null,
2698
+ "is_contractor": false,
2699
+ "phone": "+1-212-113-9956",
2700
+ "location": "New York"
2701
+ },
2702
+ {
2703
+ "emp_id": "emp_0181",
2704
+ "name": "Feng Zhu",
2705
+ "email": "feng.zhu@acmecorp.com",
2706
+ "department": "Finance",
2707
+ "level": "L1",
2708
+ "role": "Accounting Associate",
2709
+ "manager_id": "emp_0099",
2710
+ "status": "active",
2711
+ "date_of_joining": "2024-04-29",
2712
+ "date_of_leaving": null,
2713
+ "is_contractor": false,
2714
+ "phone": "+1-650-221-1167",
2715
+ "location": "San Francisco"
2716
+ },
2717
+ {
2718
+ "emp_id": "emp_0182",
2719
+ "name": "Marta Richter",
2720
+ "email": "marta.richter@acmecorp.com",
2721
+ "department": "HR",
2722
+ "level": "L2",
2723
+ "role": "Recruiter",
2724
+ "manager_id": "emp_0113",
2725
+ "status": "active",
2726
+ "date_of_joining": "2022-10-17",
2727
+ "date_of_leaving": null,
2728
+ "is_contractor": false,
2729
+ "phone": "+1-415-334-2278",
2730
+ "location": "London"
2731
+ },
2732
+ {
2733
+ "emp_id": "emp_0183",
2734
+ "name": "Ishaan Singh",
2735
+ "email": "ishaan.singh@acmecorp.com",
2736
+ "department": "HR",
2737
+ "level": "L1",
2738
+ "role": "HR Associate",
2739
+ "manager_id": "emp_0115",
2740
+ "status": "active",
2741
+ "date_of_joining": "2024-06-24",
2742
+ "date_of_leaving": null,
2743
+ "is_contractor": false,
2744
+ "phone": "+1-408-445-3389",
2745
+ "location": "Bangalore"
2746
+ },
2747
+ {
2748
+ "emp_id": "emp_0184",
2749
+ "name": "Steven Harris",
2750
+ "email": "steven.harris@acmecorp.com",
2751
+ "department": "Engineering",
2752
+ "level": "L3",
2753
+ "role": "Senior Software Engineer",
2754
+ "manager_id": "emp_0006",
2755
+ "status": "active",
2756
+ "date_of_joining": "2021-04-19",
2757
+ "date_of_leaving": null,
2758
+ "is_contractor": false,
2759
+ "phone": "+1-512-556-4490",
2760
+ "location": "Austin"
2761
+ },
2762
+ {
2763
+ "emp_id": "emp_0185",
2764
+ "name": "Riya Chopra",
2765
+ "email": "riya.chopra@acmecorp.com",
2766
+ "department": "Engineering",
2767
+ "level": "L2",
2768
+ "role": "Engineer",
2769
+ "manager_id": "emp_0010",
2770
+ "status": "active",
2771
+ "date_of_joining": "2022-06-06",
2772
+ "date_of_leaving": null,
2773
+ "is_contractor": false,
2774
+ "phone": "+1-408-667-5501",
2775
+ "location": "Bangalore"
2776
+ },
2777
+ {
2778
+ "emp_id": "emp_0186",
2779
+ "name": "Daniel Jackson",
2780
+ "email": "daniel.jackson@acmecorp.com",
2781
+ "department": "Engineering",
2782
+ "level": "L1",
2783
+ "role": "Software Engineer I",
2784
+ "manager_id": "emp_0014",
2785
+ "status": "active",
2786
+ "date_of_joining": "2024-10-07",
2787
+ "date_of_leaving": null,
2788
+ "is_contractor": false,
2789
+ "phone": "+1-212-778-6612",
2790
+ "location": "New York"
2791
+ },
2792
+ {
2793
+ "emp_id": "emp_0187",
2794
+ "name": "Yun Hu",
2795
+ "email": "yun.hu@acmecorp.com",
2796
+ "department": "Product",
2797
+ "level": "L3",
2798
+ "role": "Lead Product Designer",
2799
+ "manager_id": "emp_0033",
2800
+ "status": "active",
2801
+ "date_of_joining": "2021-08-09",
2802
+ "date_of_leaving": null,
2803
+ "is_contractor": false,
2804
+ "phone": "+1-650-889-7723",
2805
+ "location": "San Francisco"
2806
+ },
2807
+ {
2808
+ "emp_id": "emp_0188",
2809
+ "name": "Hans Fischer",
2810
+ "email": "hans.fischer@acmecorp.com",
2811
+ "department": "Sales",
2812
+ "level": "L3",
2813
+ "role": "Senior Account Executive",
2814
+ "manager_id": "emp_0069",
2815
+ "status": "active",
2816
+ "date_of_joining": "2021-11-22",
2817
+ "date_of_leaving": null,
2818
+ "is_contractor": false,
2819
+ "phone": "+1-415-445-8834",
2820
+ "location": "London"
2821
+ },
2822
+ {
2823
+ "emp_id": "emp_0189",
2824
+ "name": "Ananya Kumar",
2825
+ "email": "ananya.kumar@acmecorp.com",
2826
+ "department": "Finance",
2827
+ "level": "L3",
2828
+ "role": "Finance Manager",
2829
+ "manager_id": "emp_0092",
2830
+ "status": "active",
2831
+ "date_of_joining": "2021-06-14",
2832
+ "date_of_leaving": null,
2833
+ "is_contractor": false,
2834
+ "phone": "+1-408-113-9945",
2835
+ "location": "Bangalore"
2836
+ },
2837
+ {
2838
+ "emp_id": "emp_0190",
2839
+ "name": "Richard Thompson",
2840
+ "email": "richard.thompson@acmecorp.com",
2841
+ "department": "Marketing",
2842
+ "level": "L3",
2843
+ "role": "Senior Marketing Manager",
2844
+ "manager_id": "emp_0051",
2845
+ "status": "active",
2846
+ "date_of_joining": "2021-09-27",
2847
+ "date_of_leaving": null,
2848
+ "is_contractor": false,
2849
+ "phone": "+1-212-221-1190",
2850
+ "location": "New York"
2851
+ },
2852
+ {
2853
+ "emp_id": "emp_0191",
2854
+ "name": "Ling Guo",
2855
+ "email": "ling.guo@acmecorp.com",
2856
+ "department": "Engineering",
2857
+ "level": "L4",
2858
+ "role": "Engineering Manager",
2859
+ "manager_id": "emp_0003",
2860
+ "status": "active",
2861
+ "date_of_joining": "2020-05-11",
2862
+ "date_of_leaving": null,
2863
+ "is_contractor": false,
2864
+ "phone": "+1-650-334-5501",
2865
+ "location": "San Francisco"
2866
+ },
2867
+ {
2868
+ "emp_id": "emp_0192",
2869
+ "name": "Alexei Johansson",
2870
+ "email": "alexei.johansson@acmecorp.com",
2871
+ "department": "Marketing",
2872
+ "level": "L4",
2873
+ "role": "Marketing Director",
2874
+ "manager_id": "emp_0049",
2875
+ "status": "active",
2876
+ "date_of_joining": "2020-08-17",
2877
+ "date_of_leaving": null,
2878
+ "is_contractor": false,
2879
+ "phone": "+1-415-667-6612",
2880
+ "location": "London"
2881
+ },
2882
+ {
2883
+ "emp_id": "emp_0193",
2884
+ "name": "Priya Mehta",
2885
+ "email": "priya.mehta@acmecorp.com",
2886
+ "department": "Engineering",
2887
+ "level": "L1",
2888
+ "role": "Software Engineer I",
2889
+ "manager_id": "emp_0015",
2890
+ "status": "pending",
2891
+ "date_of_joining": "2025-11-20",
2892
+ "date_of_leaving": null,
2893
+ "is_contractor": false,
2894
+ "phone": "+1-408-889-7712",
2895
+ "location": "Bangalore"
2896
+ },
2897
+ {
2898
+ "emp_id": "emp_0194",
2899
+ "name": "Jean Weber",
2900
+ "email": "jean.weber@acmecorp.com",
2901
+ "department": "Data Science",
2902
+ "level": "L3",
2903
+ "role": "Senior ML Engineer",
2904
+ "manager_id": "emp_0127",
2905
+ "status": "active",
2906
+ "date_of_joining": "2021-04-05",
2907
+ "date_of_leaving": null,
2908
+ "is_contractor": false,
2909
+ "phone": "+1-415-992-8823",
2910
+ "location": "London"
2911
+ },
2912
+ {
2913
+ "emp_id": "emp_0195",
2914
+ "name": "Suresh Reddy",
2915
+ "email": "suresh.reddy@acmecorp.com",
2916
+ "department": "Sales",
2917
+ "level": "L1",
2918
+ "role": "Inside Sales Rep",
2919
+ "manager_id": "emp_0078",
2920
+ "status": "offboarded",
2921
+ "date_of_joining": "2022-02-14",
2922
+ "date_of_leaving": "2024-06-30",
2923
+ "is_contractor": false,
2924
+ "phone": "+1-408-113-2234",
2925
+ "location": "Bangalore"
2926
+ },
2927
+ {
2928
+ "emp_id": "emp_0196",
2929
+ "name": "Linda Rodriguez",
2930
+ "email": "linda.rodriguez@acmecorp.com",
2931
+ "department": "Finance",
2932
+ "level": "L1",
2933
+ "role": "Bookkeeper",
2934
+ "manager_id": "emp_0099",
2935
+ "status": "pending",
2936
+ "date_of_joining": "2025-09-15",
2937
+ "date_of_leaving": null,
2938
+ "is_contractor": false,
2939
+ "phone": "+1-212-334-3345",
2940
+ "location": "New York"
2941
+ },
2942
+ {
2943
+ "emp_id": "emp_0197",
2944
+ "name": "Tao Sun",
2945
+ "email": "tao.sun@acmecorp.com",
2946
+ "department": "HR",
2947
+ "level": "L3",
2948
+ "role": "HR Business Partner",
2949
+ "manager_id": "emp_0110",
2950
+ "status": "active",
2951
+ "date_of_joining": "2021-07-19",
2952
+ "date_of_leaving": null,
2953
+ "is_contractor": false,
2954
+ "phone": "+1-650-556-4456",
2955
+ "location": "San Francisco"
2956
+ },
2957
+ {
2958
+ "emp_id": "emp_0198",
2959
+ "name": "Sergio Ferrari",
2960
+ "email": "sergio.ferrari@acmecorp.com",
2961
+ "department": "Security",
2962
+ "level": "L3",
2963
+ "role": "Security Architect",
2964
+ "manager_id": "emp_0152",
2965
+ "status": "active",
2966
+ "date_of_joining": "2021-10-04",
2967
+ "date_of_leaving": null,
2968
+ "is_contractor": false,
2969
+ "phone": "+1-415-221-5567",
2970
+ "location": "London"
2971
+ },
2972
+ {
2973
+ "emp_id": "emp_0199",
2974
+ "name": "Kavya Sharma",
2975
+ "email": "kavya.sharma@acmecorp.com",
2976
+ "department": "Data Science",
2977
+ "level": "L1",
2978
+ "role": "Data Analyst",
2979
+ "manager_id": "emp_0136",
2980
+ "status": "active",
2981
+ "date_of_joining": "2025-03-10",
2982
+ "date_of_leaving": null,
2983
+ "is_contractor": false,
2984
+ "phone": "+1-408-334-8856",
2985
+ "location": "Bangalore"
2986
+ },
2987
+ {
2988
+ "emp_id": "emp_0200",
2989
+ "name": "Pablo Schmidt",
2990
+ "email": "pablo.schmidt@acmecorp.com",
2991
+ "department": "Engineering",
2992
+ "level": "L1",
2993
+ "role": "Junior Developer",
2994
+ "manager_id": "emp_0016",
2995
+ "status": "active",
2996
+ "date_of_joining": "2025-05-19",
2997
+ "date_of_leaving": null,
2998
+ "is_contractor": false,
2999
+ "phone": "+1-650-889-9934",
3000
+ "location": "San Francisco"
3001
+ }
3002
+ ]
server/data/it_assets.json ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {"asset_id": "asset_001", "type": "laptop", "brand": "Apple", "model": "MacBook Pro 16 M3", "specs": "16-inch Retina, M3 Pro, 18GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_042", "purchase_date": "2024-03-15", "location": "San Francisco HQ"},
3
+ {"asset_id": "asset_002", "type": "laptop", "brand": "Apple", "model": "MacBook Pro 14 M3", "specs": "14-inch Retina, M3, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_017", "purchase_date": "2024-06-20", "location": "San Francisco HQ"},
4
+ {"asset_id": "asset_003", "type": "laptop", "brand": "Dell", "model": "XPS 15 9530", "specs": "15.6-inch OLED, i7-13700H, 32GB RAM, 1TB SSD", "status": "available", "assigned_to": null, "purchase_date": "2024-01-10", "location": "New York Office"},
5
+ {"asset_id": "asset_004", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad X1 Carbon Gen 11", "specs": "14-inch IPS, i7-1365U, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_003", "purchase_date": "2023-11-05", "location": "Austin Office"},
6
+ {"asset_id": "asset_005", "type": "laptop", "brand": "Apple", "model": "MacBook Air 15 M3", "specs": "15.3-inch Liquid Retina, M3, 16GB RAM, 256GB SSD", "status": "assigned", "assigned_to": "emp_088", "purchase_date": "2024-07-01", "location": "San Francisco HQ"},
7
+ {"asset_id": "asset_006", "type": "laptop", "brand": "Dell", "model": "Latitude 5540", "specs": "15.6-inch FHD, i5-1345U, 16GB RAM, 256GB SSD", "status": "available", "assigned_to": null, "purchase_date": "2024-02-18", "location": "Chicago Office"},
8
+ {"asset_id": "asset_007", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad T14s Gen 4", "specs": "14-inch WUXGA, Ryzen 7 PRO 7840U, 32GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_055", "purchase_date": "2023-09-22", "location": "Austin Office"},
9
+ {"asset_id": "asset_008", "type": "laptop", "brand": "Apple", "model": "MacBook Pro 16 M3 Max", "specs": "16-inch Liquid Retina XDR, M3 Max, 36GB RAM, 1TB SSD", "status": "assigned", "assigned_to": "emp_012", "purchase_date": "2024-04-10", "location": "San Francisco HQ"},
10
+ {"asset_id": "asset_009", "type": "laptop", "brand": "HP", "model": "EliteBook 840 G10", "specs": "14-inch FHD, i7-1365U, 16GB RAM, 512GB SSD", "status": "available", "assigned_to": null, "purchase_date": "2024-05-14", "location": "New York Office"},
11
+ {"asset_id": "asset_010", "type": "laptop", "brand": "Dell", "model": "XPS 13 Plus 9320", "specs": "13.4-inch OLED, i7-1360P, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_031", "purchase_date": "2023-12-01", "location": "Chicago Office"},
12
+ {"asset_id": "asset_011", "type": "laptop", "brand": "Apple", "model": "MacBook Pro 14 M3 Pro", "specs": "14-inch Liquid Retina XDR, M3 Pro, 18GB RAM, 1TB SSD", "status": "assigned", "assigned_to": "emp_067", "purchase_date": "2024-08-05", "location": "San Francisco HQ"},
13
+ {"asset_id": "asset_012", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad X1 Yoga Gen 8", "specs": "14-inch WUXGA Touch, i7-1365U, 16GB RAM, 512GB SSD", "status": "maintenance", "assigned_to": null, "purchase_date": "2023-06-15", "location": "Austin Office"},
14
+ {"asset_id": "asset_013", "type": "laptop", "brand": "Dell", "model": "Precision 5680", "specs": "16-inch FHD+, i9-13900H, 64GB RAM, 2TB SSD, RTX 3500", "status": "assigned", "assigned_to": "emp_008", "purchase_date": "2024-01-25", "location": "San Francisco HQ"},
15
+ {"asset_id": "asset_014", "type": "laptop", "brand": "HP", "model": "ZBook Studio G10", "specs": "16-inch DreamColor, i9-13900H, 64GB RAM, 2TB SSD, RTX 4080", "status": "assigned", "assigned_to": "emp_022", "purchase_date": "2023-10-30", "location": "New York Office"},
16
+ {"asset_id": "asset_015", "type": "laptop", "brand": "Apple", "model": "MacBook Air 13 M3", "specs": "13.6-inch Liquid Retina, M3, 8GB RAM, 256GB SSD", "status": "available", "assigned_to": null, "purchase_date": "2024-09-12", "location": "Chicago Office"},
17
+ {"asset_id": "asset_016", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad P16s Gen 2", "specs": "16-inch WQXGA, i7-1360P, 32GB RAM, 1TB SSD", "status": "assigned", "assigned_to": "emp_044", "purchase_date": "2024-03-08", "location": "Austin Office"},
18
+ {"asset_id": "asset_017", "type": "laptop", "brand": "Dell", "model": "Latitude 7440", "specs": "14-inch FHD+, i7-1365U, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_071", "purchase_date": "2023-08-19", "location": "San Francisco HQ"},
19
+ {"asset_id": "asset_018", "type": "laptop", "brand": "Apple", "model": "MacBook Pro 16 M3 Pro", "specs": "16-inch Liquid Retina XDR, M3 Pro, 36GB RAM, 512GB SSD", "status": "retired", "assigned_to": null, "purchase_date": "2022-04-20", "location": "San Francisco HQ"},
20
+ {"asset_id": "asset_019", "type": "laptop", "brand": "HP", "model": "EliteBook 1040 G10", "specs": "14-inch WUXGA, i7-1365U, 32GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_033", "purchase_date": "2024-02-28", "location": "New York Office"},
21
+ {"asset_id": "asset_020", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad X13 Gen 4", "specs": "13.3-inch WUXGA, i5-1345U, 16GB RAM, 256GB SSD", "status": "available", "assigned_to": null, "purchase_date": "2024-07-22", "location": "Chicago Office"},
22
+ {"asset_id": "asset_021", "type": "laptop", "brand": "Dell", "model": "XPS 17 9730", "specs": "17-inch UHD+, i7-13700H, 32GB RAM, 1TB SSD, RTX 4070", "status": "assigned", "assigned_to": "emp_005", "purchase_date": "2023-12-15", "location": "San Francisco HQ"},
23
+ {"asset_id": "asset_022", "type": "laptop", "brand": "Apple", "model": "MacBook Pro 14 M3", "specs": "14-inch Liquid Retina XDR, M3, 16GB RAM, 1TB SSD", "status": "assigned", "assigned_to": "emp_092", "purchase_date": "2024-05-30", "location": "Austin Office"},
24
+ {"asset_id": "asset_023", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad T16 Gen 2", "specs": "16-inch WUXGA, i7-1360P, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_019", "purchase_date": "2024-01-05", "location": "New York Office"},
25
+ {"asset_id": "asset_024", "type": "laptop", "brand": "HP", "model": "ProBook 450 G10", "specs": "15.6-inch FHD, i5-1335U, 16GB RAM, 256GB SSD", "status": "available", "assigned_to": null, "purchase_date": "2024-08-18", "location": "Chicago Office"},
26
+ {"asset_id": "asset_025", "type": "laptop", "brand": "Dell", "model": "Latitude 9440 2-in-1", "specs": "14-inch QHD+ Touch, i7-1365U, 32GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_061", "purchase_date": "2023-11-20", "location": "San Francisco HQ"},
27
+ {"asset_id": "asset_026", "type": "laptop", "brand": "Apple", "model": "MacBook Pro 16 M3", "specs": "16-inch Liquid Retina XDR, M3 Pro, 18GB RAM, 1TB SSD", "status": "assigned", "assigned_to": "emp_037", "purchase_date": "2024-04-25", "location": "Austin Office"},
28
+ {"asset_id": "asset_027", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad X1 Carbon Gen 11", "specs": "14-inch WQUXGA, i7-1365U, 32GB RAM, 1TB SSD", "status": "retired", "assigned_to": null, "purchase_date": "2021-09-10", "location": "New York Office"},
29
+ {"asset_id": "asset_028", "type": "laptop", "brand": "Dell", "model": "Precision 7680", "specs": "16-inch FHD+, i9-13950HX, 128GB RAM, 4TB SSD, RTX 5000", "status": "assigned", "assigned_to": "emp_002", "purchase_date": "2024-06-01", "location": "San Francisco HQ"},
30
+ {"asset_id": "asset_029", "type": "laptop", "brand": "HP", "model": "EliteBook 860 G10", "specs": "16-inch WUXGA, i7-1365U, 16GB RAM, 512GB SSD", "status": "available", "assigned_to": null, "purchase_date": "2024-09-05", "location": "Chicago Office"},
31
+ {"asset_id": "asset_030", "type": "laptop", "brand": "Apple", "model": "MacBook Air 15 M3", "specs": "15.3-inch Liquid Retina, M3, 24GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_078", "purchase_date": "2024-07-15", "location": "San Francisco HQ"},
32
+ {"asset_id": "asset_031", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad P14s Gen 4", "specs": "14-inch WUXGA, Ryzen 7 PRO 7840U, 32GB RAM, 1TB SSD", "status": "assigned", "assigned_to": "emp_049", "purchase_date": "2024-02-10", "location": "Austin Office"},
33
+ {"asset_id": "asset_032", "type": "laptop", "brand": "Dell", "model": "XPS 15 9530", "specs": "15.6-inch FHD+, i7-13700H, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_084", "purchase_date": "2023-10-08", "location": "New York Office"},
34
+ {"asset_id": "asset_033", "type": "laptop", "brand": "Apple", "model": "MacBook Pro 14 M3 Pro", "specs": "14-inch Liquid Retina XDR, M3 Pro, 36GB RAM, 1TB SSD", "status": "available", "assigned_to": null, "purchase_date": "2024-10-01", "location": "San Francisco HQ"},
35
+ {"asset_id": "asset_034", "type": "laptop", "brand": "HP", "model": "ZBook Firefly 14 G10", "specs": "14-inch FHD, i7-1365U, 32GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_025", "purchase_date": "2024-03-20", "location": "Chicago Office"},
36
+ {"asset_id": "asset_035", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad X1 Nano Gen 3", "specs": "13-inch 2K, i7-1360P, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_056", "purchase_date": "2023-07-12", "location": "Austin Office"},
37
+ {"asset_id": "asset_036", "type": "laptop", "brand": "Dell", "model": "Latitude 5440", "specs": "14-inch FHD, i5-1345U, 16GB RAM, 256GB SSD", "status": "available", "assigned_to": null, "purchase_date": "2024-08-28", "location": "New York Office"},
38
+ {"asset_id": "asset_037", "type": "laptop", "brand": "Apple", "model": "MacBook Pro 16 M3 Max", "specs": "16-inch Liquid Retina XDR, M3 Max, 48GB RAM, 2TB SSD", "status": "assigned", "assigned_to": "emp_001", "purchase_date": "2024-05-05", "location": "San Francisco HQ"},
39
+ {"asset_id": "asset_038", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad T14 Gen 4", "specs": "14-inch WUXGA, i7-1360P, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_063", "purchase_date": "2024-01-18", "location": "Chicago Office"},
40
+ {"asset_id": "asset_039", "type": "laptop", "brand": "HP", "model": "EliteBook 640 G10", "specs": "14-inch FHD, i5-1345U, 16GB RAM, 256GB SSD", "status": "maintenance", "assigned_to": null, "purchase_date": "2023-05-22", "location": "Austin Office"},
41
+ {"asset_id": "asset_040", "type": "laptop", "brand": "Dell", "model": "XPS 13 9340", "specs": "13.4-inch FHD+, i7-1360P, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_076", "purchase_date": "2024-06-14", "location": "San Francisco HQ"},
42
+ {"asset_id": "asset_041", "type": "laptop", "brand": "Apple", "model": "MacBook Air 13 M3", "specs": "13.6-inch Liquid Retina, M3, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_098", "purchase_date": "2024-09-20", "location": "New York Office"},
43
+ {"asset_id": "asset_042", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad X1 Carbon Gen 11", "specs": "14-inch WUXGA, i5-1345U, 16GB RAM, 256GB SSD", "status": "available", "assigned_to": null, "purchase_date": "2024-04-02", "location": "Chicago Office"},
44
+ {"asset_id": "asset_043", "type": "laptop", "brand": "Dell", "model": "Precision 5570", "specs": "15.6-inch UHD+, i7-12800H, 32GB RAM, 1TB SSD, RTX A2000", "status": "retired", "assigned_to": null, "purchase_date": "2022-01-15", "location": "San Francisco HQ"},
45
+ {"asset_id": "asset_044", "type": "laptop", "brand": "HP", "model": "ZBook Studio G10", "specs": "16-inch WQUXGA, i7-13700H, 32GB RAM, 1TB SSD, RTX 4070", "status": "assigned", "assigned_to": "emp_014", "purchase_date": "2024-02-05", "location": "Austin Office"},
46
+ {"asset_id": "asset_045", "type": "laptop", "brand": "Apple", "model": "MacBook Pro 14 M3", "specs": "14-inch Liquid Retina XDR, M3, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_039", "purchase_date": "2024-07-08", "location": "New York Office"},
47
+ {"asset_id": "asset_046", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad L14 Gen 4", "specs": "14-inch FHD, Ryzen 5 PRO 7530U, 16GB RAM, 256GB SSD", "status": "available", "assigned_to": null, "purchase_date": "2024-10-10", "location": "Chicago Office"},
48
+ {"asset_id": "asset_047", "type": "laptop", "brand": "Dell", "model": "Latitude 7340", "specs": "13.3-inch FHD+, i7-1365U, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_051", "purchase_date": "2023-09-05", "location": "San Francisco HQ"},
49
+ {"asset_id": "asset_048", "type": "laptop", "brand": "Apple", "model": "MacBook Pro 16 M3 Pro", "specs": "16-inch Liquid Retina XDR, M3 Pro, 18GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_082", "purchase_date": "2024-05-18", "location": "Austin Office"},
50
+ {"asset_id": "asset_049", "type": "laptop", "brand": "HP", "model": "EliteBook 845 G10", "specs": "14-inch WUXGA, Ryzen 7 PRO 7840U, 16GB RAM, 512GB SSD", "status": "assigned", "assigned_to": "emp_027", "purchase_date": "2024-03-28", "location": "New York Office"},
51
+ {"asset_id": "asset_050", "type": "laptop", "brand": "Lenovo", "model": "ThinkPad X1 Extreme Gen 5", "specs": "16-inch WQUXGA, i9-12900H, 64GB RAM, 2TB SSD, RTX 3080 Ti", "status": "retired", "assigned_to": null, "purchase_date": "2022-06-30", "location": "San Francisco HQ"},
52
+ {"asset_id": "asset_051", "type": "monitor", "brand": "Dell", "model": "UltraSharp U2723QE", "specs": "27-inch 4K IPS, USB-C Hub, 60Hz", "status": "assigned", "assigned_to": "emp_042", "purchase_date": "2024-03-15", "location": "San Francisco HQ"},
53
+ {"asset_id": "asset_052", "type": "monitor", "brand": "LG", "model": "UltraFine 5K 27MD5KL", "specs": "27-inch 5K IPS, Thunderbolt 3, P3 Wide Color", "status": "assigned", "assigned_to": "emp_012", "purchase_date": "2024-04-10", "location": "San Francisco HQ"},
54
+ {"asset_id": "asset_053", "type": "monitor", "brand": "Samsung", "model": "ViewFinity S9 S90PC", "specs": "27-inch 5K IPS, USB-C, Matte Display", "status": "available", "assigned_to": null, "purchase_date": "2024-08-01", "location": "New York Office"},
55
+ {"asset_id": "asset_054", "type": "monitor", "brand": "Dell", "model": "UltraSharp U3423WE", "specs": "34-inch WQHD Curved IPS, USB-C Hub, 60Hz", "status": "assigned", "assigned_to": "emp_003", "purchase_date": "2023-11-05", "location": "Austin Office"},
56
+ {"asset_id": "asset_055", "type": "monitor", "brand": "LG", "model": "UltraWide 34WN80C", "specs": "34-inch WQHD IPS, USB-C, HDR10", "status": "assigned", "assigned_to": "emp_055", "purchase_date": "2023-09-22", "location": "Austin Office"},
57
+ {"asset_id": "asset_056", "type": "monitor", "brand": "Apple", "model": "Studio Display", "specs": "27-inch 5K Retina, Thunderbolt 3, 12MP Camera", "status": "assigned", "assigned_to": "emp_001", "purchase_date": "2024-05-05", "location": "San Francisco HQ"},
58
+ {"asset_id": "asset_057", "type": "monitor", "brand": "Dell", "model": "P2422H", "specs": "24-inch FHD IPS, HDMI/DP/VGA, 60Hz", "status": "available", "assigned_to": null, "purchase_date": "2024-06-20", "location": "Chicago Office"},
59
+ {"asset_id": "asset_058", "type": "monitor", "brand": "HP", "model": "Z27k G3 4K", "specs": "27-inch 4K IPS, USB-C, Thunderbolt 4", "status": "assigned", "assigned_to": "emp_022", "purchase_date": "2023-10-30", "location": "New York Office"},
60
+ {"asset_id": "asset_059", "type": "monitor", "brand": "LG", "model": "UltraFine 4K 27UQ850", "specs": "27-inch 4K IPS, USB-C, HDR400", "status": "assigned", "assigned_to": "emp_017", "purchase_date": "2024-06-20", "location": "San Francisco HQ"},
61
+ {"asset_id": "asset_060", "type": "monitor", "brand": "Dell", "model": "UltraSharp U2422H", "specs": "24-inch FHD IPS, USB-C Hub, 60Hz", "status": "available", "assigned_to": null, "purchase_date": "2024-09-15", "location": "Austin Office"},
62
+ {"asset_id": "asset_061", "type": "monitor", "brand": "Samsung", "model": "Smart Monitor M8 S32BM", "specs": "32-inch 4K VA, USB-C, Smart TV Features", "status": "assigned", "assigned_to": "emp_067", "purchase_date": "2024-08-05", "location": "San Francisco HQ"},
63
+ {"asset_id": "asset_062", "type": "monitor", "brand": "LG", "model": "DualUp 28MQ780", "specs": "27.6-inch SDQHD IPS, USB-C, Nano IPS", "status": "assigned", "assigned_to": "emp_008", "purchase_date": "2024-01-25", "location": "San Francisco HQ"},
64
+ {"asset_id": "asset_063", "type": "monitor", "brand": "Dell", "model": "UltraSharp U4924DW", "specs": "49-inch DQHD Curved IPS, USB-C, KVM", "status": "assigned", "assigned_to": "emp_002", "purchase_date": "2024-06-01", "location": "San Francisco HQ"},
65
+ {"asset_id": "asset_064", "type": "monitor", "brand": "HP", "model": "E27 G5 FHD", "specs": "27-inch FHD IPS, HDMI/DP, 60Hz", "status": "available", "assigned_to": null, "purchase_date": "2024-10-05", "location": "New York Office"},
66
+ {"asset_id": "asset_065", "type": "monitor", "brand": "LG", "model": "UltraWide 40WP95C", "specs": "39.7-inch 5K2K IPS, Thunderbolt 4, Nano IPS", "status": "assigned", "assigned_to": "emp_005", "purchase_date": "2023-12-15", "location": "San Francisco HQ"},
67
+ {"asset_id": "asset_066", "type": "monitor", "brand": "Dell", "model": "S2722QC", "specs": "27-inch 4K IPS, USB-C, 60Hz", "status": "assigned", "assigned_to": "emp_044", "purchase_date": "2024-03-08", "location": "Austin Office"},
68
+ {"asset_id": "asset_067", "type": "monitor", "brand": "Samsung", "model": "ViewFinity S8 S80UA", "specs": "32-inch 4K VA, USB-C, HDR10", "status": "available", "assigned_to": null, "purchase_date": "2024-07-30", "location": "Chicago Office"},
69
+ {"asset_id": "asset_068", "type": "monitor", "brand": "Apple", "model": "Pro Display XDR", "specs": "32-inch 6K Retina, Thunderbolt 3, XDR, P3 Wide Color", "status": "assigned", "assigned_to": "emp_014", "purchase_date": "2024-02-05", "location": "Austin Office"},
70
+ {"asset_id": "asset_069", "type": "monitor", "brand": "Dell", "model": "P2723QE", "specs": "27-inch 4K IPS, USB-C Hub, 60Hz", "status": "assigned", "assigned_to": "emp_031", "purchase_date": "2023-12-01", "location": "Chicago Office"},
71
+ {"asset_id": "asset_070", "type": "monitor", "brand": "HP", "model": "Z24f G3 FHD", "specs": "24-inch FHD IPS, HDMI/DP, 60Hz", "status": "available", "assigned_to": null, "purchase_date": "2024-11-01", "location": "New York Office"},
72
+ {"asset_id": "asset_071", "type": "monitor", "brand": "LG", "model": "27UP850N", "specs": "27-inch 4K IPS, USB-C, DCI-P3 95%", "status": "assigned", "assigned_to": "emp_049", "purchase_date": "2024-02-10", "location": "Austin Office"},
73
+ {"asset_id": "asset_072", "type": "monitor", "brand": "Dell", "model": "UltraSharp U2723QE", "specs": "27-inch 4K IPS, USB-C Hub, 60Hz", "status": "available", "assigned_to": null, "purchase_date": "2024-09-28", "location": "San Francisco HQ"},
74
+ {"asset_id": "asset_073", "type": "monitor", "brand": "Samsung", "model": "Odyssey G7 S28AG70", "specs": "28-inch 4K IPS, 144Hz, G-Sync Compatible", "status": "assigned", "assigned_to": "emp_025", "purchase_date": "2024-03-20", "location": "Chicago Office"},
75
+ {"asset_id": "asset_074", "type": "monitor", "brand": "LG", "model": "UltraGear 27GP950", "specs": "27-inch 4K Nano IPS, 160Hz, HDMI 2.1", "status": "assigned", "assigned_to": "emp_056", "purchase_date": "2023-07-12", "location": "Austin Office"},
76
+ {"asset_id": "asset_075", "type": "monitor", "brand": "Dell", "model": "P2723D", "specs": "27-inch QHD IPS, USB-C, 60Hz", "status": "maintenance", "assigned_to": null, "purchase_date": "2023-03-18", "location": "New York Office"},
77
+ {"asset_id": "asset_076", "type": "phone", "brand": "Apple", "model": "iPhone 15 Pro", "specs": "6.1-inch, A17 Pro, 256GB, 5G", "status": "assigned", "assigned_to": "emp_001", "purchase_date": "2024-05-05", "location": "San Francisco HQ"},
78
+ {"asset_id": "asset_077", "type": "phone", "brand": "Apple", "model": "iPhone 15", "specs": "6.1-inch, A16, 128GB, 5G", "status": "assigned", "assigned_to": "emp_012", "purchase_date": "2024-04-10", "location": "San Francisco HQ"},
79
+ {"asset_id": "asset_078", "type": "phone", "brand": "Samsung", "model": "Galaxy S24 Ultra", "specs": "6.8-inch, Snapdragon 8 Gen 3, 256GB, 5G", "status": "assigned", "assigned_to": "emp_003", "purchase_date": "2024-02-15", "location": "Austin Office"},
80
+ {"asset_id": "asset_079", "type": "phone", "brand": "Apple", "model": "iPhone 15 Pro Max", "specs": "6.7-inch, A17 Pro, 512GB, 5G", "status": "available", "assigned_to": null, "purchase_date": "2024-09-25", "location": "New York Office"},
81
+ {"asset_id": "asset_080", "type": "phone", "brand": "Google", "model": "Pixel 8 Pro", "specs": "6.7-inch, Tensor G3, 128GB, 5G", "status": "assigned", "assigned_to": "emp_055", "purchase_date": "2024-01-10", "location": "Austin Office"},
82
+ {"asset_id": "asset_081", "type": "phone", "brand": "Apple", "model": "iPhone 14", "specs": "6.1-inch, A15, 128GB, 5G", "status": "assigned", "assigned_to": "emp_033", "purchase_date": "2023-06-20", "location": "New York Office"},
83
+ {"asset_id": "asset_082", "type": "phone", "brand": "Samsung", "model": "Galaxy S24", "specs": "6.2-inch, Exynos 2400, 128GB, 5G", "status": "available", "assigned_to": null, "purchase_date": "2024-10-15", "location": "Chicago Office"},
84
+ {"asset_id": "asset_083", "type": "phone", "brand": "Apple", "model": "iPhone 15 Pro", "specs": "6.1-inch, A17 Pro, 128GB, 5G", "status": "assigned", "assigned_to": "emp_067", "purchase_date": "2024-08-05", "location": "San Francisco HQ"},
85
+ {"asset_id": "asset_084", "type": "phone", "brand": "Google", "model": "Pixel 8", "specs": "6.2-inch, Tensor G3, 128GB, 5G", "status": "assigned", "assigned_to": "emp_044", "purchase_date": "2024-03-08", "location": "Austin Office"},
86
+ {"asset_id": "asset_085", "type": "phone", "brand": "Apple", "model": "iPhone 15", "specs": "6.1-inch, A16, 256GB, 5G", "status": "available", "assigned_to": null, "purchase_date": "2024-11-01", "location": "San Francisco HQ"},
87
+ {"asset_id": "asset_086", "type": "phone", "brand": "Samsung", "model": "Galaxy Z Fold5", "specs": "7.6-inch Foldable, Snapdragon 8 Gen 2, 256GB, 5G", "status": "assigned", "assigned_to": "emp_005", "purchase_date": "2023-12-15", "location": "San Francisco HQ"},
88
+ {"asset_id": "asset_087", "type": "phone", "brand": "Apple", "model": "iPhone 14 Pro", "specs": "6.1-inch, A16, 256GB, 5G", "status": "retired", "assigned_to": null, "purchase_date": "2022-10-05", "location": "New York Office"},
89
+ {"asset_id": "asset_088", "type": "phone", "brand": "Google", "model": "Pixel 7a", "specs": "6.1-inch, Tensor G2, 128GB, 5G", "status": "assigned", "assigned_to": "emp_071", "purchase_date": "2023-08-19", "location": "San Francisco HQ"},
90
+ {"asset_id": "asset_089", "type": "phone", "brand": "Apple", "model": "iPhone 15 Pro", "specs": "6.1-inch, A17 Pro, 256GB, 5G", "status": "assigned", "assigned_to": "emp_022", "purchase_date": "2024-01-20", "location": "New York Office"},
91
+ {"asset_id": "asset_090", "type": "phone", "brand": "Samsung", "model": "Galaxy S23", "specs": "6.1-inch, Snapdragon 8 Gen 2, 128GB, 5G", "status": "available", "assigned_to": null, "purchase_date": "2024-07-10", "location": "Chicago Office"},
92
+ {"asset_id": "asset_091", "type": "headset", "brand": "Jabra", "model": "Evolve2 85", "specs": "ANC, Bluetooth 5.1, USB-C, 37hr battery", "status": "assigned", "assigned_to": "emp_042", "purchase_date": "2024-03-15", "location": "San Francisco HQ"},
93
+ {"asset_id": "asset_092", "type": "headset", "brand": "Poly", "model": "Voyager Focus 2", "specs": "ANC, Bluetooth 5.1, USB-A, 19hr battery", "status": "assigned", "assigned_to": "emp_017", "purchase_date": "2024-06-20", "location": "San Francisco HQ"},
94
+ {"asset_id": "asset_093", "type": "headset", "brand": "Sony", "model": "WH-1000XM5", "specs": "ANC, Bluetooth 5.2, LDAC, 30hr battery", "status": "available", "assigned_to": null, "purchase_date": "2024-08-10", "location": "New York Office"},
95
+ {"asset_id": "asset_094", "type": "headset", "brand": "Jabra", "model": "Evolve2 75", "specs": "ANC, Bluetooth 5.2, USB-C, 36hr battery", "status": "assigned", "assigned_to": "emp_003", "purchase_date": "2023-11-05", "location": "Austin Office"},
96
+ {"asset_id": "asset_095", "type": "headset", "brand": "Bose", "model": "QuietComfort Ultra", "specs": "ANC, Bluetooth 5.3, Spatial Audio, 24hr battery", "status": "assigned", "assigned_to": "emp_012", "purchase_date": "2024-04-10", "location": "San Francisco HQ"},
97
+ {"asset_id": "asset_096", "type": "headset", "brand": "Poly", "model": "Blackwire 5220", "specs": "Wired USB-C/3.5mm, Inline Controls, Noise Cancelling Mic", "status": "available", "assigned_to": null, "purchase_date": "2024-09-18", "location": "Chicago Office"},
98
+ {"asset_id": "asset_097", "type": "headset", "brand": "Jabra", "model": "Engage 50 II", "specs": "Wired USB-C, Super Wideband, Busylight", "status": "assigned", "assigned_to": "emp_055", "purchase_date": "2023-09-22", "location": "Austin Office"},
99
+ {"asset_id": "asset_098", "type": "headset", "brand": "Sony", "model": "WH-1000XM4", "specs": "ANC, Bluetooth 5.0, LDAC, 30hr battery", "status": "maintenance", "assigned_to": null, "purchase_date": "2023-02-14", "location": "New York Office"},
100
+ {"asset_id": "asset_099", "type": "headset", "brand": "Bose", "model": "700 UC", "specs": "ANC, Bluetooth 5.0, USB-A, 20hr battery", "status": "assigned", "assigned_to": "emp_008", "purchase_date": "2024-01-25", "location": "San Francisco HQ"},
101
+ {"asset_id": "asset_100", "type": "headset", "brand": "Jabra", "model": "Evolve2 55", "specs": "ANC, Bluetooth 5.2, USB-C, 16hr battery", "status": "assigned", "assigned_to": "emp_067", "purchase_date": "2024-08-05", "location": "San Francisco HQ"}
102
+ ]
server/data/policies.json ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "policies": [
3
+ {
4
+ "policy_id": "pol_001",
5
+ "title": "Onboarding Policy",
6
+ "department": "all",
7
+ "content": "All new hires must complete the onboarding process within 5 business days of their start date. The onboarding workflow is initiated automatically upon offer acceptance and must follow the department-specific step sequence without skipping or reordering steps. Failure to complete onboarding within the allotted window triggers an escalation to the department head and HR Business Partner.\n\nEach new hire must be assigned a designated onboarding buddy within their department before their first day. The buddy is responsible for guiding the new employee through cultural integration, tool setup verification, and initial project context. Managers must confirm onboarding completion in the HRIS system within 24 hours of the final step being finished.\n\nAll onboarding documentation, including signed NDAs, tax forms, and equipment receipts, must be uploaded to the employee's digital file in Workday before the end of the first week. Incomplete documentation will result in a hold on system access provisioning until resolved.",
8
+ "last_updated": "2025-11-15",
9
+ "key_rules": [
10
+ "onboarding_deadline_business_days: 5",
11
+ "buddy_assignment_required: true",
12
+ "buddy_must_be_same_department: true",
13
+ "manager_confirmation_deadline_hours: 24",
14
+ "documentation_upload_deadline: end_of_first_week",
15
+ "incomplete_docs_blocks_access: true",
16
+ "steps_must_follow_sequence: true"
17
+ ]
18
+ },
19
+ {
20
+ "policy_id": "pol_002",
21
+ "title": "Offboarding Policy",
22
+ "department": "all",
23
+ "content": "The offboarding process must be initiated within 24 hours of a resignation or termination decision. For voluntary departures, the standard notice period is 15 business days. For involuntary terminations, system access must be revoked within 2 hours of the termination meeting. All offboarding steps must be completed within 3 business days of the employee's last working day.\n\nThe departing employee's manager is responsible for ensuring knowledge transfer is completed before the last day. A knowledge transfer checklist must be signed off by both the manager and a designated knowledge recipient. Any company intellectual property, code, documents, or data on personal devices must be verified as deleted or returned.\n\nThe final paycheck, including any accrued PTO, must be processed within the pay period following the last working day. Exit interviews are mandatory for all full-time employees and must be conducted by an HR representative who is not the employee's direct HR Business Partner to ensure objectivity.",
24
+ "last_updated": "2025-12-01",
25
+ "key_rules": [
26
+ "offboarding_initiation_deadline_hours: 24",
27
+ "voluntary_notice_period_business_days: 15",
28
+ "involuntary_access_revocation_hours: 2",
29
+ "offboarding_completion_deadline_business_days: 3",
30
+ "knowledge_transfer_signoff_required: true",
31
+ "exit_interview_mandatory: true",
32
+ "exit_interview_conducted_by: independent_hr_representative"
33
+ ]
34
+ },
35
+ {
36
+ "policy_id": "pol_003",
37
+ "title": "Remote Work Policy",
38
+ "department": "all",
39
+ "content": "Employees at level L3 and above are eligible for fully remote work arrangements with manager approval. L1 and L2 employees must work on-site for a minimum of 3 days per week during their first 6 months, after which they may request remote work privileges. Remote work requests must be submitted through the HRIS system and approved by both the direct manager and department head.\n\nRemote employees must maintain a secure home office environment that meets the company's information security standards. This includes using a company-approved VPN at all times, ensuring a private workspace for confidential calls, and maintaining a reliable internet connection with a minimum speed of 50 Mbps download. Equipment provided for remote work remains company property and must be returned upon offboarding.\n\nInternational remote work is permitted for up to 30 calendar days per year with prior VP-level approval and tax compliance review by the Finance team. Employees working remotely from a different state for more than 14 consecutive days must notify HR for tax withholding adjustments.",
40
+ "last_updated": "2025-10-20",
41
+ "key_rules": [
42
+ "remote_eligible_level: L3+",
43
+ "junior_onsite_minimum_days: 3",
44
+ "junior_onsite_period_months: 6",
45
+ "vpn_required: true",
46
+ "minimum_internet_speed_mbps: 50",
47
+ "international_remote_max_days: 30",
48
+ "international_remote_approval_level: VP",
49
+ "state_change_notification_threshold_days: 14"
50
+ ]
51
+ },
52
+ {
53
+ "policy_id": "pol_004",
54
+ "title": "Equipment Return Policy",
55
+ "department": "all",
56
+ "content": "All company-issued equipment must be returned within 5 business days of an employee's last working day. Equipment includes but is not limited to laptops, monitors, keyboards, mice, headsets, security hardware tokens, and mobile devices. Remote employees will receive a prepaid shipping label and packaging materials via email on their last day.\n\nEquipment must be returned in working condition. Normal wear and tear is expected, but damage resulting from negligence or misuse may result in a deduction from the final paycheck, up to the replacement cost of the item. The IT department will conduct a condition assessment within 48 hours of receiving returned equipment and document findings in the asset management system.\n\nFailure to return equipment within the specified window will trigger an escalation process: a reminder email at day 3, a formal notice at day 5, a payroll deduction notice at day 10, and legal action for equipment valued over $1,000 if not returned within 30 days. Employees who return all equipment on or before their last day are eligible for the \"clean exit\" expedited reference process.",
57
+ "last_updated": "2025-09-30",
58
+ "key_rules": [
59
+ "return_deadline_business_days: 5",
60
+ "condition_assessment_deadline_hours: 48",
61
+ "reminder_email_day: 3",
62
+ "formal_notice_day: 5",
63
+ "payroll_deduction_notice_day: 10",
64
+ "legal_action_threshold_value: 1000",
65
+ "legal_action_deadline_days: 30",
66
+ "clean_exit_incentive: true"
67
+ ]
68
+ },
69
+ {
70
+ "policy_id": "pol_005",
71
+ "title": "Data Access Policy",
72
+ "department": "all",
73
+ "content": "Data access is granted on a strict need-to-know basis following the principle of least privilege. Access levels are categorized into four tiers: Public (Tier 1), Internal (Tier 2), Confidential (Tier 3), and Restricted (Tier 4). Tier 1 and Tier 2 access is provisioned automatically during onboarding based on department assignment. Tier 3 access requires manager approval and a completed data handling training certification. Tier 4 access requires VP-level approval, security team review, and a signed data access agreement.\n\nAll data access permissions are reviewed quarterly as part of the access recertification process. Managers must confirm or revoke access for each direct report within 10 business days of the review notification. Unconfirmed access is automatically revoked after the review window closes. Any access anomalies detected by the security team trigger an immediate investigation.\n\nUpon offboarding, all data access must be revoked within 1 hour for Tier 4, 4 hours for Tier 3, and 24 hours for Tier 1 and Tier 2. Data downloads exceeding 500 MB within 7 days of a resignation notice trigger an automatic alert to the security team for review.",
74
+ "last_updated": "2026-01-10",
75
+ "key_rules": [
76
+ "access_model: least_privilege",
77
+ "tier3_requires_manager_approval: true",
78
+ "tier3_requires_data_handling_training: true",
79
+ "tier4_requires_vp_approval: true",
80
+ "tier4_requires_security_review: true",
81
+ "quarterly_access_review: true",
82
+ "review_confirmation_deadline_business_days: 10",
83
+ "offboarding_tier4_revocation_hours: 1",
84
+ "offboarding_tier3_revocation_hours: 4",
85
+ "offboarding_tier1_tier2_revocation_hours: 24",
86
+ "download_alert_threshold_mb: 500",
87
+ "download_alert_window_days: 7"
88
+ ]
89
+ },
90
+ {
91
+ "policy_id": "pol_006",
92
+ "title": "Contractor Policy",
93
+ "department": "all",
94
+ "content": "Contractors may be engaged for a maximum duration of 12 months per engagement. Extensions beyond 12 months require VP-level approval and a written justification demonstrating why the role cannot be converted to a full-time position. No contractor may work for the company for more than 18 cumulative months within any 24-month period to maintain compliance with labor classification regulations.\n\nContractors are issued limited-access credentials that restrict them to project-specific tools and data. Contractors may not be granted Tier 3 or Tier 4 data access under any circumstances. All contractor accounts are provisioned with an automatic expiration date matching their contract end date. Contractor onboarding follows the same department-specific steps as full-time employees, with the exception of benefits enrollment and equity grant steps.\n\nContractor rate approvals follow a tiered structure: engagements under $100/hour require manager approval, $100-$200/hour require director approval, and over $200/hour require VP approval. All contractor invoices must be submitted through the procurement system and approved by the engaging manager within 5 business days.",
95
+ "last_updated": "2025-11-01",
96
+ "key_rules": [
97
+ "contractor_max_duration_months: 12",
98
+ "extension_approval_level: VP",
99
+ "max_cumulative_months_in_24: 18",
100
+ "contractor_max_data_tier: 2",
101
+ "auto_expiration_enabled: true",
102
+ "rate_approval_under_100: manager",
103
+ "rate_approval_100_to_200: director",
104
+ "rate_approval_over_200: VP",
105
+ "invoice_approval_deadline_business_days: 5"
106
+ ]
107
+ },
108
+ {
109
+ "policy_id": "pol_007",
110
+ "title": "Termination Policy",
111
+ "department": "all",
112
+ "content": "Involuntary terminations must be approved through a multi-level review process before execution. The termination request must be initiated by the direct manager, reviewed by the HR Business Partner, approved by the department head, and given final authorization by the VP of HR. For employees at L5 or above, CEO approval is additionally required. The entire approval chain must be completed within 5 business days of initiation.\n\nPerformance-based terminations require documented evidence of at least two performance improvement plan (PIP) cycles, each lasting a minimum of 30 days, unless the termination is for cause (policy violation, misconduct, or illegal activity). Terminations for cause may bypass the PIP requirement but still require the full approval chain. All termination documentation must be retained for a minimum of 7 years.\n\nSeverance packages are offered based on tenure: employees with less than 1 year receive 2 weeks of base pay, 1-3 years receive 4 weeks, 3-5 years receive 8 weeks, and 5+ years receive 12 weeks plus continued benefits for the severance period. Severance agreements must include a standard release of claims and are subject to legal review before being presented to the employee.",
113
+ "last_updated": "2025-12-15",
114
+ "key_rules": [
115
+ "approval_chain: [manager, hr_bp, dept_head, vp_hr]",
116
+ "l5_plus_requires_ceo_approval: true",
117
+ "approval_chain_deadline_business_days: 5",
118
+ "pip_cycles_required: 2",
119
+ "pip_minimum_duration_days: 30",
120
+ "for_cause_bypasses_pip: true",
121
+ "documentation_retention_years: 7",
122
+ "severance_under_1yr_weeks: 2",
123
+ "severance_1_to_3yr_weeks: 4",
124
+ "severance_3_to_5yr_weeks: 8",
125
+ "severance_over_5yr_weeks: 12"
126
+ ]
127
+ },
128
+ {
129
+ "policy_id": "pol_008",
130
+ "title": "Department Transfer Policy",
131
+ "department": "all",
132
+ "content": "Internal department transfers are available to employees who have been in their current role for a minimum of 12 months and have received a performance rating of \"Meets Expectations\" or above in their most recent review cycle. Transfer requests must be submitted through the HRIS system and require approval from both the current manager and the receiving department head. A transfer cannot be approved if the receiving department is at its headcount limit.\n\nThe transfer process follows a structured timeline: the employee submits a transfer request, the current manager has 5 business days to acknowledge, the receiving department conducts interviews within 10 business days, and a final decision is communicated within 15 business days of the initial request. If approved, the effective transfer date must be at least 30 days from approval to allow for transition planning.\n\nUpon transfer, the employee undergoes a modified onboarding process in the new department, which includes tool provisioning, team introductions, and domain-specific training. Access to the previous department's tools and data is revoked within 5 business days of the transfer date, unless a temporary exception is approved by both department heads for knowledge transfer purposes, which may not exceed 15 business days.",
133
+ "last_updated": "2025-10-05",
134
+ "key_rules": [
135
+ "minimum_tenure_months: 12",
136
+ "minimum_performance_rating: meets_expectations",
137
+ "current_manager_approval_required: true",
138
+ "receiving_head_approval_required: true",
139
+ "headcount_limit_enforced: true",
140
+ "manager_acknowledgment_deadline_business_days: 5",
141
+ "interview_deadline_business_days: 10",
142
+ "decision_deadline_business_days: 15",
143
+ "transition_buffer_days: 30",
144
+ "old_access_revocation_business_days: 5",
145
+ "temporary_access_exception_max_days: 15"
146
+ ]
147
+ },
148
+ {
149
+ "policy_id": "pol_009",
150
+ "title": "Rehire Policy",
151
+ "department": "all",
152
+ "content": "Former employees are eligible for rehire consideration if they departed in good standing and their exit record does not include any policy violations, termination for cause, or unresolved compliance issues. A minimum cooling-off period of 6 months must elapse between the departure date and any rehire application. Former employees who were terminated for cause are permanently ineligible for rehire.\n\nRehired employees retain their original employee ID with a rehire suffix (e.g., emp_0042_R1) for tracking purposes. Prior tenure is recognized for PTO accrual purposes if the gap in employment is less than 24 months; otherwise, the employee starts with the standard new-hire PTO allocation. Rehires must complete the full onboarding process regardless of prior tenure, though the onboarding buddy assignment may be waived at the manager's discretion.\n\nRehire offers must be approved by the department head and VP of HR. Compensation for rehires is benchmarked against current market rates and internal equity; prior salary does not guarantee equivalent compensation. Any prior equity grants that were forfeited are not reinstated. Rehired employees are subject to a new 90-day probationary period.",
153
+ "last_updated": "2025-08-22",
154
+ "key_rules": [
155
+ "good_standing_required: true",
156
+ "cooling_off_period_months: 6",
157
+ "terminated_for_cause_permanently_ineligible: true",
158
+ "original_id_retained_with_suffix: true",
159
+ "tenure_recognition_gap_max_months: 24",
160
+ "full_onboarding_required: true",
161
+ "buddy_waivable_for_rehire: true",
162
+ "approval_required: [dept_head, vp_hr]",
163
+ "prior_equity_not_reinstated: true",
164
+ "probation_period_days: 90"
165
+ ]
166
+ },
167
+ {
168
+ "policy_id": "pol_010",
169
+ "title": "Badge Access Policy",
170
+ "department": "Security",
171
+ "content": "Physical badge access is provisioned based on department assignment and role level. Standard office access (floors 1-3, common areas, cafeteria) is granted to all employees. Restricted areas including the server room (floor B1), executive suite (floor 4), and security operations center (floor B2) require L4+ level clearance and explicit approval from the Security department head. Temporary visitor badges are valid for 8 hours and must be signed in and out at reception.\n\nBadge access changes take effect within 4 hours of approval for standard areas and within 1 hour for restricted areas. All badge access events are logged and retained for 12 months. The security team conducts monthly audits of badge access logs to identify anomalies such as after-hours access or tailgating attempts. Employees who lose their badge must report it within 2 hours; the lost badge is immediately deactivated and a replacement is issued within 24 hours.\n\nUpon offboarding, badge access must be deactivated before the employee leaves the building on their last day. For involuntary terminations, badge deactivation occurs simultaneously with the termination notification. Badge access is automatically suspended if an employee has not badged in for 30 consecutive days, pending manager confirmation of status.",
172
+ "last_updated": "2026-01-20",
173
+ "key_rules": [
174
+ "standard_access_areas: [floors_1_3, common_areas, cafeteria]",
175
+ "restricted_access_requires_level: L4+",
176
+ "restricted_access_requires_security_head_approval: true",
177
+ "visitor_badge_validity_hours: 8",
178
+ "standard_access_change_hours: 4",
179
+ "restricted_access_change_hours: 1",
180
+ "access_log_retention_months: 12",
181
+ "lost_badge_report_deadline_hours: 2",
182
+ "replacement_badge_deadline_hours: 24",
183
+ "offboarding_deactivation: before_building_exit",
184
+ "inactivity_suspension_days: 30"
185
+ ]
186
+ },
187
+ {
188
+ "policy_id": "pol_011",
189
+ "title": "Software License Policy",
190
+ "department": "all",
191
+ "content": "Software licenses are provisioned based on department-specific required tools lists maintained by each department head. License requests for tools outside the approved department list require director-level approval and a business justification. All software must be vetted by the Security team for compliance before being added to any department's approved list. The annual software budget per employee is capped at $5,000 for standard roles and $8,000 for engineering and data science roles.\n\nLicenses are assigned to individual employees and may not be shared. When an employee goes on leave exceeding 30 days, their licenses may be temporarily reassigned with manager approval. Upon offboarding, all licenses must be deactivated within 24 hours and reallocated or released to reduce costs. The IT team maintains a license utilization dashboard and flags licenses with less than 10% usage over 90 days for review.\n\nEnterprise license agreements are negotiated annually by the procurement team. Department heads must submit license forecasts by Q3 each year for the following fiscal year's budget planning. Any mid-year license additions exceeding $10,000 in total cost require CFO approval.",
192
+ "last_updated": "2025-11-18",
193
+ "key_rules": [
194
+ "provisioning_based_on_department_list: true",
195
+ "non_standard_requires_director_approval: true",
196
+ "security_vetting_required: true",
197
+ "annual_budget_per_employee_standard: 5000",
198
+ "annual_budget_per_employee_eng_ds: 8000",
199
+ "license_sharing_prohibited: true",
200
+ "leave_reassignment_threshold_days: 30",
201
+ "offboarding_deactivation_hours: 24",
202
+ "low_usage_flag_threshold_percent: 10",
203
+ "low_usage_flag_window_days: 90",
204
+ "mid_year_addition_cfo_approval_threshold: 10000"
205
+ ]
206
+ },
207
+ {
208
+ "policy_id": "pol_012",
209
+ "title": "Approval Chain Policy",
210
+ "department": "all",
211
+ "content": "All administrative actions affecting employee status, access, or compensation follow a defined approval chain based on the action type and impact level. Standard actions (PTO requests, expense reports under $500, tool access within department list) require only direct manager approval. Elevated actions (role changes, salary adjustments, cross-department access) require manager plus director approval. Critical actions (terminations, L5+ promotions, Tier 4 data access, budget exceptions) require the full chain up to VP level.\n\nApproval requests have defined SLA windows: standard actions must be resolved within 2 business days, elevated actions within 5 business days, and critical actions within 5 business days. If an approver does not respond within the SLA window, the request automatically escalates to the next level in the chain. Approvers may delegate their approval authority to a designated backup during planned absences, but delegation must be registered in the HRIS system in advance.\n\nAll approval decisions are logged with timestamps, approver identity, and any comments. Approval logs are immutable and retained for 5 years for audit purposes. Retrospective approvals (approving an action after it has already been taken) are flagged as policy violations and trigger a compliance review.",
212
+ "last_updated": "2025-12-10",
213
+ "key_rules": [
214
+ "standard_action_approval: manager",
215
+ "elevated_action_approval: [manager, director]",
216
+ "critical_action_approval: [manager, director, vp]",
217
+ "standard_sla_business_days: 2",
218
+ "elevated_sla_business_days: 5",
219
+ "critical_sla_business_days: 5",
220
+ "auto_escalation_on_sla_breach: true",
221
+ "delegation_must_be_pre_registered: true",
222
+ "approval_log_retention_years: 5",
223
+ "retrospective_approval_triggers_compliance_review: true",
224
+ "expense_standard_threshold: 500"
225
+ ]
226
+ },
227
+ {
228
+ "policy_id": "pol_013",
229
+ "title": "Probation Policy",
230
+ "department": "all",
231
+ "content": "All new hires and rehires are subject to a 90-day probationary period beginning on their start date. During probation, employees receive bi-weekly check-ins with their manager and a formal 30-day, 60-day, and 90-day performance review. The probation period may be extended by up to 30 additional days if the employee shows improvement potential but has not yet met all performance benchmarks, subject to HR Business Partner approval.\n\nDuring probation, either the company or the employee may end the employment relationship with 5 business days' notice instead of the standard 15 business days. Employees on probation are not eligible for internal transfers, promotions, or remote work arrangements. Probationary employees accrue PTO from day one but may not use more than 3 PTO days during the probation period without manager and HR approval.\n\nSuccessful completion of probation is confirmed in writing by the manager and recorded in the HRIS system. Employees who fail probation are subject to the standard offboarding process. Probation outcomes must be documented with specific examples of performance against the role's defined success criteria established during onboarding.",
232
+ "last_updated": "2025-09-15",
233
+ "key_rules": [
234
+ "probation_duration_days: 90",
235
+ "check_in_frequency: bi_weekly",
236
+ "formal_reviews_at_days: [30, 60, 90]",
237
+ "max_extension_days: 30",
238
+ "extension_requires_hr_bp_approval: true",
239
+ "notice_period_during_probation_days: 5",
240
+ "transfer_eligible_during_probation: false",
241
+ "promotion_eligible_during_probation: false",
242
+ "remote_work_eligible_during_probation: false",
243
+ "max_pto_during_probation_days: 3",
244
+ "completion_confirmed_in_writing: true"
245
+ ]
246
+ },
247
+ {
248
+ "policy_id": "pol_014",
249
+ "title": "Exit Interview Policy",
250
+ "department": "all",
251
+ "content": "Exit interviews are mandatory for all departing full-time employees and optional for contractors with engagements exceeding 6 months. The exit interview must be conducted between 3 and 1 business days before the employee's last working day. Exit interviews are conducted by an HR representative who has not been directly involved in any HR cases related to the departing employee to ensure neutrality and encourage candid feedback.\n\nThe exit interview follows a standardized questionnaire covering job satisfaction, management effectiveness, team dynamics, growth opportunities, compensation competitiveness, and reasons for departure. The interviewer may ask follow-up questions but must not attempt to make a counter-offer or negotiate retention during the exit interview. Retention conversations, if appropriate, must happen separately and before the exit interview is scheduled.\n\nExit interview data is anonymized and aggregated quarterly for trend analysis. Individual exit interview records are classified as Confidential (Tier 3) and accessible only to HR leadership and the VP of the relevant department. Insights are presented to the executive team quarterly with recommendations for retention improvements. Departing employees may request to receive a copy of their exit interview transcript within 30 days of departure.",
252
+ "last_updated": "2025-10-30",
253
+ "key_rules": [
254
+ "mandatory_for_full_time: true",
255
+ "optional_for_contractors_over_months: 6",
256
+ "timing_window_business_days_before_last_day: [1, 3]",
257
+ "interviewer_must_be_neutral: true",
258
+ "counter_offer_during_interview_prohibited: true",
259
+ "data_classification: tier_3_confidential",
260
+ "quarterly_trend_analysis_required: true",
261
+ "transcript_request_window_days: 30",
262
+ "standardized_questionnaire_required: true",
263
+ "accessible_by: [hr_leadership, dept_vp]"
264
+ ]
265
+ },
266
+ {
267
+ "policy_id": "pol_015",
268
+ "title": "Asset Management Policy",
269
+ "department": "all",
270
+ "content": "All company assets issued to employees are tracked in the centralized asset management system with a unique asset tag. Assets are categorized as High Value (laptops, monitors, mobile devices valued over $500), Standard (keyboards, mice, headsets, adapters), and Consumable (cables, screen protectors, cleaning supplies). High Value assets require employee signature upon receipt and are associated with the employee's record in the HRIS system.\n\nAsset lifecycle management follows defined thresholds: laptops are refreshed every 36 months, monitors every 48 months, and mobile devices every 24 months. Early refresh requests require director approval and a documented business justification. Employees may purchase their assigned laptop at depreciated book value upon offboarding, subject to IT security wipe and data destruction verification.\n\nLost or stolen assets must be reported to IT and Security within 4 hours of discovery. For devices containing company data, a remote wipe is initiated immediately upon report. The employee is not held financially responsible for lost or stolen assets provided the report is filed within the required window and there is no evidence of negligence. Annual asset audits are conducted in Q1, and discrepancies must be resolved within 15 business days.",
271
+ "last_updated": "2026-02-01",
272
+ "key_rules": [
273
+ "asset_tracking_system: centralized",
274
+ "high_value_threshold: 500",
275
+ "high_value_requires_signature: true",
276
+ "laptop_refresh_months: 36",
277
+ "monitor_refresh_months: 48",
278
+ "mobile_refresh_months: 24",
279
+ "early_refresh_requires_director_approval: true",
280
+ "employee_purchase_at_depreciated_value: true",
281
+ "lost_stolen_report_deadline_hours: 4",
282
+ "remote_wipe_on_report: true",
283
+ "annual_audit_quarter: Q1",
284
+ "audit_discrepancy_resolution_business_days: 15"
285
+ ]
286
+ }
287
+ ]
288
+ }
server/data/templates.json ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "template_id": "tmpl_001",
4
+ "name": "welcome_email",
5
+ "subject": "Welcome to {{company_name}}, {{first_name}}!",
6
+ "body": "Dear {{first_name}} {{last_name}},\n\nWelcome to {{company_name}}! We are thrilled to have you join the {{department}} team as a {{job_title}}.\n\nYour start date is {{start_date}}, and your manager will be {{manager_name}}. Please arrive at {{office_location}} by {{arrival_time}} on your first day.\n\nHere are a few things to prepare before your first day:\n- Bring a valid government-issued photo ID\n- Review the pre-boarding documents sent to your personal email\n- Complete any outstanding background check forms\n\nYour onboarding buddy will be {{buddy_name}}, who will help you get settled during your first few weeks.\n\nIf you have any questions before your start date, please don't hesitate to reach out to your HR contact at {{hr_contact_email}}.\n\nWe look forward to seeing you!\n\nBest regards,\n{{hr_sender_name}}\nHuman Resources\n{{company_name}}",
7
+ "channel": "email"
8
+ },
9
+ {
10
+ "template_id": "tmpl_002",
11
+ "name": "welcome_slack",
12
+ "subject": null,
13
+ "body": "Hey everyone! :wave: Please join me in welcoming *{{first_name}} {{last_name}}* to the {{department}} team! {{first_name}} is joining us as a *{{job_title}}* starting {{start_date}}.\n\n{{first_name}}, a little about your new team: {{team_description}}\n\nYour onboarding buddy is {{buddy_name}} -- feel free to reach out to them with any questions. We're so glad to have you on board! :tada:",
14
+ "channel": "slack"
15
+ },
16
+ {
17
+ "template_id": "tmpl_003",
18
+ "name": "farewell_email",
19
+ "subject": "Farewell and Best Wishes to {{first_name}} {{last_name}}",
20
+ "body": "Dear Team,\n\nI am writing to let you know that {{first_name}} {{last_name}} will be leaving {{company_name}} effective {{last_day}}.\n\n{{first_name}} has been a valued member of the {{department}} team for {{tenure}}, and we wish them all the best in their future endeavors.\n\nPlease join us for a farewell gathering on {{farewell_event_date}} at {{farewell_event_time}} in {{farewell_event_location}}.\n\nFor any ongoing projects or handover questions, please reach out to {{handover_contact_name}} ({{handover_contact_email}}).\n\nWe thank {{first_name}} for their contributions and wish them continued success.\n\nBest regards,\n{{manager_name}}\n{{department}}",
21
+ "channel": "email"
22
+ },
23
+ {
24
+ "template_id": "tmpl_004",
25
+ "name": "farewell_slack",
26
+ "subject": null,
27
+ "body": "Hi team -- I wanted to share that *{{first_name}} {{last_name}}* will be moving on from {{company_name}}, with {{last_day}} being their last day.\n\n{{first_name}} has been an incredible part of the {{department}} team, and we're grateful for everything they've contributed over the past {{tenure}}.\n\nPlease stop by the farewell gathering on {{farewell_event_date}} to say goodbye and wish them well! :heart:\n\nFor any project handovers, please contact {{handover_contact_name}}.",
28
+ "channel": "slack"
29
+ },
30
+ {
31
+ "template_id": "tmpl_005",
32
+ "name": "onboarding_reminder",
33
+ "subject": "Reminder: Onboarding Tasks Due for {{first_name}} {{last_name}}",
34
+ "body": "Dear {{recipient_name}},\n\nThis is a reminder that the following onboarding tasks for {{first_name}} {{last_name}} ({{job_title}}, {{department}}) are due by {{due_date}}:\n\n{{task_list}}\n\nNew hire start date: {{start_date}}\nDays remaining: {{days_remaining}}\n\nPlease complete these tasks as soon as possible to ensure a smooth onboarding experience. You can track progress in the onboarding dashboard at {{dashboard_url}}.\n\nIf you have any questions or need assistance, contact {{hr_contact_name}} at {{hr_contact_email}}.\n\nThank you,\nHR Onboarding System",
35
+ "channel": "email"
36
+ },
37
+ {
38
+ "template_id": "tmpl_006",
39
+ "name": "offboarding_reminder",
40
+ "subject": "Reminder: Offboarding Tasks Due for {{first_name}} {{last_name}}",
41
+ "body": "Dear {{recipient_name}},\n\nThis is a reminder that the following offboarding tasks for {{first_name}} {{last_name}} ({{job_title}}, {{department}}) must be completed by {{due_date}}:\n\n{{task_list}}\n\nLast working day: {{last_day}}\nDays remaining: {{days_remaining}}\n\nTimely completion of these tasks is critical for security compliance and a smooth transition. Track progress at {{dashboard_url}}.\n\nIf you have any questions, contact {{hr_contact_name}} at {{hr_contact_email}}.\n\nThank you,\nHR Offboarding System",
42
+ "channel": "email"
43
+ },
44
+ {
45
+ "template_id": "tmpl_007",
46
+ "name": "asset_assignment_confirmation",
47
+ "subject": "IT Asset Assigned: {{asset_type}} - {{asset_model}}",
48
+ "body": "Dear {{first_name}} {{last_name}},\n\nThe following IT asset has been assigned to you:\n\nAsset ID: {{asset_id}}\nType: {{asset_type}}\nBrand/Model: {{asset_brand}} {{asset_model}}\nSpecs: {{asset_specs}}\nSerial Number: {{serial_number}}\n\nAssignment Date: {{assignment_date}}\nPickup Location: {{pickup_location}}\n\nPlease review and acknowledge the IT Asset Usage Policy at {{policy_url}}. By accepting this asset, you agree to the terms outlined in the policy.\n\nIf you experience any issues with your equipment, please submit a ticket at {{support_url}} or contact IT Support at {{it_support_email}}.\n\nBest regards,\n{{it_admin_name}}\nIT Department",
49
+ "channel": "email"
50
+ },
51
+ {
52
+ "template_id": "tmpl_008",
53
+ "name": "access_grant_notification",
54
+ "subject": "Access Granted: {{role_name}} Role",
55
+ "body": "Dear {{first_name}} {{last_name}},\n\nYou have been granted the following access role:\n\nRole: {{role_name}}\nRole ID: {{role_id}}\nDescription: {{role_description}}\nGranted By: {{granted_by}}\nEffective Date: {{effective_date}}\n\nPermissions included:\n{{permissions_list}}\n\nPlease review the access policies and acceptable use guidelines at {{policy_url}}. If you believe you have been granted access in error, or if you need additional permissions, please contact your manager or the Security team at {{security_email}}.\n\nThis access will be reviewed periodically as part of our security compliance program.\n\nBest regards,\nAccess Management System\n{{company_name}}",
56
+ "channel": "email"
57
+ },
58
+ {
59
+ "template_id": "tmpl_009",
60
+ "name": "orientation_invite",
61
+ "subject": "You're Invited: New Hire Orientation - {{orientation_date}}",
62
+ "body": "Dear {{first_name}} {{last_name}},\n\nWelcome to {{company_name}}! You are invited to attend the New Hire Orientation session:\n\nDate: {{orientation_date}}\nTime: {{orientation_time}}\nLocation: {{orientation_location}}\nDuration: {{orientation_duration}}\n\nAgenda:\n{{agenda}}\n\nPlease bring:\n- Government-issued photo ID\n- Completed tax forms (W-4 and state equivalent)\n- Voided check or bank details for direct deposit setup\n\nLunch will be provided. If you have dietary restrictions, please reply to this email.\n\nCalendar invite: {{calendar_link}}\n\nIf you have any questions, contact {{hr_contact_name}} at {{hr_contact_email}}.\n\nSee you there!\n\nBest regards,\n{{hr_sender_name}}\nHuman Resources",
63
+ "channel": "email"
64
+ },
65
+ {
66
+ "template_id": "tmpl_010",
67
+ "name": "exit_interview_invite",
68
+ "subject": "Exit Interview Scheduled - {{interview_date}}",
69
+ "body": "Dear {{first_name}} {{last_name}},\n\nAs part of our offboarding process, we would like to invite you to an exit interview:\n\nDate: {{interview_date}}\nTime: {{interview_time}}\nLocation: {{interview_location}}\nInterviewer: {{interviewer_name}}\nDuration: Approximately {{interview_duration}}\n\nThe exit interview is an opportunity for you to share your experiences, feedback, and suggestions. Your input is valuable and helps us improve as an organization. All responses will be kept confidential.\n\nTopics we may discuss:\n- Your overall experience at {{company_name}}\n- Team dynamics and management\n- Career development opportunities\n- Suggestions for improvement\n\nIf the scheduled time does not work, please reply to reschedule.\n\nCalendar invite: {{calendar_link}}\n\nThank you,\n{{hr_sender_name}}\nHuman Resources",
70
+ "channel": "email"
71
+ },
72
+ {
73
+ "template_id": "tmpl_011",
74
+ "name": "manager_notification_new_hire",
75
+ "subject": "New Hire Notification: {{first_name}} {{last_name}} Joining Your Team",
76
+ "body": "Dear {{manager_name}},\n\nThis is to confirm that {{first_name}} {{last_name}} will be joining your team in the {{department}} department.\n\nNew Hire Details:\n- Name: {{first_name}} {{last_name}}\n- Job Title: {{job_title}}\n- Level: {{level}}\n- Start Date: {{start_date}}\n- Office Location: {{office_location}}\n- Employee ID: {{employee_id}}\n\nAs their manager, please complete the following before their start date:\n\n1. Prepare a 30-60-90 day plan\n2. Identify and confirm an onboarding buddy (suggested: {{suggested_buddy}})\n3. Schedule a welcome 1:1 for their first day\n4. Review and approve their access role requests\n5. Ensure their workspace/desk is set up\n6. Add them to relevant team meetings and channels\n\nOnboarding dashboard: {{dashboard_url}}\n\nPlease reach out to {{hr_contact_name}} ({{hr_contact_email}}) if you have any questions.\n\nBest regards,\nHR Onboarding Team",
77
+ "channel": "email"
78
+ },
79
+ {
80
+ "template_id": "tmpl_012",
81
+ "name": "manager_notification_departure",
82
+ "subject": "Employee Departure Notification: {{first_name}} {{last_name}}",
83
+ "body": "Dear {{manager_name}},\n\nThis is to confirm that {{first_name}} {{last_name}} ({{job_title}}, {{department}}) has submitted their resignation / been approved for departure.\n\nDeparture Details:\n- Employee: {{first_name}} {{last_name}} ({{employee_id}})\n- Last Working Day: {{last_day}}\n- Reason: {{departure_reason}}\n- Notice Period: {{notice_period}}\n\nAs their manager, please ensure the following are completed before their last day:\n\n1. Knowledge transfer plan and documentation\n2. Project handover to {{handover_contact_name}}\n3. Return of all IT assets (laptop, monitor, phone, headset, badges)\n4. Removal from team meetings and channels\n5. Final performance notes\n6. Notify clients/stakeholders as appropriate\n\nIT asset return must be completed by {{asset_return_date}}.\nAccess revocation will occur automatically on {{access_revocation_date}}.\n\nOffboarding dashboard: {{dashboard_url}}\n\nPlease reach out to {{hr_contact_name}} ({{hr_contact_email}}) if you have any questions.\n\nBest regards,\nHR Offboarding Team",
84
+ "channel": "email"
85
+ }
86
+ ]
server/hr_onboarding_environment.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HR Onboarding/Offboarding Environment Implementation.
3
+
4
+ An OpenEnv environment that simulates enterprise HR workflows.
5
+ The agent calls tools (hr_create_employee, it_assign_asset, etc.)
6
+ to complete onboarding/offboarding tasks. Reward is computed via rubrics.
7
+ """
8
+
9
+ import json
10
+ import random
11
+ from typing import Any, Dict, List, Optional
12
+ from uuid import uuid4
13
+
14
+ from openenv.core.env_server.interfaces import Environment
15
+ from openenv.core.env_server.types import State
16
+
17
+ from models import HROnboardingAction, HROnboardingObservation
18
+
19
+ try:
20
+ from .world import WorldState
21
+ from .tools import ToolRegistry, TOOL_DEFINITIONS
22
+ from .tasks import TaskGenerator
23
+ from .rubrics import RubricEvaluator
24
+ except ImportError:
25
+ from world import WorldState
26
+ from tools import ToolRegistry, TOOL_DEFINITIONS
27
+ from tasks import TaskGenerator
28
+ from rubrics import RubricEvaluator
29
+
30
+
31
+ class HROnboardingEnvironment(Environment):
32
+ """
33
+ HR Onboarding/Offboarding environment.
34
+
35
+ Simulates an enterprise HR system with 200+ employees, 8 departments,
36
+ RBAC, approval chains, and IT provisioning. The agent calls one of 25
37
+ tools per step to complete onboarding/offboarding tasks.
38
+
39
+ Example:
40
+ >>> env = HROnboardingEnvironment()
41
+ >>> obs = env.reset()
42
+ >>> print(obs.instruction) # "Onboard Priya Sharma to Engineering..."
43
+ >>>
44
+ >>> obs = env.step(HROnboardingAction(
45
+ ... tool_name="hr_create_employee",
46
+ ... arguments={"name": "Priya Sharma", "department": "Engineering",
47
+ ... "level": "L2", "role": "Software Engineer"}
48
+ ... ))
49
+ >>> print(obs.tool_result) # {"success": true, "employee": {...}}
50
+ >>> print(obs.reward) # 0.0 (intermediate) or 0.85 (final)
51
+ """
52
+
53
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
54
+
55
+ def __init__(self, seed: int = 42, max_steps: int = 15):
56
+ """Initialize the HR environment."""
57
+ self._seed = seed
58
+ self._max_steps = max_steps
59
+ self._rng = random.Random(seed)
60
+
61
+ # World state + tools
62
+ self.world = WorldState()
63
+ self.tool_registry = ToolRegistry(self.world)
64
+ self.evaluator = RubricEvaluator()
65
+
66
+ # Tasks
67
+ self._task_gen = TaskGenerator(self.world, seed=seed)
68
+ self._tasks = self._task_gen.generate_all_tasks()
69
+ self._task_idx = 0
70
+ self._current_task = None
71
+
72
+ # Episode state
73
+ self._state = State(episode_id=str(uuid4()), step_count=0)
74
+ self._done = False
75
+ self._tool_names = [t["name"] for t in TOOL_DEFINITIONS]
76
+
77
+ def reset(self) -> HROnboardingObservation:
78
+ """
79
+ Reset the environment for a new episode.
80
+
81
+ Picks the next task, resets world state, returns initial observation
82
+ with the task instruction and available tools.
83
+ """
84
+ self.world.reset()
85
+ self._done = False
86
+
87
+ # Pick next task (cycle through)
88
+ self._current_task = self._tasks[self._task_idx % len(self._tasks)]
89
+ self._task_idx += 1
90
+
91
+ # Apply task setup if any
92
+ if self._current_task.setup_fn:
93
+ self._current_task.setup_fn(self.world)
94
+
95
+ self._state = State(episode_id=str(uuid4()), step_count=0)
96
+
97
+ return HROnboardingObservation(
98
+ task_id=self._current_task.task_id,
99
+ instruction=self._current_task.instruction,
100
+ tool_name="",
101
+ tool_result={},
102
+ step=0,
103
+ max_steps=self._max_steps,
104
+ available_tools=self._tool_names,
105
+ done=False,
106
+ reward=0.0,
107
+ metadata={
108
+ "difficulty": self._current_task.difficulty,
109
+ "category": self._current_task.category,
110
+ "context": self._current_task.context,
111
+ },
112
+ )
113
+
114
+ def step(self, action: HROnboardingAction) -> HROnboardingObservation: # type: ignore[override]
115
+ """
116
+ Execute one step: call the specified tool and return the result.
117
+
118
+ Args:
119
+ action: HROnboardingAction with tool_name and arguments.
120
+
121
+ Returns:
122
+ HROnboardingObservation with tool result, reward (on final step), and done flag.
123
+ """
124
+ if self._done:
125
+ return HROnboardingObservation(
126
+ task_id=self._current_task.task_id if self._current_task else "",
127
+ instruction="",
128
+ tool_name=action.tool_name,
129
+ tool_result={"error": "Episode already finished"},
130
+ step=self._state.step_count,
131
+ max_steps=self._max_steps,
132
+ available_tools=self._tool_names,
133
+ done=True,
134
+ reward=0.0,
135
+ )
136
+
137
+ self._state.step_count += 1
138
+
139
+ # Execute the tool
140
+ result = self.tool_registry.execute(action.tool_name, action.arguments)
141
+
142
+ # Check if episode is done
143
+ done = self._state.step_count >= self._max_steps
144
+ self._done = done
145
+
146
+ # Compute reward on final step
147
+ reward = 0.0
148
+ eval_info = {}
149
+ if done and self._current_task:
150
+ eval_result = self.evaluator.evaluate(self._current_task, self.world.action_log)
151
+ reward = eval_result["score"]
152
+ eval_info = eval_result
153
+
154
+ return HROnboardingObservation(
155
+ task_id=self._current_task.task_id if self._current_task else "",
156
+ instruction=self._current_task.instruction if self._current_task else "",
157
+ tool_name=action.tool_name,
158
+ tool_result=result,
159
+ step=self._state.step_count,
160
+ max_steps=self._max_steps,
161
+ available_tools=self._tool_names,
162
+ done=done,
163
+ reward=reward,
164
+ metadata={
165
+ "step": self._state.step_count,
166
+ **({"evaluation": eval_info} if eval_info else {}),
167
+ },
168
+ )
169
+
170
+ @property
171
+ def state(self) -> State:
172
+ """Get the current environment state."""
173
+ return self._state
server/rubrics.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reward/evaluation rubrics for HR Onboarding/Offboarding tasks.
2
+
3
+ Each task has a set of rubric criteria. This module evaluates agent action logs
4
+ against those criteria to compute rewards.
5
+ """
6
+
7
+ import re
8
+ from typing import Any
9
+ try:
10
+ from .tasks import Task
11
+ except ImportError:
12
+ from tasks import Task
13
+
14
+
15
+ class RubricEvaluator:
16
+ """Evaluates agent performance against task rubric criteria."""
17
+
18
+ def __init__(self):
19
+ self._checkers = {
20
+ "tool_used": self._check_tool_used,
21
+ "tool_not_used": self._check_tool_not_used,
22
+ "tool_used_any": self._check_tool_used_any,
23
+ "param_value": self._check_param_value,
24
+ "param_contains": self._check_param_contains,
25
+ "tool_order": self._check_tool_order,
26
+ "tool_count": self._check_tool_count,
27
+ "result_contains": self._check_result_contains,
28
+ }
29
+
30
+ def evaluate(self, task: Task, action_log: list[dict]) -> dict:
31
+ """Evaluate action log against task rubric criteria.
32
+
33
+ Returns:
34
+ {
35
+ "task_id": str,
36
+ "criteria_results": list of {name, passed, description},
37
+ "score": float (0.0-1.0),
38
+ "passed": bool (all criteria satisfied),
39
+ }
40
+ """
41
+ criteria_results = []
42
+ for criterion in task.rubric_criteria:
43
+ check_str = criterion["check"]
44
+ passed = self._evaluate_criterion(check_str, action_log)
45
+ criteria_results.append({
46
+ "name": criterion["name"],
47
+ "description": criterion["description"],
48
+ "passed": passed,
49
+ })
50
+
51
+ total = len(criteria_results)
52
+ passed_count = sum(1 for c in criteria_results if c["passed"])
53
+ score = passed_count / total if total > 0 else 0.0
54
+
55
+ return {
56
+ "task_id": task.task_id,
57
+ "criteria_results": criteria_results,
58
+ "score": score,
59
+ "passed": all(c["passed"] for c in criteria_results),
60
+ "passed_count": passed_count,
61
+ "total_criteria": total,
62
+ }
63
+
64
+ def _evaluate_criterion(self, check_str: str, action_log: list[dict]) -> bool:
65
+ """Parse and evaluate a single criterion check string."""
66
+ # Parse check type and args
67
+ parts = check_str.split(":", 1)
68
+ if len(parts) != 2:
69
+ return False
70
+
71
+ check_type = parts[0]
72
+ check_args = parts[1]
73
+
74
+ checker = self._checkers.get(check_type)
75
+ if not checker:
76
+ return False
77
+
78
+ return checker(check_args, action_log)
79
+
80
+ def _check_tool_used(self, tool_name: str, action_log: list[dict]) -> bool:
81
+ """Check if a specific tool was used at least once."""
82
+ return any(a["tool"] == tool_name for a in action_log)
83
+
84
+ def _check_tool_not_used(self, tool_name: str, action_log: list[dict]) -> bool:
85
+ """Check that a specific tool was NOT used."""
86
+ return not any(a["tool"] == tool_name for a in action_log)
87
+
88
+ def _check_tool_used_any(self, tools_csv: str, action_log: list[dict]) -> bool:
89
+ """Check if any of the comma-separated tools were used."""
90
+ tool_names = [t.strip() for t in tools_csv.split(",")]
91
+ return any(a["tool"] in tool_names for a in action_log)
92
+
93
+ def _check_param_value(self, spec: str, action_log: list[dict]) -> bool:
94
+ """Check if a tool was called with a specific parameter value.
95
+ Format: tool_name.param_name=expected_value
96
+ """
97
+ match = re.match(r"(\w+)\.(\w+)=(.+)", spec)
98
+ if not match:
99
+ return False
100
+ tool_name, param_name, expected_value = match.groups()
101
+
102
+ for action in action_log:
103
+ if action["tool"] == tool_name:
104
+ actual = action["params"].get(param_name)
105
+ if actual is not None and str(actual) == expected_value:
106
+ return True
107
+ # Check nested in 'updates' dict
108
+ updates = action["params"].get("updates", {})
109
+ if param_name in updates and str(updates[param_name]) == expected_value:
110
+ return True
111
+ return False
112
+
113
+ def _check_param_contains(self, spec: str, action_log: list[dict]) -> bool:
114
+ """Check if a tool parameter contains a substring.
115
+ Format: tool_name.param_name=substring
116
+ """
117
+ match = re.match(r"(\w+)\.(\w+)=(.+)", spec)
118
+ if not match:
119
+ return False
120
+ tool_name, param_name, substring = match.groups()
121
+
122
+ for action in action_log:
123
+ if action["tool"] == tool_name:
124
+ actual = action["params"].get(param_name, "")
125
+ if substring.lower() in str(actual).lower():
126
+ return True
127
+ return False
128
+
129
+ def _check_tool_order(self, spec: str, action_log: list[dict]) -> bool:
130
+ """Check that tool A was called before tool B.
131
+ Format: tool_a<tool_b
132
+ """
133
+ parts = spec.split("<")
134
+ if len(parts) != 2:
135
+ return False
136
+ tool_a, tool_b = parts
137
+
138
+ idx_a = None
139
+ idx_b = None
140
+ for i, action in enumerate(action_log):
141
+ if action["tool"] == tool_a and idx_a is None:
142
+ idx_a = i
143
+ if action["tool"] == tool_b and idx_b is None:
144
+ idx_b = i
145
+
146
+ if idx_a is None or idx_b is None:
147
+ return False
148
+ return idx_a < idx_b
149
+
150
+ def _check_tool_count(self, spec: str, action_log: list[dict]) -> bool:
151
+ """Check that a tool was called at least N times.
152
+ Format: tool_name>=N
153
+ """
154
+ match = re.match(r"(\w+)>=(\d+)", spec)
155
+ if not match:
156
+ return False
157
+ tool_name, min_count = match.groups()
158
+ min_count = int(min_count)
159
+
160
+ count = sum(1 for a in action_log if a["tool"] == tool_name)
161
+ return count >= min_count
162
+
163
+ def _check_result_contains(self, substring: str, action_log: list[dict]) -> bool:
164
+ """Check if any tool result contains a substring."""
165
+ for action in action_log:
166
+ result_str = str(action.get("result", ""))
167
+ if substring.lower() in result_str.lower():
168
+ return True
169
+ return False
170
+
171
+
172
+ def compute_reward(task: Task, action_log: list[dict]) -> float:
173
+ """Convenience function to compute reward for a task given action log."""
174
+ evaluator = RubricEvaluator()
175
+ result = evaluator.evaluate(task, action_log)
176
+ return result["score"]
server/static/index.html ADDED
@@ -0,0 +1,831 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HR Onboarding Environment</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+
10
+ body {
11
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
12
+ background: #0f1117;
13
+ color: #e0e0e0;
14
+ min-height: 100vh;
15
+ }
16
+
17
+ .header {
18
+ background: linear-gradient(135deg, #1a1f2e 0%, #0f1117 100%);
19
+ border-bottom: 1px solid #2a2f3e;
20
+ padding: 20px 32px;
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 16px;
24
+ }
25
+
26
+ .header-icon {
27
+ font-size: 32px;
28
+ }
29
+
30
+ .header h1 {
31
+ font-size: 22px;
32
+ font-weight: 700;
33
+ color: #fff;
34
+ }
35
+
36
+ .header p {
37
+ font-size: 13px;
38
+ color: #888;
39
+ margin-top: 2px;
40
+ }
41
+
42
+ .header-badges {
43
+ margin-left: auto;
44
+ display: flex;
45
+ gap: 8px;
46
+ }
47
+
48
+ .badge {
49
+ background: #1e2433;
50
+ border: 1px solid #2a3040;
51
+ border-radius: 20px;
52
+ padding: 4px 12px;
53
+ font-size: 12px;
54
+ color: #8899aa;
55
+ }
56
+
57
+ .badge b { color: #58a6ff; }
58
+
59
+ .container {
60
+ display: grid;
61
+ grid-template-columns: 340px 1fr 320px;
62
+ height: calc(100vh - 80px);
63
+ gap: 0;
64
+ }
65
+
66
+ /* Left panel - Task selector */
67
+ .panel-left {
68
+ background: #13161f;
69
+ border-right: 1px solid #1e2230;
70
+ display: flex;
71
+ flex-direction: column;
72
+ overflow: hidden;
73
+ }
74
+
75
+ .panel-header {
76
+ padding: 16px;
77
+ border-bottom: 1px solid #1e2230;
78
+ font-size: 13px;
79
+ font-weight: 600;
80
+ text-transform: uppercase;
81
+ letter-spacing: 0.5px;
82
+ color: #667;
83
+ }
84
+
85
+ .task-filters {
86
+ padding: 12px 16px;
87
+ display: flex;
88
+ gap: 6px;
89
+ flex-wrap: wrap;
90
+ border-bottom: 1px solid #1e2230;
91
+ }
92
+
93
+ .filter-btn {
94
+ background: #1a1f2e;
95
+ border: 1px solid #2a2f3e;
96
+ border-radius: 6px;
97
+ padding: 4px 10px;
98
+ font-size: 11px;
99
+ color: #889;
100
+ cursor: pointer;
101
+ transition: all 0.15s;
102
+ }
103
+
104
+ .filter-btn:hover { border-color: #58a6ff; color: #58a6ff; }
105
+ .filter-btn.active { background: #1c2d4a; border-color: #58a6ff; color: #58a6ff; }
106
+
107
+ .task-list {
108
+ flex: 1;
109
+ overflow-y: auto;
110
+ padding: 8px;
111
+ }
112
+
113
+ .task-item {
114
+ padding: 10px 12px;
115
+ border-radius: 8px;
116
+ cursor: pointer;
117
+ margin-bottom: 4px;
118
+ transition: all 0.15s;
119
+ }
120
+
121
+ .task-item:hover { background: #1a1f2e; }
122
+ .task-item.active { background: #1c2d4a; border-left: 3px solid #58a6ff; }
123
+
124
+ .task-item .task-id {
125
+ font-size: 11px;
126
+ color: #556;
127
+ font-family: monospace;
128
+ }
129
+
130
+ .task-item .task-title {
131
+ font-size: 13px;
132
+ color: #ccc;
133
+ margin-top: 2px;
134
+ line-height: 1.4;
135
+ display: -webkit-box;
136
+ -webkit-line-clamp: 2;
137
+ -webkit-box-orient: vertical;
138
+ overflow: hidden;
139
+ }
140
+
141
+ .task-item .task-meta {
142
+ display: flex;
143
+ gap: 6px;
144
+ margin-top: 6px;
145
+ }
146
+
147
+ .task-tag {
148
+ font-size: 10px;
149
+ padding: 2px 6px;
150
+ border-radius: 4px;
151
+ font-weight: 600;
152
+ text-transform: uppercase;
153
+ }
154
+
155
+ .tag-simple { background: #1a3a2a; color: #4ade80; }
156
+ .tag-medium { background: #3a3a1a; color: #facc15; }
157
+ .tag-complex { background: #3a1a1a; color: #f87171; }
158
+ .tag-edge_case { background: #2a1a3a; color: #c084fc; }
159
+ .tag-lookup { background: #1a2a3a; color: #60a5fa; }
160
+ .tag-onboarding { background: #1a3a2a; color: #34d399; }
161
+ .tag-offboarding { background: #3a2a1a; color: #fb923c; }
162
+ .tag-cross_workflow { background: #2a1a3a; color: #a78bfa; }
163
+
164
+ /* Center panel - Main interaction area */
165
+ .panel-center {
166
+ display: flex;
167
+ flex-direction: column;
168
+ overflow: hidden;
169
+ }
170
+
171
+ .task-instruction {
172
+ padding: 20px 24px;
173
+ background: #161a24;
174
+ border-bottom: 1px solid #1e2230;
175
+ }
176
+
177
+ .task-instruction h3 {
178
+ font-size: 14px;
179
+ color: #58a6ff;
180
+ margin-bottom: 8px;
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 8px;
184
+ }
185
+
186
+ .task-instruction p {
187
+ font-size: 14px;
188
+ line-height: 1.6;
189
+ color: #d0d0d0;
190
+ }
191
+
192
+ .step-indicator {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 8px;
196
+ margin-top: 12px;
197
+ font-size: 12px;
198
+ color: #667;
199
+ }
200
+
201
+ .step-bar {
202
+ flex: 1;
203
+ height: 4px;
204
+ background: #1e2230;
205
+ border-radius: 2px;
206
+ overflow: hidden;
207
+ }
208
+
209
+ .step-bar-fill {
210
+ height: 100%;
211
+ background: #58a6ff;
212
+ border-radius: 2px;
213
+ transition: width 0.3s ease;
214
+ }
215
+
216
+ .action-log {
217
+ flex: 1;
218
+ overflow-y: auto;
219
+ padding: 16px 24px;
220
+ }
221
+
222
+ .log-entry {
223
+ margin-bottom: 16px;
224
+ animation: fadeIn 0.3s ease;
225
+ }
226
+
227
+ @keyframes fadeIn {
228
+ from { opacity: 0; transform: translateY(8px); }
229
+ to { opacity: 1; transform: translateY(0); }
230
+ }
231
+
232
+ .log-step-label {
233
+ font-size: 11px;
234
+ color: #556;
235
+ margin-bottom: 4px;
236
+ font-family: monospace;
237
+ }
238
+
239
+ .log-action {
240
+ background: #1a1f2e;
241
+ border: 1px solid #252b3b;
242
+ border-radius: 8px;
243
+ padding: 12px;
244
+ margin-bottom: 6px;
245
+ }
246
+
247
+ .log-action .tool-name {
248
+ color: #58a6ff;
249
+ font-family: monospace;
250
+ font-size: 13px;
251
+ font-weight: 600;
252
+ }
253
+
254
+ .log-action pre {
255
+ margin-top: 6px;
256
+ font-size: 12px;
257
+ color: #8899aa;
258
+ white-space: pre-wrap;
259
+ word-break: break-word;
260
+ font-family: 'JetBrains Mono', monospace;
261
+ max-height: 120px;
262
+ overflow-y: auto;
263
+ }
264
+
265
+ .log-result {
266
+ background: #141820;
267
+ border: 1px solid #1e2430;
268
+ border-radius: 8px;
269
+ padding: 12px;
270
+ }
271
+
272
+ .log-result.success { border-left: 3px solid #4ade80; }
273
+ .log-result.error { border-left: 3px solid #f87171; }
274
+
275
+ .log-result pre {
276
+ font-size: 12px;
277
+ color: #8899aa;
278
+ white-space: pre-wrap;
279
+ word-break: break-word;
280
+ font-family: 'JetBrains Mono', monospace;
281
+ max-height: 150px;
282
+ overflow-y: auto;
283
+ }
284
+
285
+ /* Input area */
286
+ .input-area {
287
+ border-top: 1px solid #1e2230;
288
+ padding: 16px 24px;
289
+ background: #13161f;
290
+ }
291
+
292
+ .tool-select-row {
293
+ display: flex;
294
+ gap: 8px;
295
+ margin-bottom: 10px;
296
+ }
297
+
298
+ .tool-select-row select {
299
+ flex: 1;
300
+ background: #1a1f2e;
301
+ border: 1px solid #2a3040;
302
+ border-radius: 8px;
303
+ padding: 8px 12px;
304
+ color: #d0d0d0;
305
+ font-size: 13px;
306
+ font-family: monospace;
307
+ }
308
+
309
+ .tool-select-row select:focus { outline: none; border-color: #58a6ff; }
310
+
311
+ .params-input {
312
+ width: 100%;
313
+ background: #1a1f2e;
314
+ border: 1px solid #2a3040;
315
+ border-radius: 8px;
316
+ padding: 10px 14px;
317
+ color: #d0d0d0;
318
+ font-size: 13px;
319
+ font-family: 'JetBrains Mono', monospace;
320
+ resize: vertical;
321
+ min-height: 60px;
322
+ }
323
+
324
+ .params-input:focus { outline: none; border-color: #58a6ff; }
325
+
326
+ .params-input::placeholder { color: #445; }
327
+
328
+ .input-buttons {
329
+ display: flex;
330
+ gap: 8px;
331
+ margin-top: 10px;
332
+ }
333
+
334
+ .btn {
335
+ padding: 8px 20px;
336
+ border-radius: 8px;
337
+ font-size: 13px;
338
+ font-weight: 600;
339
+ cursor: pointer;
340
+ border: none;
341
+ transition: all 0.15s;
342
+ }
343
+
344
+ .btn-primary {
345
+ background: #58a6ff;
346
+ color: #000;
347
+ }
348
+ .btn-primary:hover { background: #79b8ff; }
349
+ .btn-primary:disabled { background: #2a3a4a; color: #556; cursor: not-allowed; }
350
+
351
+ .btn-secondary {
352
+ background: #1e2433;
353
+ color: #889;
354
+ border: 1px solid #2a3040;
355
+ }
356
+ .btn-secondary:hover { border-color: #58a6ff; color: #58a6ff; }
357
+
358
+ .btn-danger {
359
+ background: #3a1a1a;
360
+ color: #f87171;
361
+ border: 1px solid #4a2020;
362
+ }
363
+ .btn-danger:hover { background: #4a2020; }
364
+
365
+ /* Right panel - Info & Evaluation */
366
+ .panel-right {
367
+ background: #13161f;
368
+ border-left: 1px solid #1e2230;
369
+ display: flex;
370
+ flex-direction: column;
371
+ overflow: hidden;
372
+ }
373
+
374
+ .tools-section {
375
+ flex: 1;
376
+ overflow-y: auto;
377
+ padding: 12px;
378
+ }
379
+
380
+ .tool-info {
381
+ padding: 8px 10px;
382
+ border-radius: 6px;
383
+ cursor: pointer;
384
+ font-size: 12px;
385
+ font-family: monospace;
386
+ color: #8899aa;
387
+ transition: all 0.15s;
388
+ }
389
+
390
+ .tool-info:hover { background: #1a1f2e; color: #58a6ff; }
391
+
392
+ .tool-info .tool-desc {
393
+ font-family: 'Inter', sans-serif;
394
+ font-size: 11px;
395
+ color: #556;
396
+ margin-top: 2px;
397
+ display: none;
398
+ }
399
+
400
+ .tool-info:hover .tool-desc { display: block; }
401
+
402
+ /* Evaluation panel */
403
+ .eval-section {
404
+ border-top: 1px solid #1e2230;
405
+ padding: 16px;
406
+ max-height: 50%;
407
+ overflow-y: auto;
408
+ }
409
+
410
+ .eval-header {
411
+ display: flex;
412
+ align-items: center;
413
+ justify-content: space-between;
414
+ margin-bottom: 12px;
415
+ }
416
+
417
+ .eval-score {
418
+ font-size: 28px;
419
+ font-weight: 800;
420
+ color: #58a6ff;
421
+ }
422
+
423
+ .eval-score.pass { color: #4ade80; }
424
+ .eval-score.fail { color: #f87171; }
425
+ .eval-score.partial { color: #facc15; }
426
+
427
+ .eval-criteria {
428
+ list-style: none;
429
+ }
430
+
431
+ .eval-criteria li {
432
+ padding: 6px 0;
433
+ font-size: 12px;
434
+ display: flex;
435
+ align-items: flex-start;
436
+ gap: 8px;
437
+ border-bottom: 1px solid #1a1f2a;
438
+ }
439
+
440
+ .eval-criteria .icon-pass { color: #4ade80; }
441
+ .eval-criteria .icon-fail { color: #f87171; }
442
+
443
+ .eval-criteria .criteria-desc {
444
+ color: #889;
445
+ font-size: 11px;
446
+ }
447
+
448
+ /* Welcome state */
449
+ .welcome {
450
+ display: flex;
451
+ flex-direction: column;
452
+ align-items: center;
453
+ justify-content: center;
454
+ height: 100%;
455
+ text-align: center;
456
+ padding: 40px;
457
+ }
458
+
459
+ .welcome h2 {
460
+ font-size: 20px;
461
+ color: #fff;
462
+ margin-bottom: 8px;
463
+ }
464
+
465
+ .welcome p {
466
+ color: #667;
467
+ font-size: 14px;
468
+ max-width: 400px;
469
+ line-height: 1.6;
470
+ }
471
+
472
+ /* Scrollbar styling */
473
+ ::-webkit-scrollbar { width: 6px; }
474
+ ::-webkit-scrollbar-track { background: transparent; }
475
+ ::-webkit-scrollbar-thumb { background: #2a3040; border-radius: 3px; }
476
+ ::-webkit-scrollbar-thumb:hover { background: #3a4050; }
477
+
478
+ /* Loading spinner */
479
+ .spinner {
480
+ display: inline-block;
481
+ width: 14px;
482
+ height: 14px;
483
+ border: 2px solid #2a3040;
484
+ border-top-color: #58a6ff;
485
+ border-radius: 50%;
486
+ animation: spin 0.6s linear infinite;
487
+ }
488
+
489
+ @keyframes spin { to { transform: rotate(360deg); } }
490
+
491
+ @media (max-width: 1024px) {
492
+ .container {
493
+ grid-template-columns: 1fr;
494
+ grid-template-rows: auto 1fr;
495
+ }
496
+ .panel-left, .panel-right { display: none; }
497
+ }
498
+ </style>
499
+ </head>
500
+ <body>
501
+ <div class="header">
502
+ <div class="header-icon">🏢</div>
503
+ <div>
504
+ <h1>HR Onboarding & Offboarding Environment</h1>
505
+ <p>OpenEnv RL Environment — Interactive Playground</p>
506
+ </div>
507
+ <div class="header-badges">
508
+ <span class="badge"><b>25</b> Tools</span>
509
+ <span class="badge"><b>77</b> Tasks</span>
510
+ <span class="badge"><b>200</b> Employees</span>
511
+ <span class="badge"><b>15</b> Max Steps</span>
512
+ </div>
513
+ </div>
514
+
515
+ <div class="container">
516
+ <!-- Left: Task Selector -->
517
+ <div class="panel-left">
518
+ <div class="panel-header">Tasks</div>
519
+ <div class="task-filters">
520
+ <button class="filter-btn active" data-filter="all">All</button>
521
+ <button class="filter-btn" data-filter="simple">Simple</button>
522
+ <button class="filter-btn" data-filter="medium">Medium</button>
523
+ <button class="filter-btn" data-filter="complex">Complex</button>
524
+ <button class="filter-btn" data-filter="edge_case">Edge Case</button>
525
+ </div>
526
+ <div class="task-list" id="taskList">
527
+ <div style="padding: 20px; color: #556; font-size: 13px;">Loading tasks...</div>
528
+ </div>
529
+ </div>
530
+
531
+ <!-- Center: Interaction Area -->
532
+ <div class="panel-center">
533
+ <div class="task-instruction" id="taskInstruction">
534
+ <div class="welcome">
535
+ <h2>Select a task to begin</h2>
536
+ <p>Pick a task from the left panel. You'll get an instruction, then call tools step by step to complete it. Your performance is scored by a rubric at the end.</p>
537
+ </div>
538
+ </div>
539
+
540
+ <div class="action-log" id="actionLog"></div>
541
+
542
+ <div class="input-area" id="inputArea" style="display: none;">
543
+ <div class="tool-select-row">
544
+ <select id="toolSelect">
545
+ <option value="">-- select a tool --</option>
546
+ </select>
547
+ </div>
548
+ <textarea class="params-input" id="paramsInput" placeholder='{"emp_id": "emp_0001"}'></textarea>
549
+ <div class="input-buttons">
550
+ <button class="btn btn-primary" id="btnStep" onclick="sendStep()">Send Tool Call</button>
551
+ <button class="btn btn-secondary" id="btnDone" onclick="finishEpisode()">Finish & Evaluate</button>
552
+ <button class="btn btn-danger" id="btnReset" onclick="resetCurrentTask()">Reset Task</button>
553
+ </div>
554
+ </div>
555
+ </div>
556
+
557
+ <!-- Right: Tools & Evaluation -->
558
+ <div class="panel-right">
559
+ <div class="panel-header">Available Tools</div>
560
+ <div class="tools-section" id="toolsSection"></div>
561
+ <div class="eval-section" id="evalSection" style="display: none;">
562
+ <div class="panel-header" style="padding: 0 0 8px 0; border: none;">Evaluation</div>
563
+ <div class="eval-header">
564
+ <span class="eval-score" id="evalScore">--</span>
565
+ <span id="evalLabel" style="font-size: 12px; color: #667;"></span>
566
+ </div>
567
+ <ul class="eval-criteria" id="evalCriteria"></ul>
568
+ </div>
569
+ </div>
570
+ </div>
571
+
572
+ <script>
573
+ const API_BASE = window.location.origin;
574
+ let tasks = [];
575
+ let currentTaskIdx = null;
576
+ let currentStep = 0;
577
+ let maxSteps = 15;
578
+ let episodeDone = false;
579
+ let availableTools = [];
580
+ let toolDefs = [];
581
+
582
+ // --- Init ---
583
+ async function init() {
584
+ await loadTasks();
585
+ await loadToolDefs();
586
+ renderTaskList();
587
+ renderToolsPanel();
588
+ }
589
+
590
+ async function loadTasks() {
591
+ const res = await fetch(`${API_BASE}/api/tasks`);
592
+ tasks = await res.json();
593
+ }
594
+
595
+ async function loadToolDefs() {
596
+ const res = await fetch(`${API_BASE}/api/tool_definitions`);
597
+ toolDefs = await res.json();
598
+ }
599
+
600
+ // --- Task List ---
601
+ function renderTaskList(filter = 'all') {
602
+ const list = document.getElementById('taskList');
603
+ const filtered = filter === 'all' ? tasks : tasks.filter(t => t.difficulty === filter);
604
+ list.innerHTML = filtered.map((t, i) => `
605
+ <div class="task-item" data-idx="${t.index}" onclick="selectTask(${t.index})">
606
+ <div class="task-id">${t.task_id}</div>
607
+ <div class="task-title">${t.instruction}</div>
608
+ <div class="task-meta">
609
+ <span class="task-tag tag-${t.difficulty}">${t.difficulty}</span>
610
+ <span class="task-tag tag-${t.category}">${t.category}</span>
611
+ </div>
612
+ </div>
613
+ `).join('');
614
+ }
615
+
616
+ // Filter buttons
617
+ document.querySelectorAll('.filter-btn').forEach(btn => {
618
+ btn.addEventListener('click', () => {
619
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
620
+ btn.classList.add('active');
621
+ renderTaskList(btn.dataset.filter);
622
+ });
623
+ });
624
+
625
+ // --- Select & Reset Task ---
626
+ async function selectTask(idx) {
627
+ currentTaskIdx = idx;
628
+ currentStep = 0;
629
+ episodeDone = false;
630
+
631
+ // Highlight active
632
+ document.querySelectorAll('.task-item').forEach(el => el.classList.remove('active'));
633
+ const active = document.querySelector(`.task-item[data-idx="${idx}"]`);
634
+ if (active) active.classList.add('active');
635
+
636
+ // Reset env
637
+ const res = await fetch(`${API_BASE}/api/reset`, {
638
+ method: 'POST',
639
+ headers: { 'Content-Type': 'application/json' },
640
+ body: JSON.stringify({ task_idx: idx }),
641
+ });
642
+ const data = await res.json();
643
+
644
+ // Update instruction
645
+ const instrEl = document.getElementById('taskInstruction');
646
+ const task = tasks.find(t => t.index === idx);
647
+ instrEl.innerHTML = `
648
+ <h3>
649
+ <span class="task-tag tag-${task.difficulty}">${task.difficulty}</span>
650
+ <span class="task-tag tag-${task.category}">${task.category}</span>
651
+ ${data.task_id}
652
+ </h3>
653
+ <p>${data.instruction}</p>
654
+ <div class="step-indicator">
655
+ <span>Step ${currentStep}/${maxSteps}</span>
656
+ <div class="step-bar"><div class="step-bar-fill" style="width: 0%"></div></div>
657
+ </div>
658
+ `;
659
+
660
+ // Clear log & show input
661
+ document.getElementById('actionLog').innerHTML = '';
662
+ document.getElementById('inputArea').style.display = 'block';
663
+ document.getElementById('evalSection').style.display = 'none';
664
+
665
+ // Populate tool select
666
+ availableTools = data.available_tools || [];
667
+ const sel = document.getElementById('toolSelect');
668
+ sel.innerHTML = '<option value="">-- select a tool --</option>' +
669
+ availableTools.map(t => `<option value="${t}">${t}</option>`).join('');
670
+
671
+ updateButtons();
672
+ }
673
+
674
+ async function resetCurrentTask() {
675
+ if (currentTaskIdx !== null) {
676
+ await selectTask(currentTaskIdx);
677
+ }
678
+ }
679
+
680
+ // --- Send Step ---
681
+ async function sendStep() {
682
+ const toolName = document.getElementById('toolSelect').value;
683
+ if (!toolName) { alert('Select a tool first'); return; }
684
+
685
+ let params = {};
686
+ const paramsText = document.getElementById('paramsInput').value.trim();
687
+ if (paramsText) {
688
+ try {
689
+ params = JSON.parse(paramsText);
690
+ } catch (e) {
691
+ alert('Invalid JSON in parameters: ' + e.message);
692
+ return;
693
+ }
694
+ }
695
+
696
+ document.getElementById('btnStep').disabled = true;
697
+ document.getElementById('btnStep').innerHTML = '<span class="spinner"></span> Running...';
698
+
699
+ const res = await fetch(`${API_BASE}/api/step`, {
700
+ method: 'POST',
701
+ headers: { 'Content-Type': 'application/json' },
702
+ body: JSON.stringify({ tool_name: toolName, arguments: params }),
703
+ });
704
+ const data = await res.json();
705
+
706
+ currentStep = data.step || currentStep + 1;
707
+ episodeDone = data.done || false;
708
+
709
+ // Add to log
710
+ addLogEntry(toolName, params, data.tool_result, data.done, data.reward);
711
+
712
+ // Update step indicator
713
+ updateStepIndicator();
714
+
715
+ // If done, show evaluation
716
+ if (episodeDone && data.metadata && data.metadata.evaluation) {
717
+ showEvaluation(data.metadata.evaluation);
718
+ }
719
+
720
+ // Clear input
721
+ document.getElementById('paramsInput').value = '';
722
+ updateButtons();
723
+ }
724
+
725
+ async function finishEpisode() {
726
+ // Keep calling step until done to trigger evaluation
727
+ if (!episodeDone) {
728
+ // Send a no-op step to trigger final evaluation
729
+ const res = await fetch(`${API_BASE}/api/evaluate`, {
730
+ method: 'POST',
731
+ headers: { 'Content-Type': 'application/json' },
732
+ });
733
+ const data = await res.json();
734
+ episodeDone = true;
735
+ showEvaluation(data);
736
+ updateButtons();
737
+ }
738
+ }
739
+
740
+ // --- Log ---
741
+ function addLogEntry(toolName, params, result, done, reward) {
742
+ const log = document.getElementById('actionLog');
743
+ const isSuccess = result && result.success !== false;
744
+ const resultJson = JSON.stringify(result, null, 2);
745
+ const paramsJson = JSON.stringify(params, null, 2);
746
+
747
+ const entry = document.createElement('div');
748
+ entry.className = 'log-entry';
749
+ entry.innerHTML = `
750
+ <div class="log-step-label">Step ${currentStep}</div>
751
+ <div class="log-action">
752
+ <span class="tool-name">${toolName}</span>
753
+ <pre>${paramsJson}</pre>
754
+ </div>
755
+ <div class="log-result ${isSuccess ? 'success' : 'error'}">
756
+ <pre>${resultJson.length > 800 ? resultJson.substring(0, 800) + '\n...' : resultJson}</pre>
757
+ </div>
758
+ `;
759
+ log.appendChild(entry);
760
+ log.scrollTop = log.scrollHeight;
761
+ }
762
+
763
+ // --- Evaluation ---
764
+ function showEvaluation(evalData) {
765
+ const section = document.getElementById('evalSection');
766
+ section.style.display = 'block';
767
+
768
+ const score = evalData.score || 0;
769
+ const scoreEl = document.getElementById('evalScore');
770
+ scoreEl.textContent = `${Math.round(score * 100)}%`;
771
+ scoreEl.className = 'eval-score ' + (score >= 1 ? 'pass' : score >= 0.5 ? 'partial' : 'fail');
772
+
773
+ const labelEl = document.getElementById('evalLabel');
774
+ labelEl.textContent = evalData.passed ? 'ALL CRITERIA MET' : `${evalData.passed_count}/${evalData.total_criteria} criteria`;
775
+
776
+ const criteria = document.getElementById('evalCriteria');
777
+ criteria.innerHTML = (evalData.criteria_results || []).map(c => `
778
+ <li>
779
+ <span class="${c.passed ? 'icon-pass' : 'icon-fail'}">${c.passed ? '✓' : '✗'}</span>
780
+ <div>
781
+ <div style="color: ${c.passed ? '#4ade80' : '#f87171'}">${c.name}</div>
782
+ <div class="criteria-desc">${c.description}</div>
783
+ </div>
784
+ </li>
785
+ `).join('');
786
+ }
787
+
788
+ // --- Helpers ---
789
+ function updateStepIndicator() {
790
+ const pct = (currentStep / maxSteps) * 100;
791
+ const indicator = document.querySelector('.step-indicator');
792
+ if (indicator) {
793
+ indicator.querySelector('span').textContent = `Step ${currentStep}/${maxSteps}`;
794
+ indicator.querySelector('.step-bar-fill').style.width = `${pct}%`;
795
+ }
796
+ }
797
+
798
+ function updateButtons() {
799
+ document.getElementById('btnStep').disabled = episodeDone;
800
+ document.getElementById('btnStep').innerHTML = episodeDone ? 'Episode Done' : 'Send Tool Call';
801
+ }
802
+
803
+ function renderToolsPanel() {
804
+ const section = document.getElementById('toolsSection');
805
+ section.innerHTML = toolDefs.map(t => `
806
+ <div class="tool-info" onclick="selectTool('${t.name}')">
807
+ ${t.name}
808
+ <div class="tool-desc">${t.description.substring(0, 80)}${t.description.length > 80 ? '...' : ''}</div>
809
+ </div>
810
+ `).join('');
811
+ }
812
+
813
+ function selectTool(name) {
814
+ document.getElementById('toolSelect').value = name;
815
+ // Show parameter hints
816
+ const tool = toolDefs.find(t => t.name === name);
817
+ if (tool && tool.parameters && tool.parameters.properties) {
818
+ const props = tool.parameters.properties;
819
+ const required = tool.parameters.required || [];
820
+ const hint = {};
821
+ for (const [key, val] of Object.entries(props)) {
822
+ hint[key] = val.description || val.type;
823
+ }
824
+ document.getElementById('paramsInput').placeholder = JSON.stringify(hint, null, 2);
825
+ }
826
+ }
827
+
828
+ init();
829
+ </script>
830
+ </body>
831
+ </html>
server/tasks.py ADDED
@@ -0,0 +1,871 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task definitions and generation for HR Onboarding/Offboarding environment.
2
+
3
+ Each task has:
4
+ - A natural language instruction
5
+ - Difficulty level (simple, medium, complex, edge_case)
6
+ - Category (onboarding, offboarding, cross_workflow, lookup)
7
+ - Expected tool sequence (for rubric evaluation)
8
+ - Rubric criteria
9
+ - World state setup (pre-conditions to set before the task)
10
+ """
11
+
12
+ import random
13
+ import copy
14
+ from typing import Any, Optional
15
+ try:
16
+ from .world import WorldState
17
+ except ImportError:
18
+ from world import WorldState
19
+
20
+
21
+ class Task:
22
+ """A single task definition."""
23
+
24
+ def __init__(self, task_id: str, instruction: str, difficulty: str, category: str,
25
+ expected_tools: list[str], rubric_criteria: list[dict],
26
+ setup_fn: Any = None, context: dict = None):
27
+ self.task_id = task_id
28
+ self.instruction = instruction
29
+ self.difficulty = difficulty # simple, medium, complex, edge_case
30
+ self.category = category # onboarding, offboarding, cross_workflow, lookup
31
+ self.expected_tools = expected_tools
32
+ self.rubric_criteria = rubric_criteria
33
+ self.setup_fn = setup_fn # function to prepare world state
34
+ self.context = context or {} # dynamic context (emp_ids, etc.)
35
+
36
+ def to_dict(self) -> dict:
37
+ return {
38
+ "task_id": self.task_id,
39
+ "instruction": self.instruction,
40
+ "difficulty": self.difficulty,
41
+ "category": self.category,
42
+ "expected_tools": self.expected_tools,
43
+ "rubric_criteria": [c for c in self.rubric_criteria],
44
+ "context": self.context,
45
+ }
46
+
47
+
48
+ def _pick_employee(world: WorldState, status: str = "active", department: str = None,
49
+ level: str = None, has_manager: bool = None) -> Optional[dict]:
50
+ """Pick a random employee matching criteria."""
51
+ candidates = world.state["employees"]
52
+ if status:
53
+ candidates = [e for e in candidates if e["status"] == status]
54
+ if department:
55
+ candidates = [e for e in candidates if e["department"] == department]
56
+ if level:
57
+ candidates = [e for e in candidates if e["level"] == level]
58
+ if has_manager is True:
59
+ candidates = [e for e in candidates if e.get("manager_id")]
60
+ if has_manager is False:
61
+ candidates = [e for e in candidates if not e.get("manager_id")]
62
+ return random.choice(candidates) if candidates else None
63
+
64
+
65
+ def _pick_manager_in_dept(world: WorldState, department: str, min_level: str = "L3") -> Optional[dict]:
66
+ """Pick a manager-level employee in a department."""
67
+ min_lvl = int(min_level[1])
68
+ candidates = [e for e in world.state["employees"]
69
+ if e["department"] == department
70
+ and e["status"] == "active"
71
+ and int(e["level"][1]) >= min_lvl]
72
+ return random.choice(candidates) if candidates else None
73
+
74
+
75
+ class TaskGenerator:
76
+ """Generates tasks from templates, binding them to specific world state entities."""
77
+
78
+ def __init__(self, world: WorldState, seed: int = 42):
79
+ self.world = world
80
+ self.rng = random.Random(seed)
81
+ self._task_counter = 0
82
+
83
+ def _next_id(self) -> str:
84
+ self._task_counter += 1
85
+ return f"task_{self._task_counter:04d}"
86
+
87
+ def generate_all_tasks(self) -> list[Task]:
88
+ """Generate the full task set (~100 tasks)."""
89
+ tasks = []
90
+ tasks.extend(self._simple_lookup_tasks())
91
+ tasks.extend(self._simple_onboarding_tasks())
92
+ tasks.extend(self._medium_onboarding_tasks())
93
+ tasks.extend(self._complex_onboarding_tasks())
94
+ tasks.extend(self._simple_offboarding_tasks())
95
+ tasks.extend(self._medium_offboarding_tasks())
96
+ tasks.extend(self._complex_offboarding_tasks())
97
+ tasks.extend(self._edge_case_tasks())
98
+ tasks.extend(self._cross_workflow_tasks())
99
+ return tasks
100
+
101
+ def generate_train_eval_split(self, eval_ratio: float = 0.2) -> tuple[list[Task], list[Task]]:
102
+ """Split tasks into training and evaluation sets."""
103
+ all_tasks = self.generate_all_tasks()
104
+ self.rng.shuffle(all_tasks)
105
+ split_idx = int(len(all_tasks) * (1 - eval_ratio))
106
+ return all_tasks[:split_idx], all_tasks[split_idx:]
107
+
108
+ # ---- Simple Lookup Tasks (10) ----
109
+ def _simple_lookup_tasks(self) -> list[Task]:
110
+ tasks = []
111
+ depts = ["Engineering", "Product", "Marketing", "Sales", "Finance", "HR", "Data Science", "Security"]
112
+
113
+ # 1. Look up employee by ID
114
+ for _ in range(3):
115
+ emp = _pick_employee(self.world, status="active")
116
+ if not emp:
117
+ continue
118
+ tasks.append(Task(
119
+ task_id=self._next_id(),
120
+ instruction=f"Look up the employee record for {emp['name']} (ID: {emp['emp_id']}).",
121
+ difficulty="simple",
122
+ category="lookup",
123
+ expected_tools=["hr_read_employee"],
124
+ rubric_criteria=[
125
+ {"name": "correct_tool", "description": "Used hr_read_employee", "check": "tool_used:hr_read_employee"},
126
+ {"name": "correct_id", "description": "Passed correct emp_id", "check": f"param_value:hr_read_employee.emp_id={emp['emp_id']}"},
127
+ ],
128
+ context={"target_emp_id": emp["emp_id"], "target_name": emp["name"]},
129
+ ))
130
+
131
+ # 2. Search employees by department
132
+ for dept in self.rng.sample(depts, 2):
133
+ tasks.append(Task(
134
+ task_id=self._next_id(),
135
+ instruction=f"List all employees in the {dept} department.",
136
+ difficulty="simple",
137
+ category="lookup",
138
+ expected_tools=["hr_search_employees"],
139
+ rubric_criteria=[
140
+ {"name": "correct_tool", "description": "Used hr_search_employees", "check": "tool_used:hr_search_employees"},
141
+ {"name": "correct_dept", "description": "Filtered by correct department", "check": f"param_value:hr_search_employees.department={dept}"},
142
+ ],
143
+ context={"department": dept},
144
+ ))
145
+
146
+ # 3. Get org chart
147
+ dept = self.rng.choice(depts)
148
+ tasks.append(Task(
149
+ task_id=self._next_id(),
150
+ instruction=f"Show me the organizational chart for the {dept} department.",
151
+ difficulty="simple",
152
+ category="lookup",
153
+ expected_tools=["hr_get_org_chart"],
154
+ rubric_criteria=[
155
+ {"name": "correct_tool", "description": "Used hr_get_org_chart", "check": "tool_used:hr_get_org_chart"},
156
+ {"name": "correct_dept", "description": "Passed correct department", "check": f"param_value:hr_get_org_chart.department={dept}"},
157
+ ],
158
+ context={"department": dept},
159
+ ))
160
+
161
+ # 4. Check available assets
162
+ tasks.append(Task(
163
+ task_id=self._next_id(),
164
+ instruction="What laptops are currently available for assignment?",
165
+ difficulty="simple",
166
+ category="lookup",
167
+ expected_tools=["it_get_available_assets"],
168
+ rubric_criteria=[
169
+ {"name": "correct_tool", "description": "Used it_get_available_assets", "check": "tool_used:it_get_available_assets"},
170
+ {"name": "correct_type", "description": "Filtered by laptop type", "check": "param_value:it_get_available_assets.asset_type=laptop"},
171
+ ],
172
+ ))
173
+
174
+ # 5. Check software licenses
175
+ tasks.append(Task(
176
+ task_id=self._next_id(),
177
+ instruction="Check how many Jira license seats are available.",
178
+ difficulty="simple",
179
+ category="lookup",
180
+ expected_tools=["it_get_software_licenses"],
181
+ rubric_criteria=[
182
+ {"name": "correct_tool", "description": "Used it_get_software_licenses", "check": "tool_used:it_get_software_licenses"},
183
+ {"name": "correct_software", "description": "Filtered by Jira", "check": "param_value:it_get_software_licenses.software_name=Jira"},
184
+ ],
185
+ ))
186
+
187
+ # 6. Look up policy
188
+ tasks.append(Task(
189
+ task_id=self._next_id(),
190
+ instruction="What is the company's policy on onboarding new employees?",
191
+ difficulty="simple",
192
+ category="lookup",
193
+ expected_tools=["policy_lookup"],
194
+ rubric_criteria=[
195
+ {"name": "correct_tool", "description": "Used policy_lookup", "check": "tool_used:policy_lookup"},
196
+ {"name": "relevant_topic", "description": "Searched for onboarding topic", "check": "param_contains:policy_lookup.topic=onboard"},
197
+ ],
198
+ ))
199
+
200
+ # 7. Get security groups
201
+ tasks.append(Task(
202
+ task_id=self._next_id(),
203
+ instruction="List all security groups and their accessible resources.",
204
+ difficulty="simple",
205
+ category="lookup",
206
+ expected_tools=["access_get_security_groups"],
207
+ rubric_criteria=[
208
+ {"name": "correct_tool", "description": "Used access_get_security_groups", "check": "tool_used:access_get_security_groups"},
209
+ ],
210
+ ))
211
+
212
+ return tasks
213
+
214
+ # ---- Simple Onboarding Tasks (5) ----
215
+ def _simple_onboarding_tasks(self) -> list[Task]:
216
+ tasks = []
217
+
218
+ # Check onboarding status for a pending employee
219
+ for _ in range(3):
220
+ emp = _pick_employee(self.world, status="pending")
221
+ if not emp:
222
+ continue
223
+ tasks.append(Task(
224
+ task_id=self._next_id(),
225
+ instruction=f"Check the onboarding status for employee {emp['name']} ({emp['emp_id']}).",
226
+ difficulty="simple",
227
+ category="onboarding",
228
+ expected_tools=["onboarding_get_status"],
229
+ rubric_criteria=[
230
+ {"name": "correct_tool", "description": "Used onboarding_get_status", "check": "tool_used:onboarding_get_status"},
231
+ {"name": "correct_emp", "description": "Checked correct employee", "check": f"param_value:onboarding_get_status.employee_id={emp['emp_id']}"},
232
+ ],
233
+ context={"target_emp_id": emp["emp_id"]},
234
+ ))
235
+
236
+ # Check available assets for a department
237
+ dept = self.rng.choice(["Engineering", "Data Science"])
238
+ tasks.append(Task(
239
+ task_id=self._next_id(),
240
+ instruction=f"Check if there are available laptops and Jira licenses for a new {dept} hire.",
241
+ difficulty="simple",
242
+ category="onboarding",
243
+ expected_tools=["it_get_available_assets", "it_get_software_licenses"],
244
+ rubric_criteria=[
245
+ {"name": "checked_assets", "description": "Checked available assets", "check": "tool_used:it_get_available_assets"},
246
+ {"name": "checked_licenses", "description": "Checked software licenses", "check": "tool_used:it_get_software_licenses"},
247
+ ],
248
+ context={"department": dept},
249
+ ))
250
+
251
+ return tasks
252
+
253
+ # ---- Medium Onboarding Tasks (10) ----
254
+ def _medium_onboarding_tasks(self) -> list[Task]:
255
+ tasks = []
256
+ names = [
257
+ ("Priya Sharma", "Engineering", "L2", "Software Engineer"),
258
+ ("Alex Chen", "Product", "L2", "Product Analyst"),
259
+ ("Maria Garcia", "Marketing", "L1", "Marketing Associate"),
260
+ ("James Wilson", "Data Science", "L2", "Data Analyst"),
261
+ ("Aisha Patel", "Sales", "L1", "Sales Representative"),
262
+ ("Tom Nguyen", "Finance", "L2", "Financial Analyst"),
263
+ ("Sara Kim", "HR", "L1", "HR Coordinator"),
264
+ ("David Brown", "Security", "L2", "Security Analyst"),
265
+ ("Li Wei", "Engineering", "L3", "Senior Engineer"),
266
+ ("Emma Davis", "Product", "L3", "Senior PM"),
267
+ ]
268
+
269
+ for name, dept, level, role in names:
270
+ manager = _pick_manager_in_dept(self.world, dept)
271
+ manager_name = manager["name"] if manager else "their department head"
272
+ manager_id = manager["emp_id"] if manager else None
273
+
274
+ tasks.append(Task(
275
+ task_id=self._next_id(),
276
+ instruction=f"Onboard new hire {name} to {dept} as {level} {role}. "
277
+ f"Create their employee record and initiate the onboarding request.",
278
+ difficulty="medium",
279
+ category="onboarding",
280
+ expected_tools=["hr_create_employee", "onboarding_create_request"],
281
+ rubric_criteria=[
282
+ {"name": "created_employee", "description": "Created employee record", "check": "tool_used:hr_create_employee"},
283
+ {"name": "correct_name", "description": "Used correct name", "check": f"param_value:hr_create_employee.name={name}"},
284
+ {"name": "correct_dept", "description": "Assigned to correct department", "check": f"param_value:hr_create_employee.department={dept}"},
285
+ {"name": "correct_level", "description": "Set correct level", "check": f"param_value:hr_create_employee.level={level}"},
286
+ {"name": "correct_role", "description": "Set correct role", "check": f"param_value:hr_create_employee.role={role}"},
287
+ {"name": "initiated_onboarding", "description": "Created onboarding request", "check": "tool_used:onboarding_create_request"},
288
+ {"name": "sequencing", "description": "Created employee before onboarding request", "check": "tool_order:hr_create_employee<onboarding_create_request"},
289
+ ],
290
+ context={"new_hire_name": name, "department": dept, "level": level, "role": role, "manager_id": manager_id},
291
+ ))
292
+
293
+ return tasks
294
+
295
+ # ---- Complex Onboarding Tasks (10) ----
296
+ def _complex_onboarding_tasks(self) -> list[Task]:
297
+ tasks = []
298
+
299
+ # Full onboarding with everything
300
+ complex_hires = [
301
+ ("John Lee", "Data Science", "L3", "Team Lead - ML"),
302
+ ("Fatima Al-Rashid", "Engineering", "L4", "Engineering Manager"),
303
+ ("Carlos Mendez", "Security", "L3", "Senior Security Engineer"),
304
+ ("Rachel Green", "Product", "L2", "Product Designer"),
305
+ ("Raj Kapoor", "Engineering", "L2", "Backend Developer"),
306
+ ]
307
+
308
+ for name, dept, level, role in complex_hires:
309
+ manager = _pick_manager_in_dept(self.world, dept)
310
+ manager_ref = f" Their manager will be {manager['name']} ({manager['emp_id']})." if manager else ""
311
+
312
+ tasks.append(Task(
313
+ task_id=self._next_id(),
314
+ instruction=f"Fully onboard {name} as {level} {role} in {dept}.{manager_ref} "
315
+ f"Create the employee record, initiate onboarding, assign a laptop, "
316
+ f"create IT accounts (email, Slack, VPN), set up appropriate access roles "
317
+ f"for their level, send a welcome email to the team channel, "
318
+ f"and schedule an orientation meeting with their manager.",
319
+ difficulty="complex",
320
+ category="onboarding",
321
+ expected_tools=[
322
+ "hr_create_employee", "onboarding_create_request", "it_get_available_assets",
323
+ "it_assign_asset", "it_create_account", "access_assign_role",
324
+ "slack_send_message", "email_send", "meeting_schedule",
325
+ "onboarding_complete_step",
326
+ ],
327
+ rubric_criteria=[
328
+ {"name": "created_employee", "description": "Created employee record", "check": "tool_used:hr_create_employee"},
329
+ {"name": "initiated_onboarding", "description": "Created onboarding request", "check": "tool_used:onboarding_create_request"},
330
+ {"name": "assigned_laptop", "description": "Assigned a laptop", "check": "tool_used:it_assign_asset"},
331
+ {"name": "created_accounts", "description": "Created IT accounts", "check": "tool_used:it_create_account"},
332
+ {"name": "assigned_access", "description": "Assigned access roles", "check": "tool_used:access_assign_role"},
333
+ {"name": "sent_welcome", "description": "Sent welcome communication", "check": "tool_used_any:email_send,slack_send_message"},
334
+ {"name": "scheduled_orientation", "description": "Scheduled orientation meeting", "check": "tool_used:meeting_schedule"},
335
+ {"name": "sequencing_create_first", "description": "Created employee before other steps", "check": "tool_order:hr_create_employee<onboarding_create_request"},
336
+ {"name": "sequencing_asset_check", "description": "Checked available assets before assigning", "check": "tool_order:it_get_available_assets<it_assign_asset"},
337
+ {"name": "completeness", "description": "Completed at least 3 onboarding steps", "check": "tool_count:onboarding_complete_step>=3"},
338
+ ],
339
+ context={"new_hire_name": name, "department": dept, "level": level, "role": role,
340
+ "manager_id": manager["emp_id"] if manager else None},
341
+ ))
342
+
343
+ # Complex with approval chains
344
+ for name, dept, level, role in [
345
+ ("Sanjay Gupta", "Security", "L2", "Security Analyst"),
346
+ ("Nina Petrova", "Engineering", "L4", "Director of Platform"),
347
+ ("Hassan Ahmed", "Data Science", "L3", "Lead Data Scientist"),
348
+ ("Laura Martinez", "Finance", "L3", "Senior Financial Analyst"),
349
+ ("Kevin O'Brien", "Product", "L4", "VP of Product"),
350
+ ]:
351
+ manager = _pick_manager_in_dept(self.world, dept, min_level="L4")
352
+ needs_security = dept == "Security" or int(level[1]) >= 4
353
+
354
+ instruction = (
355
+ f"Onboard {name} as {level} {role} in {dept}. "
356
+ f"Create the employee record, initiate onboarding, and obtain all necessary approvals. "
357
+ )
358
+ if needs_security:
359
+ instruction += "Note: this role requires security approval for badge access. "
360
+ instruction += (
361
+ "Then assign appropriate assets, create accounts, provision access roles, "
362
+ "create a physical badge, send welcome communications, and schedule orientation."
363
+ )
364
+
365
+ criteria = [
366
+ {"name": "created_employee", "description": "Created employee record", "check": "tool_used:hr_create_employee"},
367
+ {"name": "initiated_onboarding", "description": "Created onboarding request", "check": "tool_used:onboarding_create_request"},
368
+ {"name": "got_approval", "description": "Submitted approval request", "check": "tool_used:approval_request"},
369
+ {"name": "assigned_asset", "description": "Assigned an asset", "check": "tool_used:it_assign_asset"},
370
+ {"name": "created_accounts", "description": "Created IT accounts", "check": "tool_used:it_create_account"},
371
+ {"name": "assigned_role", "description": "Assigned access role", "check": "tool_used:access_assign_role"},
372
+ {"name": "created_badge", "description": "Created physical badge", "check": "tool_used:access_create_badge"},
373
+ {"name": "sent_communications", "description": "Sent welcome communications", "check": "tool_used_any:email_send,slack_send_message"},
374
+ {"name": "scheduled_meeting", "description": "Scheduled orientation", "check": "tool_used:meeting_schedule"},
375
+ ]
376
+ if needs_security:
377
+ criteria.append({"name": "security_approval", "description": "Got security approval before badge",
378
+ "check": "tool_order:approval_request<access_create_badge"})
379
+
380
+ tasks.append(Task(
381
+ task_id=self._next_id(),
382
+ instruction=instruction,
383
+ difficulty="complex",
384
+ category="onboarding",
385
+ expected_tools=["hr_create_employee", "onboarding_create_request", "approval_request",
386
+ "it_get_available_assets", "it_assign_asset", "it_create_account",
387
+ "access_assign_role", "access_create_badge", "email_send",
388
+ "slack_send_message", "meeting_schedule", "onboarding_complete_step"],
389
+ rubric_criteria=criteria,
390
+ context={"new_hire_name": name, "department": dept, "level": level, "role": role,
391
+ "manager_id": manager["emp_id"] if manager else None,
392
+ "needs_security_approval": needs_security},
393
+ ))
394
+
395
+ return tasks
396
+
397
+ # ---- Simple Offboarding Tasks (5) ----
398
+ def _simple_offboarding_tasks(self) -> list[Task]:
399
+ tasks = []
400
+
401
+ for _ in range(5):
402
+ emp = _pick_employee(self.world, status="active")
403
+ if not emp:
404
+ continue
405
+ tasks.append(Task(
406
+ task_id=self._next_id(),
407
+ instruction=f"Check the offboarding status for {emp['name']} ({emp['emp_id']}).",
408
+ difficulty="simple",
409
+ category="offboarding",
410
+ expected_tools=["offboarding_get_status"],
411
+ rubric_criteria=[
412
+ {"name": "correct_tool", "description": "Used offboarding_get_status", "check": "tool_used:offboarding_get_status"},
413
+ {"name": "correct_emp", "description": "Checked correct employee", "check": f"param_value:offboarding_get_status.employee_id={emp['emp_id']}"},
414
+ ],
415
+ context={"target_emp_id": emp["emp_id"]},
416
+ ))
417
+
418
+ return tasks
419
+
420
+ # ---- Medium Offboarding Tasks (8) ----
421
+ def _medium_offboarding_tasks(self) -> list[Task]:
422
+ tasks = []
423
+ offboarding_scenarios = [
424
+ ("resignation", "Sarah Kim is resigning"),
425
+ ("resignation", "Michael Torres is leaving for another opportunity"),
426
+ ("resignation", "Ananya Desai is moving to a different city"),
427
+ ("termination", "Jake Powell is being terminated for policy violations"),
428
+ ("resignation", "Sophie Liu has accepted an offer elsewhere"),
429
+ ("resignation", "Daniel Park is retiring"),
430
+ ("resignation", "Christina Muller is taking a career break"),
431
+ ("resignation", "Yuki Tanaka is going back to school"),
432
+ ]
433
+
434
+ for reason, scenario in offboarding_scenarios:
435
+ emp = _pick_employee(self.world, status="active", has_manager=True)
436
+ if not emp:
437
+ continue
438
+
439
+ name = emp["name"]
440
+ instruction = (
441
+ f"Initiate offboarding for {name} ({emp['emp_id']}) who {scenario.split(' is ')[1] if ' is ' in scenario else 'is leaving'}. "
442
+ f"Revoke their system access and notify IT."
443
+ )
444
+
445
+ criteria = [
446
+ {"name": "created_request", "description": "Created offboarding request", "check": "tool_used:offboarding_create_request"},
447
+ {"name": "correct_reason", "description": "Set correct reason", "check": f"param_value:offboarding_create_request.reason={reason}"},
448
+ {"name": "revoked_access", "description": "Revoked IT access", "check": "tool_used:it_revoke_access"},
449
+ {"name": "notified", "description": "Sent notification", "check": "tool_used_any:email_send,slack_send_message"},
450
+ ]
451
+
452
+ tasks.append(Task(
453
+ task_id=self._next_id(),
454
+ instruction=instruction,
455
+ difficulty="medium",
456
+ category="offboarding",
457
+ expected_tools=["offboarding_create_request", "it_revoke_access", "email_send"],
458
+ rubric_criteria=criteria,
459
+ context={"target_emp_id": emp["emp_id"], "reason": reason},
460
+ ))
461
+
462
+ return tasks
463
+
464
+ # ---- Complex Offboarding Tasks (8) ----
465
+ def _complex_offboarding_tasks(self) -> list[Task]:
466
+ tasks = []
467
+
468
+ # Full offboarding for managers/directors with reports
469
+ for _ in range(4):
470
+ # Find an employee who has direct reports
471
+ candidates = [e for e in self.world.state["employees"]
472
+ if e["status"] == "active" and int(e["level"][1]) >= 3]
473
+ if not candidates:
474
+ continue
475
+ emp = self.rng.choice(candidates)
476
+ reports = self.world.get_direct_reports(emp["emp_id"])
477
+ skip_mgr = self.world.get_skip_level_manager(emp["emp_id"])
478
+ skip_mgr_ref = f" Reassign their reports to {skip_mgr['name']} ({skip_mgr['emp_id']})." if skip_mgr else " Reassign their reports to their skip-level manager."
479
+
480
+ tasks.append(Task(
481
+ task_id=self._next_id(),
482
+ instruction=(
483
+ f"Fully offboard {emp['name']} ({emp['emp_id']}), a {emp['level']} {emp['role']} in {emp['department']} "
484
+ f"who is resigning. Revoke all access roles and IT access, reclaim their assigned assets, "
485
+ f"revoke their badge.{skip_mgr_ref} "
486
+ f"Send a farewell email to the team, schedule an exit interview, "
487
+ f"and complete all offboarding steps."
488
+ ),
489
+ difficulty="complex",
490
+ category="offboarding",
491
+ expected_tools=[
492
+ "offboarding_create_request", "it_revoke_access", "access_revoke_role",
493
+ "email_send", "slack_send_message", "meeting_schedule",
494
+ "offboarding_complete_step",
495
+ ],
496
+ rubric_criteria=[
497
+ {"name": "created_request", "description": "Created offboarding request", "check": "tool_used:offboarding_create_request"},
498
+ {"name": "revoked_it", "description": "Revoked IT access", "check": "tool_used:it_revoke_access"},
499
+ {"name": "revoked_roles", "description": "Revoked access roles", "check": "tool_used_any:access_revoke_role"},
500
+ {"name": "farewell", "description": "Sent farewell communication", "check": "tool_used_any:email_send,slack_send_message"},
501
+ {"name": "exit_interview", "description": "Scheduled exit interview", "check": "tool_used:meeting_schedule"},
502
+ {"name": "completed_steps", "description": "Completed offboarding steps", "check": "tool_count:offboarding_complete_step>=2"},
503
+ ],
504
+ context={"target_emp_id": emp["emp_id"], "has_reports": len(reports) > 0,
505
+ "skip_manager_id": skip_mgr["emp_id"] if skip_mgr else None},
506
+ ))
507
+
508
+ # Offboarding with asset reclamation
509
+ for _ in range(4):
510
+ emp = _pick_employee(self.world, status="active")
511
+ if not emp:
512
+ continue
513
+
514
+ tasks.append(Task(
515
+ task_id=self._next_id(),
516
+ instruction=(
517
+ f"Process the complete offboarding for {emp['name']} ({emp['emp_id']}) from {emp['department']}. "
518
+ f"Create the offboarding request, revoke all system access and roles, "
519
+ f"check for and reclaim any assigned assets, send farewell notifications "
520
+ f"via email and Slack, and complete all offboarding steps."
521
+ ),
522
+ difficulty="complex",
523
+ category="offboarding",
524
+ expected_tools=[
525
+ "offboarding_create_request", "it_revoke_access",
526
+ "email_send", "slack_send_message", "offboarding_complete_step",
527
+ ],
528
+ rubric_criteria=[
529
+ {"name": "created_request", "description": "Created offboarding request", "check": "tool_used:offboarding_create_request"},
530
+ {"name": "revoked_it", "description": "Revoked IT access", "check": "tool_used:it_revoke_access"},
531
+ {"name": "farewell_email", "description": "Sent farewell email", "check": "tool_used:email_send"},
532
+ {"name": "farewell_slack", "description": "Sent farewell Slack message", "check": "tool_used:slack_send_message"},
533
+ {"name": "completed_steps", "description": "Completed offboarding steps", "check": "tool_count:offboarding_complete_step>=2"},
534
+ ],
535
+ context={"target_emp_id": emp["emp_id"]},
536
+ ))
537
+
538
+ return tasks
539
+
540
+ # ---- Edge Case Tasks (15) ----
541
+ def _edge_case_tasks(self) -> list[Task]:
542
+ tasks = []
543
+
544
+ # 1. Headcount limit exceeded
545
+ for dept in ["Marketing", "Finance"]:
546
+ tasks.append(Task(
547
+ task_id=self._next_id(),
548
+ instruction=f"Onboard a new L1 Associate to the {dept} department. Create their employee record and start onboarding.",
549
+ difficulty="edge_case",
550
+ category="onboarding",
551
+ expected_tools=["hr_create_employee"],
552
+ rubric_criteria=[
553
+ {"name": "attempted_create", "description": "Attempted to create employee", "check": "tool_used:hr_create_employee"},
554
+ {"name": "handled_limit", "description": "Recognized or handled headcount limit error", "check": "result_contains:headcount_limit"},
555
+ ],
556
+ context={"department": dept, "edge_case": "headcount_limit"},
557
+ ))
558
+
559
+ # 2. Software license full
560
+ tasks.append(Task(
561
+ task_id=self._next_id(),
562
+ instruction="Assign a Netsuite license to a new Finance hire. Check license availability first.",
563
+ difficulty="edge_case",
564
+ category="onboarding",
565
+ expected_tools=["it_get_software_licenses"],
566
+ rubric_criteria=[
567
+ {"name": "checked_licenses", "description": "Checked license availability", "check": "tool_used:it_get_software_licenses"},
568
+ {"name": "identified_full", "description": "Recognized licenses are full", "check": "result_contains:no available seats"},
569
+ ],
570
+ context={"edge_case": "license_full", "software": "Netsuite"},
571
+ ))
572
+
573
+ # 3. LinkedIn Sales Navigator also full
574
+ tasks.append(Task(
575
+ task_id=self._next_id(),
576
+ instruction="Check if there are available LinkedIn Sales Navigator licenses for a new Sales hire.",
577
+ difficulty="edge_case",
578
+ category="onboarding",
579
+ expected_tools=["it_get_software_licenses"],
580
+ rubric_criteria=[
581
+ {"name": "checked_licenses", "description": "Checked licenses", "check": "tool_used:it_get_software_licenses"},
582
+ ],
583
+ context={"edge_case": "license_full", "software": "LinkedIn Sales Navigator"},
584
+ ))
585
+
586
+ # 4. Manager on leave — find skip-level
587
+ emp = _pick_employee(self.world, status="active", has_manager=True)
588
+ if emp:
589
+ tasks.append(Task(
590
+ task_id=self._next_id(),
591
+ instruction=(
592
+ f"Onboard a new hire to {emp['department']} but their designated manager "
593
+ f"({emp['manager_id']}) is on leave. Find the skip-level manager to handle approvals "
594
+ f"and proceed with onboarding."
595
+ ),
596
+ difficulty="edge_case",
597
+ category="onboarding",
598
+ expected_tools=["hr_read_employee", "hr_get_org_chart", "hr_create_employee", "onboarding_create_request", "approval_request"],
599
+ rubric_criteria=[
600
+ {"name": "looked_up_manager", "description": "Looked up the manager or org chart", "check": "tool_used_any:hr_read_employee,hr_get_org_chart"},
601
+ {"name": "found_skip_level", "description": "Identified skip-level manager", "check": "tool_count:hr_read_employee>=2"},
602
+ {"name": "proceeded", "description": "Proceeded with onboarding", "check": "tool_used:hr_create_employee"},
603
+ ],
604
+ context={"edge_case": "manager_on_leave", "department": emp["department"], "manager_id": emp["manager_id"]},
605
+ ))
606
+
607
+ # 5. Onboard contractor (different rules)
608
+ tasks.append(Task(
609
+ task_id=self._next_id(),
610
+ instruction=(
611
+ "Onboard contractor Amit Verma to Engineering as an L2 Contract Developer. "
612
+ "Contractors have limited access — no VPN, restricted to Jira and Slack only, "
613
+ "and require legal approval. Create the record, initiate onboarding, "
614
+ "get legal approval, and provision appropriate (limited) access."
615
+ ),
616
+ difficulty="edge_case",
617
+ category="onboarding",
618
+ expected_tools=["hr_create_employee", "onboarding_create_request", "approval_request",
619
+ "it_create_account", "access_assign_role"],
620
+ rubric_criteria=[
621
+ {"name": "created_contractor", "description": "Created employee with is_contractor=true", "check": "param_value:hr_create_employee.is_contractor=True"},
622
+ {"name": "initiated_onboarding", "description": "Created onboarding request", "check": "tool_used:onboarding_create_request"},
623
+ {"name": "legal_approval", "description": "Got legal approval", "check": "param_value:approval_request.approval_type=legal_approval"},
624
+ {"name": "limited_access", "description": "Created limited accounts", "check": "tool_used:it_create_account"},
625
+ ],
626
+ context={"edge_case": "contractor_onboarding", "name": "Amit Verma"},
627
+ ))
628
+
629
+ # 6. Offboard employee with unreturned assets
630
+ emp = _pick_employee(self.world, status="active")
631
+ if emp:
632
+ tasks.append(Task(
633
+ task_id=self._next_id(),
634
+ instruction=(
635
+ f"Offboard {emp['name']} ({emp['emp_id']}) who has company assets that need to be returned. "
636
+ f"Check what assets they have assigned, create the offboarding request, "
637
+ f"reclaim all assets, revoke access, and complete the process."
638
+ ),
639
+ difficulty="edge_case",
640
+ category="offboarding",
641
+ expected_tools=["hr_read_employee", "offboarding_create_request", "it_revoke_access"],
642
+ rubric_criteria=[
643
+ {"name": "checked_employee", "description": "Looked up employee record", "check": "tool_used:hr_read_employee"},
644
+ {"name": "created_request", "description": "Created offboarding request", "check": "tool_used:offboarding_create_request"},
645
+ {"name": "revoked_access", "description": "Revoked access", "check": "tool_used:it_revoke_access"},
646
+ ],
647
+ context={"target_emp_id": emp["emp_id"], "edge_case": "unreturned_assets"},
648
+ ))
649
+
650
+ # 7. Offboard someone mid-onboarding (offer rescinded)
651
+ emp = _pick_employee(self.world, status="pending")
652
+ if emp:
653
+ tasks.append(Task(
654
+ task_id=self._next_id(),
655
+ instruction=(
656
+ f"The offer for {emp['name']} ({emp['emp_id']}) has been rescinded. "
657
+ f"They are currently mid-onboarding. Cancel their onboarding, revoke any "
658
+ f"provisioned access, and update their status to offboarded."
659
+ ),
660
+ difficulty="edge_case",
661
+ category="offboarding",
662
+ expected_tools=["hr_read_employee", "onboarding_get_status", "it_revoke_access",
663
+ "hr_update_employee"],
664
+ rubric_criteria=[
665
+ {"name": "checked_onboarding", "description": "Checked onboarding status", "check": "tool_used_any:onboarding_get_status,hr_read_employee"},
666
+ {"name": "revoked_access", "description": "Revoked any provisioned access", "check": "tool_used:it_revoke_access"},
667
+ {"name": "updated_status", "description": "Updated employee status to offboarded", "check": "tool_used:hr_update_employee"},
668
+ ],
669
+ context={"target_emp_id": emp["emp_id"], "edge_case": "offer_rescinded"},
670
+ ))
671
+
672
+ # 8. Termination (different policy)
673
+ emp = _pick_employee(self.world, status="active", has_manager=True)
674
+ if emp:
675
+ tasks.append(Task(
676
+ task_id=self._next_id(),
677
+ instruction=(
678
+ f"{emp['name']} ({emp['emp_id']}) is being terminated effective immediately. "
679
+ f"Follow the termination policy: immediately revoke all access, reclaim assets, "
680
+ f"create termination offboarding request, and handle final payroll. "
681
+ f"Do NOT send farewell communications for terminations."
682
+ ),
683
+ difficulty="edge_case",
684
+ category="offboarding",
685
+ expected_tools=["offboarding_create_request", "it_revoke_access", "offboarding_complete_step"],
686
+ rubric_criteria=[
687
+ {"name": "created_request", "description": "Created offboarding with termination reason", "check": "param_value:offboarding_create_request.reason=termination"},
688
+ {"name": "revoked_access", "description": "Revoked all access", "check": "tool_used:it_revoke_access"},
689
+ {"name": "no_farewell", "description": "Did NOT send farewell communications", "check": "tool_not_used:slack_send_message"},
690
+ {"name": "completed_steps", "description": "Completed termination steps", "check": "tool_used:offboarding_complete_step"},
691
+ ],
692
+ context={"target_emp_id": emp["emp_id"], "edge_case": "termination"},
693
+ ))
694
+
695
+ # 9. Role level mismatch
696
+ tasks.append(Task(
697
+ task_id=self._next_id(),
698
+ instruction=(
699
+ "Assign the security_admin access role to a new L1 Security Associate. "
700
+ "The security_admin role requires L4+ level."
701
+ ),
702
+ difficulty="edge_case",
703
+ category="onboarding",
704
+ expected_tools=["access_assign_role"],
705
+ rubric_criteria=[
706
+ {"name": "attempted_assign", "description": "Attempted to assign role", "check": "tool_used:access_assign_role"},
707
+ {"name": "handled_error", "description": "Recognized level requirement error", "check": "result_contains:does not meet minimum"},
708
+ ],
709
+ context={"edge_case": "level_mismatch"},
710
+ ))
711
+
712
+ # 10. Department restriction on role
713
+ tasks.append(Task(
714
+ task_id=self._next_id(),
715
+ instruction=(
716
+ "A Marketing employee needs access to the Engineering GitHub repository. "
717
+ "Try to assign them the engineering_developer role."
718
+ ),
719
+ difficulty="edge_case",
720
+ category="onboarding",
721
+ expected_tools=["access_assign_role"],
722
+ rubric_criteria=[
723
+ {"name": "attempted_assign", "description": "Attempted to assign role", "check": "tool_used:access_assign_role"},
724
+ {"name": "handled_restriction", "description": "Recognized department restriction", "check": "result_contains:restricted to"},
725
+ ],
726
+ context={"edge_case": "department_restriction"},
727
+ ))
728
+
729
+ # 11. Look up policy before action
730
+ tasks.append(Task(
731
+ task_id=self._next_id(),
732
+ instruction=(
733
+ "Before onboarding a new Security team member, look up the badge access policy "
734
+ "and the onboarding policy to understand what approvals are needed. "
735
+ "Then explain the requirements."
736
+ ),
737
+ difficulty="edge_case",
738
+ category="lookup",
739
+ expected_tools=["policy_lookup"],
740
+ rubric_criteria=[
741
+ {"name": "looked_up_badge", "description": "Looked up badge/access policy", "check": "tool_used:policy_lookup"},
742
+ {"name": "multiple_lookups", "description": "Looked up multiple policies", "check": "tool_count:policy_lookup>=2"},
743
+ ],
744
+ context={"edge_case": "policy_check"},
745
+ ))
746
+
747
+ return tasks
748
+
749
+ # ---- Cross-Workflow Tasks (10) ----
750
+ def _cross_workflow_tasks(self) -> list[Task]:
751
+ tasks = []
752
+
753
+ # 1-3. Department transfer
754
+ transfers = [
755
+ ("Engineering", "Product"),
756
+ ("Sales", "Marketing"),
757
+ ("Data Science", "Engineering"),
758
+ ]
759
+ for from_dept, to_dept in transfers:
760
+ emp = _pick_employee(self.world, status="active", department=from_dept)
761
+ if not emp:
762
+ continue
763
+ tasks.append(Task(
764
+ task_id=self._next_id(),
765
+ instruction=(
766
+ f"{emp['name']} ({emp['emp_id']}) is transferring from {from_dept} to {to_dept}. "
767
+ f"Process the department transfer: offboard them from {from_dept} "
768
+ f"(revoke department-specific access), update their department, "
769
+ f"and onboard them to {to_dept} (assign new access roles, notify new team)."
770
+ ),
771
+ difficulty="complex",
772
+ category="cross_workflow",
773
+ expected_tools=[
774
+ "hr_read_employee", "it_revoke_access", "hr_update_employee",
775
+ "access_assign_role", "slack_send_message", "email_send",
776
+ ],
777
+ rubric_criteria=[
778
+ {"name": "read_employee", "description": "Read employee record", "check": "tool_used:hr_read_employee"},
779
+ {"name": "revoked_old_access", "description": "Revoked old department access", "check": "tool_used:it_revoke_access"},
780
+ {"name": "updated_dept", "description": "Updated department", "check": "tool_used:hr_update_employee"},
781
+ {"name": "new_access", "description": "Assigned new department roles", "check": "tool_used:access_assign_role"},
782
+ {"name": "notified_team", "description": "Notified new team", "check": "tool_used_any:email_send,slack_send_message"},
783
+ ],
784
+ context={"target_emp_id": emp["emp_id"], "from_dept": from_dept, "to_dept": to_dept},
785
+ ))
786
+
787
+ # 4-5. Rehire previously offboarded employee
788
+ for _ in range(2):
789
+ emp = _pick_employee(self.world, status="offboarded")
790
+ if not emp:
791
+ continue
792
+ tasks.append(Task(
793
+ task_id=self._next_id(),
794
+ instruction=(
795
+ f"Rehire {emp['name']} ({emp['emp_id']}) who was previously offboarded. "
796
+ f"Update their status, create a new onboarding request, "
797
+ f"provision IT accounts, assign appropriate access, and send welcome-back communications."
798
+ ),
799
+ difficulty="complex",
800
+ category="cross_workflow",
801
+ expected_tools=[
802
+ "hr_read_employee", "hr_update_employee", "onboarding_create_request",
803
+ "it_create_account", "access_assign_role", "email_send", "slack_send_message",
804
+ ],
805
+ rubric_criteria=[
806
+ {"name": "read_employee", "description": "Read employee record first", "check": "tool_used:hr_read_employee"},
807
+ {"name": "updated_status", "description": "Updated status to pending/active", "check": "tool_used:hr_update_employee"},
808
+ {"name": "new_onboarding", "description": "Created new onboarding request", "check": "tool_used:onboarding_create_request"},
809
+ {"name": "provisioned_accounts", "description": "Created IT accounts", "check": "tool_used:it_create_account"},
810
+ {"name": "welcome_back", "description": "Sent welcome-back communication", "check": "tool_used_any:email_send,slack_send_message"},
811
+ ],
812
+ context={"target_emp_id": emp["emp_id"], "rehire": True},
813
+ ))
814
+
815
+ # 6-8. Bulk operations
816
+ for dept in self.rng.sample(["Engineering", "Product", "Data Science"], 3):
817
+ tasks.append(Task(
818
+ task_id=self._next_id(),
819
+ instruction=(
820
+ f"The {dept} team is onboarding 2 new hires at the same time. "
821
+ f"Check available assets and licenses, then report what resources "
822
+ f"are available for the new hires."
823
+ ),
824
+ difficulty="medium",
825
+ category="cross_workflow",
826
+ expected_tools=["it_get_available_assets", "it_get_software_licenses", "hr_search_employees"],
827
+ rubric_criteria=[
828
+ {"name": "checked_assets", "description": "Checked available assets", "check": "tool_used:it_get_available_assets"},
829
+ {"name": "checked_licenses", "description": "Checked software licenses", "check": "tool_used:it_get_software_licenses"},
830
+ ],
831
+ context={"department": dept},
832
+ ))
833
+
834
+ # 9-10. Manager leaving — handle succession
835
+ for _ in range(2):
836
+ candidates = [e for e in self.world.state["employees"]
837
+ if e["status"] == "active" and int(e["level"][1]) >= 3
838
+ and e.get("manager_id")]
839
+ if not candidates:
840
+ continue
841
+ mgr = self.rng.choice(candidates)
842
+ reports = self.world.get_direct_reports(mgr["emp_id"])
843
+ skip = self.world.get_skip_level_manager(mgr["emp_id"])
844
+
845
+ tasks.append(Task(
846
+ task_id=self._next_id(),
847
+ instruction=(
848
+ f"Manager {mgr['name']} ({mgr['emp_id']}) in {mgr['department']} is leaving. "
849
+ f"They have {len(reports)} direct reports. Process their offboarding: "
850
+ f"reassign their direct reports to the skip-level manager, "
851
+ f"revoke all their access, create the offboarding request, "
852
+ f"and notify the team about the transition."
853
+ ),
854
+ difficulty="complex",
855
+ category="cross_workflow",
856
+ expected_tools=[
857
+ "hr_read_employee", "hr_get_org_chart", "offboarding_create_request",
858
+ "hr_update_employee", "it_revoke_access", "email_send", "slack_send_message",
859
+ ],
860
+ rubric_criteria=[
861
+ {"name": "read_manager", "description": "Looked up manager info", "check": "tool_used:hr_read_employee"},
862
+ {"name": "offboarding", "description": "Created offboarding request", "check": "tool_used:offboarding_create_request"},
863
+ {"name": "reassigned", "description": "Updated reports' manager", "check": "tool_used:hr_update_employee"},
864
+ {"name": "revoked_access", "description": "Revoked manager's access", "check": "tool_used:it_revoke_access"},
865
+ {"name": "notified_team", "description": "Notified team", "check": "tool_used_any:email_send,slack_send_message"},
866
+ ],
867
+ context={"target_emp_id": mgr["emp_id"], "report_count": len(reports),
868
+ "skip_manager_id": skip["emp_id"] if skip else None},
869
+ ))
870
+
871
+ return tasks
server/tools.py ADDED
@@ -0,0 +1,515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MCP Tool definitions for HR Onboarding/Offboarding environment.
2
+
3
+ Each tool is a callable that takes parameters and operates on the WorldState.
4
+ Tools are designed to mirror realistic enterprise HR/IT APIs.
5
+ """
6
+
7
+ import json
8
+ from typing import Any, Callable
9
+ try:
10
+ from .world import WorldState
11
+ except ImportError:
12
+ from world import WorldState
13
+
14
+
15
+ TOOL_DEFINITIONS = [
16
+ {
17
+ "name": "hr_create_employee",
18
+ "description": "Create a new employee record in the HR system. Requires name, department, level, and role. Returns the created employee record with generated emp_id.",
19
+ "parameters": {
20
+ "type": "object",
21
+ "properties": {
22
+ "name": {"type": "string", "description": "Full name of the employee"},
23
+ "department": {"type": "string", "description": "Department name (Engineering, Product, Marketing, Sales, Finance, HR, Data Science, Security)"},
24
+ "level": {"type": "string", "description": "Employee level (L1-L6)"},
25
+ "role": {"type": "string", "description": "Job title/role"},
26
+ "manager_id": {"type": "string", "description": "Employee ID of the manager"},
27
+ "is_contractor": {"type": "boolean", "description": "Whether this is a contractor (default: false)"},
28
+ "location": {"type": "string", "description": "Office location"},
29
+ "date_of_joining": {"type": "string", "description": "Start date (YYYY-MM-DD)"},
30
+ },
31
+ "required": ["name", "department", "level", "role"],
32
+ },
33
+ },
34
+ {
35
+ "name": "hr_read_employee",
36
+ "description": "Look up an employee by their employee ID or email address. Returns full employee record.",
37
+ "parameters": {
38
+ "type": "object",
39
+ "properties": {
40
+ "emp_id": {"type": "string", "description": "Employee ID (e.g. emp_0001)"},
41
+ "email": {"type": "string", "description": "Employee email address"},
42
+ },
43
+ },
44
+ },
45
+ {
46
+ "name": "hr_update_employee",
47
+ "description": "Update fields on an existing employee record. Cannot modify emp_id.",
48
+ "parameters": {
49
+ "type": "object",
50
+ "properties": {
51
+ "emp_id": {"type": "string", "description": "Employee ID to update"},
52
+ "updates": {"type": "object", "description": "Dictionary of fields to update (e.g. {status: 'active', department: 'Engineering'})"},
53
+ },
54
+ "required": ["emp_id", "updates"],
55
+ },
56
+ },
57
+ {
58
+ "name": "hr_search_employees",
59
+ "description": "Search employees by criteria. Filter by department, level, status, location, or role.",
60
+ "parameters": {
61
+ "type": "object",
62
+ "properties": {
63
+ "department": {"type": "string", "description": "Filter by department"},
64
+ "level": {"type": "string", "description": "Filter by level (L1-L6)"},
65
+ "status": {"type": "string", "description": "Filter by status (active, pending, offboarded)"},
66
+ "location": {"type": "string", "description": "Filter by location"},
67
+ "role": {"type": "string", "description": "Filter by role/title"},
68
+ "name": {"type": "string", "description": "Filter by name (exact match)"},
69
+ },
70
+ },
71
+ },
72
+ {
73
+ "name": "hr_get_org_chart",
74
+ "description": "Get the organizational hierarchy/reporting structure for a department. Shows manager-report relationships.",
75
+ "parameters": {
76
+ "type": "object",
77
+ "properties": {
78
+ "department": {"type": "string", "description": "Department name"},
79
+ },
80
+ "required": ["department"],
81
+ },
82
+ },
83
+ {
84
+ "name": "onboarding_create_request",
85
+ "description": "Initiate an onboarding request for a new hire. The employee must exist in the HR system with 'pending' status. Creates a checklist of onboarding steps based on department.",
86
+ "parameters": {
87
+ "type": "object",
88
+ "properties": {
89
+ "employee_id": {"type": "string", "description": "Employee ID of the new hire"},
90
+ },
91
+ "required": ["employee_id"],
92
+ },
93
+ },
94
+ {
95
+ "name": "onboarding_get_status",
96
+ "description": "Check the status of an onboarding request. Can look up by request ID or employee ID.",
97
+ "parameters": {
98
+ "type": "object",
99
+ "properties": {
100
+ "request_id": {"type": "string", "description": "Onboarding request ID"},
101
+ "employee_id": {"type": "string", "description": "Employee ID"},
102
+ },
103
+ },
104
+ },
105
+ {
106
+ "name": "onboarding_complete_step",
107
+ "description": "Mark an onboarding step as completed. Steps must be valid for the request.",
108
+ "parameters": {
109
+ "type": "object",
110
+ "properties": {
111
+ "request_id": {"type": "string", "description": "Onboarding request ID"},
112
+ "step": {"type": "string", "description": "Step name to mark as completed"},
113
+ },
114
+ "required": ["request_id", "step"],
115
+ },
116
+ },
117
+ {
118
+ "name": "offboarding_create_request",
119
+ "description": "Initiate an offboarding request for a departing employee. Specify reason (resignation, termination, transfer) and optional exit date.",
120
+ "parameters": {
121
+ "type": "object",
122
+ "properties": {
123
+ "employee_id": {"type": "string", "description": "Employee ID"},
124
+ "reason": {"type": "string", "description": "Reason for offboarding (resignation, termination, transfer)"},
125
+ "exit_date": {"type": "string", "description": "Last working date (YYYY-MM-DD)"},
126
+ },
127
+ "required": ["employee_id"],
128
+ },
129
+ },
130
+ {
131
+ "name": "offboarding_get_status",
132
+ "description": "Check the status of an offboarding request. Can look up by request ID or employee ID.",
133
+ "parameters": {
134
+ "type": "object",
135
+ "properties": {
136
+ "request_id": {"type": "string", "description": "Offboarding request ID"},
137
+ "employee_id": {"type": "string", "description": "Employee ID"},
138
+ },
139
+ },
140
+ },
141
+ {
142
+ "name": "offboarding_complete_step",
143
+ "description": "Mark an offboarding step as completed.",
144
+ "parameters": {
145
+ "type": "object",
146
+ "properties": {
147
+ "request_id": {"type": "string", "description": "Offboarding request ID"},
148
+ "step": {"type": "string", "description": "Step name to mark as completed"},
149
+ },
150
+ "required": ["request_id", "step"],
151
+ },
152
+ },
153
+ {
154
+ "name": "it_assign_asset",
155
+ "description": "Assign an IT asset (laptop, monitor, phone, headset) to an employee. Asset must be available.",
156
+ "parameters": {
157
+ "type": "object",
158
+ "properties": {
159
+ "asset_id": {"type": "string", "description": "Asset ID to assign"},
160
+ "employee_id": {"type": "string", "description": "Employee ID to assign to"},
161
+ },
162
+ "required": ["asset_id", "employee_id"],
163
+ },
164
+ },
165
+ {
166
+ "name": "it_get_available_assets",
167
+ "description": "List available (unassigned) IT assets. Optionally filter by type.",
168
+ "parameters": {
169
+ "type": "object",
170
+ "properties": {
171
+ "asset_type": {"type": "string", "description": "Filter by asset type (laptop, monitor, phone, headset)"},
172
+ },
173
+ },
174
+ },
175
+ {
176
+ "name": "it_create_account",
177
+ "description": "Create IT accounts (email, Slack, VPN, etc.) for an employee.",
178
+ "parameters": {
179
+ "type": "object",
180
+ "properties": {
181
+ "employee_id": {"type": "string", "description": "Employee ID"},
182
+ "account_types": {
183
+ "type": "array",
184
+ "items": {"type": "string"},
185
+ "description": "Types of accounts to create (email, slack, vpn, github, jira, etc.)",
186
+ },
187
+ },
188
+ "required": ["employee_id"],
189
+ },
190
+ },
191
+ {
192
+ "name": "it_revoke_access",
193
+ "description": "Revoke all IT system access for an employee (used during offboarding).",
194
+ "parameters": {
195
+ "type": "object",
196
+ "properties": {
197
+ "employee_id": {"type": "string", "description": "Employee ID"},
198
+ },
199
+ "required": ["employee_id"],
200
+ },
201
+ },
202
+ {
203
+ "name": "it_get_software_licenses",
204
+ "description": "Check software license availability. Optionally filter by software name.",
205
+ "parameters": {
206
+ "type": "object",
207
+ "properties": {
208
+ "software_name": {"type": "string", "description": "Filter by software name"},
209
+ },
210
+ },
211
+ },
212
+ {
213
+ "name": "access_assign_role",
214
+ "description": "Assign an access role to an employee. Checks level requirements and department restrictions.",
215
+ "parameters": {
216
+ "type": "object",
217
+ "properties": {
218
+ "employee_id": {"type": "string", "description": "Employee ID"},
219
+ "role_id": {"type": "string", "description": "Access role ID to assign"},
220
+ },
221
+ "required": ["employee_id", "role_id"],
222
+ },
223
+ },
224
+ {
225
+ "name": "access_create_badge",
226
+ "description": "Create a physical access badge for an employee. Server room access requires L4+ security approval.",
227
+ "parameters": {
228
+ "type": "object",
229
+ "properties": {
230
+ "employee_id": {"type": "string", "description": "Employee ID"},
231
+ "access_zones": {
232
+ "type": "array",
233
+ "items": {"type": "string"},
234
+ "description": "List of access zones (main_entrance, floor_X, server_room, parking)",
235
+ },
236
+ },
237
+ "required": ["employee_id"],
238
+ },
239
+ },
240
+ {
241
+ "name": "access_revoke_role",
242
+ "description": "Revoke a specific access role from an employee.",
243
+ "parameters": {
244
+ "type": "object",
245
+ "properties": {
246
+ "employee_id": {"type": "string", "description": "Employee ID"},
247
+ "role_id": {"type": "string", "description": "Access role ID to revoke"},
248
+ },
249
+ "required": ["employee_id", "role_id"],
250
+ },
251
+ },
252
+ {
253
+ "name": "access_get_security_groups",
254
+ "description": "List all security groups. Optionally see members and resources.",
255
+ "parameters": {
256
+ "type": "object",
257
+ "properties": {},
258
+ },
259
+ },
260
+ {
261
+ "name": "email_send",
262
+ "description": "Send an email. Used for welcome emails, notifications, farewell messages, etc.",
263
+ "parameters": {
264
+ "type": "object",
265
+ "properties": {
266
+ "from_address": {"type": "string", "description": "Sender email address"},
267
+ "to_address": {"type": "string", "description": "Recipient email address"},
268
+ "subject": {"type": "string", "description": "Email subject line"},
269
+ "body": {"type": "string", "description": "Email body text"},
270
+ },
271
+ "required": ["from_address", "to_address", "subject", "body"],
272
+ },
273
+ },
274
+ {
275
+ "name": "slack_send_message",
276
+ "description": "Post a message in a Slack channel or send a DM.",
277
+ "parameters": {
278
+ "type": "object",
279
+ "properties": {
280
+ "channel": {"type": "string", "description": "Slack channel (e.g. #engineering, #general, @username)"},
281
+ "sender": {"type": "string", "description": "Sender username or bot name"},
282
+ "text": {"type": "string", "description": "Message text"},
283
+ },
284
+ "required": ["channel", "sender", "text"],
285
+ },
286
+ },
287
+ {
288
+ "name": "meeting_schedule",
289
+ "description": "Schedule a meeting (orientation, 1-on-1, exit interview, etc.).",
290
+ "parameters": {
291
+ "type": "object",
292
+ "properties": {
293
+ "title": {"type": "string", "description": "Meeting title"},
294
+ "attendees": {
295
+ "type": "array",
296
+ "items": {"type": "string"},
297
+ "description": "List of attendee employee IDs or emails",
298
+ },
299
+ "datetime": {"type": "string", "description": "Meeting date and time (ISO format)"},
300
+ "meeting_type": {"type": "string", "description": "Type of meeting (orientation, one_on_one, exit_interview, team_intro)"},
301
+ },
302
+ "required": ["title", "attendees", "datetime"],
303
+ },
304
+ },
305
+ {
306
+ "name": "policy_lookup",
307
+ "description": "Look up company policies by topic or department. Returns policy content and key rules.",
308
+ "parameters": {
309
+ "type": "object",
310
+ "properties": {
311
+ "topic": {"type": "string", "description": "Policy topic to search for"},
312
+ "department": {"type": "string", "description": "Department-specific policies"},
313
+ "policy_id": {"type": "string", "description": "Specific policy ID"},
314
+ },
315
+ },
316
+ },
317
+ {
318
+ "name": "approval_request",
319
+ "description": "Submit an approval request (manager approval, IT approval, security approval). Approver must meet level requirements.",
320
+ "parameters": {
321
+ "type": "object",
322
+ "properties": {
323
+ "request_id": {"type": "string", "description": "The onboarding/offboarding request ID this approval is for"},
324
+ "approver_id": {"type": "string", "description": "Employee ID of the approver"},
325
+ "approval_type": {"type": "string", "description": "Type of approval (manager_approval, it_approval, security_approval, legal_approval)"},
326
+ },
327
+ "required": ["request_id", "approver_id", "approval_type"],
328
+ },
329
+ },
330
+ ]
331
+
332
+
333
+ class ToolRegistry:
334
+ """Registry of all available tools, mapping names to executors."""
335
+
336
+ def __init__(self, world: WorldState):
337
+ self.world = world
338
+ self._tools: dict[str, Callable] = self._register_tools()
339
+
340
+ def _register_tools(self) -> dict[str, Callable]:
341
+ return {
342
+ "hr_create_employee": self._hr_create_employee,
343
+ "hr_read_employee": self._hr_read_employee,
344
+ "hr_update_employee": self._hr_update_employee,
345
+ "hr_search_employees": self._hr_search_employees,
346
+ "hr_get_org_chart": self._hr_get_org_chart,
347
+ "onboarding_create_request": self._onboarding_create_request,
348
+ "onboarding_get_status": self._onboarding_get_status,
349
+ "onboarding_complete_step": self._onboarding_complete_step,
350
+ "offboarding_create_request": self._offboarding_create_request,
351
+ "offboarding_get_status": self._offboarding_get_status,
352
+ "offboarding_complete_step": self._offboarding_complete_step,
353
+ "it_assign_asset": self._it_assign_asset,
354
+ "it_get_available_assets": self._it_get_available_assets,
355
+ "it_create_account": self._it_create_account,
356
+ "it_revoke_access": self._it_revoke_access,
357
+ "it_get_software_licenses": self._it_get_software_licenses,
358
+ "access_assign_role": self._access_assign_role,
359
+ "access_create_badge": self._access_create_badge,
360
+ "access_revoke_role": self._access_revoke_role,
361
+ "access_get_security_groups": self._access_get_security_groups,
362
+ "email_send": self._email_send,
363
+ "slack_send_message": self._slack_send_message,
364
+ "meeting_schedule": self._meeting_schedule,
365
+ "policy_lookup": self._policy_lookup,
366
+ "approval_request": self._approval_request,
367
+ }
368
+
369
+ @property
370
+ def tool_definitions(self) -> list[dict]:
371
+ return TOOL_DEFINITIONS
372
+
373
+ @property
374
+ def tool_names(self) -> list[str]:
375
+ return list(self._tools.keys())
376
+
377
+ def execute(self, tool_name: str, params: dict) -> dict:
378
+ """Execute a tool by name with given parameters. Returns result dict."""
379
+ if tool_name not in self._tools:
380
+ return {"success": False, "error": f"Unknown tool: {tool_name}. Available tools: {self.tool_names}"}
381
+ try:
382
+ result = self._tools[tool_name](params)
383
+ self.world.log_action(tool_name, params, result)
384
+ return result
385
+ except Exception as e:
386
+ error_result = {"success": False, "error": f"Tool execution error: {str(e)}"}
387
+ self.world.log_action(tool_name, params, error_result)
388
+ return error_result
389
+
390
+ # ---- Tool Implementations ----
391
+
392
+ def _hr_create_employee(self, params: dict) -> dict:
393
+ return self.world.create_employee(params)
394
+
395
+ def _hr_read_employee(self, params: dict) -> dict:
396
+ emp = None
397
+ if "emp_id" in params:
398
+ emp = self.world.get_employee(params["emp_id"])
399
+ elif "email" in params:
400
+ emp = self.world.get_employee_by_email(params["email"])
401
+ if emp:
402
+ return {"success": True, "employee": emp}
403
+ return {"success": False, "error": "Employee not found"}
404
+
405
+ def _hr_update_employee(self, params: dict) -> dict:
406
+ return self.world.update_employee(params["emp_id"], params["updates"])
407
+
408
+ def _hr_search_employees(self, params: dict) -> dict:
409
+ results = self.world.search_employees(**params)
410
+ return {"success": True, "count": len(results), "employees": results}
411
+
412
+ def _hr_get_org_chart(self, params: dict) -> dict:
413
+ chart = self.world.get_org_chart(params["department"])
414
+ if chart:
415
+ return {"success": True, "org_chart": chart}
416
+ return {"success": False, "error": f"No employees found in department '{params['department']}'"}
417
+
418
+ def _onboarding_create_request(self, params: dict) -> dict:
419
+ return self.world.create_onboarding_request(params["employee_id"])
420
+
421
+ def _onboarding_get_status(self, params: dict) -> dict:
422
+ status = self.world.get_onboarding_status(
423
+ request_id=params.get("request_id"),
424
+ emp_id=params.get("employee_id"),
425
+ )
426
+ if status:
427
+ return {"success": True, "onboarding_status": status}
428
+ return {"success": False, "error": "Onboarding request not found"}
429
+
430
+ def _onboarding_complete_step(self, params: dict) -> dict:
431
+ return self.world.complete_onboarding_step(params["request_id"], params["step"])
432
+
433
+ def _offboarding_create_request(self, params: dict) -> dict:
434
+ return self.world.create_offboarding_request(
435
+ params["employee_id"],
436
+ reason=params.get("reason", "resignation"),
437
+ exit_date=params.get("exit_date"),
438
+ )
439
+
440
+ def _offboarding_get_status(self, params: dict) -> dict:
441
+ status = self.world.get_offboarding_status(
442
+ request_id=params.get("request_id"),
443
+ emp_id=params.get("employee_id"),
444
+ )
445
+ if status:
446
+ return {"success": True, "offboarding_status": status}
447
+ return {"success": False, "error": "Offboarding request not found"}
448
+
449
+ def _offboarding_complete_step(self, params: dict) -> dict:
450
+ return self.world.complete_offboarding_step(params["request_id"], params["step"])
451
+
452
+ def _it_assign_asset(self, params: dict) -> dict:
453
+ return self.world.assign_asset(params["asset_id"], params["employee_id"])
454
+
455
+ def _it_get_available_assets(self, params: dict) -> dict:
456
+ assets = self.world.get_available_assets(params.get("asset_type"))
457
+ return {"success": True, "count": len(assets), "assets": assets}
458
+
459
+ def _it_create_account(self, params: dict) -> dict:
460
+ return self.world.create_it_account(
461
+ params["employee_id"],
462
+ account_types=params.get("account_types"),
463
+ )
464
+
465
+ def _it_revoke_access(self, params: dict) -> dict:
466
+ return self.world.revoke_it_access(params["employee_id"])
467
+
468
+ def _it_get_software_licenses(self, params: dict) -> dict:
469
+ licenses = self.world.get_software_licenses(params.get("software_name"))
470
+ return {"success": True, "licenses": licenses}
471
+
472
+ def _access_assign_role(self, params: dict) -> dict:
473
+ return self.world.assign_role(params["employee_id"], params["role_id"])
474
+
475
+ def _access_create_badge(self, params: dict) -> dict:
476
+ return self.world.create_badge(
477
+ params["employee_id"],
478
+ access_zones=params.get("access_zones"),
479
+ )
480
+
481
+ def _access_revoke_role(self, params: dict) -> dict:
482
+ return self.world.revoke_role(params["employee_id"], params["role_id"])
483
+
484
+ def _access_get_security_groups(self, params: dict) -> dict:
485
+ return {"success": True, "security_groups": self.world.state["security_groups"]}
486
+
487
+ def _email_send(self, params: dict) -> dict:
488
+ return self.world.send_email(
489
+ params["from_address"], params["to_address"],
490
+ params["subject"], params["body"],
491
+ )
492
+
493
+ def _slack_send_message(self, params: dict) -> dict:
494
+ return self.world.send_slack_message(
495
+ params["channel"], params["sender"], params["text"],
496
+ )
497
+
498
+ def _meeting_schedule(self, params: dict) -> dict:
499
+ return self.world.schedule_meeting(
500
+ params["title"], params["attendees"],
501
+ params["datetime"], params.get("meeting_type", "general"),
502
+ )
503
+
504
+ def _policy_lookup(self, params: dict) -> dict:
505
+ policies = self.world.lookup_policy(
506
+ topic=params.get("topic"),
507
+ department=params.get("department"),
508
+ policy_id=params.get("policy_id"),
509
+ )
510
+ return {"success": True, "count": len(policies), "policies": policies}
511
+
512
+ def _approval_request(self, params: dict) -> dict:
513
+ return self.world.create_approval(
514
+ params["request_id"], params["approver_id"], params["approval_type"],
515
+ )
server/world.py ADDED
@@ -0,0 +1,737 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """World state management for HR Onboarding/Offboarding environment.
2
+
3
+ Manages all entities (employees, departments, assets, etc.) and enforces
4
+ business rules like RBAC, approval chains, and headcount limits.
5
+ """
6
+
7
+ import json
8
+ import copy
9
+ import os
10
+ from datetime import datetime, timedelta
11
+ from typing import Any, Optional
12
+ from pathlib import Path
13
+
14
+ DATA_DIR = Path(__file__).parent / "data"
15
+
16
+
17
+ class WorldState:
18
+ """Manages the full world state: all entities and their relationships."""
19
+
20
+ def __init__(self):
21
+ self._initial_state: dict[str, Any] = {}
22
+ self.state: dict[str, Any] = {}
23
+ self._action_log: list[dict] = []
24
+ self._load_data()
25
+
26
+ def _load_data(self):
27
+ """Load all entity data from JSON files."""
28
+ data_files = {
29
+ "employees": "employees.json",
30
+ "departments": "departments.json",
31
+ "policies": "policies.json",
32
+ "it_assets": "it_assets.json",
33
+ "access_roles": "access_roles.json",
34
+ "templates": "templates.json",
35
+ }
36
+ for key, filename in data_files.items():
37
+ path = DATA_DIR / filename
38
+ if path.exists():
39
+ with open(path) as f:
40
+ data = json.load(f)
41
+ # Handle wrapped data (e.g. {"departments": [...]})
42
+ if isinstance(data, dict) and key in data:
43
+ data = data[key]
44
+ self.state[key] = data
45
+ else:
46
+ self.state[key] = []
47
+
48
+ # Initialize dynamic collections
49
+ self.state.setdefault("onboarding_requests", [])
50
+ self.state.setdefault("offboarding_requests", [])
51
+ self.state.setdefault("approvals", [])
52
+ self.state.setdefault("emails", [])
53
+ self.state.setdefault("slack_messages", [])
54
+ self.state.setdefault("meetings", [])
55
+ self.state.setdefault("badges", [])
56
+ self.state.setdefault("security_groups", self._init_security_groups())
57
+
58
+ # Build indexes for fast lookup
59
+ self._build_indexes()
60
+
61
+ # Save initial state for reset
62
+ self._initial_state = copy.deepcopy(self.state)
63
+
64
+ def _init_security_groups(self) -> list[dict]:
65
+ """Create default security groups."""
66
+ groups = [
67
+ {"group_id": "sg_001", "name": "all_employees", "members": [], "resources_accessible": ["email", "slack", "intranet"]},
68
+ {"group_id": "sg_002", "name": "engineering_team", "members": [], "resources_accessible": ["github", "aws_dev", "ci_cd", "jira"]},
69
+ {"group_id": "sg_003", "name": "prod_access", "members": [], "resources_accessible": ["aws_prod", "monitoring", "pagerduty"]},
70
+ {"group_id": "sg_004", "name": "data_team", "members": [], "resources_accessible": ["databricks", "snowflake", "jupyter"]},
71
+ {"group_id": "sg_005", "name": "finance_access", "members": [], "resources_accessible": ["netsuite", "expense_system", "payroll"]},
72
+ {"group_id": "sg_006", "name": "hr_access", "members": [], "resources_accessible": ["workday", "benefits_portal", "recruiting"]},
73
+ {"group_id": "sg_007", "name": "security_team", "members": [], "resources_accessible": ["siem", "vault", "firewall_mgmt"]},
74
+ {"group_id": "sg_008", "name": "sales_team", "members": [], "resources_accessible": ["salesforce", "hubspot", "linkedin_sales"]},
75
+ {"group_id": "sg_009", "name": "server_room_access", "members": [], "resources_accessible": ["server_room_a", "server_room_b"]},
76
+ {"group_id": "sg_010", "name": "vpn_users", "members": [], "resources_accessible": ["vpn_corporate", "vpn_dev"]},
77
+ {"group_id": "sg_011", "name": "admin_access", "members": [], "resources_accessible": ["admin_console", "user_mgmt", "audit_logs"]},
78
+ {"group_id": "sg_012", "name": "product_team", "members": [], "resources_accessible": ["figma", "amplitude", "productboard"]},
79
+ {"group_id": "sg_013", "name": "marketing_team", "members": [], "resources_accessible": ["hubspot", "canva", "google_analytics"]},
80
+ {"group_id": "sg_014", "name": "executives", "members": [], "resources_accessible": ["board_docs", "exec_dashboard", "all_financials"]},
81
+ {"group_id": "sg_015", "name": "contractors", "members": [], "resources_accessible": ["email", "slack", "jira"]},
82
+ ]
83
+ # Populate members from employees
84
+ return groups
85
+
86
+ def _build_indexes(self):
87
+ """Build lookup indexes for fast entity access."""
88
+ self._emp_by_id = {}
89
+ self._emp_by_email = {}
90
+ self._emp_by_dept = {}
91
+ for emp in self.state.get("employees", []):
92
+ self._emp_by_id[emp["emp_id"]] = emp
93
+ self._emp_by_email[emp.get("email", "")] = emp
94
+ dept = emp.get("department", "")
95
+ self._emp_by_dept.setdefault(dept, []).append(emp)
96
+
97
+ self._dept_by_id = {d["dept_id"]: d for d in self.state.get("departments", [])}
98
+ self._dept_by_name = {d["name"]: d for d in self.state.get("departments", [])}
99
+ self._asset_by_id = {a["asset_id"]: a for a in self.state.get("it_assets", [])}
100
+ self._role_by_id = {r["role_id"]: r for r in self.state.get("access_roles", [])}
101
+ self._policy_by_id = {p["policy_id"]: p for p in self.state.get("policies", [])}
102
+
103
+ def reset(self):
104
+ """Reset world state to initial conditions and clear action log."""
105
+ self.state = copy.deepcopy(self._initial_state)
106
+ self._action_log = []
107
+ self._build_indexes()
108
+
109
+ def snapshot(self) -> dict:
110
+ """Return a deep copy of the current state (for debugging/evaluation)."""
111
+ return copy.deepcopy(self.state)
112
+
113
+ def log_action(self, tool_name: str, params: dict, result: Any):
114
+ """Log a tool call for rubric evaluation."""
115
+ self._action_log.append({
116
+ "tool": tool_name,
117
+ "params": params,
118
+ "result": result,
119
+ "timestamp": datetime.now().isoformat(),
120
+ })
121
+
122
+ @property
123
+ def action_log(self) -> list[dict]:
124
+ return list(self._action_log)
125
+
126
+ # ---- Entity Lookup Methods ----
127
+
128
+ def get_employee(self, emp_id: str) -> Optional[dict]:
129
+ return self._emp_by_id.get(emp_id)
130
+
131
+ def get_employee_by_email(self, email: str) -> Optional[dict]:
132
+ return self._emp_by_email.get(email)
133
+
134
+ def search_employees(self, **filters) -> list[dict]:
135
+ results = list(self.state["employees"])
136
+ for key, value in filters.items():
137
+ if value is not None:
138
+ results = [e for e in results if str(e.get(key, "")).lower() == str(value).lower()]
139
+ return results
140
+
141
+ def get_department(self, dept_id: str = None, name: str = None) -> Optional[dict]:
142
+ if dept_id:
143
+ return self._dept_by_id.get(dept_id)
144
+ if name:
145
+ return self._dept_by_name.get(name)
146
+ return None
147
+
148
+ def get_employees_in_dept(self, department: str) -> list[dict]:
149
+ return self._emp_by_dept.get(department, [])
150
+
151
+ def get_manager(self, emp_id: str) -> Optional[dict]:
152
+ emp = self.get_employee(emp_id)
153
+ if emp and emp.get("manager_id"):
154
+ return self.get_employee(emp["manager_id"])
155
+ return None
156
+
157
+ def get_skip_level_manager(self, emp_id: str) -> Optional[dict]:
158
+ manager = self.get_manager(emp_id)
159
+ if manager:
160
+ return self.get_manager(manager["emp_id"])
161
+ return None
162
+
163
+ def get_direct_reports(self, emp_id: str) -> list[dict]:
164
+ return [e for e in self.state["employees"] if e.get("manager_id") == emp_id]
165
+
166
+ def get_org_chart(self, department: str) -> dict:
167
+ """Build org chart for a department as a tree."""
168
+ dept_emps = self.get_employees_in_dept(department)
169
+ if not dept_emps:
170
+ return {}
171
+
172
+ # Find the root (highest level with no manager in dept, or L6)
173
+ def build_tree(emp):
174
+ reports = [e for e in dept_emps if e.get("manager_id") == emp["emp_id"]]
175
+ return {
176
+ "emp_id": emp["emp_id"],
177
+ "name": emp["name"],
178
+ "level": emp["level"],
179
+ "role": emp["role"],
180
+ "status": emp["status"],
181
+ "reports": [build_tree(r) for r in reports],
182
+ }
183
+
184
+ roots = [e for e in dept_emps if e.get("manager_id") is None
185
+ or e["manager_id"] not in {x["emp_id"] for x in dept_emps}]
186
+ if not roots:
187
+ roots = sorted(dept_emps, key=lambda e: e.get("level", "L1"), reverse=True)[:1]
188
+
189
+ return {"department": department, "org_tree": [build_tree(r) for r in roots]}
190
+
191
+ # ---- Asset Methods ----
192
+
193
+ def get_available_assets(self, asset_type: str = None) -> list[dict]:
194
+ assets = [a for a in self.state["it_assets"] if a["status"] == "available"]
195
+ if asset_type:
196
+ assets = [a for a in assets if a["type"].lower() == asset_type.lower()]
197
+ return assets
198
+
199
+ def assign_asset(self, asset_id: str, emp_id: str) -> dict:
200
+ asset = self._asset_by_id.get(asset_id)
201
+ if not asset:
202
+ return {"success": False, "error": f"Asset {asset_id} not found"}
203
+ if asset["status"] != "available":
204
+ return {"success": False, "error": f"Asset {asset_id} is not available (status: {asset['status']})"}
205
+ emp = self.get_employee(emp_id)
206
+ if not emp:
207
+ return {"success": False, "error": f"Employee {emp_id} not found"}
208
+
209
+ asset["status"] = "assigned"
210
+ asset["assigned_to"] = emp_id
211
+ return {"success": True, "asset_id": asset_id, "assigned_to": emp_id}
212
+
213
+ def reclaim_asset(self, asset_id: str) -> dict:
214
+ asset = self._asset_by_id.get(asset_id)
215
+ if not asset:
216
+ return {"success": False, "error": f"Asset {asset_id} not found"}
217
+ if asset["status"] != "assigned":
218
+ return {"success": False, "error": f"Asset {asset_id} is not currently assigned"}
219
+ prev_owner = asset["assigned_to"]
220
+ asset["status"] = "available"
221
+ asset["assigned_to"] = None
222
+ return {"success": True, "asset_id": asset_id, "reclaimed_from": prev_owner}
223
+
224
+ def get_assets_for_employee(self, emp_id: str) -> list[dict]:
225
+ return [a for a in self.state["it_assets"] if a.get("assigned_to") == emp_id]
226
+
227
+ # ---- Software License Methods ----
228
+
229
+ def get_software_licenses(self, software_name: str = None) -> list[dict]:
230
+ licenses = self.state.get("access_roles", [])
231
+ # Licenses are tracked in departments' required_tools + a separate license pool
232
+ # For simplicity, we embed license info in a dedicated structure
233
+ if not self.state.get("software_licenses"):
234
+ self.state["software_licenses"] = self._init_software_licenses()
235
+ result = self.state["software_licenses"]
236
+ if software_name:
237
+ result = [l for l in result if l["software_name"].lower() == software_name.lower()]
238
+ return result
239
+
240
+ def _init_software_licenses(self) -> list[dict]:
241
+ return [
242
+ {"license_id": "lic_001", "software_name": "Jira", "total_seats": 80, "used_seats": 72, "department_restriction": None},
243
+ {"license_id": "lic_002", "software_name": "GitHub", "total_seats": 60, "used_seats": 55, "department_restriction": "Engineering"},
244
+ {"license_id": "lic_003", "software_name": "AWS", "total_seats": 40, "used_seats": 35, "department_restriction": "Engineering"},
245
+ {"license_id": "lic_004", "software_name": "Slack", "total_seats": 250, "used_seats": 195, "department_restriction": None},
246
+ {"license_id": "lic_005", "software_name": "Salesforce", "total_seats": 35, "used_seats": 30, "department_restriction": "Sales"},
247
+ {"license_id": "lic_006", "software_name": "Hubspot", "total_seats": 30, "used_seats": 28, "department_restriction": "Marketing"},
248
+ {"license_id": "lic_007", "software_name": "Figma", "total_seats": 25, "used_seats": 20, "department_restriction": "Product"},
249
+ {"license_id": "lic_008", "software_name": "Databricks", "total_seats": 20, "used_seats": 18, "department_restriction": "Data Science"},
250
+ {"license_id": "lic_009", "software_name": "Netsuite", "total_seats": 15, "used_seats": 15, "department_restriction": "Finance"}, # Full!
251
+ {"license_id": "lic_010", "software_name": "Workday", "total_seats": 20, "used_seats": 12, "department_restriction": "HR"},
252
+ {"license_id": "lic_011", "software_name": "Canva", "total_seats": 25, "used_seats": 20, "department_restriction": "Marketing"},
253
+ {"license_id": "lic_012", "software_name": "Google Analytics", "total_seats": 30, "used_seats": 22, "department_restriction": None},
254
+ {"license_id": "lic_013", "software_name": "VSCode License", "total_seats": 70, "used_seats": 60, "department_restriction": None},
255
+ {"license_id": "lic_014", "software_name": "Amplitude", "total_seats": 20, "used_seats": 15, "department_restriction": "Product"},
256
+ {"license_id": "lic_015", "software_name": "LinkedIn Sales Navigator", "total_seats": 25, "used_seats": 25, "department_restriction": "Sales"}, # Full!
257
+ ]
258
+
259
+ def consume_license(self, license_id: str) -> dict:
260
+ if not self.state.get("software_licenses"):
261
+ self.state["software_licenses"] = self._init_software_licenses()
262
+ for lic in self.state["software_licenses"]:
263
+ if lic["license_id"] == license_id:
264
+ if lic["used_seats"] >= lic["total_seats"]:
265
+ return {"success": False, "error": f"No available seats for {lic['software_name']} (all {lic['total_seats']} seats in use)"}
266
+ lic["used_seats"] += 1
267
+ return {"success": True, "software": lic["software_name"], "remaining_seats": lic["total_seats"] - lic["used_seats"]}
268
+ return {"success": False, "error": f"License {license_id} not found"}
269
+
270
+ def release_license(self, license_id: str) -> dict:
271
+ if not self.state.get("software_licenses"):
272
+ return {"success": False, "error": "No licenses initialized"}
273
+ for lic in self.state["software_licenses"]:
274
+ if lic["license_id"] == license_id:
275
+ if lic["used_seats"] > 0:
276
+ lic["used_seats"] -= 1
277
+ return {"success": True, "software": lic["software_name"], "remaining_seats": lic["total_seats"] - lic["used_seats"]}
278
+ return {"success": False, "error": f"License {license_id} not found"}
279
+
280
+ # ---- Employee Mutation Methods ----
281
+
282
+ def create_employee(self, data: dict) -> dict:
283
+ required = ["name", "department", "level", "role"]
284
+ for field in required:
285
+ if field not in data:
286
+ return {"success": False, "error": f"Missing required field: {field}"}
287
+
288
+ dept = self.get_department(name=data["department"])
289
+ if not dept:
290
+ return {"success": False, "error": f"Department '{data['department']}' not found"}
291
+
292
+ # Check headcount limit
293
+ current = len([e for e in self.state["employees"]
294
+ if e["department"] == data["department"] and e["status"] in ("active", "pending")])
295
+ if current >= dept.get("headcount_limit", 999):
296
+ return {"success": False, "error": f"Department '{data['department']}' has reached its headcount limit ({dept['headcount_limit']})"}
297
+
298
+ # Generate emp_id
299
+ existing_ids = [int(e["emp_id"].split("_")[1]) for e in self.state["employees"]]
300
+ new_id = f"emp_{max(existing_ids) + 1:04d}" if existing_ids else "emp_0001"
301
+
302
+ email = f"{data['name'].lower().replace(' ', '.')}@acmecorp.com"
303
+
304
+ employee = {
305
+ "emp_id": new_id,
306
+ "name": data["name"],
307
+ "email": data.get("email", email),
308
+ "department": data["department"],
309
+ "level": data["level"],
310
+ "role": data["role"],
311
+ "manager_id": data.get("manager_id"),
312
+ "status": "pending",
313
+ "date_of_joining": data.get("date_of_joining", datetime.now().strftime("%Y-%m-%d")),
314
+ "date_of_leaving": None,
315
+ "is_contractor": data.get("is_contractor", False),
316
+ "phone": data.get("phone", ""),
317
+ "location": data.get("location", "San Francisco"),
318
+ }
319
+
320
+ self.state["employees"].append(employee)
321
+ self._emp_by_id[new_id] = employee
322
+ self._emp_by_email[employee["email"]] = employee
323
+ self._emp_by_dept.setdefault(employee["department"], []).append(employee)
324
+
325
+ return {"success": True, "employee": employee}
326
+
327
+ def update_employee(self, emp_id: str, updates: dict) -> dict:
328
+ emp = self.get_employee(emp_id)
329
+ if not emp:
330
+ return {"success": False, "error": f"Employee {emp_id} not found"}
331
+
332
+ protected_fields = {"emp_id"}
333
+ for key, value in updates.items():
334
+ if key in protected_fields:
335
+ return {"success": False, "error": f"Cannot modify protected field: {key}"}
336
+ emp[key] = value
337
+
338
+ return {"success": True, "employee": emp}
339
+
340
+ # ---- Onboarding/Offboarding Request Methods ----
341
+
342
+ def create_onboarding_request(self, emp_id: str) -> dict:
343
+ emp = self.get_employee(emp_id)
344
+ if not emp:
345
+ return {"success": False, "error": f"Employee {emp_id} not found"}
346
+ if emp["status"] == "active":
347
+ return {"success": False, "error": f"Employee {emp_id} is already active"}
348
+
349
+ existing = [r for r in self.state["onboarding_requests"]
350
+ if r["employee_id"] == emp_id and r["status"] != "completed"]
351
+ if existing:
352
+ return {"success": False, "error": f"Active onboarding request already exists for {emp_id}"}
353
+
354
+ dept = self.get_department(name=emp["department"])
355
+ steps = dept.get("onboarding_steps", [
356
+ "hr_paperwork", "it_account_setup", "asset_assignment",
357
+ "access_provisioning", "orientation_scheduled", "manager_intro",
358
+ "welcome_communications"
359
+ ]) if dept else ["hr_paperwork", "it_account_setup", "asset_assignment",
360
+ "access_provisioning", "orientation_scheduled", "manager_intro",
361
+ "welcome_communications"]
362
+
363
+ req_ids = [int(r["request_id"].split("_")[1]) for r in self.state["onboarding_requests"]] or [0]
364
+ new_id = f"onb_{max(req_ids) + 1:04d}"
365
+
366
+ request = {
367
+ "request_id": new_id,
368
+ "employee_id": emp_id,
369
+ "department": emp["department"],
370
+ "status": "in_progress",
371
+ "steps": {step: "pending" for step in steps},
372
+ "steps_completed": [],
373
+ "approvals_required": self._get_required_approvals(emp),
374
+ "approvals_received": [],
375
+ "created_date": datetime.now().strftime("%Y-%m-%d"),
376
+ }
377
+
378
+ self.state["onboarding_requests"].append(request)
379
+ return {"success": True, "request": request}
380
+
381
+ def create_offboarding_request(self, emp_id: str, reason: str = "resignation", exit_date: str = None) -> dict:
382
+ emp = self.get_employee(emp_id)
383
+ if not emp:
384
+ return {"success": False, "error": f"Employee {emp_id} not found"}
385
+ if emp["status"] == "offboarded":
386
+ return {"success": False, "error": f"Employee {emp_id} is already offboarded"}
387
+
388
+ existing = [r for r in self.state["offboarding_requests"]
389
+ if r["employee_id"] == emp_id and r["status"] != "completed"]
390
+ if existing:
391
+ return {"success": False, "error": f"Active offboarding request already exists for {emp_id}"}
392
+
393
+ dept = self.get_department(name=emp["department"])
394
+ steps = dept.get("offboarding_steps", [
395
+ "access_revocation", "asset_return", "knowledge_transfer",
396
+ "exit_interview", "final_payroll", "farewell_communications"
397
+ ]) if dept else ["access_revocation", "asset_return", "knowledge_transfer",
398
+ "exit_interview", "final_payroll", "farewell_communications"]
399
+
400
+ if reason == "termination":
401
+ steps = ["access_revocation", "asset_return", "final_payroll", "legal_review"]
402
+
403
+ req_ids = [int(r["request_id"].split("_")[1]) for r in self.state["offboarding_requests"]] or [0]
404
+ new_id = f"off_{max(req_ids) + 1:04d}"
405
+
406
+ if not exit_date:
407
+ exit_date = (datetime.now() + timedelta(days=14)).strftime("%Y-%m-%d")
408
+
409
+ request = {
410
+ "request_id": new_id,
411
+ "employee_id": emp_id,
412
+ "department": emp["department"],
413
+ "reason": reason,
414
+ "status": "in_progress",
415
+ "exit_date": exit_date,
416
+ "steps": {step: "pending" for step in steps},
417
+ "steps_completed": [],
418
+ "handover_status": "pending",
419
+ "created_date": datetime.now().strftime("%Y-%m-%d"),
420
+ }
421
+
422
+ self.state["offboarding_requests"].append(request)
423
+ return {"success": True, "request": request}
424
+
425
+ def complete_onboarding_step(self, request_id: str, step: str) -> dict:
426
+ req = next((r for r in self.state["onboarding_requests"] if r["request_id"] == request_id), None)
427
+ if not req:
428
+ return {"success": False, "error": f"Onboarding request {request_id} not found"}
429
+ if req["status"] == "completed":
430
+ return {"success": False, "error": f"Onboarding request {request_id} is already completed"}
431
+ if step not in req["steps"]:
432
+ return {"success": False, "error": f"Invalid step '{step}'. Valid steps: {list(req['steps'].keys())}"}
433
+ if req["steps"][step] == "completed":
434
+ return {"success": False, "error": f"Step '{step}' is already completed"}
435
+
436
+ req["steps"][step] = "completed"
437
+ req["steps_completed"].append(step)
438
+
439
+ # Check if all steps are done
440
+ if all(v == "completed" for v in req["steps"].values()):
441
+ req["status"] = "completed"
442
+ emp = self.get_employee(req["employee_id"])
443
+ if emp:
444
+ emp["status"] = "active"
445
+
446
+ return {"success": True, "request_id": request_id, "step": step,
447
+ "all_complete": req["status"] == "completed",
448
+ "remaining_steps": [k for k, v in req["steps"].items() if v != "completed"]}
449
+
450
+ def complete_offboarding_step(self, request_id: str, step: str) -> dict:
451
+ req = next((r for r in self.state["offboarding_requests"] if r["request_id"] == request_id), None)
452
+ if not req:
453
+ return {"success": False, "error": f"Offboarding request {request_id} not found"}
454
+ if req["status"] == "completed":
455
+ return {"success": False, "error": f"Offboarding request {request_id} is already completed"}
456
+ if step not in req["steps"]:
457
+ return {"success": False, "error": f"Invalid step '{step}'. Valid steps: {list(req['steps'].keys())}"}
458
+ if req["steps"][step] == "completed":
459
+ return {"success": False, "error": f"Step '{step}' is already completed"}
460
+
461
+ req["steps"][step] = "completed"
462
+ req["steps_completed"].append(step)
463
+
464
+ if all(v == "completed" for v in req["steps"].values()):
465
+ req["status"] = "completed"
466
+ emp = self.get_employee(req["employee_id"])
467
+ if emp:
468
+ emp["status"] = "offboarded"
469
+ emp["date_of_leaving"] = req["exit_date"]
470
+
471
+ return {"success": True, "request_id": request_id, "step": step,
472
+ "all_complete": req["status"] == "completed",
473
+ "remaining_steps": [k for k, v in req["steps"].items() if v != "completed"]}
474
+
475
+ def get_onboarding_status(self, request_id: str = None, emp_id: str = None) -> Optional[dict]:
476
+ if request_id:
477
+ return next((r for r in self.state["onboarding_requests"] if r["request_id"] == request_id), None)
478
+ if emp_id:
479
+ reqs = [r for r in self.state["onboarding_requests"] if r["employee_id"] == emp_id]
480
+ return reqs[-1] if reqs else None
481
+ return None
482
+
483
+ def get_offboarding_status(self, request_id: str = None, emp_id: str = None) -> Optional[dict]:
484
+ if request_id:
485
+ return next((r for r in self.state["offboarding_requests"] if r["request_id"] == request_id), None)
486
+ if emp_id:
487
+ reqs = [r for r in self.state["offboarding_requests"] if r["employee_id"] == emp_id]
488
+ return reqs[-1] if reqs else None
489
+ return None
490
+
491
+ # ---- Approval Methods ----
492
+
493
+ def _get_required_approvals(self, emp: dict) -> list[str]:
494
+ """Determine what approvals are needed based on employee level and department."""
495
+ approvals = ["manager_approval"]
496
+ level_num = int(emp["level"][1])
497
+ if level_num >= 3:
498
+ approvals.append("it_approval")
499
+ if emp["department"] == "Security" or level_num >= 4:
500
+ approvals.append("security_approval")
501
+ if emp.get("is_contractor"):
502
+ approvals.append("legal_approval")
503
+ return approvals
504
+
505
+ def create_approval(self, request_id: str, approver_id: str, approval_type: str) -> dict:
506
+ emp = self.get_employee(approver_id)
507
+ if not emp:
508
+ return {"success": False, "error": f"Approver {approver_id} not found"}
509
+
510
+ level_num = int(emp["level"][1])
511
+ if approval_type == "manager_approval" and level_num < 3:
512
+ return {"success": False, "error": f"Approver must be L3+ for manager approval (current: {emp['level']})"}
513
+ if approval_type == "security_approval" and level_num < 4:
514
+ return {"success": False, "error": f"Approver must be L4+ for security approval (current: {emp['level']})"}
515
+
516
+ approval_ids = [int(a["approval_id"].split("_")[1]) for a in self.state["approvals"]] or [0]
517
+ new_id = f"apr_{max(approval_ids) + 1:04d}"
518
+
519
+ approval = {
520
+ "approval_id": new_id,
521
+ "request_id": request_id,
522
+ "approver_id": approver_id,
523
+ "type": approval_type,
524
+ "status": "approved",
525
+ "timestamp": datetime.now().isoformat(),
526
+ }
527
+ self.state["approvals"].append(approval)
528
+
529
+ # Update onboarding/offboarding request approvals
530
+ for req in self.state["onboarding_requests"] + self.state["offboarding_requests"]:
531
+ if req["request_id"] == request_id:
532
+ if approval_type not in req.get("approvals_received", []):
533
+ req.setdefault("approvals_received", []).append(approval_type)
534
+
535
+ return {"success": True, "approval": approval}
536
+
537
+ # ---- Access Role Methods ----
538
+
539
+ def assign_role(self, emp_id: str, role_id: str) -> dict:
540
+ emp = self.get_employee(emp_id)
541
+ if not emp:
542
+ return {"success": False, "error": f"Employee {emp_id} not found"}
543
+ role = self._role_by_id.get(role_id)
544
+ if not role:
545
+ return {"success": False, "error": f"Role {role_id} not found"}
546
+
547
+ # Check level requirement
548
+ emp_level = int(emp["level"][1])
549
+ req_level = int(role.get("level_requirement", "L1")[1])
550
+ if emp_level < req_level:
551
+ return {"success": False, "error": f"Employee level {emp['level']} does not meet minimum {role['level_requirement']} for role {role['name']}"}
552
+
553
+ # Check department restriction
554
+ if role["department"] != "all" and role["department"] != emp["department"]:
555
+ return {"success": False, "error": f"Role {role['name']} is restricted to {role['department']} department"}
556
+
557
+ emp.setdefault("assigned_roles", []).append(role_id)
558
+ return {"success": True, "emp_id": emp_id, "role": role["name"], "permissions": role["permissions"]}
559
+
560
+ def revoke_role(self, emp_id: str, role_id: str) -> dict:
561
+ emp = self.get_employee(emp_id)
562
+ if not emp:
563
+ return {"success": False, "error": f"Employee {emp_id} not found"}
564
+ roles = emp.get("assigned_roles", [])
565
+ if role_id not in roles:
566
+ return {"success": False, "error": f"Employee {emp_id} does not have role {role_id}"}
567
+ roles.remove(role_id)
568
+ return {"success": True, "emp_id": emp_id, "revoked_role": role_id}
569
+
570
+ def revoke_all_access(self, emp_id: str) -> dict:
571
+ emp = self.get_employee(emp_id)
572
+ if not emp:
573
+ return {"success": False, "error": f"Employee {emp_id} not found"}
574
+ revoked_roles = emp.get("assigned_roles", []).copy()
575
+ emp["assigned_roles"] = []
576
+
577
+ # Remove from security groups
578
+ for sg in self.state["security_groups"]:
579
+ if emp_id in sg["members"]:
580
+ sg["members"].remove(emp_id)
581
+
582
+ return {"success": True, "emp_id": emp_id, "revoked_roles": revoked_roles}
583
+
584
+ # ---- Badge Methods ----
585
+
586
+ def create_badge(self, emp_id: str, access_zones: list[str] = None) -> dict:
587
+ emp = self.get_employee(emp_id)
588
+ if not emp:
589
+ return {"success": False, "error": f"Employee {emp_id} not found"}
590
+
591
+ # Server room access requires L4+ approval
592
+ if access_zones and "server_room" in access_zones:
593
+ level_num = int(emp["level"][1])
594
+ if level_num < 4:
595
+ approvals = [a for a in self.state["approvals"]
596
+ if a["type"] == "security_approval"
597
+ and a["status"] == "approved"]
598
+ relevant = [a for a in approvals
599
+ if any(r["employee_id"] == emp_id
600
+ for r in self.state["onboarding_requests"]
601
+ if r["request_id"] == a["request_id"])]
602
+ if not relevant:
603
+ return {"success": False, "error": "Server room access requires L4+ security approval"}
604
+
605
+ badge_ids = [int(b["badge_id"].split("_")[1]) for b in self.state["badges"]] or [0]
606
+ new_id = f"badge_{max(badge_ids) + 1:04d}"
607
+
608
+ if not access_zones:
609
+ access_zones = ["main_entrance", "floor_" + emp.get("location", "sf").lower().replace(" ", "_")]
610
+
611
+ badge = {
612
+ "badge_id": new_id,
613
+ "employee_id": emp_id,
614
+ "access_zones": access_zones,
615
+ "status": "active",
616
+ "issued_date": datetime.now().strftime("%Y-%m-%d"),
617
+ }
618
+ self.state["badges"].append(badge)
619
+ return {"success": True, "badge": badge}
620
+
621
+ def revoke_badge(self, badge_id: str) -> dict:
622
+ badge = next((b for b in self.state["badges"] if b["badge_id"] == badge_id), None)
623
+ if not badge:
624
+ return {"success": False, "error": f"Badge {badge_id} not found"}
625
+ badge["status"] = "revoked"
626
+ return {"success": True, "badge_id": badge_id, "status": "revoked"}
627
+
628
+ def get_badges_for_employee(self, emp_id: str) -> list[dict]:
629
+ return [b for b in self.state["badges"] if b["employee_id"] == emp_id and b["status"] == "active"]
630
+
631
+ # ---- Communication Methods ----
632
+
633
+ def send_email(self, from_addr: str, to_addr: str, subject: str, body: str) -> dict:
634
+ email_ids = [int(e["email_id"].split("_")[1]) for e in self.state["emails"]] or [0]
635
+ new_id = f"email_{max(email_ids) + 1:04d}"
636
+ email = {
637
+ "email_id": new_id,
638
+ "from": from_addr,
639
+ "to": to_addr,
640
+ "subject": subject,
641
+ "body": body,
642
+ "timestamp": datetime.now().isoformat(),
643
+ }
644
+ self.state["emails"].append(email)
645
+ return {"success": True, "email": email}
646
+
647
+ def send_slack_message(self, channel: str, sender: str, text: str) -> dict:
648
+ msg_ids = [int(m["msg_id"].split("_")[1]) for m in self.state["slack_messages"]] or [0]
649
+ new_id = f"msg_{max(msg_ids) + 1:04d}"
650
+ message = {
651
+ "msg_id": new_id,
652
+ "channel": channel,
653
+ "sender": sender,
654
+ "text": text,
655
+ "timestamp": datetime.now().isoformat(),
656
+ }
657
+ self.state["slack_messages"].append(message)
658
+ return {"success": True, "message": message}
659
+
660
+ def schedule_meeting(self, title: str, attendees: list[str], meeting_datetime: str,
661
+ meeting_type: str = "general") -> dict:
662
+ meeting_ids = [int(m["meeting_id"].split("_")[1]) for m in self.state["meetings"]] or [0]
663
+ new_id = f"mtg_{max(meeting_ids) + 1:04d}"
664
+ meeting = {
665
+ "meeting_id": new_id,
666
+ "title": title,
667
+ "attendees": attendees,
668
+ "datetime": meeting_datetime,
669
+ "type": meeting_type,
670
+ }
671
+ self.state["meetings"].append(meeting)
672
+ return {"success": True, "meeting": meeting}
673
+
674
+ # ---- Policy Methods ----
675
+
676
+ def lookup_policy(self, topic: str = None, department: str = None, policy_id: str = None) -> list[dict]:
677
+ policies = self.state.get("policies", [])
678
+ if policy_id:
679
+ return [p for p in policies if p["policy_id"] == policy_id]
680
+ results = policies
681
+ if topic:
682
+ results = [p for p in results if topic.lower() in p.get("title", "").lower()
683
+ or topic.lower() in p.get("content", "").lower()]
684
+ if department:
685
+ results = [p for p in results if p.get("department") in (department, "all")]
686
+ return results
687
+
688
+ # ---- IT Account Methods ----
689
+
690
+ def create_it_account(self, emp_id: str, account_types: list[str] = None) -> dict:
691
+ emp = self.get_employee(emp_id)
692
+ if not emp:
693
+ return {"success": False, "error": f"Employee {emp_id} not found"}
694
+
695
+ if not account_types:
696
+ account_types = ["email", "slack", "vpn"]
697
+
698
+ created = []
699
+ for acct_type in account_types:
700
+ created.append({
701
+ "type": acct_type,
702
+ "username": emp["email"].split("@")[0],
703
+ "status": "active",
704
+ })
705
+
706
+ emp.setdefault("it_accounts", []).extend(created)
707
+ return {"success": True, "emp_id": emp_id, "accounts_created": created}
708
+
709
+ def revoke_it_access(self, emp_id: str) -> dict:
710
+ emp = self.get_employee(emp_id)
711
+ if not emp:
712
+ return {"success": False, "error": f"Employee {emp_id} not found"}
713
+
714
+ accounts = emp.get("it_accounts", [])
715
+ for acct in accounts:
716
+ acct["status"] = "revoked"
717
+
718
+ return {"success": True, "emp_id": emp_id, "accounts_revoked": len(accounts)}
719
+
720
+ # ---- Reassignment (for offboarding) ----
721
+
722
+ def reassign_reports(self, from_emp_id: str, to_emp_id: str) -> dict:
723
+ from_emp = self.get_employee(from_emp_id)
724
+ to_emp = self.get_employee(to_emp_id)
725
+ if not from_emp:
726
+ return {"success": False, "error": f"Employee {from_emp_id} not found"}
727
+ if not to_emp:
728
+ return {"success": False, "error": f"Employee {to_emp_id} not found"}
729
+
730
+ reports = self.get_direct_reports(from_emp_id)
731
+ for report in reports:
732
+ report["manager_id"] = to_emp_id
733
+
734
+ self._build_indexes()
735
+ return {"success": True, "reassigned_count": len(reports),
736
+ "from": from_emp_id, "to": to_emp_id,
737
+ "reassigned_employees": [r["emp_id"] for r in reports]}
test_with_llm.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Test HR Onboarding Environment with OpenAI GPT as the agent."""
2
+
3
+ import sys
4
+ import json
5
+ import os
6
+ import re
7
+
8
+ from dotenv import load_dotenv
9
+ load_dotenv()
10
+
11
+ sys.path.insert(0, ".")
12
+ sys.path.insert(0, "./server")
13
+
14
+ from openai import OpenAI
15
+ from server.hr_onboarding_environment import HROnboardingEnvironment
16
+ from models import HROnboardingAction, HROnboardingObservation
17
+ from server.tools import TOOL_DEFINITIONS
18
+ from server.rubrics import RubricEvaluator
19
+
20
+ # --- Setup ---
21
+ client = OpenAI()
22
+ env = HROnboardingEnvironment(seed=42, max_steps=15)
23
+ tool_desc = json.dumps(TOOL_DEFINITIONS, indent=2)
24
+
25
+ # Pick which task to test (default: 0, or pass via CLI)
26
+ task_idx = int(sys.argv[1]) if len(sys.argv) > 1 else 0
27
+
28
+ # --- Reset to the desired task ---
29
+ for _ in range(task_idx + 1):
30
+ obs = env.reset()
31
+
32
+ print("=" * 70)
33
+ print("HR ONBOARDING ENVIRONMENT — LLM AGENT TEST")
34
+ print("=" * 70)
35
+ print(f"\nTask ID: {obs.task_id}")
36
+ print(f"Difficulty: {obs.metadata.get('difficulty', '?')}")
37
+ print(f"Category: {obs.metadata.get('category', '?')}")
38
+ print(f"\nInstruction: {obs.instruction}")
39
+ print(f"\nAvailable tools ({len(obs.available_tools)}): {', '.join(obs.available_tools[:10])}...")
40
+ print("=" * 70)
41
+
42
+ system_prompt = f"""You are an HR automation agent for AcmeCorp. You help with employee onboarding and offboarding by calling the appropriate tools.
43
+
44
+ For each step, respond with ONLY a JSON tool call in this exact format:
45
+ {{"tool": "<tool_name>", "params": {{<parameters>}}}}
46
+
47
+ When you believe the task is complete, respond with:
48
+ {{"tool": "__done__", "params": {{}}}}
49
+
50
+ Important rules:
51
+ - Respond with ONLY the JSON object, no other text
52
+ - Use the exact tool names and parameter names from the tool definitions
53
+ - Think about what information you need and what tools to call in what order
54
+
55
+ Available tools:
56
+ {tool_desc}
57
+ """
58
+
59
+ messages = [
60
+ {"role": "system", "content": system_prompt},
61
+ {"role": "user", "content": obs.instruction},
62
+ ]
63
+
64
+ # --- Agent loop ---
65
+ for step in range(1, obs.max_steps + 1):
66
+ print(f"\n--- Step {step}/{obs.max_steps} ---")
67
+
68
+ response = client.chat.completions.create(
69
+ model="gpt-4o-mini",
70
+ messages=messages,
71
+ temperature=0.1,
72
+ max_tokens=512,
73
+ )
74
+
75
+ assistant_msg = response.choices[0].message.content.strip()
76
+ print(f"LLM: {assistant_msg[:200]}")
77
+
78
+ # Parse tool call
79
+ try:
80
+ json_match = re.search(r'\{.*\}', assistant_msg, re.DOTALL)
81
+ if json_match:
82
+ tool_call = json.loads(json_match.group())
83
+ else:
84
+ tool_call = json.loads(assistant_msg)
85
+ except json.JSONDecodeError:
86
+ print(f" ERROR: Could not parse JSON")
87
+ messages.append({"role": "assistant", "content": assistant_msg})
88
+ messages.append({"role": "user", "content": 'Respond with valid JSON: {"tool": "<name>", "params": {<args>}}'})
89
+ continue
90
+
91
+ tool_name = tool_call.get("tool", "")
92
+ params = tool_call.get("params", {})
93
+
94
+ if tool_name == "__done__":
95
+ print("\n Agent signaled DONE.")
96
+ break
97
+
98
+ # Execute action
99
+ action = HROnboardingAction(tool_name=tool_name, arguments=params)
100
+ obs = env.step(action)
101
+
102
+ result_str = json.dumps(obs.tool_result, indent=2)
103
+ print(f" Tool: {tool_name}")
104
+ print(f" Result: {result_str[:300]}{'...' if len(result_str) > 300 else ''}")
105
+
106
+ messages.append({"role": "assistant", "content": assistant_msg})
107
+ messages.append({"role": "user", "content": f"Tool result:\n{result_str}\n\nContinue with next tool call, or {{\"tool\": \"__done__\", \"params\": {{}}}} if done."})
108
+
109
+ if obs.done:
110
+ print(f"\n Episode done. Reward: {obs.reward}")
111
+ break
112
+
113
+ # --- Final evaluation ---
114
+ print("\n" + "=" * 70)
115
+ print("FINAL EVALUATION")
116
+ print("=" * 70)
117
+
118
+ evaluator = RubricEvaluator()
119
+ task = env._current_task
120
+ eval_result = evaluator.evaluate(task, env.world.action_log)
121
+
122
+ print(f"\nTask: {task.task_id}")
123
+ print(f"Score: {eval_result['score']:.0%} ({eval_result['passed_count']}/{eval_result['total_criteria']} criteria)")
124
+ print(f"Passed: {eval_result['passed']}")
125
+ print(f"\nCriteria breakdown:")
126
+ for c in eval_result["criteria_results"]:
127
+ status = "PASS" if c["passed"] else "FAIL"
128
+ print(f" [{status}] {c['name']}: {c['description']}")
129
+
130
+ print(f"\nAction log ({len(env.world.action_log)} calls):")
131
+ for i, a in enumerate(env.world.action_log):
132
+ print(f" {i+1}. {a['tool']}({json.dumps(a['params'])[:80]})")
uv.lock CHANGED
@@ -1,5 +1,5 @@
1
  version = 1
2
- revision = 3
3
  requires-python = ">=3.10"
4
 
5
  [[package]]
@@ -1070,28 +1070,6 @@ wheels = [
1070
  { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" },
1071
  ]
1072
 
1073
- [[package]]
1074
- name = "openenv-basic-openenv"
1075
- version = "0.1.0"
1076
- source = { editable = "." }
1077
- dependencies = [
1078
- { name = "openenv-core", extra = ["core"] },
1079
- ]
1080
-
1081
- [package.optional-dependencies]
1082
- dev = [
1083
- { name = "pytest" },
1084
- { name = "pytest-cov" },
1085
- ]
1086
-
1087
- [package.metadata]
1088
- requires-dist = [
1089
- { name = "openenv-core", extras = ["core"], specifier = ">=0.2.0" },
1090
- { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
1091
- { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" },
1092
- ]
1093
- provides-extras = ["dev"]
1094
-
1095
  [[package]]
1096
  name = "openenv-core"
1097
  version = "0.2.1"
@@ -1125,6 +1103,28 @@ core = [
1125
  { name = "websockets" },
1126
  ]
1127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1128
  [[package]]
1129
  name = "opentelemetry-api"
1130
  version = "1.40.0"
 
1
  version = 1
2
+ revision = 2
3
  requires-python = ">=3.10"
4
 
5
  [[package]]
 
1070
  { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" },
1071
  ]
1072
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1073
  [[package]]
1074
  name = "openenv-core"
1075
  version = "0.2.1"
 
1103
  { name = "websockets" },
1104
  ]
1105
 
1106
+ [[package]]
1107
+ name = "openenv-hr-onboarding-env"
1108
+ version = "0.1.0"
1109
+ source = { editable = "." }
1110
+ dependencies = [
1111
+ { name = "openenv-core", extra = ["core"] },
1112
+ ]
1113
+
1114
+ [package.optional-dependencies]
1115
+ dev = [
1116
+ { name = "pytest" },
1117
+ { name = "pytest-cov" },
1118
+ ]
1119
+
1120
+ [package.metadata]
1121
+ requires-dist = [
1122
+ { name = "openenv-core", extras = ["core"], specifier = ">=0.2.0" },
1123
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
1124
+ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" },
1125
+ ]
1126
+ provides-extras = ["dev"]
1127
+
1128
  [[package]]
1129
  name = "opentelemetry-api"
1130
  version = "1.40.0"