Taniieeee83 commited on
Commit
03d30a6
Β·
1 Parent(s): 05d8492

better workflows

Browse files
baseline_scores.json CHANGED
@@ -1,8 +1,8 @@
1
  {
2
  "scores": {
3
- "workflow_A": 0.613,
4
- "workflow_B": 0.65,
5
- "workflow_C": 0.5
6
  },
7
- "average": 0.5877
8
  }
 
1
  {
2
  "scores": {
3
+ "workflow_A": 0.697,
4
+ "workflow_B": 0.744,
5
+ "workflow_C": 0.722
6
  },
7
+ "average": 0.721
8
  }
inference.py CHANGED
@@ -76,18 +76,38 @@ CRITICAL RULES:
76
  1. Read schema_hints FIRST β€” if "jira.priority" β†’ "severity", use "severity" not "priority" in args.
77
  2. Complete ALL pending_steps in order.
78
  3. Do not repeat a successful action.
79
- 4. If an operation fails, read the message carefully and adapt.
80
- 5. Use list_* operations to discover record IDs when needed.
81
- 6. Stop when pending_steps is empty or done=true.
 
 
82
 
83
  Example actions:
 
84
  {"app": "zendesk", "operation": "list_tickets", "args": {"state": "new"}}
85
  {"app": "zendesk", "operation": "acknowledge_ticket", "args": {"ticket_number": "<ticket_number from list_tickets>"}}
86
  {"app": "jira", "operation": "create_issue", "args": {"title": "Bug fix for <customer>", "linked_zendesk": "<ticket_number>"}}
87
- {"app": "salesforce", "operation": "list_accounts", "args": {"health": "red"}}
88
- {"app": "salesforce", "operation": "get_account", "args": {"account_id": "<account_id from list_accounts>"}}
 
 
89
  {"app": "workday", "operation": "list_employees", "args": {"status": "pending"}}
90
- {"app": "workday", "operation": "log_sla_event", "args": {"ticket_id": "<ticket_number>", "sla_met": true}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  """
92
 
93
  WORKFLOW_NAMES = {
 
76
  1. Read schema_hints FIRST β€” if "jira.priority" β†’ "severity", use "severity" not "priority" in args.
77
  2. Complete ALL pending_steps in order.
78
  3. Do not repeat a successful action.
79
+ 4. When an operation creates a new resource (e.g. create_issue returns issue_id "JIRA-051"),
80
+ use THAT returned ID for all subsequent operations on that resource β€” not a pre-existing ID.
81
+ 5. If an operation fails, read the message carefully and adapt.
82
+ 6. Use list_* operations to discover record IDs when needed β€” never assume an ID.
83
+ 7. Stop when pending_steps is empty or done=true.
84
 
85
  Example actions:
86
+ # Workflow A (bug fix) β€” discover then chain ticket_id and returned issue_id:
87
  {"app": "zendesk", "operation": "list_tickets", "args": {"state": "new"}}
88
  {"app": "zendesk", "operation": "acknowledge_ticket", "args": {"ticket_number": "<ticket_number from list_tickets>"}}
89
  {"app": "jira", "operation": "create_issue", "args": {"title": "Bug fix for <customer>", "linked_zendesk": "<ticket_number>"}}
90
+ # β†’ create_issue returns {"issue_id": "JIRA-051", ...} β€” use JIRA-051 for assign_owner below
91
+ {"app": "jira", "operation": "assign_owner", "args": {"issue_id": "<issue_id from create_issue>", "assignee": "<engineer>"}}
92
+
93
+ # Workflow B (onboarding) β€” find the pending employee, then thread employee_id + territory:
94
  {"app": "workday", "operation": "list_employees", "args": {"status": "pending"}}
95
+ # β†’ returns exactly one record. Capture employee_id (e.g. "EMP-NEW-001") AND territory (e.g. "west").
96
+ {"app": "workday", "operation": "create_onboarding_task", "args": {"employee_id": "<employee_id from list>"}}
97
+ {"app": "workday", "operation": "provision_access", "args": {"employee_id": "<employee_id from list>", "app_name": "jira"}}
98
+ # SF: assign that employee as owner of an account in THEIR territory:
99
+ {"app": "salesforce", "operation": "list_accounts", "args": {"territory": "<territory from list_employees>"}}
100
+ {"app": "salesforce", "operation": "assign_account_owner", "args": {"account_id": "<account_id from list>", "owner": "<employee_id from list>"}}
101
+ # Jira: assign an open issue to the new hire's employee_id:
102
+ {"app": "jira", "operation": "list_issues", "args": {"status": "open"}}
103
+ {"app": "jira", "operation": "assign_owner", "args": {"issue_id": "<issue_id from list>", "assignee": "<employee_id from list_employees>"}}
104
+
105
+ # Workflow C (churn) β€” discover the at-risk account, then scope all queries to it:
106
+ {"app": "salesforce", "operation": "list_accounts", "args": {"health": "red"}}
107
+ {"app": "salesforce", "operation": "flag_churn_risk", "args": {"account_id": "<account_id from list>"}}
108
+ {"app": "zendesk", "operation": "get_ticket", "args": {"customer_id": "<account_id from list>"}}
109
+ {"app": "jira", "operation": "list_issues", "args": {"customer_id": "<account_id from list>"}}
110
+ {"app": "salesforce", "operation": "assign_account_owner", "args": {"account_id": "<account_id from list>", "owner": "<engineer>"}}
111
  """
112
 
113
  WORKFLOW_NAMES = {
server/apps/jira.py CHANGED
@@ -19,7 +19,7 @@ class JiraApp(BaseApp):
19
  # Workflow completion state tracking
20
  self._linked_issues: set = set() # issue_ids linked to a Zendesk ticket
21
  self._assigned_issues: set = set() # issue_ids with a non-null assignee
22
- self._bugs_checked: bool = False # list_issues was called (Workflow C)
23
 
24
  # ------------------------------------------------------------------
25
  # BaseApp interface
@@ -29,7 +29,7 @@ class JiraApp(BaseApp):
29
  self._records = {r["issue_id"]: r for r in records}
30
  self._linked_issues.clear()
31
  self._assigned_issues.clear()
32
- self._bugs_checked = False
33
  # Seed state from loaded data
34
  for issue_id, rec in self._records.items():
35
  if rec.get("assignee"):
@@ -78,12 +78,33 @@ class JiraApp(BaseApp):
78
  return len(self._linked_issues) > 0
79
 
80
  def issue_assigned(self) -> bool:
81
- """True once JIRA-001 (primary bug) has an assignee (Workflow A step A4)."""
82
- return bool(self._records.get("JIRA-001", {}).get("assignee"))
83
-
84
- def bugs_checked(self) -> bool:
85
- """True once list_issues has been called (Workflow C step C3)."""
86
- return self._bugs_checked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  # ------------------------------------------------------------------
89
  # Operations
@@ -229,7 +250,10 @@ class JiraApp(BaseApp):
229
 
230
  def _op_list_issues(self, status: str = "open", customer_id: Optional[str] = None,
231
  limit: int = 10) -> Dict:
232
- self._bugs_checked = True
 
 
 
233
  matching = [
234
  r for r in self._records.values()
235
  if (status == "all" or r.get("status") == status)
@@ -244,248 +268,4 @@ class JiraApp(BaseApp):
244
  for r in drifted]
245
  return {"success": True, "data": compact,
246
  "message": f"Found {len(compact)} {status} issues"
247
- + (f" for {customer_id}" if customer_id else "")}
248
- # """Jira-like app β€” engineering ticket management."""
249
-
250
- # from typing import Dict, List, Optional
251
- # from server.apps.base_app import BaseApp
252
- # from server.schema_drift import SchemaDriftEngine
253
-
254
-
255
- # class JiraApp(BaseApp):
256
- # APP_NAME = "jira"
257
-
258
- # OPERATIONS = [
259
- # "get_issue", "create_issue", "update_status", "set_priority",
260
- # "assign_owner", "add_label", "link_zendesk_ticket", "close_issue", "list_issues",
261
- # ]
262
-
263
- # def __init__(self, drift: SchemaDriftEngine):
264
- # super().__init__(drift)
265
- # self._records: Dict[str, Dict] = {}
266
- # # Workflow completion state tracking
267
- # self._linked_issues: set = set() # issue_ids linked to a Zendesk ticket
268
- # self._assigned_issues: set = set() # issue_ids with a non-null assignee
269
- # self._bugs_checked: bool = False # list_issues was called (Workflow C)
270
-
271
- # # ------------------------------------------------------------------
272
- # # BaseApp interface
273
- # # ------------------------------------------------------------------
274
-
275
- # def initialize(self, records: List[Dict]) -> None:
276
- # self._records = {r["issue_id"]: r for r in records}
277
- # self._linked_issues.clear()
278
- # self._assigned_issues.clear()
279
- # self._bugs_checked = False
280
- # # Seed state from loaded data
281
- # for issue_id, rec in self._records.items():
282
- # if rec.get("assignee"):
283
- # self._assigned_issues.add(issue_id)
284
- # if rec.get("linked_zendesk"):
285
- # self._linked_issues.add(issue_id)
286
-
287
- # def execute(self, operation: str, args: Dict) -> Dict:
288
- # method = getattr(self, f"_op_{operation}", None)
289
- # if method is None:
290
- # return {
291
- # "success": False,
292
- # "message": f"Unknown operation '{operation}'. Available: {', '.join(self.OPERATIONS)}",
293
- # }
294
- # try:
295
- # return method(**args)
296
- # except TypeError as exc:
297
- # return {"success": False, "message": f"Bad args for '{operation}': {exc}"}
298
-
299
- # def get_state_view(self, max_rows: int = 5) -> str:
300
- # open_issues = [r for r in self._records.values()
301
- # if r.get("status") not in ("closed",)][:max_rows]
302
- # if not open_issues:
303
- # return "No open issues."
304
- # lines = []
305
- # for rec in open_issues:
306
- # view = self._to_agent_view(rec)
307
- # keep = ["issue_id", "title",
308
- # "priority", "severity", "urgency_level",
309
- # "assignee", "owner", "assigned_to",
310
- # "status", "state", "current_state",
311
- # "customer_id", "linked_zendesk"]
312
- # compact = {k: v for k, v in view.items() if k in keep and v is not None}
313
- # lines.append(str(compact))
314
- # return "\n".join(lines)
315
-
316
- # def count_open_items(self) -> int:
317
- # return sum(1 for r in self._records.values() if r.get("status") != "closed")
318
-
319
- # # ------------------------------------------------------------------
320
- # # Workflow completion state checks
321
- # # ------------------------------------------------------------------
322
-
323
- # def has_linked_issue(self) -> bool:
324
- # """True once any issue is linked to a Zendesk ticket (Workflow A step A2)."""
325
- # return len(self._linked_issues) > 0
326
-
327
- # def issue_assigned(self) -> bool:
328
- # """True once JIRA-001 (primary bug) has an assignee (Workflow A step A4)."""
329
- # return bool(self._records.get("JIRA-001", {}).get("assignee"))
330
-
331
- # def bugs_checked(self) -> bool:
332
- # """True once list_issues has been called (Workflow C step C3)."""
333
- # return self._bugs_checked
334
-
335
- # # ------------------------------------------------------------------
336
- # # Operations
337
- # # ------------------------------------------------------------------
338
-
339
- # def _op_get_issue(self, issue_id: str) -> Dict:
340
- # rec = self._records.get(issue_id)
341
- # if not rec:
342
- # return {"success": False, "message": f"Issue {issue_id} not found. Use list_issues to browse."}
343
- # return {"success": True, "data": self._to_agent_view(rec),
344
- # "message": f"Retrieved {issue_id}"}
345
-
346
- # def _op_create_issue(self, title: str, **kwargs) -> Dict:
347
- # schema_error, schema_adapted = self._check_schema_drift(kwargs)
348
- # if schema_error:
349
- # return {
350
- # "success": False,
351
- # "schema_error": schema_error,
352
- # "message": (f"Schema error: field '{schema_error}' is not in the current schema. "
353
- # f"Check schema_hints for the correct field name."),
354
- # }
355
-
356
- # issue_id = f"JIRA-{len(self._records) + 1:03d}"
357
- # # Accept both canonical and drifted names for priority / assignee
358
- # priority = (kwargs.get("priority") or kwargs.get("severity")
359
- # or kwargs.get("urgency_level", "p2"))
360
- # linked = kwargs.get("linked_zendesk") or kwargs.get("zendesk_ticket")
361
-
362
- # rec = {
363
- # "issue_id": issue_id,
364
- # "title": title,
365
- # "priority": priority,
366
- # "assignee": kwargs.get("assignee") or kwargs.get("owner") or kwargs.get("assigned_to"),
367
- # "status": "open",
368
- # "reporter": kwargs.get("reporter", "agent"),
369
- # "customer_id": kwargs.get("customer_id"),
370
- # "linked_zendesk": linked,
371
- # "labels": [],
372
- # "created_at": "2026-04-21T09:00:00",
373
- # }
374
- # self._records[issue_id] = rec
375
-
376
- # if linked:
377
- # self._linked_issues.add(issue_id)
378
- # if rec["assignee"]:
379
- # self._assigned_issues.add(issue_id)
380
-
381
- # return {
382
- # "success": True,
383
- # "data": {"issue_id": issue_id},
384
- # "schema_adapted": schema_adapted,
385
- # "message": f"Created {issue_id}: '{title}'"
386
- # + (f" linked to {linked}" if linked else ""),
387
- # }
388
-
389
- # def _op_update_status(self, issue_id: str, **kwargs) -> Dict:
390
- # schema_error, schema_adapted = self._check_schema_drift(kwargs)
391
- # if schema_error:
392
- # return {"success": False, "schema_error": schema_error,
393
- # "message": f"Schema error: use current field name, not '{schema_error}'"}
394
-
395
- # rec = self._records.get(issue_id)
396
- # if not rec:
397
- # return {"success": False, "message": f"Issue {issue_id} not found"}
398
-
399
- # new_status = (kwargs.get("status") or kwargs.get("state")
400
- # or kwargs.get("current_state"))
401
- # if not new_status:
402
- # return {"success": False, "message": "Provide status/state/current_state value"}
403
-
404
- # rec["status"] = new_status
405
- # return {"success": True, "schema_adapted": schema_adapted,
406
- # "message": f"{issue_id} status β†’ '{new_status}'"}
407
-
408
- # def _op_set_priority(self, issue_id: str, **kwargs) -> Dict:
409
- # schema_error, schema_adapted = self._check_schema_drift(kwargs)
410
- # if schema_error:
411
- # return {"success": False, "schema_error": schema_error,
412
- # "message": f"Schema error: '{schema_error}' is a stale field name"}
413
-
414
- # rec = self._records.get(issue_id)
415
- # if not rec:
416
- # return {"success": False, "message": f"Issue {issue_id} not found"}
417
-
418
- # new_priority = (kwargs.get("priority") or kwargs.get("severity")
419
- # or kwargs.get("urgency_level"))
420
- # if not new_priority:
421
- # return {"success": False,
422
- # "message": "Provide priority / severity / urgency_level value"}
423
-
424
- # rec["priority"] = new_priority
425
- # return {"success": True, "schema_adapted": schema_adapted,
426
- # "message": f"{issue_id} priority β†’ '{new_priority}'"}
427
-
428
- # def _op_assign_owner(self, issue_id: str, **kwargs) -> Dict:
429
- # schema_error, schema_adapted = self._check_schema_drift(kwargs)
430
- # if schema_error:
431
- # hint = self._drift.translate_field("assignee", self.APP_NAME)
432
- # return {"success": False, "schema_error": schema_error,
433
- # "message": f"Schema error: use '{hint}' instead of '{schema_error}'"}
434
-
435
- # rec = self._records.get(issue_id)
436
- # if not rec:
437
- # return {"success": False, "message": f"Issue {issue_id} not found"}
438
-
439
- # assignee = (kwargs.get("assignee") or kwargs.get("owner")
440
- # or kwargs.get("assigned_to"))
441
- # if not assignee:
442
- # correct_field = self._drift.translate_field("assignee", self.APP_NAME)
443
- # return {"success": False,
444
- # "message": f"Missing assignee field. Use '{correct_field}' as the arg key for this episode."}
445
-
446
- # rec["assignee"] = assignee
447
- # self._assigned_issues.add(issue_id)
448
- # return {"success": True, "schema_adapted": schema_adapted,
449
- # "message": f"{issue_id} assigned to '{assignee}'"}
450
-
451
- # def _op_add_label(self, issue_id: str, label: str) -> Dict:
452
- # rec = self._records.get(issue_id)
453
- # if not rec:
454
- # return {"success": False, "message": f"Issue {issue_id} not found"}
455
- # rec.setdefault("labels", []).append(label)
456
- # return {"success": True, "message": f"Added label '{label}' to {issue_id}"}
457
-
458
- # def _op_link_zendesk_ticket(self, issue_id: str, zendesk_ticket_number: str) -> Dict:
459
- # rec = self._records.get(issue_id)
460
- # if not rec:
461
- # return {"success": False, "message": f"Issue {issue_id} not found"}
462
- # rec["linked_zendesk"] = zendesk_ticket_number
463
- # self._linked_issues.add(issue_id)
464
- # return {"success": True,
465
- # "message": f"Linked {issue_id} ↔ Zendesk {zendesk_ticket_number}"}
466
-
467
- # def _op_close_issue(self, issue_id: str) -> Dict:
468
- # rec = self._records.get(issue_id)
469
- # if not rec:
470
- # return {"success": False, "message": f"Issue {issue_id} not found"}
471
- # rec["status"] = "closed"
472
- # return {"success": True, "message": f"Closed {issue_id}"}
473
-
474
- # def _op_list_issues(self, status: str = "open", customer_id: Optional[str] = None,
475
- # limit: int = 10) -> Dict:
476
- # self._bugs_checked = True
477
- # matching = [
478
- # r for r in self._records.values()
479
- # if (status == "all" or r.get("status") == status)
480
- # and (customer_id is None or r.get("customer_id") == customer_id)
481
- # ][:limit]
482
- # drifted = [self._to_agent_view(r) for r in matching]
483
- # keep = ["issue_id", "title", "priority", "severity", "urgency_level",
484
- # "assignee", "owner", "assigned_to",
485
- # "status", "state", "current_state",
486
- # "customer_id", "linked_zendesk"]
487
- # compact = [{k: v for k, v in r.items() if k in keep and v is not None}
488
- # for r in drifted]
489
- # return {"success": True, "data": compact,
490
- # "message": f"Found {len(compact)} {status} issues"
491
- # + (f" for {customer_id}" if customer_id else "")}
 
19
  # Workflow completion state tracking
20
  self._linked_issues: set = set() # issue_ids linked to a Zendesk ticket
21
  self._assigned_issues: set = set() # issue_ids with a non-null assignee
22
+ self._bugs_checked_for: set = set() # customer_ids queried via list_issues (Workflow C)
23
 
24
  # ------------------------------------------------------------------
25
  # BaseApp interface
 
29
  self._records = {r["issue_id"]: r for r in records}
30
  self._linked_issues.clear()
31
  self._assigned_issues.clear()
32
+ self._bugs_checked_for.clear()
33
  # Seed state from loaded data
34
  for issue_id, rec in self._records.items():
35
  if rec.get("assignee"):
 
78
  return len(self._linked_issues) > 0
79
 
80
  def issue_assigned(self) -> bool:
81
+ """True once any linked issue has an assignee (Workflow A step A4).
82
+ Prefers the primary bug; falls back to any linked+assigned issue."""
83
+ # Primary path: the primary bug was linked and assigned
84
+ primary = next(
85
+ (r for r in self._records.values() if r.get("_is_primary_bug")), None
86
+ )
87
+ if primary and primary.get("assignee") and primary["issue_id"] in self._linked_issues:
88
+ return True
89
+ # Fallback: any issue that's both linked to Zendesk and has an assignee
90
+ return any(
91
+ issue_id in self._linked_issues and self._records[issue_id].get("assignee")
92
+ for issue_id in self._linked_issues
93
+ )
94
+
95
+ def bugs_checked_for(self, account_id: str) -> bool:
96
+ """True once list_issues was called WITH customer_id=account_id (Workflow C step C3).
97
+ Tightened from the old free-pass `bugs_checked` flag β€” the agent must now scope the
98
+ query to the at-risk account specifically, forcing real cross-app data flow from C1."""
99
+ return bool(account_id) and account_id in self._bugs_checked_for
100
+
101
+ def new_hire_assigned_to_issue(self, employee_id: str) -> bool:
102
+ """True once any Jira issue's assignee equals employee_id (Workflow B step B4).
103
+ Forces the agent to use the new hire's employee_id (discovered in B1) when
104
+ assigning a Jira issue β€” real Workday β†’ Jira data flow, no free-pass."""
105
+ if not employee_id:
106
+ return False
107
+ return any(r.get("assignee") == employee_id for r in self._records.values())
108
 
109
  # ------------------------------------------------------------------
110
  # Operations
 
250
 
251
  def _op_list_issues(self, status: str = "open", customer_id: Optional[str] = None,
252
  limit: int = 10) -> Dict:
253
+ # Track which customer_id was queried β€” used by bugs_checked_for() (Workflow C C3).
254
+ # Bare list_issues() with no filter no longer satisfies C3.
255
+ if customer_id:
256
+ self._bugs_checked_for.add(customer_id)
257
  matching = [
258
  r for r in self._records.values()
259
  if (status == "all" or r.get("status") == status)
 
268
  for r in drifted]
269
  return {"success": True, "data": compact,
270
  "message": f"Found {len(compact)} {status} issues"
271
+ + (f" for {customer_id}" if customer_id else "")}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/apps/salesforce.py CHANGED
@@ -65,20 +65,44 @@ class SalesforceApp(BaseApp):
65
  # ------------------------------------------------------------------
66
 
67
  def account_checked(self) -> bool:
68
- """True once get_account was called for ACME-001 (Workflow A step A3)."""
69
- return bool(self._records.get("ACME-001", {}).get("_account_checked"))
 
 
 
70
 
71
  def churn_flagged(self) -> bool:
72
- """True once flag_churn_risk was called for ACME-003 (Workflow C step C1)."""
73
- return bool(self._records.get("ACME-003", {}).get("_churn_flagged"))
 
 
 
74
 
75
  def team_assigned(self) -> bool:
76
- """True once assign_account_owner was called (Workflow B step B3)."""
 
77
  return any(r.get("_team_assigned") for r in self._records.values())
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  def intervention_assigned(self) -> bool:
80
- """True once assign_account_owner called on ACME-003 (Workflow C step C4)."""
81
- return bool(self._records.get("ACME-003", {}).get("_intervention_assigned"))
 
 
 
82
 
83
  # ------------------------------------------------------------------
84
  # Operations
@@ -168,7 +192,9 @@ class SalesforceApp(BaseApp):
168
 
169
  rec["owner"] = new_owner
170
  rec["_team_assigned"] = True
171
- if account_id == "ACME-003":
 
 
172
  rec["_intervention_assigned"] = True
173
 
174
  return {"success": True, "schema_adapted": schema_adapted,
 
65
  # ------------------------------------------------------------------
66
 
67
  def account_checked(self) -> bool:
68
+ """True once get_account was called for the Workflow A customer (Workflow A step A3)."""
69
+ return any(
70
+ r.get("_is_workflow_a_account") and r.get("_account_checked")
71
+ for r in self._records.values()
72
+ )
73
 
74
  def churn_flagged(self) -> bool:
75
+ """True once flag_churn_risk was called for the at-risk account (Workflow C step C1)."""
76
+ return any(
77
+ r.get("_is_churn_target") and r.get("_churn_flagged")
78
+ for r in self._records.values()
79
+ )
80
 
81
  def team_assigned(self) -> bool:
82
+ """Legacy free-pass check β€” kept for backwards compatibility.
83
+ Workflow B no longer uses this; see new_hire_assigned_in_territory()."""
84
  return any(r.get("_team_assigned") for r in self._records.values())
85
 
86
+ def new_hire_assigned_in_territory(self, employee_id: str, territory: str) -> bool:
87
+ """True once an SF account in `territory` has `employee_id` as its owner
88
+ (Workflow B step B3 β€” tightened from the free-pass team_assigned check).
89
+ Forces real cross-app data flow: the agent must use the employee_id and territory
90
+ discovered in B1 to satisfy this check."""
91
+ if not employee_id or not territory:
92
+ return False
93
+ return any(
94
+ r.get("territory") == territory
95
+ and r.get("owner") == employee_id
96
+ and r.get("_team_assigned")
97
+ for r in self._records.values()
98
+ )
99
+
100
  def intervention_assigned(self) -> bool:
101
+ """True once assign_account_owner called on the churn-risk account (Workflow C step C4)."""
102
+ return any(
103
+ r.get("_is_churn_target") and r.get("_intervention_assigned")
104
+ for r in self._records.values()
105
+ )
106
 
107
  # ------------------------------------------------------------------
108
  # Operations
 
192
 
193
  rec["owner"] = new_owner
194
  rec["_team_assigned"] = True
195
+ # Semantic-marker driven: any churn target getting an owner is an intervention.
196
+ # Replaces the old `account_id == "ACME-003"` hardcoded ID check.
197
+ if rec.get("_is_churn_target"):
198
  rec["_intervention_assigned"] = True
199
 
200
  return {"success": True, "schema_adapted": schema_adapted,
server/apps/workday.py CHANGED
@@ -68,16 +68,29 @@ class WorkdayApp(BaseApp):
68
  return any(r.get("_sla_logged") for r in self._records.values())
69
 
70
  def employee_created(self) -> bool:
71
- """True once create_onboarding_task was called for EMP-NEW-001 (Workflow B step B1)."""
72
- return bool(self._records.get("EMP-NEW-001", {}).get("_onboarding_created"))
 
 
 
73
 
74
  def access_provisioned(self, app_name: str) -> bool:
75
- """True once provision_access was called for the given app (Workflow B step B2)."""
 
 
76
  return any(
77
- r.get("_access_provisioned", {}).get(app_name)
78
  for r in self._records.values()
79
  )
80
 
 
 
 
 
 
 
 
 
81
  # ------------------------------------------------------------------
82
  # Operations
83
  # ------------------------------------------------------------------
@@ -125,7 +138,7 @@ class WorkdayApp(BaseApp):
125
  if not rec:
126
  return {"success": False, "message": f"Employee {employee_id} not found"}
127
 
128
- rec.setdefault("_access_provisioned", {})[app_name] = True
129
  return {"success": True, "schema_adapted": schema_adapted,
130
  "message": f"Provisioned {app_name} access for {employee_id} ({rec.get('name', '')})"}
131
 
 
68
  return any(r.get("_sla_logged") for r in self._records.values())
69
 
70
  def employee_created(self) -> bool:
71
+ """True once create_onboarding_task was called for the pending new hire (Workflow B step B1)."""
72
+ return any(
73
+ r.get("_is_new_hire") and r.get("_onboarding_created")
74
+ for r in self._records.values()
75
+ )
76
 
77
  def access_provisioned(self, app_name: str) -> bool:
78
+ """True once provision_access was called for the NEW HIRE specifically (Workflow B step B2).
79
+ Tightened from any-employee free-pass to requiring _is_new_hire β€” eliminates the
80
+ old training dead-zone where provisioning a random employee satisfied the check."""
81
  return any(
82
+ r.get("_is_new_hire") and r.get("_access_provisioned", {}).get(app_name.lower())
83
  for r in self._records.values()
84
  )
85
 
86
+ def get_new_hire(self) -> Optional[Dict]:
87
+ """Return the new-hire employee record (the one with _is_new_hire), or None.
88
+ Used by workflow_engine.py to thread employee_id / territory into B3 / B4 checks."""
89
+ return next(
90
+ (r for r in self._records.values() if r.get("_is_new_hire")),
91
+ None,
92
+ )
93
+
94
  # ------------------------------------------------------------------
95
  # Operations
96
  # ------------------------------------------------------------------
 
138
  if not rec:
139
  return {"success": False, "message": f"Employee {employee_id} not found"}
140
 
141
+ rec.setdefault("_access_provisioned", {})[app_name.lower()] = True
142
  return {"success": True, "schema_adapted": schema_adapted,
143
  "message": f"Provisioned {app_name} access for {employee_id} ({rec.get('name', '')})"}
144
 
server/apps/zendesk.py CHANGED
@@ -62,8 +62,11 @@ class ZendeskApp(BaseApp):
62
  # ------------------------------------------------------------------
63
 
64
  def ticket_acknowledged(self) -> bool:
65
- """True once ZD-001 has been acknowledged (Workflow A step A1)."""
66
- return bool(self._records.get("ZD-001", {}).get("_acknowledged"))
 
 
 
67
 
68
  def support_queried(self, account_id: str) -> bool:
69
  """True once tickets for account_id were listed (Workflow C step C2)."""
@@ -256,262 +259,4 @@ class ZendeskApp(BaseApp):
256
  }
257
  self._records[employee_id] = profile
258
  return {"success": True,
259
- "message": f"Created Zendesk agent profile for {name or employee_id} ({email})"}
260
- # """Zendesk-like app β€” customer support ticket management."""
261
-
262
- # from typing import Dict, List, Optional
263
- # from server.apps.base_app import BaseApp
264
- # from server.schema_drift import SchemaDriftEngine
265
-
266
-
267
- # class ZendeskApp(BaseApp):
268
- # APP_NAME = "zendesk"
269
-
270
- # OPERATIONS = [
271
- # "get_ticket", "acknowledge_ticket", "set_urgency", "assign_agent",
272
- # "escalate_to_jira", "resolve_ticket", "add_note", "list_tickets","create_agent_profile",
273
- # ]
274
-
275
- # def __init__(self, drift: SchemaDriftEngine):
276
- # super().__init__(drift)
277
- # self._records: Dict[str, Dict] = {}
278
-
279
- # # ------------------------------------------------------------------
280
- # # BaseApp interface
281
- # # ------------------------------------------------------------------
282
-
283
- # def initialize(self, records: List[Dict]) -> None:
284
- # self._records = {r["ticket_number"]: r for r in records}
285
-
286
- # def execute(self, operation: str, args: Dict) -> Dict:
287
- # method = getattr(self, f"_op_{operation}", None)
288
- # if method is None:
289
- # return {
290
- # "success": False,
291
- # "message": f"Unknown operation '{operation}'. Available: {', '.join(self.OPERATIONS)}",
292
- # }
293
- # try:
294
- # return method(**args)
295
- # except TypeError as exc:
296
- # return {"success": False, "message": f"Bad args for '{operation}': {exc}"}
297
-
298
- # def get_state_view(self, max_rows: int = 5) -> str:
299
- # open_tickets = [r for r in self._records.values()
300
- # if r.get("state") not in ("resolved", "closed")][:max_rows]
301
- # if not open_tickets:
302
- # return "No open tickets."
303
- # lines = []
304
- # for rec in open_tickets:
305
- # view = self._to_agent_view(rec)
306
- # keep = ["ticket_number", "title",
307
- # "urgency", "priority", "impact_level",
308
- # "agent_email", "handler", "assigned_agent",
309
- # "state", "ticket_state", "resolution_status",
310
- # "customer_id"]
311
- # compact = {k: v for k, v in view.items() if k in keep and v is not None}
312
- # lines.append(str(compact))
313
- # return "\n".join(lines)
314
-
315
- # def count_open_items(self) -> int:
316
- # return sum(1 for r in self._records.values()
317
- # if r.get("state") not in ("resolved", "closed"))
318
-
319
- # # ------------------------------------------------------------------
320
- # # Workflow completion state checks
321
- # # ------------------------------------------------------------------
322
-
323
- # def ticket_acknowledged(self) -> bool:
324
- # """True once ZD-001 has been acknowledged (Workflow A step A1)."""
325
- # return bool(self._records.get("ZD-001", {}).get("_acknowledged"))
326
-
327
- # def support_queried(self, account_id: str) -> bool:
328
- # """True once tickets for account_id were listed (Workflow C step C2)."""
329
- # return account_id in self._records.get("ZD-001", {}).get("_queried_accounts", []) or \
330
- # any(account_id in r.get("_queried_accounts", []) for r in self._records.values())
331
-
332
- # def profile_created(self) -> bool:
333
- # """True once a new agent profile was created (Workflow B step B4)."""
334
- # return any(r.get("_profile_created") for r in self._records.values())
335
-
336
- # # ------------------------------------------------------------------
337
- # # Operations
338
- # # ------------------------------------------------------------------
339
-
340
- # def _op_get_ticket(self, ticket_number: str, customer_id: Optional[str] = None) -> Dict:
341
- # # If customer_id provided, look up all tickets for that customer
342
- # if customer_id:
343
- # matching = [r for r in self._records.values()
344
- # if r.get("customer_id") == customer_id]
345
- # # Mark as queried for Workflow C
346
- # for r in matching:
347
- # r.setdefault("_queried_accounts", [])
348
- # if customer_id not in r["_queried_accounts"]:
349
- # r["_queried_accounts"].append(customer_id)
350
- # if not matching:
351
- # return {"success": True, "data": [],
352
- # "message": f"No tickets found for customer {customer_id}"}
353
- # return {
354
- # "success": True,
355
- # "data": [self._to_agent_view(r) for r in matching[:5]],
356
- # "message": f"Found {len(matching)} tickets for {customer_id}",
357
- # }
358
-
359
- # rec = self._records.get(ticket_number)
360
- # if not rec:
361
- # return {"success": False,
362
- # "message": f"Ticket {ticket_number} not found. Use list_tickets to browse."}
363
- # rec.setdefault("_queried_accounts", [])
364
- # cid = rec.get("customer_id")
365
- # if cid and cid not in rec["_queried_accounts"]:
366
- # rec["_queried_accounts"].append(cid)
367
-
368
- # return {"success": True, "data": self._to_agent_view(rec),
369
- # "ticket": rec,
370
- # "message": f"Retrieved {ticket_number}"}
371
-
372
- # def _op_acknowledge_ticket(self, ticket_number: str) -> Dict:
373
- # rec = self._records.get(ticket_number)
374
- # if not rec:
375
- # return {"success": False, "message": f"Ticket {ticket_number} not found"}
376
- # rec["_acknowledged"] = True
377
- # if rec.get("state") == "new":
378
- # rec["state"] = "open"
379
- # return {"success": True, "ticket": rec,
380
- # "message": f"Acknowledged {ticket_number} β€” status β†’ open"}
381
-
382
- # def _op_set_urgency(self, ticket_number: str, **kwargs) -> Dict:
383
- # schema_error, schema_adapted = self._check_schema_drift(kwargs)
384
- # if schema_error:
385
- # hint = self._drift.translate_field("urgency", self.APP_NAME)
386
- # return {"success": False, "schema_error": schema_error,
387
- # "message": f"Schema error: use '{hint}' not '{schema_error}'"}
388
-
389
- # rec = self._records.get(ticket_number)
390
- # if not rec:
391
- # return {"success": False, "message": f"Ticket {ticket_number} not found"}
392
-
393
- # new_urgency = (kwargs.get("urgency") or kwargs.get("priority")
394
- # or kwargs.get("impact_level"))
395
- # if not new_urgency:
396
- # return {"success": False,
397
- # "message": "Provide urgency / priority / impact_level value"}
398
-
399
- # rec["urgency"] = new_urgency
400
- # return {"success": True, "schema_adapted": schema_adapted,
401
- # "message": f"{ticket_number} urgency β†’ '{new_urgency}'"}
402
-
403
- # def _op_assign_agent(self, ticket_number: str, **kwargs) -> Dict:
404
- # schema_error, schema_adapted = self._check_schema_drift(kwargs)
405
- # if schema_error:
406
- # hint = self._drift.translate_field("agent_email", self.APP_NAME)
407
- # return {"success": False, "schema_error": schema_error,
408
- # "message": f"Schema error: use '{hint}' not '{schema_error}'"}
409
-
410
- # rec = self._records.get(ticket_number)
411
- # # For Workflow B profile creation: allow creating a new agent entry
412
- # if not rec:
413
- # # Create a minimal profile record for the new agent
414
- # email = (kwargs.get("agent_email") or kwargs.get("handler")
415
- # or kwargs.get("assigned_agent"))
416
- # if not email:
417
- # return {"success": False, "message": f"Ticket {ticket_number} not found"}
418
- # # Create a synthetic profile ticket
419
- # profile_rec = {
420
- # "ticket_number": ticket_number,
421
- # "title": "Agent profile",
422
- # "urgency": "p3",
423
- # "agent_email": email,
424
- # "state": "closed",
425
- # "customer_id": None,
426
- # "_acknowledged": False,
427
- # "_queried_accounts": [],
428
- # "_profile_created": True,
429
- # }
430
- # self._records[ticket_number] = profile_rec
431
- # return {"success": True, "schema_adapted": schema_adapted,
432
- # "message": f"Created Zendesk profile for agent '{email}'"}
433
-
434
- # email = (kwargs.get("agent_email") or kwargs.get("handler")
435
- # or kwargs.get("assigned_agent"))
436
- # if not email:
437
- # return {"success": False,
438
- # "message": "Provide agent_email / handler / assigned_agent value"}
439
-
440
- # rec["agent_email"] = email
441
- # rec["_profile_created"] = True
442
- # return {"success": True, "schema_adapted": schema_adapted,
443
- # "message": f"{ticket_number} assigned to '{email}'"}
444
-
445
- # def _op_escalate_to_jira(self, ticket_number: str,
446
- # jira_issue_id: Optional[str] = None) -> Dict:
447
- # rec = self._records.get(ticket_number)
448
- # if not rec:
449
- # return {"success": False, "message": f"Ticket {ticket_number} not found"}
450
- # rec["state"] = "pending"
451
- # rec["escalated_to_jira"] = jira_issue_id or "pending"
452
- # return {"success": True,
453
- # "message": f"{ticket_number} escalated to Jira"
454
- # + (f" ({jira_issue_id})" if jira_issue_id else "")}
455
-
456
- # def _op_resolve_ticket(self, ticket_number: str) -> Dict:
457
- # rec = self._records.get(ticket_number)
458
- # if not rec:
459
- # return {"success": False, "message": f"Ticket {ticket_number} not found"}
460
- # rec["state"] = "resolved"
461
- # return {"success": True, "message": f"{ticket_number} resolved"}
462
-
463
- # def _op_add_note(self, ticket_number: str, note: str) -> Dict:
464
- # rec = self._records.get(ticket_number)
465
- # if not rec:
466
- # return {"success": False, "message": f"Ticket {ticket_number} not found"}
467
- # rec.setdefault("notes", []).append(note)
468
- # return {"success": True, "message": f"Note added to {ticket_number}"}
469
-
470
- # def _op_list_tickets(self, state: str = "open", customer_id: Optional[str] = None,
471
- # limit: int = 10) -> Dict:
472
- # matching = [
473
- # r for r in self._records.values()
474
- # if (state == "all" or r.get("state") == state)
475
- # and (customer_id is None or r.get("customer_id") == customer_id)
476
- # ][:limit]
477
- # # Mark accounts as queried
478
- # if customer_id:
479
- # for r in matching:
480
- # r.setdefault("_queried_accounts", [])
481
- # if customer_id not in r["_queried_accounts"]:
482
- # r["_queried_accounts"].append(customer_id)
483
-
484
- # drifted = [self._to_agent_view(r) for r in matching]
485
- # keep = ["ticket_number", "title",
486
- # "urgency", "priority", "impact_level",
487
- # "agent_email", "handler", "assigned_agent",
488
- # "state", "ticket_state", "resolution_status",
489
- # "customer_id"]
490
- # compact = [{k: v for k, v in r.items() if k in keep and v is not None}
491
- # for r in drifted]
492
- # return {
493
- # "success": True,
494
- # "data": compact,
495
- # "message": f"Found {len(compact)} {state} tickets"
496
- # + (f" for {customer_id}" if customer_id else ""),
497
- # }
498
- # def _op_create_agent_profile(self, employee_id: str, email: str,
499
- # name: Optional[str] = None, **kwargs) -> Dict:
500
- # """Create a Zendesk support agent profile (Workflow B step B4)."""
501
- # if employee_id in self._records:
502
- # return {"success": False,
503
- # "message": f"Profile for {employee_id} already exists"}
504
- # profile = {
505
- # "ticket_number": employee_id,
506
- # "title": f"Agent profile β€” {name or employee_id}",
507
- # "urgency": "p3",
508
- # "agent_email": email,
509
- # "state": "closed",
510
- # "customer_id": None,
511
- # "_acknowledged": False,
512
- # "_queried_accounts": [],
513
- # "_profile_created": True,
514
- # }
515
- # self._records[employee_id] = profile
516
- # return {"success": True,
517
- # "message": f"Created Zendesk agent profile for {name or employee_id} ({email})"}
 
62
  # ------------------------------------------------------------------
63
 
64
  def ticket_acknowledged(self) -> bool:
65
+ """True once the primary p1/p0 ticket has been acknowledged (Workflow A step A1)."""
66
+ return any(
67
+ r.get("_is_primary_ticket") and r.get("_acknowledged")
68
+ for r in self._records.values()
69
+ )
70
 
71
  def support_queried(self, account_id: str) -> bool:
72
  """True once tickets for account_id were listed (Workflow C step C2)."""
 
259
  }
260
  self._records[employee_id] = profile
261
  return {"success": True,
262
+ "message": f"Created Zendesk agent profile for {name or employee_id} ({email})"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/data_generator.py CHANGED
@@ -226,12 +226,13 @@ def generate_jira_records(n: int = 50, seed: int = SEED) -> List[Dict]:
226
 
227
  # Workflow A primary issue: JIRA-001 is unassigned, linked to ACME-001
228
  records[0].update({
229
- "title": "Customer login fails intermittently",
230
- "priority": "p1",
231
- "status": "open",
232
- "customer_id": "ACME-001",
233
- "assignee": None,
234
- "linked_zendesk": None,
 
235
  })
236
 
237
  return records
@@ -264,11 +265,12 @@ def generate_zendesk_records(n: int = 40, seed: int = SEED) -> List[Dict]:
264
 
265
  # Workflow A primary: ZD-001 is unacknowledged, from ACME-001
266
  records[0].update({
267
- "title": "Login issue β€” cannot access my account",
268
- "urgency": "p1",
269
- "state": "new",
270
- "customer_id": "ACME-001",
271
- "_acknowledged": False,
 
272
  })
273
 
274
  # Workflow C: several tickets from ACME-003
@@ -313,22 +315,24 @@ def generate_salesforce_records(n: int = 30, seed: int = SEED) -> List[Dict]:
313
 
314
  # Workflow A: ACME-001 is a paying customer with yellow health
315
  records[0].update({
316
- "company_name": "Acme Corporation",
317
- "deal_stage": "closed_won",
318
- "health": "yellow",
319
- "is_paying": True,
320
- "arr": 50_000,
321
- "territory": "west",
 
322
  })
323
 
324
  # Workflow C: ACME-003 is at churn risk
325
  records[2].update({
326
- "company_name": "Globex Systems",
327
- "health": "red",
328
- "deal_stage": "negotiation",
329
- "is_paying": True,
330
- "arr": 30_000,
331
- "_churn_flagged": False,
 
332
  })
333
 
334
  return records
@@ -348,7 +352,9 @@ def generate_workday_records(n: int = 20, seed: int = SEED) -> List[Dict]:
348
  "name": f"{random.choice(FIRST_NAMES)} {random.choice(LAST_NAMES)}",
349
  "level": random.choice(levels),
350
  "manager_id": f"EMP-{random.randint(1, min(i, 5)):03d}" if i > 1 else None,
351
- "status": random.choices(["active", "pending"], weights=[90, 10])[0],
 
 
352
  "department": random.choice(departments),
353
  "territory": random.choice(territories),
354
  "email": f"emp{i}@company.com",
@@ -371,6 +377,7 @@ def generate_workday_records(n: int = 20, seed: int = SEED) -> List[Dict]:
371
  "_access_provisioned": {},
372
  "_sla_logged": False,
373
  "_onboarding_created": False,
 
374
  })
375
 
376
  return records
 
226
 
227
  # Workflow A primary issue: JIRA-001 is unassigned, linked to ACME-001
228
  records[0].update({
229
+ "title": "Customer login fails intermittently",
230
+ "priority": "p1",
231
+ "status": "open",
232
+ "customer_id": "ACME-001",
233
+ "assignee": None,
234
+ "linked_zendesk": None,
235
+ "_is_primary_bug": True, # semantic marker β€” used by issue_assigned() check
236
  })
237
 
238
  return records
 
265
 
266
  # Workflow A primary: ZD-001 is unacknowledged, from ACME-001
267
  records[0].update({
268
+ "title": "Login issue β€” cannot access my account",
269
+ "urgency": "p1",
270
+ "state": "new",
271
+ "customer_id": "ACME-001",
272
+ "_acknowledged": False,
273
+ "_is_primary_ticket": True, # semantic marker β€” used by ticket_acknowledged() check
274
  })
275
 
276
  # Workflow C: several tickets from ACME-003
 
315
 
316
  # Workflow A: ACME-001 is a paying customer with yellow health
317
  records[0].update({
318
+ "company_name": "Acme Corporation",
319
+ "deal_stage": "closed_won",
320
+ "health": "yellow",
321
+ "is_paying": True,
322
+ "arr": 50_000,
323
+ "territory": "west",
324
+ "_is_workflow_a_account": True, # semantic marker β€” used by account_checked() check
325
  })
326
 
327
  # Workflow C: ACME-003 is at churn risk
328
  records[2].update({
329
+ "company_name": "Globex Systems",
330
+ "health": "red",
331
+ "deal_stage": "negotiation",
332
+ "is_paying": True,
333
+ "arr": 30_000,
334
+ "_churn_flagged": False,
335
+ "_is_churn_target": True, # semantic marker β€” used by churn_flagged() / intervention_assigned()
336
  })
337
 
338
  return records
 
352
  "name": f"{random.choice(FIRST_NAMES)} {random.choice(LAST_NAMES)}",
353
  "level": random.choice(levels),
354
  "manager_id": f"EMP-{random.randint(1, min(i, 5)):03d}" if i > 1 else None,
355
+ # All random employees are "active" β€” only the new hire below has status="pending"
356
+ # so list_employees(status="pending") returns exactly one (discoverable) result.
357
+ "status": "active",
358
  "department": random.choice(departments),
359
  "territory": random.choice(territories),
360
  "email": f"emp{i}@company.com",
 
377
  "_access_provisioned": {},
378
  "_sla_logged": False,
379
  "_onboarding_created": False,
380
+ "_is_new_hire": True, # semantic marker β€” used by employee_created() check
381
  })
382
 
383
  return records
server/environment.py CHANGED
@@ -137,12 +137,21 @@ class OrgOSEnvironment:
137
  self._schema_score = min(1.0, self._schema_score + 0.10)
138
  self._policy_score = min(1.0, self._policy_score + 0.05)
139
 
140
- # Earn compliance + efficiency for every successful action
141
- self._rule_score = min(1.0, self._rule_score + 0.10)
142
- self._efficiency = min(1.0, self._efficiency + 0.10)
 
 
143
 
144
  # 5. Re-evaluate workflow completion
145
- self._wf_score = self._workflow.evaluate(self._apps)
 
 
 
 
 
 
 
146
 
147
  # 6. SLA check (only if a ticket was touched)
148
  sla_ok, sla_pen = self._rules.check_sla(
 
137
  self._schema_score = min(1.0, self._schema_score + 0.10)
138
  self._policy_score = min(1.0, self._policy_score + 0.05)
139
 
140
+ # Earn compliance for every successful compliant action,
141
+ # normalised so completing all workflow steps earns exactly 1.0.
142
+ total_steps = max(1, len(self._workflow._steps))
143
+ earn_rate = 1.0 / total_steps
144
+ self._rule_score = min(1.0, self._rule_score + earn_rate)
145
 
146
  # 5. Re-evaluate workflow completion
147
+ old_wf_score = self._wf_score
148
+ self._wf_score = self._workflow.evaluate(self._apps)
149
+ wf_advanced = self._wf_score > old_wf_score
150
+
151
+ # Earn efficiency ONLY when a new workflow step was just completed.
152
+ # This penalises padding: closing random tickets, repeating ops, etc.
153
+ if wf_advanced:
154
+ self._efficiency = min(1.0, self._efficiency + earn_rate)
155
 
156
  # 6. SLA check (only if a ticket was touched)
157
  sla_ok, sla_pen = self._rules.check_sla(
server/workflow_engine.py CHANGED
@@ -1,7 +1,7 @@
1
  """Workflow engine β€” defines and evaluates multi-app workflow completion."""
2
 
3
  from dataclasses import dataclass
4
- from typing import Callable, Dict, List
5
 
6
 
7
  @dataclass
@@ -14,6 +14,51 @@ class WorkflowStep:
14
  completion_check: Callable[[Dict], bool]
15
 
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  # ---------------------------------------------------------------------------
18
  # Workflow A: Customer Bug Fix (Zendesk β†’ Jira β†’ Salesforce β†’ Workday)
19
  # Agent role: support
@@ -35,7 +80,7 @@ WORKFLOW_A_STEPS = [
35
  lambda apps: apps["salesforce"].account_checked(),
36
  ),
37
  WorkflowStep(
38
- "A4", "Assign the pre-existing Jira bug for this customer to an engineer",
39
  "jira", "assign_owner",
40
  lambda apps: apps["jira"].issue_assigned(),
41
  ),
@@ -52,24 +97,30 @@ WORKFLOW_A_STEPS = [
52
  # ---------------------------------------------------------------------------
53
  WORKFLOW_B_STEPS = [
54
  WorkflowStep(
55
- "B1", "Find the pending new hire in Workday and create their onboarding record",
 
 
56
  "workday", "create_onboarding_task",
57
  lambda apps: apps["workday"].employee_created(),
58
  ),
59
  WorkflowStep(
60
- "B2", "Provision Jira access for the new employee via Workday",
 
61
  "workday", "provision_access",
62
  lambda apps: apps["workday"].access_provisioned("jira"),
63
  ),
64
  WorkflowStep(
65
- "B3", "Assign the new employee to a Salesforce territory account (west region)",
 
 
66
  "salesforce", "assign_account_owner",
67
- lambda apps: apps["salesforce"].team_assigned(),
68
  ),
69
  WorkflowStep(
70
- "B4", "Create a Zendesk support agent profile for the new employee",
71
- "zendesk", "assign_agent",
72
- lambda apps: apps["zendesk"].profile_created(),
 
73
  ),
74
  ]
75
 
@@ -84,14 +135,16 @@ WORKFLOW_C_STEPS = [
84
  lambda apps: apps["salesforce"].churn_flagged(),
85
  ),
86
  WorkflowStep(
87
- "C2", "Query recent support tickets for the at-risk account in Zendesk",
 
88
  "zendesk", "get_ticket",
89
- lambda apps: apps["zendesk"].support_queried("ACME-003"),
90
  ),
91
  WorkflowStep(
92
- "C3", "List open Jira bugs linked to the at-risk account",
 
93
  "jira", "list_issues",
94
- lambda apps: apps["jira"].bugs_checked(),
95
  ),
96
  WorkflowStep(
97
  "C4", "Assign an intervention owner to the at-risk account in Salesforce",
@@ -113,11 +166,13 @@ WORKFLOW_GOALS: Dict[str, str] = {
113
  "Use list operations to discover relevant record IDs before acting."
114
  ),
115
  "B": (
116
- "Workflow B β€” Employee Onboarding: "
117
- "A new support engineer has joined the West team and needs to be fully set up. "
118
- "Ensure their employment record exists, provision the appropriate tooling access, "
119
- "assign them to the correct territory in your CRM, and create their support profile. "
120
- "Query the relevant systems to identify the new employee and required accounts."
 
 
121
  ),
122
  "C": (
123
  "Workflow C β€” Churn Risk Alert: "
 
1
  """Workflow engine β€” defines and evaluates multi-app workflow completion."""
2
 
3
  from dataclasses import dataclass
4
+ from typing import Callable, Dict, List, Optional
5
 
6
 
7
  @dataclass
 
14
  completion_check: Callable[[Dict], bool]
15
 
16
 
17
+ # ---------------------------------------------------------------------------
18
+ # Helpers β€” look up semantic-marker targets at evaluation time so completion
19
+ # checks are not coupled to specific record IDs in the data generator.
20
+ # ---------------------------------------------------------------------------
21
+
22
+ def _churn_target_account_id(apps: Dict) -> Optional[str]:
23
+ """Return the SF account_id flagged as the churn target this episode."""
24
+ for aid, rec in apps["salesforce"]._records.items():
25
+ if rec.get("_is_churn_target"):
26
+ return aid
27
+ return None
28
+
29
+
30
+ def _new_hire_assigned_sf(apps: Dict) -> bool:
31
+ """Workflow B step B3: the new hire (from Workday) must be the SF owner of an account
32
+ in their own territory. Threads employee_id + territory across apps automatically."""
33
+ new_hire = apps["workday"].get_new_hire()
34
+ if not new_hire:
35
+ return False
36
+ return apps["salesforce"].new_hire_assigned_in_territory(
37
+ new_hire.get("employee_id", ""),
38
+ new_hire.get("territory", ""),
39
+ )
40
+
41
+
42
+ def _new_hire_assigned_jira(apps: Dict) -> bool:
43
+ """Workflow B step B4: an open Jira issue must be assigned to the new hire's employee_id."""
44
+ new_hire = apps["workday"].get_new_hire()
45
+ if not new_hire:
46
+ return False
47
+ return apps["jira"].new_hire_assigned_to_issue(new_hire.get("employee_id", ""))
48
+
49
+
50
+ def _support_queried_for_churn_target(apps: Dict) -> bool:
51
+ """Workflow C step C2: Zendesk must have been queried for the churn target's account."""
52
+ aid = _churn_target_account_id(apps)
53
+ return bool(aid) and apps["zendesk"].support_queried(aid)
54
+
55
+
56
+ def _bugs_checked_for_churn_target(apps: Dict) -> bool:
57
+ """Workflow C step C3: Jira list_issues must have been called with customer_id=<churn target>."""
58
+ aid = _churn_target_account_id(apps)
59
+ return bool(aid) and apps["jira"].bugs_checked_for(aid)
60
+
61
+
62
  # ---------------------------------------------------------------------------
63
  # Workflow A: Customer Bug Fix (Zendesk β†’ Jira β†’ Salesforce β†’ Workday)
64
  # Agent role: support
 
80
  lambda apps: apps["salesforce"].account_checked(),
81
  ),
82
  WorkflowStep(
83
+ "A4", "Assign the Jira issue you just created (linked to the Zendesk ticket) to an engineer",
84
  "jira", "assign_owner",
85
  lambda apps: apps["jira"].issue_assigned(),
86
  ),
 
97
  # ---------------------------------------------------------------------------
98
  WORKFLOW_B_STEPS = [
99
  WorkflowStep(
100
+ "B1",
101
+ "Find the pending new hire in Workday (list_employees status=pending returns one result) "
102
+ "and create their onboarding record",
103
  "workday", "create_onboarding_task",
104
  lambda apps: apps["workday"].employee_created(),
105
  ),
106
  WorkflowStep(
107
+ "B2",
108
+ "Provision Jira access for THAT new hire (use the employee_id from B1)",
109
  "workday", "provision_access",
110
  lambda apps: apps["workday"].access_provisioned("jira"),
111
  ),
112
  WorkflowStep(
113
+ "B3",
114
+ "Assign the new hire as Salesforce account owner for an account in their own territory "
115
+ "(use employee_id and territory from B1)",
116
  "salesforce", "assign_account_owner",
117
+ _new_hire_assigned_sf,
118
  ),
119
  WorkflowStep(
120
+ "B4",
121
+ "Assign an open Jira issue to the new hire (use employee_id from B1 as the assignee)",
122
+ "jira", "assign_owner",
123
+ _new_hire_assigned_jira,
124
  ),
125
  ]
126
 
 
135
  lambda apps: apps["salesforce"].churn_flagged(),
136
  ),
137
  WorkflowStep(
138
+ "C2", "Query recent support tickets for the at-risk account in Zendesk "
139
+ "(use the account_id from C1, not a hardcoded value)",
140
  "zendesk", "get_ticket",
141
+ _support_queried_for_churn_target,
142
  ),
143
  WorkflowStep(
144
+ "C3", "List open Jira bugs for the at-risk account "
145
+ "(call list_issues with customer_id=<churn account>)",
146
  "jira", "list_issues",
147
+ _bugs_checked_for_churn_target,
148
  ),
149
  WorkflowStep(
150
  "C4", "Assign an intervention owner to the at-risk account in Salesforce",
 
166
  "Use list operations to discover relevant record IDs before acting."
167
  ),
168
  "B": (
169
+ "Workflow B β€” Manager-Aware Onboarding: "
170
+ "One employee in Workday is currently pending onboarding (status=pending). "
171
+ "Find that pending employee, create their onboarding record, provision their Jira access, "
172
+ "assign them as the Salesforce account owner for an account in THEIR OWN territory, "
173
+ "and assign them ownership of an open Jira issue. "
174
+ "Each step's output feeds the next β€” capture the employee_id and territory from step 1 "
175
+ "and reuse them in subsequent steps."
176
  ),
177
  "C": (
178
  "Workflow C β€” Churn Risk Alert: "