Skip to content

Commit 382ef79

Browse files
solaris007Copilot
andauthored
fix: agent executor slack notification (#137)
* fix: agent executor slack notification * chore: copilot instructions * fix: urls * Update src/agents/brand-profile/index.js Co-authored-by: Copilot <[email protected]> * fix: newlines --------- Co-authored-by: Copilot <[email protected]>
1 parent fa5dfb6 commit 382ef79

File tree

8 files changed

+628
-20
lines changed

8 files changed

+628
-20
lines changed

.github/copilot-instructions.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Copilot PR Review Instructions
2+
3+
## 1. Review Goals and Priorities
4+
5+
Your primary purpose is to identify **behavior-breaking defects**, **security/auth gaps**, and **missing tests for changed behavior**.
6+
When such issues are present, prioritize them above all other considerations (performance, cost, style).
7+
8+
Use **three severities**:
9+
10+
* **Critical** – Bugs, regressions, security issues, missing required validation, or missing tests for changed behavior.
11+
*Respond with:* “This PR should not be merged until this is fixed.”
12+
* **Major** – Missing documentation, missing but non-blocking tests, realistic performance or cost concerns.
13+
* **Minor** – Stylistic suggestions or optional improvements.
14+
*Only list Minor issues if no Critical issues exist.*
15+
16+
If you find any Critical issue, list it first and deprioritize all other feedback.
17+
18+
---
19+
20+
## 2. Output Format (Always Required)
21+
22+
Respond using the following structure:
23+
24+
### Summary
25+
26+
1–3 sentences describing the overall health of the PR.
27+
28+
### Issues
29+
30+
#### Critical
31+
32+
* List each issue, quoting relevant code and suggesting a concrete fix.
33+
34+
#### Major
35+
36+
* As above.
37+
38+
#### Minor
39+
40+
* As above. Only include if there are no Critical issues.
41+
42+
### Suggested Tests
43+
44+
* Describe which tests should be added or updated (if applicable). If no test changes are needed, state that clearly.
45+
46+
---
47+
48+
## 3. Core Checks (Apply to Every PR)
49+
50+
### 3.1 Bug & Regression Scan
51+
52+
Look for defects including:
53+
54+
* Missing or incorrect null/undefined checks.
55+
* Incorrect async/await handling.
56+
* Logic changes without corresponding test updates.
57+
58+
**If you see changed behavior without new or updated tests, mark as Critical.**
59+
60+
---
61+
62+
### 3.2 Use of shared utility functions
63+
64+
Check that utility functions available here: https://github.com/adobe/spacecat-shared/blob/main/packages/spacecat-shared-utils/src/index.js are used where appropriate and instead of self-made checks.
65+
66+
---
67+
68+
### 3.3 Required Tests
69+
70+
For any non-trivial code change:
71+
72+
* Require unit tests under `test/**` using Mocha/Chai/Sinon/esmock.
73+
* Integration tests where relevant.
74+
* Tests must assert behavior, not just shallow coverage.
75+
* Fixtures and helpers must be updated consistently.
76+
77+
**If behavior changes but tests do not → Critical.**
78+
79+
If a PR is documentation-only or comment-only, explicitly mark tests as not required.
80+
81+
---
82+
83+
## 4. Performance Scan (Secondary Priority)
84+
85+
Raise **Major** issues for realistic performance risks:
86+
87+
* Repeated DAO calls inside loops.
88+
* Redundant fetches or HTTP calls.
89+
* Blocking or synchronous operations where async or batching exists.
90+
* Unbounded payload handling without streaming.
91+
92+
Do **not** speculate without evidence.
93+
94+
---
95+
96+
## 5. Cost Impact Scan (Secondary Priority)
97+
98+
Flag potential cost increases only when the diff clearly adds:
99+
100+
* New SQS calls, queue consumers, cron jobs.
101+
* Large CSV/JSON generation.
102+
* Long-running processing.
103+
* Removal of rate limits such as `SANDBOX_AUDIT_RATE_LIMIT_HOURS`.
104+
105+
Tie comments to specific code, not general assumptions.
106+
107+
---
108+
109+
## 6. Config, Documentation, and Change Control
110+
111+
For any new:
112+
113+
* Env var
114+
* Queue
115+
* Feature flag
116+
* Controller surface area
117+
118+
Require updates to:
119+
120+
* `README.md`
121+
122+
Missing required docs → **Major**.
123+
124+
---
125+
126+
## 7. Final Quality Pass
127+
128+
Once all Critical and Major issues are addressed:
129+
130+
* Ensure handlers, tests, routing, and docs are consistent.
131+
* Ensure no lint rules are violated.
132+
* Ensure logging is structured and avoids PII.
133+
* Only then offer stylistic suggestions (Minor).

README.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,45 @@ $ npm run lint
107107

108108
## Message Body Formats
109109

110-
Task processor consumes the `SPACECAT-TASK-PROCESSOR-JOBS` queue, performs the requested task and sends a notification to slack as needed.
110+
Task processor consumes the `SPACECAT-TASK-PROCESSOR-JOBS` queue, performs the requested task and sends a notification to Slack as needed.
111111

112-
Expected message body format in `SPACECAT-TASK-PROCESSOR-JOBS` is:
112+
### SQS (legacy) envelope
113113

114114
```json
115115
{
116116
"type": "string",
117117
"siteId": "string"
118118
}
119119
```
120+
121+
### Agent workflow (direct invoke) payload
122+
123+
When the AWS Step Functions Agent Workflow invokes the Lambda directly, it sends the same top-level envelope but without SQS metadata. The payload **must** include `type: "agent-executor"` plus the following fields:
124+
125+
```json
126+
{
127+
"type": "agent-executor",
128+
"agentId": "brand-profile",
129+
"siteId": "123e4567-e89b-12d3-a456-426614174000",
130+
"context": {
131+
"baseURL": "https://example.com",
132+
"params": {
133+
"crawlDepth": 2
134+
}
135+
},
136+
"slackContext": {
137+
"channelId": "C123456",
138+
"threadTs": "1731111111.000200"
139+
},
140+
"idempotencyKey": "brand-profile-123e4567-e89b-12d3-a456-426614174000-1731111111000"
141+
}
142+
```
143+
144+
Field descriptions:
145+
- `agentId` *(required)* – must match a registered agent (e.g., `brand-profile`).
146+
- `siteId` *(required)* – kept at the envelope level for logging/metrics. Agents can still read it from the message passed into `agent.persist`.
147+
- `context` *(required)* – forwarded to `agent.run`. At minimum it must include `baseURL`; additional agent-specific params live here.
148+
- `slackContext` *(optional)* – when present, the workflow sends pre-/post-execution Slack notifications using `channelId` and `threadTs`. Provide an empty object `{}` if no Slack context is available.
149+
- `idempotencyKey` *(required by workflow)* – generated by the caller to deduplicate executions. The task processor treats it as opaque metadata but logs it for traceability.
150+
151+
Any additional properties are passed through to the agent and appear in the executor response body so the workflow can inspect them.

src/agents/brand-profile/index.js

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
*/
1212
import { Config } from '@adobe/spacecat-shared-data-access/src/models/site/config.js';
1313
import { AzureOpenAIClient } from '@adobe/spacecat-shared-gpt-client';
14-
import { isNonEmptyObject, isValidUrl, isValidUUID } from '@adobe/spacecat-shared-utils';
14+
import {
15+
hasText,
16+
isNonEmptyObject,
17+
isValidUrl,
18+
isValidUUID,
19+
} from '@adobe/spacecat-shared-utils';
1520

1621
import { readPromptFile, renderTemplate } from '../base.js';
1722

@@ -57,21 +62,22 @@ async function persist(message, context, result) {
5762

5863
if (!isValidUUID(siteId)) {
5964
log.warn(`brand-profile persist: invalid siteId ${siteId}`);
60-
return;
65+
return {};
6166
}
6267

6368
if (!isNonEmptyObject(result)) {
6469
log.warn(`brand-profile persist: empty result for site ${siteId}`);
65-
return;
70+
return {};
6671
}
6772

6873
const { Site } = dataAccess;
6974
const site = await Site.findById(siteId);
7075
if (!site) {
7176
log.warn(`brand-profile persist: site not found ${siteId}`);
72-
return;
77+
return {};
7378
}
7479
const cfg = site.getConfig();
80+
const baseURL = site.getBaseURL();
7581
const before = cfg.getBrandProfile?.() || {};
7682
const beforeHash = before?.contentHash || null;
7783
cfg.updateBrandProfile(result);
@@ -82,11 +88,11 @@ async function persist(message, context, result) {
8288
await site.save();
8389

8490
// Emit concise summary for observability/Slack step consumers via logs
85-
const baseURL = message?.context?.baseURL;
8691
const version = after?.version;
92+
const isDev = context.env.AWS_ENV === 'dev';
8793
const summary = changed
88-
? `Brand profile updated to v${version} for site ${siteId}${baseURL}.`
89-
: `Brand profile unchanged (v${version}) for site ${siteId}${baseURL}.`;
94+
? `:white_check_mark: Brand profile updated to v${version} for ${baseURL}.`
95+
: `:information_source: Brand profile already up to date (v${version}) for ${baseURL}.`;
9096
log.info('brand-profile persist:', {
9197
siteId,
9298
version,
@@ -95,6 +101,70 @@ async function persist(message, context, result) {
95101
baseURL,
96102
summary,
97103
});
104+
105+
const primaryVoice = Array.isArray(after?.main_profile?.tone_attributes?.primary)
106+
? after.main_profile.tone_attributes.primary.slice(0, 3)
107+
: [];
108+
const highlightLines = [];
109+
if (primaryVoice.length > 0) {
110+
highlightLines.push(`*Primary voice:* ${primaryVoice.join(', ')}`);
111+
}
112+
if (hasText(after?.main_profile?.communication_style)) {
113+
highlightLines.push(`*Style:* ${after.main_profile.communication_style}`);
114+
}
115+
if (hasText(after?.main_profile?.target_audience)) {
116+
highlightLines.push(`*Audience:* ${after.main_profile.target_audience}`);
117+
}
118+
const highlightText = highlightLines.join('\n');
119+
const blocks = [
120+
{
121+
type: 'section',
122+
text: {
123+
type: 'mrkdwn',
124+
text: `${summary}\n*Site:* ${baseURL}`,
125+
},
126+
},
127+
];
128+
if (hasText(highlightText)) {
129+
blocks.push({
130+
type: 'section',
131+
text: {
132+
type: 'mrkdwn',
133+
text: highlightText,
134+
},
135+
});
136+
}
137+
const contextElements = [
138+
{ type: 'mrkdwn', text: `*Site ID:* ${siteId}` },
139+
];
140+
if (version) {
141+
contextElements.push({ type: 'mrkdwn', text: `*Version:* ${version}` });
142+
}
143+
if (afterHash) {
144+
contextElements.push({ type: 'mrkdwn', text: `*Hash:* \`${afterHash}\`` });
145+
}
146+
contextElements.push({
147+
type: 'mrkdwn',
148+
text: `https://spacecat.experiencecloud.live/api/${isDev ? 'ci' : 'v1'}/sites/${siteId}/brand-profile`,
149+
});
150+
blocks.push({
151+
type: 'context',
152+
elements: contextElements,
153+
});
154+
155+
return {
156+
siteId,
157+
version,
158+
changed,
159+
contentHash: afterHash,
160+
summary,
161+
notifications: {
162+
success: {
163+
text: summary,
164+
blocks,
165+
},
166+
},
167+
};
98168
}
99169

100170
export default {

src/tasks/agent-executor/handler.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,43 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212
import { ok, badRequest } from '@adobe/spacecat-shared-http-utils';
13-
import { hasText, isNonEmptyObject } from '@adobe/spacecat-shared-utils';
13+
import { hasText, isNonEmptyArray, isNonEmptyObject } from '@adobe/spacecat-shared-utils';
1414

1515
import { getAgent } from '../../agents/registry.js';
1616

17+
const NOTIFICATION_KEYS = ['success', 'failure'];
18+
19+
function normalizeNotifications(source) {
20+
if (!isNonEmptyObject(source)) {
21+
return undefined;
22+
}
23+
24+
const normalized = {};
25+
NOTIFICATION_KEYS.forEach((key) => {
26+
const value = source[key];
27+
if (!isNonEmptyObject(value)) {
28+
return;
29+
}
30+
31+
const entry = {};
32+
if (hasText(value.text)) {
33+
entry.text = value.text;
34+
}
35+
if (isNonEmptyArray(value.blocks)) {
36+
entry.blocks = value.blocks;
37+
}
38+
if (isNonEmptyArray(value.attachments)) {
39+
entry.attachments = value.attachments;
40+
}
41+
42+
if (Object.keys(entry).length > 0) {
43+
normalized[key] = entry;
44+
}
45+
});
46+
47+
return Object.keys(normalized).length > 0 ? normalized : undefined;
48+
}
49+
1750
/**
1851
* Message shape:
1952
* {
@@ -41,6 +74,7 @@ export async function runAgentExecutor(message, context) {
4174
const result = await agent.run(agentContext, context.env, log);
4275
// Optionally persist if the agent supports persistence
4376
let persistMeta;
77+
let notifications;
4478
if (typeof agent.persist === 'function') {
4579
try {
4680
persistMeta = await agent.persist(message, context, result);
@@ -55,7 +89,17 @@ export async function runAgentExecutor(message, context) {
5589
result,
5690
};
5791
if (isNonEmptyObject(persistMeta)) {
58-
payload.persistMeta = persistMeta;
92+
const { notifications: persistNotifications, ...rest } = persistMeta;
93+
if (isNonEmptyObject(rest)) {
94+
payload.persistMeta = rest;
95+
}
96+
const normalized = normalizeNotifications(persistNotifications);
97+
if (normalized) {
98+
notifications = normalized;
99+
}
100+
}
101+
if (notifications) {
102+
payload.notifications = notifications;
59103
}
60104
return ok(payload);
61105
}

0 commit comments

Comments
 (0)