Skip to content

feat: add AI redaction tool with stage/apply options#15

Merged
jdrhyne merged 2 commits intomainfrom
feat/ai-redaction
Feb 6, 2026
Merged

feat: add AI redaction tool with stage/apply options#15
jdrhyne merged 2 commits intomainfrom
feat/ai-redaction

Conversation

@jdrhyne
Copy link
Contributor

@jdrhyne jdrhyne commented Feb 5, 2026

Summary

Adds AI-powered document redaction using the Nutrient DWS /ai/redact endpoint.

Features

ai_redactor MCP Tool:

  • AI-powered PII detection and permanent removal
  • Customizable criteria (default: "All personally identifiable information")
  • stage option — stage redactions without applying (per Nick's review feedback)
  • apply option — apply staged redactions
  • Input/output path validation with overwrite protection

Files

File Change
src/dws/ai-redact.ts New AI redaction implementation
src/schemas.ts AiRedactArgsSchema with stage/apply options
src/index.ts Tool registration
tests/unit.test.ts 3 schema validation tests

Testing

  • Build compiles clean
  • 43 tests passing (3 new)

Addresses Nick's Review Point #4

AI redaction should support stage and apply options in the payload.

The schema now accepts optional stage and apply boolean parameters that map to the API's redaction_state field.


Split from #12 per review feedback to keep PRs focused and reviewable.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an AI-powered document redaction tool (ai_redactor) that integrates with the Nutrient DWS /ai/redact endpoint. The feature was split from PR #12 per review feedback and specifically addresses Nick's Review Point #4 regarding support for stage and apply options. The implementation follows established codebase patterns for API integration, file handling, and error management, providing a clean interface for AI-driven PII detection and permanent removal from documents.

Changes:

  • Added ai_redactor MCP tool with AI-powered PII detection supporting customizable criteria and stage/apply workflow options
  • Implemented path validation with overwrite protection and mutually exclusive stage/apply flag validation
  • Added schema validation tests for default values, flag acceptance, and empty criteria rejection

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
src/dws/ai-redact.ts New implementation for AI redaction API calls with file validation, stage/apply logic, and FormData payload construction
src/schemas.ts Added AiRedactArgsSchema with optional stage/apply boolean flags and type export
src/index.ts Registered ai_redactor tool with descriptive documentation of capabilities and detected content types
tests/unit.test.ts Added 3 schema validation tests covering default criteria, flag acceptance, and empty criteria rejection

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +661 to +696
describe('AiRedactArgsSchema', () => {
it('should apply default criteria when omitted', () => {
const result = AiRedactArgsSchema.parse({ filePath: '/input.pdf', outputPath: '/output.pdf' })

expect(result.criteria).toBe('All personally identifiable information')
expect(result.stage).toBeUndefined()
expect(result.apply).toBeUndefined()
})

it('should accept stage and apply flags when provided', () => {
const stageResult = AiRedactArgsSchema.parse({
filePath: '/input.pdf',
outputPath: '/output.pdf',
stage: true,
})

const applyResult = AiRedactArgsSchema.parse({
filePath: '/input.pdf',
outputPath: '/output.pdf',
apply: true,
})

expect(stageResult.stage).toBe(true)
expect(applyResult.apply).toBe(true)
})

it('should reject empty criteria', () => {
expect(() =>
AiRedactArgsSchema.parse({
filePath: '/input.pdf',
outputPath: '/output.pdf',
criteria: '',
}),
).toThrow()
})
})
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test coverage for performAiRedactCall is incomplete. While the schema validation tests are present (lines 661-696), there are no integration tests for the actual performAiRedactCall function. Looking at the existing patterns in this file, performBuildCall (lines 83-239) and performSignCall (lines 241-365) both have comprehensive integration tests that verify error handling, API interactions, file operations, and success cases. The performAiRedactCall function should have similar tests, including: testing file not found errors, API key validation, testing stage/apply flag handling, verifying the form data sent to the API, testing output file writing, and handling API errors. This ensures consistent test coverage across all API functions.

Copilot uses AI. Check for mistakes.
src/index.ts Outdated
• Protected health information (PHI)
• Any custom criteria you specify

Use stage to create redactions without applying them. Use apply to apply staged redactions.`,
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description for the stage and apply options is incomplete. It should clarify what happens when neither option is set (the default behavior when redaction_state is not included in the API payload). Based on the code at line 45 in ai-redact.ts, when both stage and apply are undefined, no redaction_state is sent to the API. The description should explain: (1) The default behavior when neither flag is set, (2) Whether stage creates new staged redactions or requires a previously staged document, and (3) Whether apply requires a previously staged document. Consider updating the description to something like: "By default (when neither flag is set), redactions are detected and immediately applied. Set stage to true to detect and stage redactions without applying them. Set apply to true to apply previously staged redactions."

Suggested change
Use stage to create redactions without applying them. Use apply to apply staged redactions.`,
By default (when neither stage nor apply is set), redactions are detected and immediately applied. Set stage to true to detect and stage redactions without applying them. Set apply to true to apply previously staged redactions.`,

Copilot uses AI. Check for mistakes.
src/schemas.ts Outdated
Comment on lines +160 to +161
.describe('Whether to stage redactions instead of applying them immediately.'),
apply: z.boolean().optional().describe('Whether to apply staged redactions.'),
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The descriptions for the stage and apply options don't explain the default behavior or their relationship. Currently, the descriptions just state what each flag does in isolation. They should clarify: (1) What happens when neither is set (default behavior - immediate detection and application), (2) That stage and apply are mutually exclusive (validated at line 31-33 in ai-redact.ts), and (3) The typical workflow (first call with stage=true, then second call with apply=true on the staged document). Consider updating the descriptions to be more informative about the complete redaction workflow.

Suggested change
.describe('Whether to stage redactions instead of applying them immediately.'),
apply: z.boolean().optional().describe('Whether to apply staged redactions.'),
.describe(
'Whether to stage redactions instead of applying them immediately. If both stage and apply are omitted (or false), the AI will detect and immediately apply redactions to the output file in a single step. ' +
'This option is mutually exclusive with apply: only one of stage or apply should be true in a single call. A typical workflow is to first call with stage=true to create a staged document, then make a second call with apply=true on that staged document.',
),
apply: z
.boolean()
.optional()
.describe(
'Whether to apply previously staged redactions. This option is mutually exclusive with stage: only one of stage or apply should be true in a single call. ' +
'If both stage and apply are omitted (or false), redactions are detected and applied immediately without staging. Use apply=true in a follow-up call that references a document where redactions were previously staged.',
),

Copilot uses AI. Check for mistakes.
stage?: boolean,
apply?: boolean,
): Promise<CallToolResult> {
try {
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the "fail early" comment that is present in similar code in build.ts (line 23) and sign.ts (line 21). Consider adding a comment like "// Resolve paths first to fail early" before line 21 to maintain consistency with other API functions and clarify the intent of resolving paths before file operations.

Suggested change
try {
try {
// Resolve paths first to fail early

Copilot uses AI. Check for mistakes.
}

// Guard against output overwriting input
if (path.resolve(resolvedInputPath) === path.resolve(resolvedOutputPath)) {
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path.resolve() calls on line 36 are redundant. Both resolvedInputPath and resolvedOutputPath are already resolved absolute paths returned from resolveReadFilePath (line 21) and resolveWriteFilePath (line 22). You can simplify this to: if (resolvedInputPath === resolvedOutputPath). This maintains the same functionality while being more efficient and clearer about the fact that the paths are already resolved.

Suggested change
if (path.resolve(resolvedInputPath) === path.resolve(resolvedOutputPath)) {
if (resolvedInputPath === resolvedOutputPath) {

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +30
// Verify input file exists
try {
await fs.promises.access(resolvedInputPath, fs.constants.R_OK)
} catch {
return createErrorResponse(`Error: Input file not found or not readable: ${filePath}`)
}

Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file access check is redundant. The resolveReadFilePath function at line 21 already performs access checks (fs.promises.access and fs.promises.stat) to verify the file exists and is readable. This creates unnecessary I/O operations. You can safely remove this try-catch block since any file access issues will be caught by resolveReadFilePath and will throw an error that gets handled by the outer catch block at line 63.

Suggested change
// Verify input file exists
try {
await fs.promises.access(resolvedInputPath, fs.constants.R_OK)
} catch {
return createErrorResponse(`Error: Input file not found or not readable: ${filePath}`)
}

Copilot uses AI. Check for mistakes.
@jdrhyne
Copy link
Contributor Author

jdrhyne commented Feb 6, 2026

Addressed all 6 Copilot review comments in a180a74:

Code cleanup (src/dws/ai-redact.ts):

  1. ✅ Added // Resolve paths first to fail early comment — matches build.ts/sign.ts pattern
  2. ✅ Removed redundant fs.promises.access check — resolveReadFilePath already validates
  3. ✅ Removed redundant path.resolve() calls — paths already resolved

Description improvements:
4. ✅ Updated ai_redactor tool description (src/index.ts) — explains default behavior, stage, and apply
5. ✅ Updated stage/apply schema descriptions (src/schemas.ts) — mutual exclusivity, default behavior, typical workflow

Integration tests (tests/unit.test.ts):
6. ✅ Added performAiRedactCall integration tests:

  • Stage/apply mutual exclusivity error
  • Output path same as input path error
  • Successful API call with mocked response
  • API error handling

Build clean, all tests passing (93 insertions, 11 deletions).

Copy link
Collaborator

@danielmartin danielmartin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@jdrhyne jdrhyne merged commit af067e5 into main Feb 6, 2026
@jdrhyne jdrhyne deleted the feat/ai-redaction branch February 6, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants