Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions pieces/agentmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# @thinkfleet/piece-agentmark

[Activepieces](https://www.activepieces.com/) piece for [AgentMark](https://github.com/ThinkfleetAI/agentmark) — convert any web page or PDF into a compact AgentMark snapshot from inside a flow, and fill PDF forms with data from previous flow steps.

## What this piece adds

| Action | What it does | Inputs | Outputs |
|---|---|---|---|
| **Capture Web Page** | Launch Chromium, snapshot a URL, return AgentMark | `url`, `wait_until`, `timeout_ms`, `headless` | `agentmark`, `url`, `title`, `kind`, `action_count`, `bytes` |
| **Capture PDF** | Convert a PDF (URL/file/base64/data URI) to AgentMark; optional OCR | `source`, `source_url`, `title`, `password`, `enable_ocr`, `ocr_language` | `agentmark`, `source_url`, `bytes`, `ocr_used` |
| **Fill PDF Form** | Fill an AcroForm PDF and return the filled bytes | `source`, `values`, `flatten`, `return_format`, `password` | `filled_pdf` (data URI or raw base64), `bytes`, `fields_applied`, `fields_skipped`, `flattened` |

All actions run on the Activepieces worker — no external service required. Browser-based snapshots use Chromium via Playwright; PDF support uses pdfjs-dist + pdf-lib; optional OCR uses Tesseract.js with Poppler (`pdftoppm`).

## Why use this in a flow

- **Drop AgentMark into any agent flow** without writing code. The agent loop lives in your flow — call `Capture Page`, pass the snapshot to your AI step, then `Page Execute` (coming soon) or chain another snapshot.
- **Fill insurance/government/vendor PDFs** from CRM data. Map field action IDs from a previous step to records pulled from your data store, run `Fill PDF Form`, attach the result to an email.
- **Extract structured data from scanned docs** by enabling OCR on `Capture PDF`. Works on "Microsoft Print To PDF" output and scanner outputs.

## Activepieces setup

This piece depends on:

- `@thinkfleet/agentmark` (the core library)
- `playwright-core` for browser-based actions
- `pdfjs-dist` (optional, required for any PDF action)
- `pdf-lib` (optional, required for `Fill PDF Form`)
- `tesseract.js` + Poppler installed on the worker (optional, required for OCR)

Install Chromium binaries on the worker once:

```bash
npx playwright-core install chromium
```

Install Poppler on the worker (only if using OCR):

```bash
brew install poppler # macOS
apt-get install poppler-utils # Ubuntu/Debian
```

## License

MIT.
61 changes: 61 additions & 0 deletions pieces/agentmark/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "@thinkfleet/piece-agentmark",
"version": "0.1.0",
"description": "Activepieces piece — convert any web page or PDF into an AgentMark snapshot, then drive it from any flow.",
"type": "commonjs",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ThinkfleetAI/agentmark.git",
"directory": "pieces/agentmark"
},
"homepage": "https://agentmark.dev",
"keywords": [
"activepieces",
"piece",
"agentmark",
"ai",
"browser",
"pdf",
"ocr",
"form-fill"
],
"scripts": {
"build": "tsc -p tsconfig.lib.json && cp package.json dist/",
"test": "vitest run"
},
"files": [
"dist",
"README.md"
],
"dependencies": {
"@thinkfleet/agentmark": "file:../..",
"tslib": "^2.3.0",
"undici": "^7.0.0"
},
"_publishing_note": "@thinkfleet/agentmark is `file:../..` for in-monorepo development; bump to ^0.7.0 (or whatever version is on npm) before publishing this piece.",
"peerDependencies": {
"@activepieces/pieces-framework": ">=0.7.0",
"playwright-core": ">=1.40.0"
},
"peerDependenciesMeta": {
"@activepieces/pieces-framework": {
"optional": false
},
"playwright-core": {
"optional": false
}
},
"devDependencies": {
"@activepieces/pieces-framework": "*",
"@types/node": "20.19.9",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^4.10.38",
"playwright-core": "^1.40.0",
"tesseract.js": "^5.1.1",
"typescript": "^5.4.0",
"vitest": "3.0.8"
}
}
27 changes: 27 additions & 0 deletions pieces/agentmark/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @thinkfleet/piece-agentmark — Activepieces piece for AgentMark.
*
* Drop into any Activepieces flow to convert web pages or PDFs into
* AgentMark snapshots and fill PDF forms — all without leaving the flow
* builder. Wraps the core @thinkfleet/agentmark library.
*/

import { createPiece, PieceAuth } from '@activepieces/pieces-framework'
import { snapshotWebPage } from './lib/actions/snapshot-web-page'
import { snapshotPdf } from './lib/actions/snapshot-pdf'
import { fillPdfForm } from './lib/actions/fill-pdf-form'

export const agentmark = createPiece({
displayName: 'AgentMark',
description:
'Convert web pages and PDFs into compact AgentMark snapshots; fill '
+ 'AcroForm PDFs from flow data. Powered by @thinkfleet/agentmark.',
auth: PieceAuth.None(),
minimumSupportedRelease: '0.78.0',
logoUrl: 'https://agentmark.dev/logo.svg',
authors: ['thinkfleet'],
actions: [snapshotWebPage, snapshotPdf, fillPdfForm],
triggers: [],
})

export { snapshotWebPage, snapshotPdf, fillPdfForm }
105 changes: 105 additions & 0 deletions pieces/agentmark/src/lib/actions/fill-pdf-form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { createAction, Property } from '@activepieces/pieces-framework'
import { openPdfDocument } from '@thinkfleet/agentmark'
import { resolveBytes, bytesToBase64DataUri } from '../common'

export const fillPdfForm = createAction({
name: 'fill_pdf_form',
displayName: 'Fill PDF Form',
description:
'Fill an AcroForm PDF in one atomic step. Pass a values object keyed '
+ 'by AgentMark action ID OR by original field name; the action '
+ 'matches either. Returns the filled PDF as a base64 data URI.',
props: {
source: Property.LongText({
displayName: 'PDF Source',
description:
'HTTP(S) URL, file path, file:// URI, data: URI, or base64 string.',
required: true,
}),
values: Property.Json({
displayName: 'Field Values',
description:
'Object mapping field IDs (action IDs like `act_field_1`) or '
+ 'field names (e.g. `applicant.first_name`) to values. '
+ 'Strings for text/select/radio, booleans for checkboxes, '
+ 'arrays for multi-select.',
required: true,
defaultValue: {},
}),
flatten: Property.Checkbox({
displayName: 'Flatten',
description:
'Bake values into page content. Resulting PDF is no longer fillable.',
required: false,
defaultValue: false,
}),
return_format: Property.StaticDropdown({
displayName: 'Return Format',
description: 'How the filled PDF is returned in the action output.',
required: false,
defaultValue: 'data_uri',
options: {
disabled: false,
options: [
{ label: 'Base64 data URI', value: 'data_uri' },
{ label: 'Raw base64 (no scheme)', value: 'base64' },
],
},
}),
password: Property.ShortText({
displayName: 'Password',
description: 'Password for encrypted PDFs.',
required: false,
}),
},
async run(context) {
const { source, values, flatten, return_format, password } = context.propsValue
const data = await resolveBytes(source)
const doc = await openPdfDocument({
data,
sourceUrl: source.startsWith('http') ? source : 'inline:pdf',
password,
})

try {
const valuesMap = (values ?? {}) as Record<string, unknown>

// Build action-id-keyed dispatch map from BOTH action IDs and
// original field names. Caller can use whichever is convenient.
const actionIdByName = new Map<string, string>()
for (const [actionId, field] of doc.fields) {
actionIdByName.set(field.fieldName, actionId)
}

const summary: Array<{ key: string; resolved_action_id: string }> = []
const skipped: string[] = []

for (const [key, value] of Object.entries(valuesMap)) {
const resolved = doc.fields.has(key)
? key
: actionIdByName.get(key)
if (!resolved) {
skipped.push(key)
continue
}
await doc.execute(resolved, value)
summary.push({ key, resolved_action_id: resolved })
}

const filled = await doc.save({ flatten: flatten === true })
const out = return_format === 'base64'
? Buffer.from(filled).toString('base64')
: bytesToBase64DataUri(filled)

return {
filled_pdf: out,
bytes: filled.length,
fields_applied: summary,
fields_skipped: skipped,
flattened: flatten === true,
}
} finally {
await doc.close()
}
},
})
101 changes: 101 additions & 0 deletions pieces/agentmark/src/lib/actions/snapshot-pdf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { createAction, Property } from '@activepieces/pieces-framework'
import {
convertPdf,
PopplerRenderBackend,
TesseractOcrBackend,
} from '@thinkfleet/agentmark'
import { resolveBytes } from '../common'

export const snapshotPdf = createAction({
name: 'snapshot_pdf',
displayName: 'Capture PDF',
description:
'Convert a PDF (URL, file path, base64, or data URI) into a compact '
+ 'AgentMark snapshot. PDFs with form fields produce kind: \'form\'; '
+ 'plain documents produce kind: \'document\'. Optionally OCR pages '
+ 'with no extractable text using Tesseract + Poppler.',
props: {
source: Property.LongText({
displayName: 'Source',
description:
'HTTP(S) URL, file path, file:// URI, data:application/pdf;base64,... '
+ 'URI, or a bare base64 string.',
required: true,
}),
source_url: Property.ShortText({
displayName: 'Source URL (override)',
description:
'Optional URI to record as the snapshot\'s `url` field. Useful '
+ 'when the input is a data URI or in-memory base64 and you '
+ 'want a stable identifier for downstream steps.',
required: false,
}),
title: Property.ShortText({
displayName: 'Title (override)',
description: 'Override the document title. Leave blank to use the PDF metadata title.',
required: false,
}),
password: Property.ShortText({
displayName: 'Password',
description: 'Password for encrypted PDFs.',
required: false,
}),
enable_ocr: Property.Checkbox({
displayName: 'Enable OCR',
description:
'Run Tesseract OCR on pages with no extractable text. Required '
+ 'for scanned PDFs and "Microsoft Print To PDF" output. Slower; '
+ 'requires Poppler installed on the worker host (pdftoppm).',
required: false,
defaultValue: false,
}),
ocr_language: Property.ShortText({
displayName: 'OCR Language',
description: 'BCP-47 language hint. Default: eng.',
required: false,
defaultValue: 'eng',
}),
},
async run(context) {
const {
source,
source_url,
title,
password,
enable_ocr,
ocr_language,
} = context.propsValue

const data = await resolveBytes(source)
const sourceUrl = source_url
?? (source.startsWith('http') ? source : 'inline:pdf')

const ocrBackend = enable_ocr ? new TesseractOcrBackend({ language: ocr_language ?? 'eng' }) : undefined
try {
const { agentmark } = await convertPdf({
data,
sourceUrl,
title,
password,
ocr: enable_ocr
? {
render: new PopplerRenderBackend(),
ocr: ocrBackend!,
mode: 'auto',
dpi: 200,
language: ocr_language ?? 'eng',
}
: undefined,
})

return {
agentmark,
source_url: sourceUrl,
bytes: agentmark.length,
ocr_used: enable_ocr === true,
}
} finally {
await ocrBackend?.close().catch(() => {})
}
},
})
Loading