Skip to content

Commit 01684da

Browse files
Enhance AI Web API integration validation and documentation
- Updated `validate-ai-webapi.js` to enforce OData version headers and Content-Type for Search Summary calls. - Added warnings for potential issues with disabled-state envelopes and plain-scalar prompt values. - Improved YAML parsing for site settings to handle block-literal and folded-scalar formats. - Documented caller-suppress mode in `create-webroles/SKILL.md` to streamline deployment prompts. - Clarified decision framework for backend integration in `decision-framework.md`, addressing field-list drift. - Revised `SKILL.md` for `/integrate-webapi` to defer git commits and suppress deploy prompts in AI-only read mode. - Introduced new prompt templates for agent invocation and exploration in `add-ai-webapi` references. - Added structured follow-up questions for scope confirmation and list-trigger choices in `scope-classification.md`.
1 parent 3112da9 commit 01684da

12 files changed

Lines changed: 1076 additions & 451 deletions

File tree

plugins/power-pages/agents/ai-webapi-integration.md

Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,21 @@ tools:
3232
# AI Summarization Web API Integration Agent
3333

3434
You are a Power Pages generative-AI summarization integration specialist. You implement production
35-
code for one or more of the three summarization endpoints, following the same shape as the
35+
code for one or both of the two summarization endpoints — **Search Summary**
36+
(`/_api/search/v1.0/summary`) and **Data Summarization**
37+
(`/_api/summarization/data/v1.0/<entitySet>(<id>)?...`) — following the same shape as the
3638
`webapi-integration` agent (raw `fetch`, CSRF token, framework-idiomatic hook/composable/service,
37-
wire into UI, no duplicate helpers).
39+
wire into UI, no duplicate helpers). The Microsoft-shipped support-case Copilot summary is a
40+
configuration of Data Summarization (specific entity set, `$select`/`$expand`, and prompt
41+
identifier), not a third endpoint.
3842

3943
## Reference docs
4044

4145
Read these first — they have the authoritative API shapes, headers, request bodies, and error
4246
codes:
4347

4448
- `${CLAUDE_PLUGIN_ROOT}/skills/add-ai-webapi/references/ai-api-reference.md` — canonical reference
45-
for all three APIs with the CSRF rules
49+
for both APIs (Search Summary, Data Summarization) with the CSRF rules
4650
- `${CLAUDE_PLUGIN_ROOT}/agents/webapi-integration.md` — general Web API integration patterns
4751
(framework detection, file placement, hook conventions)
4852

@@ -62,9 +66,9 @@ Upstream Microsoft Learn sources (already captured in the reference above — on
6266
- **Reuse `getCsrfToken`** — if an existing helper is present (from `/add-cloud-flow`, a prior
6367
`/add-ai-webapi` run, or any custom code), import and reuse it. Only create it if nothing
6468
suitable exists.
65-
- **One service file for all three APIs** — group `fetchSearchSummary`, `fetchDataSummary`, and
66-
`fetchCaseSummary` in a single service (e.g. `src/services/aiSummaryService.ts`). Do not create
67-
one file per endpoint.
69+
- **One service file for both APIs** — group `fetchSearchSummary`, `fetchDataSummary`,
70+
`fetchListSummary`, and the optional `fetchCaseSummary` wrapper in a single service (e.g.
71+
`src/services/aiSummaryService.ts`). Do not create one file per endpoint.
6872
- **CSRF token is mandatory; X-Requested-With is recommended.** Every summarization POST must
6973
include `__RequestVerificationToken` (fetched from `/_layout/tokenhtml`) — without it, the
7074
Power Pages anti-forgery layer rejects the request before the summariser ever sees it.
@@ -659,26 +663,28 @@ below.
659663
- **Data summarization on a support-case detail page (when the caller specified the
660664
Microsoft-shipped support-case scenario)** → find the case/incident detail page (look for
661665
components that read a case `id` from the URL, or match names like `Case*`, `Incident*`,
662-
`Ticket*`). Add a collapsible summary section at the top that mirrors the Microsoft-shipped
663-
Copilot summary card. Reproduce all the affordances from the MS Learn case-page template — not
664-
just the chevron:
665-
666-
| Element | Purpose | Source |
667-
|---------|---------|--------|
668-
| Gradient border | Visual marker that this is AI output (blue → cyan → purple) | `border-image: linear-gradient(90deg, rgb(70,79,235) 35%, rgb(71,207,250) 70%, rgb(180,124,248) 92%) 1` |
669-
| Sparkle icon + "Summary" label | Header of the section | MS Learn SVG (paths omitted here — copy verbatim from the case-page article) |
670-
| Chevron (rotates on toggle) | Collapses/expands the summary | `.chevron` with `transform: rotate(45deg)` / `rotate(-135deg)` |
671-
| Shimmer block | Loading state while the request is in flight | `.shimmer` keyframes from MS Learn |
672-
| Summary text | Output of `response.Summary` | Service call result |
673-
| Copy button (page icon) | One-click copy of the summary to clipboard (`navigator.clipboard.writeText`) | MS Learn SVG |
674-
| Thumbs-up + thumbs-down | User feedback buttons (wire to telemetry or leave as placeholder) | MS Learn SVGs |
675-
| "AI-generated content may be incorrect" | Disclaimer alongside the feedback buttons | MS Learn text |
676-
677-
Keep the gradient border exact — it's the Copilot brand cue. Copy the two feedback SVGs and the
678-
copy-button SVG directly from the MS Learn article's `<svg>` definitions so the section renders
679-
identical to first-party Power Pages pages. The feedback click handlers can be no-ops (or wire
680-
to `console.log` / a project-local analytics hook) — the important thing is the UI presence, so
681-
users see the same affordances they would on a Microsoft-shipped portal template.
666+
`Ticket*`). Add a collapsible summary section at the top that uses the **Copilot card visual
667+
language** so the surface reads as AI output. The card shape is what matters — concrete
668+
styling can adapt to the site's design system. Required affordances:
669+
670+
| Element | Purpose | Notes |
671+
|---------|---------|-------|
672+
| Gradient/accent border or background | Visual marker that this is AI output | The MS Learn case-page article uses `border-image: linear-gradient(90deg, rgb(70,79,235) 35%, rgb(71,207,250) 70%, rgb(180,124,248) 92%) 1`. Use that verbatim if the site has no design system; otherwise pick an equivalent accent that reads as Copilot/AI in this site's palette. |
673+
| Sparkle icon + "Summary" label | Header of the section | The MS Learn SVG is the safe default; the site's icon library can replace it with an equivalent AI/sparkle glyph. |
674+
| Chevron (rotates on toggle) | Collapses/expands the summary | Any rotating-chevron pattern is fine. |
675+
| Loading state | Shimmer block (or the site's standard loading affordance) | Don't leave the slot empty during the request. |
676+
| Summary text | Output of `response.Summary` | Run through the safe-markdown renderer for tabular-insight prompts. |
677+
| Copy button | One-click copy to clipboard via `navigator.clipboard.writeText` | Standard icon button. |
678+
| Thumbs-up + thumbs-down | User feedback affordance (handlers may be no-ops or wire to a project-local analytics hook) | Presence matters more than wiring — leaves room for telemetry later without redesign. |
679+
| "AI-generated content may be incorrect" disclaimer | Required text alongside the feedback buttons | Use this exact wording. |
680+
681+
**Defaults vs. customisation.** When the site has no design system in place, copy the
682+
MS Learn case-page CSS and SVGs verbatim — they ship working Copilot-grade output and
683+
match what the user expects from first-party Power Pages templates. When the site does
684+
have a design system (custom theme, tokens, icon set), match its conventions and use a
685+
Copilot-equivalent accent — what matters is that users recognise the card as AI output,
686+
not that every pixel matches Microsoft's reference. Do NOT skip any of the affordances
687+
above; the row of affordances is the contract.
682688
- **Generic data summarization (single record)** → find the detail page for the target table and
683689
add a summary section next to the main content.
684690
- **List summary (collection)** → find the list / history / results page for the target table
@@ -915,9 +921,44 @@ the summarization service to import it.
915921

916922
---
917923

924+
## Return value (contract with the orchestrator)
925+
926+
When you complete (or fail and unwind), return a structured summary so the calling
927+
orchestrator can stage commits, present a final summary, and detect partial work. The
928+
orchestrator's Phase 5.5 explicitly relies on this list to do per-file `git add` instead of
929+
a broad `git add -A`.
930+
931+
```text
932+
Files touched:
933+
- <project-relative path> [created | modified | replaced-marker]
934+
- ...
935+
936+
Markers consumed:
937+
- <project-relative path>:<line> kind=<search-summary | data-summarization>
938+
- ... (omit this section when no markers were involved)
939+
940+
Reused infrastructure:
941+
- getCsrfToken: <"defined inline" | "imported from <path>">
942+
- aiSummaryService: <"created at <path>" | "extended at <path>">
943+
- powerPagesApi.ts: <"present" | "absent">
944+
945+
Follow-ups:
946+
- <any post-Phase-5 work the orchestrator should know about — e.g. "ContentSizeLimit
947+
setting still needs to be created in Phase 6 for the WorkOrderList list-summary target">
948+
```
949+
950+
`replaced-marker` means the file already had a `POWERPAGES:AI-SLOT` comment and you both
951+
inserted the generated UI at the marker line and deleted the comment in the same edit.
952+
`modified` means an existing file was updated (e.g., extending an existing
953+
`aiSummaryService.ts` with another exported function); `created` means the file did not
954+
exist before this invocation.
955+
956+
Do not return a long prose narrative — the orchestrator parses this list. Keep paths
957+
project-relative (relative to the project root containing `powerpages.config.json`).
958+
918959
## Key rules
919960

920-
1. **Raw `fetch` for all three APIs** — never `powerPagesFetch` or `HttpClient` wrappers that inject
961+
1. **Raw `fetch` for both APIs** — never `powerPagesFetch` or `HttpClient` wrappers that inject
921962
OData-specific headers.
922963
2. **CSRF token is mandatory; X-Requested-With is recommended.** Always set
923964
`__RequestVerificationToken` (omitting it triggers anti-forgery rejection). Always set

plugins/power-pages/agents/ai-webapi-settings-architect.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,16 @@ Prompt selection depends on the URL form captured in 3.2:
278278

279279
## Step 4: Cross-Check Web API Prerequisites
280280

281-
For every table that will be summarised via the data summarization API (primary table **and** every
282-
`$expand` target), verify both `Webapi/<table>/enabled` and `Webapi/<table>/fields` exist in
283-
`.powerpages-site/site-settings/`.
281+
**Skip this step entirely when the only summarisation calls in the site are to
282+
`/_api/search/v1.0/summary`.** Search Summary has no per-table prereqs (no `Webapi/<table>/*`,
283+
no table permissions tied to a specific Dataverse table) — its enablement lives in the
284+
Copilot workspace toggle. If Step 3.1 found Search Summary calls AND Step 3.2 found **no**
285+
Data Summarization calls, jump to Step 5; the prerequisite block below would over-block a
286+
legitimate search-only run.
287+
288+
Otherwise, for every table that will be summarised via the data summarization API (primary
289+
table **and** every `$expand` target), verify both `Webapi/<table>/enabled` and
290+
`Webapi/<table>/fields` exist in `.powerpages-site/site-settings/`.
284291

285292
- **If the Web API site settings already exist**: include them in the plan as `✓ Prerequisite met`.
286293
- **If the Web API site settings are missing**: include a blocker in the plan that asks the user

plugins/power-pages/scripts/tests/validate-ai-webapi.test.js

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,189 @@ test('project with no Summarization/prompt settings does not warn', (t) => {
393393
assert.equal(result.status, 0, result.stderr);
394394
assert.doesNotMatch(result.stderr, /Summarization-prompt/);
395395
});
396+
397+
// --- OData 4.0 header checks (data summarization) ---
398+
399+
test('data summarization missing OData-MaxVersion header is blocked', (t) => {
400+
const projectRoot = createTempProject(t);
401+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
402+
writeProjectFile(
403+
projectRoot,
404+
'src/services/aiSummaryService.ts',
405+
VALID_DATA_SERVICE.replace("'OData-MaxVersion': '4.0',", '')
406+
);
407+
408+
const result = runValidator(projectRoot);
409+
assert.equal(result.status, 2);
410+
assert.match(result.stderr, /missing OData-MaxVersion: 4\.0/);
411+
});
412+
413+
test('data summarization missing OData-Version header is blocked', (t) => {
414+
const projectRoot = createTempProject(t);
415+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
416+
writeProjectFile(
417+
projectRoot,
418+
'src/services/aiSummaryService.ts',
419+
VALID_DATA_SERVICE.replace("'OData-Version': '4.0',", '')
420+
);
421+
422+
const result = runValidator(projectRoot);
423+
assert.equal(result.status, 2);
424+
assert.match(result.stderr, /missing OData-Version: 4\.0/);
425+
});
426+
427+
test('search-only project does not require OData headers', (t) => {
428+
const projectRoot = createTempProject(t);
429+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
430+
writeProjectFile(projectRoot, 'src/services/aiSummaryService.ts', VALID_SEARCH_SERVICE);
431+
432+
const result = runValidator(projectRoot);
433+
assert.equal(result.status, 0, result.stderr);
434+
assert.doesNotMatch(result.stderr, /OData-/);
435+
});
436+
437+
// --- Search Summary content-type check ---
438+
439+
test('Search Summary with application/json instead of urlencoded is blocked', (t) => {
440+
const projectRoot = createTempProject(t);
441+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
442+
writeProjectFile(
443+
projectRoot,
444+
'src/services/aiSummaryService.ts',
445+
VALID_SEARCH_SERVICE.replace(
446+
"'Content-Type': 'application/x-www-form-urlencoded',",
447+
"'Content-Type': 'application/json',"
448+
)
449+
);
450+
451+
const result = runValidator(projectRoot);
452+
assert.equal(result.status, 2);
453+
assert.match(result.stderr, /missing Content-Type: application\/x-www-form-urlencoded/);
454+
});
455+
456+
// --- Disabled-state envelope advisory ---
457+
458+
test('Search Summary without SearchSummaryApiError is warned but not blocked', (t) => {
459+
const projectRoot = createTempProject(t);
460+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
461+
writeProjectFile(projectRoot, 'src/services/aiSummaryService.ts', VALID_SEARCH_SERVICE);
462+
463+
const result = runValidator(projectRoot);
464+
assert.equal(result.status, 0, result.stderr);
465+
assert.match(result.stderr, /SearchSummaryApiError/);
466+
});
467+
468+
test('Search Summary with SearchSummaryApiError export does not warn about envelope', (t) => {
469+
const projectRoot = createTempProject(t);
470+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
471+
writeProjectFile(
472+
projectRoot,
473+
'src/services/aiSummaryService.ts',
474+
VALID_SEARCH_SERVICE +
475+
'\nexport class SearchSummaryApiError extends Error {}\nexport function isGenAiSearchDisabled() { return false; }\n'
476+
);
477+
478+
const result = runValidator(projectRoot);
479+
assert.equal(result.status, 0, result.stderr);
480+
assert.doesNotMatch(result.stderr, /SearchSummaryApiError/);
481+
});
482+
483+
test('Search Summary with inline body.Code === pattern does not warn about envelope', (t) => {
484+
const projectRoot = createTempProject(t);
485+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
486+
writeProjectFile(projectRoot, 'src/services/aiSummaryService.ts', VALID_SEARCH_SERVICE);
487+
writeProjectFile(
488+
projectRoot,
489+
'src/components/SearchSummaryCard.tsx',
490+
"export function detect(body) { return body.Code === 400 && body.Message !== ''; }"
491+
);
492+
493+
const result = runValidator(projectRoot);
494+
assert.equal(result.status, 0, result.stderr);
495+
assert.doesNotMatch(result.stderr, /SearchSummaryApiError/);
496+
});
497+
498+
test('Data-only project does not warn about disabled-state envelope', (t) => {
499+
const projectRoot = createTempProject(t);
500+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
501+
writeProjectFile(projectRoot, 'src/services/aiSummaryService.ts', VALID_DATA_SERVICE);
502+
503+
const result = runValidator(projectRoot);
504+
assert.equal(result.status, 0, result.stderr);
505+
assert.doesNotMatch(result.stderr, /SearchSummaryApiError/);
506+
});
507+
508+
// --- Plain-scalar long prompt advisory ---
509+
510+
test('plain-scalar prompt over 200 chars warns about block-literal', (t) => {
511+
const projectRoot = createTempProject(t);
512+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
513+
writeProjectFile(projectRoot, 'src/services/aiSummaryService.ts', VALID_DATA_SERVICE);
514+
const longPrompt = 'X'.repeat(300);
515+
writeProjectFile(
516+
projectRoot,
517+
'.powerpages-site/site-settings/Summarization-prompt-plain_long.sitesetting.yml',
518+
`id: 44444444-4444-4444-8444-444444444444\nname: Summarization/prompt/plain_long\nvalue: ${longPrompt}\n`
519+
);
520+
521+
const result = runValidator(projectRoot);
522+
assert.equal(result.status, 0, result.stderr);
523+
assert.match(result.stderr, /Summarization-prompt-plain_long\.sitesetting\.yml: prompt value is 300 characters and uses plain-scalar YAML/);
524+
});
525+
526+
test('block-literal prompt over 200 chars does not warn about plain-scalar', (t) => {
527+
const projectRoot = createTempProject(t);
528+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
529+
writeProjectFile(projectRoot, 'src/services/aiSummaryService.ts', VALID_DATA_SERVICE);
530+
writeProjectFile(
531+
projectRoot,
532+
'.powerpages-site/site-settings/Summarization-prompt-block_long.sitesetting.yml',
533+
promptSettingYaml('Y'.repeat(300))
534+
);
535+
536+
const result = runValidator(projectRoot);
537+
assert.equal(result.status, 0, result.stderr);
538+
assert.doesNotMatch(result.stderr, /uses plain-scalar YAML/);
539+
});
540+
541+
test('plain-scalar prompt under 200 chars does not warn about plain-scalar', (t) => {
542+
const projectRoot = createTempProject(t);
543+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
544+
writeProjectFile(projectRoot, 'src/services/aiSummaryService.ts', VALID_DATA_SERVICE);
545+
writeProjectFile(
546+
projectRoot,
547+
'.powerpages-site/site-settings/Summarization-prompt-short_plain.sitesetting.yml',
548+
"id: 55555555-5555-4555-8555-555555555555\nname: Summarization/prompt/short_plain\nvalue: Summarize key details and critical information\n"
549+
);
550+
551+
const result = runValidator(projectRoot);
552+
assert.equal(result.status, 0, result.stderr);
553+
assert.doesNotMatch(result.stderr, /uses plain-scalar YAML/);
554+
});
555+
556+
// --- Folded scalar (`>`) parsing ---
557+
558+
test('folded-scalar prompt is parsed and size-checked', (t) => {
559+
const projectRoot = createTempProject(t);
560+
writeProjectFile(projectRoot, 'powerpages.config.json', '{}');
561+
writeProjectFile(projectRoot, 'src/services/aiSummaryService.ts', VALID_DATA_SERVICE);
562+
// Build a folded-scalar YAML file by hand. Folded scalars use `>`; the validator
563+
// should pick up the indented body (size-check it) instead of treating it as null.
564+
const longBody = 'Z'.repeat(2200);
565+
const yaml = [
566+
'id: 66666666-6666-4666-8666-666666666666',
567+
'name: Summarization/prompt/folded_one',
568+
'value: >',
569+
' ' + longBody,
570+
'',
571+
].join('\n');
572+
writeProjectFile(
573+
projectRoot,
574+
'.powerpages-site/site-settings/Summarization-prompt-folded_one.sitesetting.yml',
575+
yaml
576+
);
577+
578+
const result = runValidator(projectRoot);
579+
assert.notEqual(result.status, 0);
580+
assert.match(result.stderr, /Summarization-prompt-folded_one\.sitesetting\.yml: prompt value is \d+ characters, exceeding the supported maximum of 2000/);
581+
});

0 commit comments

Comments
 (0)