Skip to content

Commit 632361e

Browse files
authored
Merge pull request #3600 from codeeu/dev
Dev
2 parents d551b2a + bbf94b5 commit 632361e

18 files changed

Lines changed: 999 additions & 35 deletions

app/Jobs/Support/ExecuteApprovedSupportActionJob.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
use App\Services\Support\SupportApprovalEmailService;
1212
use App\Services\Support\UserProfileUpdateService;
1313
use App\Services\Support\UserRestoreService;
14+
use App\Services\Support\UserEmailUpdateService;
1415
use App\Services\Support\UserRoleAddService;
16+
use App\Services\Support\UserRoleRemoveService;
1517
use Illuminate\Bus\Queueable;
1618
use Illuminate\Contracts\Queue\ShouldQueue;
1719
use Illuminate\Foundation\Bus\Dispatchable;
@@ -35,6 +37,8 @@ public function handle(
3537
ArtisanCommandRunner $artisanRunner,
3638
ContentUpdateService $contentUpdate,
3739
UserRoleAddService $userRoleAdd,
40+
UserRoleRemoveService $userRoleRemove,
41+
UserEmailUpdateService $userEmailUpdate,
3842
): void
3943
{
4044
$approval = SupportApproval::findOrFail($this->supportApprovalId);
@@ -92,6 +96,10 @@ public function handle(
9296
} elseif ($action === 'user_role_add') {
9397
// Re-read the role + emails from the case email at execution time.
9498
$result = $userRoleAdd->addFromCase($case, dryRun: false, viaEmailApproval: true);
99+
} elseif ($action === 'user_role_remove') {
100+
$result = $userRoleRemove->removeFromCase($case, dryRun: false, viaEmailApproval: true);
101+
} elseif ($action === 'user_email_update') {
102+
$result = $userEmailUpdate->updateFromCase($case, dryRun: false, viaEmailApproval: true);
95103
} elseif ($action === 'code_change') {
96104
$result = $cursorAgent->launchCodeAgent(
97105
prompt: (string) ($payload['cursor_prompt'] ?? ''),

app/Jobs/Support/ProcessSupportCaseDiagnosticsJob.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use App\Services\Support\UserProfileUpdateService;
1313
use App\Services\Support\UserRestoreService;
1414
use App\Services\Support\UserRoleAddService;
15+
use App\Services\Support\UserEmailUpdateService;
16+
use App\Services\Support\UserRoleRemoveService;
1517
use Illuminate\Bus\Queueable;
1618
use Illuminate\Contracts\Queue\ShouldQueue;
1719
use Illuminate\Foundation\Bus\Dispatchable;
@@ -32,6 +34,8 @@ public function handle(
3234
UserRestoreService $userRestore,
3335
UserProfileUpdateService $userProfileUpdate,
3436
UserRoleAddService $userRoleAdd,
37+
UserRoleRemoveService $userRoleRemove,
38+
UserEmailUpdateService $userEmailUpdate,
3539
ArtisanCommandRunner $artisanRunner,
3640
ContentUpdateService $contentUpdate,
3741
): void {
@@ -79,6 +83,34 @@ public function handle(
7983
);
8084
}
8185

86+
if ($case->case_type === 'email_change') {
87+
$dryRunResult = $userEmailUpdate->updateFromCase($case, dryRun: true);
88+
$logger->log(
89+
case: $case,
90+
actionName: 'user_email_update',
91+
actionType: 'write',
92+
input: ['dry_run' => true],
93+
output: $dryRunResult,
94+
succeeded: (bool) ($dryRunResult['ok'] ?? false),
95+
executedBy: 'agent',
96+
correlationId: $case->correlation_id,
97+
);
98+
}
99+
100+
if ($case->case_type === 'role_remove') {
101+
$dryRunResult = $userRoleRemove->removeFromCase($case, dryRun: true);
102+
$logger->log(
103+
case: $case,
104+
actionName: 'user_role_remove',
105+
actionType: 'write',
106+
input: ['dry_run' => true],
107+
output: $dryRunResult,
108+
succeeded: (bool) ($dryRunResult['ok'] ?? false),
109+
executedBy: 'agent',
110+
correlationId: $case->correlation_id,
111+
);
112+
}
113+
82114
if ($case->case_type === 'role_add') {
83115
$dryRunResult = $userRoleAdd->addFromCase($case, dryRun: true);
84116
$logger->log(

app/Services/Support/Agents/CursorCliTriageProvider.php

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class CursorCliTriageProvider implements TriageProvider
2828
'certificate_issue',
2929
'role_issue',
3030
'role_add',
31+
'role_remove',
32+
'email_change',
3133
'code_change',
3234
'artisan_command',
3335
'content_update',
@@ -138,6 +140,11 @@ private function buildPrompt(SupportCase $case, string $rawText): string
138140
users identified by email. Put the affected emails in target_email/secondary_emails, put the
139141
role being granted in "role_name" (singular, e.g. "leading teacher"), and set "role_operation"
140142
to "add". Include EVERY email listed in the request (one per person), even for long pasted tables.
143+
Use "role_remove" when the request is to remove/revoke/delete a role (e.g. "ambassador") from one
144+
or more users. Use the same target_email/secondary_emails and role_name fields, and set
145+
"role_operation" to "remove".
146+
Use "email_change" when the request is to change/update a user's login email address. Put the
147+
current account email in target_email/current_email and the requested new email in new_email.
141148
{$artisanBlock}{$contentBlock}
142149
JSON schema to return:
143150
{
@@ -151,8 +158,10 @@ private function buildPrompt(SupportCase $case, string $rawText): string
151158
"reasoning_summary": "<one sentence>",
152159
"profile_firstname": "<requested first name or null>",
153160
"profile_lastname": "<requested last name or null>",
154-
"role_name": "<for role_add: the role to grant, e.g. leading teacher, else null>",
155-
"role_operation": "<for role_add: add (remove is not supported), else null>",
161+
"role_name": "<for role_add/role_remove: the role affected, else null>",
162+
"role_operation": "<for role_add: add; for role_remove: remove; else null>",
163+
"current_email": "<for email_change: the account email today, else null>",
164+
"new_email": "<for email_change: the requested new email, else null>",
156165
"change_summary": "<for code_change: one sentence describing the fix, else null>",
157166
"change_area": "<for code_change: e.g. frontend/blade/css/js, else null>",
158167
"cursor_prompt": "<for code_change: a precise instruction for a coding agent to implement the fix and open a PR, else null>",
@@ -296,6 +305,8 @@ private function normalize(array $data): array
296305
'profile_update' => 'user_profile_update',
297306
'account_restore' => 'user_restore',
298307
'role_add' => 'user_role_add',
308+
'role_remove' => 'user_role_remove',
309+
'email_change' => 'user_email_update',
299310
'code_change' => 'code_change',
300311
'artisan_command' => 'artisan_command',
301312
'content_update' => 'content_update',
@@ -327,6 +338,8 @@ private function normalize(array $data): array
327338
'profile_lastname' => $this->stringOrNull($data['profile_lastname'] ?? null),
328339
'role_name' => $this->stringOrNull($data['role_name'] ?? null),
329340
'role_operation' => $this->normalizeRoleOperation($data['role_operation'] ?? null),
341+
'current_email' => $this->stringOrNull($data['current_email'] ?? null),
342+
'new_email' => $this->stringOrNull($data['new_email'] ?? null),
330343
'risk_level' => $risk,
331344
'recommended_runbook' => $this->stringOrNull($data['recommended_runbook'] ?? null) ?? $caseType,
332345
'needs_human_review' => (bool) ($data['needs_human_review'] ?? false),
@@ -352,7 +365,11 @@ private function normalizeRoleOperation(mixed $value): ?string
352365
}
353366
$value = strtolower(trim($value));
354367

355-
return in_array($value, ['add', 'grant', 'assign'], true) ? 'add' : null;
368+
return match (true) {
369+
in_array($value, ['add', 'grant', 'assign'], true) => 'add',
370+
in_array($value, ['remove', 'revoke', 'delete'], true) => 'remove',
371+
default => null,
372+
};
356373
}
357374

358375
private function stringOrNull(mixed $value): ?string

app/Services/Support/Agents/TriageAgentService.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Services\Support\Agents;
44

55
use App\Models\Support\SupportCase;
6+
use App\Services\Support\SupportEmailChangeRequestParser;
67
use App\Services\Support\SupportProfileRequestParser;
78
use App\Services\Support\SupportRoleRequestParser;
89
use Illuminate\Support\Str;
@@ -11,6 +12,7 @@ class TriageAgentService
1112
{
1213
public function __construct(
1314
private readonly SupportProfileRequestParser $profileParser,
15+
private readonly SupportEmailChangeRequestParser $emailChangeParser,
1416
private readonly CursorCliTriageProvider $aiProvider,
1517
private readonly SupportRoleRequestParser $roleParser,
1618
) {
@@ -58,17 +60,29 @@ private function heuristicTriage(SupportCase $case): array
5860
$rawText = (string) ($case->normalized_message ?? $case->raw_message ?? '');
5961
$text = Str::lower($rawText);
6062
$profile = $this->profileParser->parse($rawText);
63+
$emailChange = $this->emailChangeParser->parse($rawText);
6164
$roleRequest = $this->roleParser->parse($rawText);
62-
$hasRoleRequest = $roleRequest['role'] !== null
65+
$hasEmailChangeRequest = $emailChange['from_email'] !== null && $emailChange['to_email'] !== null;
66+
$hasRoleAddRequest = $roleRequest['role'] !== null
6367
&& $roleRequest['operation'] === 'add'
6468
&& $roleRequest['emails'] !== [];
69+
$hasRoleRemoveRequest = $roleRequest['role'] !== null
70+
&& $roleRequest['operation'] === 'remove'
71+
&& $roleRequest['emails'] !== [];
72+
$hasRoleRequest = $hasRoleAddRequest || $hasRoleRemoveRequest;
6573

6674
// V1 heuristic placeholder (replace with LLM later, keep output schema stable).
6775
$caseType = 'unknown';
6876
$runbook = 'unknown';
69-
if ($hasRoleRequest) {
77+
if ($hasRoleRemoveRequest) {
78+
$caseType = 'role_remove';
79+
$runbook = 'remove_user_role';
80+
} elseif ($hasRoleAddRequest) {
7081
$caseType = 'role_add';
7182
$runbook = 'add_user_role';
83+
} elseif ($hasEmailChangeRequest) {
84+
$caseType = 'email_change';
85+
$runbook = 'update_user_email';
7286
} elseif (Str::contains($text, ['soft-deleted', 'deleted', 'restore account', 'account missing'])) {
7387
$caseType = 'account_restore';
7488
$runbook = 'restore_deleted_account';
@@ -100,6 +114,9 @@ private function heuristicTriage(SupportCase $case): array
100114
if ($hasRoleRequest) {
101115
$targetEmail = $roleRequest['emails'][0];
102116
$secondary = array_values(array_slice($roleRequest['emails'], 1));
117+
} elseif ($hasEmailChangeRequest) {
118+
$targetEmail = $emailChange['from_email'];
119+
$secondary = [$emailChange['to_email']];
103120
} else {
104121
$targetEmail = $profile['email'] ?? $this->extractFirstEmail($text);
105122
$secondary = $this->extractAllEmails($text);
@@ -110,6 +127,9 @@ private function heuristicTriage(SupportCase $case): array
110127
if ($caseType === 'profile_update') {
111128
$risk = 'low';
112129
}
130+
if ($caseType === 'email_change') {
131+
$risk = 'medium';
132+
}
113133
if ($hasRoleRequest && $this->roleLooksPrivileged((string) $roleRequest['role'])) {
114134
$risk = 'high';
115135
}
@@ -120,6 +140,8 @@ private function heuristicTriage(SupportCase $case): array
120140
$requestedAction = match ($caseType) {
121141
'profile_update' => 'user_profile_update',
122142
'role_add' => 'user_role_add',
143+
'role_remove' => 'user_role_remove',
144+
'email_change' => 'user_email_update',
123145
default => null,
124146
};
125147

@@ -134,6 +156,8 @@ private function heuristicTriage(SupportCase $case): array
134156
'profile_lastname' => $profile['lastname'],
135157
'role_name' => $hasRoleRequest ? $roleRequest['role'] : null,
136158
'role_operation' => $hasRoleRequest ? $roleRequest['operation'] : null,
159+
'current_email' => $hasEmailChangeRequest ? $emailChange['from_email'] : null,
160+
'new_email' => $hasEmailChangeRequest ? $emailChange['to_email'] : null,
137161
'risk_level' => $risk,
138162
'recommended_runbook' => $runbook,
139163
'needs_human_review' => $needsHuman,

0 commit comments

Comments
 (0)