diff --git a/.editorconfig b/.editorconfig index 5e54457699..872fe6c2ca 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,7 +14,7 @@ indent_size = 4 max_line_length = 100 [*.py] -indent_size = 2 +indent_size = 4 [*.gradle] indent_size = 4 diff --git a/.github/workflows/ai-engine.yml b/.github/workflows/ai-engine.yml new file mode 100644 index 0000000000..ed0569c312 --- /dev/null +++ b/.github/workflows/ai-engine.yml @@ -0,0 +1,83 @@ +name: AI Engine CI + +on: + push: + branches: [main] + pull_request: + +jobs: + engine: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + defaults: + run: + working-directory: engine + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Install dependencies + run: make install + + - name: Run fixers + # Ignore errors here because we're going to add comments for them in the following steps before actually failing + run: make fix || true + + - name: Check for fixer changes + id: fixer_changes + run: | + if git diff --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Post fixer suggestions + if: steps.fixer_changes.outputs.changed == 'true' && github.event_name == 'pull_request' + uses: reviewdog/action-suggester@v1 + continue-on-error: true + with: + tool_name: engine-make-fix + github_token: ${{ secrets.GITHUB_TOKEN }} + filter_mode: file + fail_level: any + level: info + + - name: Comment on fixer suggestions + if: steps.fixer_changes.outputs.changed == 'true' && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: "The Python code in your PR has formatting/linting issues. Consider running `make fix` locally or setting up your editor's Ruff integration to auto-format and lint your files as you go, or commit the suggested changes on this PR.", + }); + + - name: Verify fixer changes are committed + if: steps.fixer_changes.outputs.changed == 'true' + run: | + if ! git diff --exit-code; then + echo "Fixes are out of date." + echo "Apply the reviewdog suggestions or run 'make fix' from engine/ and commit the updated files." + git --no-pager diff --stat + exit 1 + fi + + - name: Run linting + run: make lint + + - name: Run type checking + run: make typecheck + + - name: Run tests + run: make test diff --git a/AGENTS.md b/AGENTS.md index 99e563c963..d67040db64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,18 @@ This file provides guidance to AI Agents when working with code in this reposito ### Security Mode Development Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally. +### Python Development +Development for the AI engine happens in the `engine/` folder. It's built with Langchain and Pydantic and allows for the creation and editing of PDF documents. The frontend calls the Python via Java as a proxy. + +- Python version is 3.13; use modern Python features (type aliases, pattern matching, dataclasses, etc.) where they help clarity. +- Write fully type-correct code; keep pyright clean and avoid `Any` unless strictly necessary. +- JSON handling: deserialize into fully typed Pydantic models as early as possible, and serialize back from Pydantic models as late as possible. +- Use Makefile commands for Python work: + - From `engine/`: `make check` to lint, type-check, test, etc. and `make fix` to fix easily fixable linting & formatting issues. +- The project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed appropriately there, followed by running `make install`. +- Prefer using classes to nesting functions, and make other similar architectural decisions to improve testability. Do not nest classes or functions unless specifically required to for the code construct (like a decorator). +- All environment variables used within the code must begin with the `STIRLING_` prefix in order to keep them unique and easier to find. + ### Frontend Development - **Frontend dev server**: `cd frontend && npm run dev` (requires backend on localhost:8080) - **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS @@ -82,7 +94,7 @@ export function RightRailFooterExtensions(_props: RightRailFooterExtensionsProps } ``` -```typescript +```tsx // desktop/components/rightRail/RightRailFooterExtensions.tsx (real implementation) import { Box } from '@mantine/core'; import { BackendHealthIndicator } from '@app/components/BackendHealthIndicator'; @@ -100,7 +112,7 @@ export function RightRailFooterExtensions({ className }: RightRailFooterExtensio } ``` -```typescript +```tsx // core/components/shared/RightRail.tsx (usage - works in ALL builds) import { RightRailFooterExtensions } from '@app/components/rightRail/RightRailFooterExtensions'; diff --git a/LICENSE b/LICENSE index c572f474d7..e7a8034e41 100644 --- a/LICENSE +++ b/LICENSE @@ -6,6 +6,8 @@ Portions of this software are licensed as follows: * All content that resides under the "app/proprietary/" directory of this repository, if that directory exists, is licensed under the license defined in "app/proprietary/LICENSE". +* All content that resides under the "engine/" directory of this repository, +if that directory exists, is licensed under the license defined in "engine/LICENSE". * All content that resides under the "frontend/src/proprietary/" directory of this repository, if that directory exists, is licensed under the license defined in "frontend/src/proprietary/LICENSE". * All content that resides under the "frontend/src/desktop/" directory of this repository, diff --git a/engine/.gitignore b/engine/.gitignore new file mode 100644 index 0000000000..1116f48815 --- /dev/null +++ b/engine/.gitignore @@ -0,0 +1,46 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +build/ +dist/ +*.egg-info/ + +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.vite/ + +# Environment +.env +.env.local + +# LaTeX outputs +*.aux +*.log +*.out +*.toc +*.pdf +*.tex +!src/default_templates/*.tex +data/ +output/ +logs/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/engine/.python-version b/engine/.python-version new file mode 100644 index 0000000000..86105ead5f --- /dev/null +++ b/engine/.python-version @@ -0,0 +1 @@ +3.13.8 diff --git a/engine/Dockerfile b/engine/Dockerfile new file mode 100644 index 0000000000..e419c380c3 --- /dev/null +++ b/engine/Dockerfile @@ -0,0 +1,63 @@ +# syntax=docker/dockerfile:1.5 +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim + +# Install system deps: Node.js + Puppeteer/Chromium runtime libraries +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + poppler-utils \ + nodejs \ + npm \ + # Fonts for correct text rendering in generated PDFs + fonts-liberation \ + fonts-dejavu-core \ + # Chromium headless runtime deps + libnss3 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxrandr2 \ + libpango-1.0-0 \ + libcairo2 \ + libasound2 \ + libx11-6 \ + libxext6 \ + libxfixes3 \ + libgbm1 \ + libdrm2 \ + libxshmfence1 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install Node dependencies (Puppeteer) first for layer caching +COPY package.json package-lock.json ./ +RUN npm ci + +# Install Python dependencies +COPY pyproject.toml uv.lock ./ +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --no-dev + +# Copy Python source into /app/src/ +COPY src/ ./src/ + +# Create output directories +RUN mkdir -p /app/output /app/data + +# Expose port +EXPOSE 5001 + +# Environment variables for Gunicorn configuration +ENV GUNICORN_WORKERS=4 +ENV GUNICORN_TIMEOUT=120 +# Disable Python stdout/stderr buffering so log output appears immediately in Docker logs +ENV PYTHONUNBUFFERED=1 + +ENV PATH="/app/.venv/bin:$PATH" +WORKDIR /app/src +CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:5001 --workers ${GUNICORN_WORKERS} --timeout ${GUNICORN_TIMEOUT} --worker-class gthread --threads 4 app:app"] diff --git a/engine/Dockerfile.dev b/engine/Dockerfile.dev new file mode 100644 index 0000000000..2c3649d233 --- /dev/null +++ b/engine/Dockerfile.dev @@ -0,0 +1,59 @@ +# syntax=docker/dockerfile:1.5 +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim + +# Install system deps: Node.js + Puppeteer/Chromium runtime libraries +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + poppler-utils \ + nodejs \ + npm \ + # Fonts for correct text rendering in generated PDFs + fonts-liberation \ + fonts-dejavu-core \ + # Chromium headless runtime deps + libnss3 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxrandr2 \ + libpango-1.0-0 \ + libcairo2 \ + libasound2 \ + libx11-6 \ + libxext6 \ + libxfixes3 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install Node dependencies (Puppeteer) first for layer caching +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy only pyproject.toml and uv.lock first for dependency caching +COPY pyproject.toml uv.lock ./ + +# Install dependencies including dev extras +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen + +# Create output directories +RUN mkdir -p /app/output /app/data + +# Expose port +EXPOSE 5001 + +# Set environment for Flask development +ENV FLASK_ENV=development +ENV FLASK_DEBUG=1 +ENV PYTHONUNBUFFERED=1 + +# Run the Flask app with hot reload +# Note: src/ will be mounted as a volume at runtime +WORKDIR /app/src +CMD ["uv", "run", "python", "app.py"] diff --git a/engine/LICENSE b/engine/LICENSE new file mode 100644 index 0000000000..d268556808 --- /dev/null +++ b/engine/LICENSE @@ -0,0 +1,51 @@ +Stirling PDF User License + +Copyright (c) 2025 Stirling PDF Inc. + +License Scope & Usage Rights + +Production use of the Stirling PDF Software is only permitted with a valid Stirling PDF User License. + +For purposes of this license, “the Software” refers to the Stirling PDF application and any associated documentation files +provided by Stirling PDF Inc. You or your organization may not use the Software in production, at scale, or for business-critical +processes unless you have agreed to, and remain in compliance with, the Stirling PDF Subscription Terms of Service +(https://www.stirlingpdf.com/terms) or another valid agreement with Stirling PDF, and hold an active User License subscription +covering the appropriate number of licensed users. + +Trial and Minimal Use + +You may use the Software without a paid subscription for the sole purposes of internal trial, evaluation, or minimal use, provided that: +* Use is limited to the capabilities and restrictions defined by the Software itself; +* You do not copy, distribute, sublicense, reverse-engineer, or use the Software in client-facing or commercial contexts. + +Continued use beyond this scope requires a valid Stirling PDF User License. + +Modifications and Derivative Works + +You may modify the Software only for development or internal testing purposes. Any such modifications or derivative works: + +* May not be deployed in production environments without a valid User License; +* May not be distributed or sublicensed; +* Remain the intellectual property of Stirling PDF and/or its licensors; +* May only be used, copied, or exploited in accordance with the terms of a valid Stirling PDF User License subscription. + +Prohibited Actions + +Unless explicitly permitted by a paid license or separate agreement, you may not: + +* Use the Software in production environments; +* Copy, merge, distribute, sublicense, or sell the Software; +* Remove or alter any licensing or copyright notices; +* Circumvent access restrictions or licensing requirements. + +Third-Party Components + +The Stirling PDF Software may include components subject to separate open source licenses. Such components remain governed by +their original license terms as provided by their respective owners. + +Disclaimer + +THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/engine/Makefile b/engine/Makefile new file mode 100644 index 0000000000..64f91131b9 --- /dev/null +++ b/engine/Makefile @@ -0,0 +1,87 @@ +.PHONY: help install prep check fix lint lint-fix lint-fix-unsafe format format-check typecheck test run run-dev clean tool-models docker-build docker-run + +REPO_ROOT_DIR = .. +ROOT_DIR = . +VENV_DIR = $(ROOT_DIR)/.venv +DEPS_STAMP := $(VENV_DIR)/.deps-installed +NODE_DEPS_STAMP := node_modules/.deps-installed +TOOL_MODELS := $(CURDIR)/src/models/tool_models.py +FRONTEND_DIR := $(REPO_ROOT_DIR)/frontend +FRONTEND_TSX := $(FRONTEND_DIR)/node_modules/.bin/tsx + +$(DEPS_STAMP): $(ROOT_DIR)/uv.lock $(ROOT_DIR)/pyproject.toml + uv python install 3.13.8 + uv sync + touch $(DEPS_STAMP) + +$(NODE_DEPS_STAMP): package-lock.json + npm ci + touch $(NODE_DEPS_STAMP) + +help: + @echo "Engine commands:" + @echo " make install - Install production dependencies" + @echo " make prep - Set up .env file from .env.example" + @echo " make lint - Run linting checks" + @echo " make format - Format code" + @echo " make format-check - Show whether code is correctly formatted" + @echo " make typecheck - Run type checking" + @echo " make test - Run tests" + @echo " make run - Run the Flask backend with gunicorn (production)" + @echo " make run-dev - Run the Flask backend with dev server (development only)" + @echo " make tool-models - Generate src/models/tool_models_*.py from frontend TypeScript tool defs" + @echo " make clean - Clean up generated files" + @echo " make docker-build - Build Docker image" + @echo " make docker-run - Run Docker container" + +install: $(DEPS_STAMP) $(NODE_DEPS_STAMP) + +prep: install + uv run scripts/setup_env.py + +check: typecheck lint format-check test + +fix: lint-fix format + +lint: install + uv run ruff check . + +lint-fix: install + uv run ruff check . --fix + +lint-fix-unsafe: install + uv run ruff check . --fix --unsafe-fixes + +format: install + uv run ruff format . + +format-check: install + uv run ruff format . --diff + +typecheck: install + uv run pyright . --warnings + +test: prep + uv run pytest tests + +run: prep + cd src && PYTHONUNBUFFERED=1 uv run gunicorn --bind 0.0.0.0:5001 --workers 4 --timeout 120 --worker-class gthread --threads 4 app:app + +run-dev: prep + cd src && PYTHONUNBUFFERED=1 uv run python app.py + +frontend-deps: + if [ ! -x "$(FRONTEND_TSX)" ]; then cd $(FRONTEND_DIR) && npm install; fi + +tool-models: install frontend-deps + uv run python scripts/generate_tool_models.py --output $(TOOL_MODELS) + $(MAKE) fix + +clean: + rm -rf $(VENV_DIR) data logs output + +docker-build: + docker build -t stirling-pdf-engine . + +docker-run: + docker run -p 5001:5001 stirling-pdf-engine diff --git a/engine/config/.env.example b/engine/config/.env.example new file mode 100644 index 0000000000..c898254e66 --- /dev/null +++ b/engine/config/.env.example @@ -0,0 +1,51 @@ +# Claude API (Anthropic) +STIRLING_ANTHROPIC_API_KEY= + +# OpenAI API (Optional - for GPT models) +STIRLING_OPENAI_API_KEY= +STIRLING_OPENAI_BASE_URL= + +# Model Configuration +STIRLING_SMART_MODEL=claude-sonnet-4-5-20250929 +STIRLING_FAST_MODEL=claude-haiku-4-5-20251001 + +# GPT-5 Reasoning Settings (only used if SMART_MODEL is gpt-5) +# Options: minimal, low, medium, high, xhigh +STIRLING_SMART_MODEL_REASONING_EFFORT=medium +STIRLING_FAST_MODEL_REASONING_EFFORT=minimal + +# GPT-5 Text Verbosity (only used if SMART_MODEL is gpt-5) +# Options: minimal, low, medium, high +STIRLING_SMART_MODEL_TEXT_VERBOSITY=medium +STIRLING_FAST_MODEL_TEXT_VERBOSITY=low + +# Output token limits. STIRLING_AI_MAX_TOKENS is a global override (leave empty to use per-model defaults) +STIRLING_AI_MAX_TOKENS= +STIRLING_SMART_MODEL_MAX_TOKENS=8192 +STIRLING_FAST_MODEL_MAX_TOKENS=2048 +STIRLING_CLAUDE_MAX_TOKENS=4096 +STIRLING_DEFAULT_MODEL_MAX_TOKENS=4096 + +# PostHog Analytics +STIRLING_POSTHOG_API_KEY= +STIRLING_POSTHOG_HOST=https://eu.i.posthog.com + +# Backend Configuration +STIRLING_JAVA_BACKEND_URL=http://localhost:8080 +STIRLING_JAVA_BACKEND_API_KEY=123456789 +STIRLING_JAVA_REQUEST_TIMEOUT_SECONDS=30 + +# Logging and Debugging +STIRLING_AI_RAW_DEBUG=0 +STIRLING_FLASK_DEBUG=0 +# Override the log file directory (defaults to ./logs in server mode, OS log dir in desktop mode) +STIRLING_LOG_PATH= +# Enable verbose table detection debug logging in the PDF text editor +STIRLING_PDF_EDITOR_TABLE_DEBUG=0 +# Set to true when running as a Tauri desktop app (switches to OS-native log directory) +STIRLING_PDF_TAURI_MODE=false + +# AI Settings +STIRLING_AI_STREAMING=true +STIRLING_AI_PREVIEW_MAX_INFLIGHT=3 +STIRLING_AI_REQUEST_TIMEOUT=70 diff --git a/engine/package-lock.json b/engine/package-lock.json new file mode 100644 index 0000000000..8decf3bba9 --- /dev/null +++ b/engine/package-lock.json @@ -0,0 +1,1138 @@ +{ + "name": "stirling-docgen", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "stirling-docgen", + "version": "1.0.0", + "hasInstallScript": true, + "dependencies": { + "puppeteer": "^24.25.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.5.tgz", + "integrity": "sha512-iEsKNwDh1wiWTps1/hdkNdmBgDlDVZP5U57ZVOlt+dNFqpc/lpPouCIxZw+DYBgc4P9NDfIZMPNR4CHNhzwLIA==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz", + "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz", + "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1566079", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", + "integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==", + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "24.37.4", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.37.4.tgz", + "integrity": "sha512-SMSq+FL3gnglolhrIks3maRkrdQEjoDCesy6FXziMPWsF1DxoX+GVxRa82y+euzkzS52/UujM/BoaFPQ+AnPXQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1566079", + "puppeteer-core": "24.37.4", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.37.4", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.37.4.tgz", + "integrity": "sha512-sQYtYgaNaLYO82k2FHmr7bR1tCmo2fBupEI7Kd0WpBlMropNcfxSTLOJXVRkhiHig0dUiMI7g0yq+HJI1IDCzg==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1566079", + "typed-query-selector": "^2.12.0", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT", + "optional": true + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/engine/package.json b/engine/package.json new file mode 100644 index 0000000000..04976d5df9 --- /dev/null +++ b/engine/package.json @@ -0,0 +1,13 @@ +{ + "name": "stirling-docgen", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Node tooling for Stirling PDF docgen backend (HTML→PDF via Puppeteer)", + "scripts": { + "postinstall": "node -e \"require('puppeteer')\" 2>/dev/null || true" + }, + "dependencies": { + "puppeteer": "^24.25.0" + } +} diff --git a/engine/pyproject.toml b/engine/pyproject.toml new file mode 100644 index 0000000000..7688b67522 --- /dev/null +++ b/engine/pyproject.toml @@ -0,0 +1,69 @@ +[project] +name = "engine" +version = "0.1.0" +description = "AI Document Engine" +requires-python = ">=3.13" +dependencies = [ + "flask>=3.0.0", + "flask-cors>=4.0.0", + "gunicorn>=25.0.3", + "openai>=2.20.0", + "langchain-core>=1.2.11", + "langchain-openai>=1.1.9", + "langchain-anthropic>=1.1.9", + "platformdirs>=4.5.1", + "pypdf>=6.7.0", + "pydantic>=2.0.0", + "posthog>=7.8.6", + "python-dotenv>=1.2.1", + "beautifulsoup4>=4.12.0", +] + +[dependency-groups] +dev = [ + "pytest>=8.0.0", + "pytest-cov>=4.1.0", + "ruff>=0.14.10", + "pyright>=1.1.408", + "datamodel-code-generator[ruff]>=0.27.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] +exclude = [ + "tests", +] + +[tool.ruff] +line-length = 120 +target-version = "py313" + +[tool.ruff.lint] +select = [ + "E", + "F", + "I", + "N", + "W", + "RUF100", + "UP", +] +ignore = [ + "E501", # Temporarily disable line length limit until codebase conformat +] + +[tool.pyright] +pythonVersion = "3.13" + +reportImportCycles = "warning" +reportUnnecessaryCast = "warning" +reportUnusedImport = "warning" +#reportUnknownParameterType = "warning" + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] diff --git a/engine/scripts/generate_tool_models.py b/engine/scripts/generate_tool_models.py new file mode 100644 index 0000000000..abefeb5307 --- /dev/null +++ b/engine/scripts/generate_tool_models.py @@ -0,0 +1,510 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import keyword +import re +import subprocess +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +TOOL_MODELS_HEADER = """# AUTO-GENERATED FILE. DO NOT EDIT. +# Generated by scripts/generate_tool_models.py from frontend TypeScript sources. +# ruff: noqa: N815 +""" + + +OPERATION_TYPE_RE = re.compile(r"operationType\s*:\s*['\"]([A-Za-z0-9_]+)['\"]") +DEFAULT_REF_RE = re.compile(r"defaultParameters\s*:\s*([A-Za-z0-9_]+)") +DEFAULT_SHORTHAND_RE = re.compile(r"\bdefaultParameters\b") +IMPORT_RE = re.compile(r"import\s*\{([^}]+)\}\s*from\s*['\"]([^'\"]+)['\"]") +VAR_OBJ_RE_TEMPLATE = r"(?:export\s+)?const\s+{name}\b[^=]*=\s*\{{" + + +@dataclass +class ToolModelSpec: + tool_id: str + params: dict[str, Any] + param_types: dict[str, Any] + + +class ParseError(Exception): + pass + + +def _find_matching(text: str, start: int, open_char: str, close_char: str) -> int: + depth = 0 + i = start + in_str: str | None = None + while i < len(text): + ch = text[i] + if in_str: + if ch == "\\": + i += 2 + continue + if ch == in_str: + in_str = None + i += 1 + continue + if ch in {"'", '"'}: + in_str = ch + elif ch == open_char: + depth += 1 + elif ch == close_char: + depth -= 1 + if depth == 0: + return i + i += 1 + raise ParseError(f"Unmatched {open_char}{close_char} block") + + +def _extract_block(text: str, pattern: str) -> str | None: + match = re.search(pattern, text) + if not match: + return None + brace_start = text.find("{", match.end() - 1) + if brace_start == -1: + return None + brace_end = _find_matching(text, brace_start, "{", "}") + return text[brace_start : brace_end + 1] + + +def _split_top_level_items(obj_body: str) -> list[str]: + items: list[str] = [] + depth_obj = depth_arr = 0 + in_str: str | None = None + token_start = 0 + i = 0 + while i < len(obj_body): + ch = obj_body[i] + if in_str: + if ch == "\\": + i += 2 + continue + if ch == in_str: + in_str = None + i += 1 + continue + if ch in {"'", '"'}: + in_str = ch + elif ch == "{": + depth_obj += 1 + elif ch == "}": + depth_obj -= 1 + elif ch == "[": + depth_arr += 1 + elif ch == "]": + depth_arr -= 1 + elif ch == "," and depth_obj == 0 and depth_arr == 0: + piece = obj_body[token_start:i].strip() + if piece: + items.append(piece) + token_start = i + 1 + i += 1 + tail = obj_body[token_start:].strip() + if tail: + items.append(tail) + return items + + +def _resolve_import_path(repo_root: Path, current_file: Path, module_path: str) -> Path | None: + candidates: list[Path] = [] + if module_path.startswith("@app/"): + rel = module_path[len("@app/") :] + candidates.extend( + [ + repo_root / "frontend/src/core" / f"{rel}.ts", + repo_root / "frontend/src/core" / f"{rel}.tsx", + repo_root / "frontend/src/saas" / f"{rel}.ts", + repo_root / "frontend/src/saas" / f"{rel}.tsx", + repo_root / "frontend/src" / f"{rel}.ts", + repo_root / "frontend/src" / f"{rel}.tsx", + ] + ) + elif module_path.startswith("."): + base = (current_file.parent / module_path).resolve() + candidates.extend([Path(f"{base}.ts"), Path(f"{base}.tsx")]) + for candidate in candidates: + if candidate.exists(): + return candidate + return None + + +def _parse_literal_value(value: str, resolver: Callable[[str], dict[str, Any] | None]) -> Any: + value = value.strip() + if not value: + return None + if value.startswith("{") and value.endswith("}"): + return _parse_object_literal(value, resolver) + if value.startswith("[") and value.endswith("]"): + inner = value[1:-1].strip() + if not inner: + return [] + return [_parse_literal_value(item, resolver) for item in _split_top_level_items(inner)] + if value.startswith(("'", '"')) and value.endswith(("'", '"')): + return value[1:-1] + if value in {"true", "false"}: + return value == "true" + if value == "null": + return None + if re.fullmatch(r"-?\d+", value): + return int(value) + if re.fullmatch(r"-?\d+\.\d+", value): + return float(value) + resolved = resolver(value) + if resolved is not None: + return resolved + return None + + +def _parse_object_literal(obj_text: str, resolver: Callable[[str], dict[str, Any] | None]) -> dict[str, Any]: + body = obj_text.strip()[1:-1] + result: dict[str, Any] = {} + for item in _split_top_level_items(body): + if item.startswith("..."): + spread_name = item[3:].strip() + spread = resolver(spread_name) + if isinstance(spread, dict): + result.update(spread) + continue + if ":" not in item: + continue + key, raw_value = item.split(":", 1) + key = key.strip().strip("'\"") + result[key] = _parse_literal_value(raw_value.strip(), resolver) + return result + + +def _extract_imports(source: str) -> dict[str, str]: + imports: dict[str, str] = {} + for names, module_path in IMPORT_RE.findall(source): + for part in names.split(","): + segment = part.strip() + if not segment: + continue + if " as " in segment: + original, alias = [x.strip() for x in segment.split(" as ", 1)] + imports[alias] = module_path + imports[original] = module_path + else: + imports[segment] = module_path + return imports + + +def _resolve_object_identifier(repo_root: Path, file_path: Path, source: str, identifier: str) -> dict[str, Any] | None: + var_pattern = VAR_OBJ_RE_TEMPLATE.format(name=re.escape(identifier)) + block = _extract_block(source, var_pattern) + imports = _extract_imports(source) + + def resolver(name: str) -> dict[str, Any] | None: + local_block = _extract_block(source, VAR_OBJ_RE_TEMPLATE.format(name=re.escape(name))) + if local_block: + return _parse_object_literal(local_block, resolver) + import_path = imports.get(name) + if not import_path: + return None + resolved_file = _resolve_import_path(repo_root, file_path, import_path) + if not resolved_file: + return None + imported_source = resolved_file.read_text(encoding="utf-8") + return _resolve_object_identifier(repo_root, resolved_file, imported_source, name) + + if block: + return _parse_object_literal(block, resolver) + import_path = imports.get(identifier) + if not import_path: + return None + resolved_file = _resolve_import_path(repo_root, file_path, import_path) + if not resolved_file: + return None + imported_source = resolved_file.read_text(encoding="utf-8") + return _resolve_object_identifier(repo_root, resolved_file, imported_source, identifier) + + +def _infer_py_type(value: Any) -> str: + if isinstance(value, bool): + return "bool" + if isinstance(value, int): + return "int" + if isinstance(value, float): + return "float" + if isinstance(value, str): + return "str" + if isinstance(value, list): + return "list[Any]" + if isinstance(value, dict): + return "dict[str, Any]" + return "Any" + + +def _spec_is_none(spec: dict[str, Any]) -> bool: + return spec.get("kind") == "null" + + +def _py_type_from_spec(spec: dict[str, Any]) -> str: + kind = spec.get("kind") + if kind == "string": + return "str" + if kind == "number": + return "float" + if kind == "boolean": + return "bool" + if kind == "date": + return "str" + if kind == "enum": + values = spec.get("values") + if isinstance(values, list) and values: + literal_values = ", ".join(_py_repr(v) for v in values) + return f"Literal[{literal_values}]" + if kind == "ref": + ref_name = spec.get("name") + if isinstance(ref_name, str) and ref_name.endswith("Parameters"): + return f"{ref_name[:-10]}Params" + if kind == "array": + element = spec.get("element") + inner = _py_type_from_spec(element) if isinstance(element, dict) else "Any" + return f"list[{inner}]" + if kind == "object": + dict_value = spec.get("dictValue") + if isinstance(dict_value, dict): + inner = _py_type_from_spec(dict_value) + return f"dict[str, {inner}]" + properties = spec.get("properties") + if isinstance(properties, dict) and properties: + property_types = {_py_type_from_spec(p) for p in properties.values() if isinstance(p, dict)} + if len(property_types) == 1: + inner = next(iter(property_types)) + return f"dict[str, {inner}]" + return "dict[str, Any]" + if kind in {"null"}: + return "Any" + return "Any" + + +def _to_class_name(tool_id: str) -> str: + cleaned = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", tool_id) + cleaned = re.sub(r"[^A-Za-z0-9]+", " ", cleaned) + parts = [part.capitalize() for part in cleaned.split() if part] + return "".join(parts) + "Params" + + +def _to_snake_case(name: str) -> str: + snake = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name) + snake = re.sub(r"[^A-Za-z0-9]+", "_", snake).strip("_").lower() + if not snake: + snake = "param" + if snake[0].isdigit(): + snake = f"param_{snake}" + if keyword.iskeyword(snake): + snake = f"{snake}_" + return snake + + +def _build_field_name_map(params: dict[str, Any]) -> dict[str, str]: + field_map: dict[str, str] = {} + used: set[str] = set() + for original_key in sorted(params): + base_name = _to_snake_case(original_key) + candidate = base_name + suffix = 2 + while candidate in used: + candidate = f"{base_name}_{suffix}" + suffix += 1 + used.add(candidate) + field_map[original_key] = candidate + return field_map + + +def _to_enum_member_name(tool_id: str) -> str: + return _to_snake_case(tool_id).upper() + + +def _build_enum_member_map(specs: list[ToolModelSpec]) -> dict[str, str]: + member_map: dict[str, str] = {} + used: set[str] = set() + for spec in specs: + base_name = _to_enum_member_name(spec.tool_id) + candidate = base_name + suffix = 2 + while candidate in used: + candidate = f"{base_name}_{suffix}" + suffix += 1 + used.add(candidate) + member_map[spec.tool_id] = candidate + return member_map + + +def _py_repr(value: Any) -> str: + return ( + json.dumps(value, ensure_ascii=True).replace("true", "True").replace("false", "False").replace("null", "None") + ) + + +def discover_tool_specs(repo_root: Path) -> list[ToolModelSpec]: + frontend_dir = repo_root / "frontend" + extractor = frontend_dir / "scripts/export-tool-specs.ts" + command = ["node", "--import", "tsx", str(extractor)] + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + cwd=str(frontend_dir), + ) + raw = json.loads(result.stdout) + specs: list[ToolModelSpec] = [] + for item in raw: + tool_id = item.get("tool_id") + if not isinstance(tool_id, str) or not tool_id: + continue + params = item.get("params") + param_types = item.get("param_types") + specs.append( + ToolModelSpec( + tool_id=tool_id, + params=params if isinstance(params, dict) else {}, + param_types=param_types if isinstance(param_types, dict) else {}, + ) + ) + return sorted(specs, key=lambda spec: spec.tool_id) + + +def write_models_module(out_path: Path, specs: list[ToolModelSpec]) -> None: + lines: list[str] = [ + TOOL_MODELS_HEADER, + "from __future__ import annotations\n\n", + "from enum import StrEnum\n", + "from typing import Any, Literal\n\n", + "from models.base import ApiModel\n", + ] + + class_names: dict[str, str] = {spec.tool_id: _to_class_name(spec.tool_id) for spec in specs} + class_name_to_tool_id = {name: tool_id for tool_id, name in class_names.items()} + + def extract_class_dependencies(spec: ToolModelSpec) -> set[str]: + deps: set[str] = set() + if not isinstance(spec.param_types, dict): + return deps + for entry in spec.param_types.values(): + if not isinstance(entry, dict): + continue + type_spec = entry + if "type" in entry and isinstance(entry.get("type"), dict): + type_spec = entry["type"] + if not isinstance(type_spec, dict): + continue + if type_spec.get("kind") != "ref": + continue + ref_name = type_spec.get("name") + if isinstance(ref_name, str) and ref_name.endswith("Parameters"): + ref_class = f"{ref_name[:-10]}Params" + if ref_class in class_name_to_tool_id: + deps.add(ref_class) + return deps + + dependencies_by_class: dict[str, set[str]] = {} + for spec in specs: + class_name = class_names[spec.tool_id] + dependencies_by_class[class_name] = extract_class_dependencies(spec) + + remaining = set(class_names.values()) + ordered_class_names: list[str] = [] + while remaining: + progress = False + for class_name in sorted(remaining): + deps = dependencies_by_class.get(class_name, set()) + if deps.issubset(set(ordered_class_names)): + ordered_class_names.append(class_name) + remaining.remove(class_name) + progress = True + break + if not progress: + ordered_class_names.extend(sorted(remaining)) + break + + ordered_specs = [next(spec for spec in specs if class_names[spec.tool_id] == name) for name in ordered_class_names] + + for spec in ordered_specs: + class_name = class_names[spec.tool_id] + lines.append(f"class {class_name}(ApiModel):\n") + all_param_keys = set(spec.params) + if isinstance(spec.param_types, dict): + all_param_keys.update(spec.param_types.keys()) + + if not all_param_keys: + lines.append(" pass\n\n\n") + continue + + field_name_map = _build_field_name_map({key: True for key in all_param_keys}) + for key in sorted(all_param_keys): + field_name = field_name_map[key] + value = spec.params.get(key) + type_spec = spec.param_types.get(key) if isinstance(spec.param_types, dict) else None + if isinstance(type_spec, dict): + py_type = _py_type_from_spec(type_spec) + else: + py_type = _infer_py_type(value) + + if value is None and (isinstance(type_spec, dict) and _spec_is_none(type_spec)): + if py_type != "Any" and "| None" not in py_type: + py_type = f"{py_type} | None" + lines.append(f" {field_name}: {py_type} = None\n") + elif value is None: + lines.append(f" {field_name}: {py_type} | None = None\n") + else: + if isinstance(type_spec, dict) and type_spec.get("kind") == "ref" and isinstance(value, dict): + lines.append(f" {field_name}: {py_type} = {py_type}.model_validate({_py_repr(value)})\n") + continue + lines.append(f" {field_name}: {py_type} = {_py_repr(value)}\n") + lines.append("\n\n") + + if class_names: + union_members = " | ".join(class_names[tool_id] for tool_id in sorted(class_names)) + lines.append(f"type ParamToolModel = {union_members}\n") + lines.append("type ParamToolModelType = type[ParamToolModel]\n\n") + else: + lines.append("type ParamToolModel = ApiModel\n") + lines.append("type ParamToolModelType = type[ParamToolModel]\n\n") + + enum_member_map = _build_enum_member_map(specs) + + lines.append("class OperationId(StrEnum):\n") + + for spec in specs: + lines.append(f" {enum_member_map[spec.tool_id]} = {spec.tool_id!r}\n") + + lines.extend( + [ + "\n\n", + "OPERATIONS: dict[OperationId, ParamToolModelType] = {\n", + ] + ) + + for spec in specs: + model_name = _to_class_name(spec.tool_id) + lines.append(f" OperationId.{enum_member_map[spec.tool_id]}: {model_name},\n") + lines.append("}\n") + out_path.write_text("".join(lines), encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate tool models from frontend TypeScript tool definitions") + parser.add_argument("--spec", help="Deprecated (ignored)", default="") + parser.add_argument("--output", default="", help="Path to tool_models.py") + parser.add_argument("--ai-output", default="", help="Deprecated (ignored)") + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[3] + specs = discover_tool_specs(repo_root) + + output_path = Path(args.output) if args.output else (repo_root / "docgen/backend/models/tool_models.py") + + write_models_module(output_path, specs) + print(f"Wrote {len(specs)} tool model specs") + + +if __name__ == "__main__": + main() diff --git a/engine/scripts/setup_env.py b/engine/scripts/setup_env.py new file mode 100644 index 0000000000..9626e2c8cf --- /dev/null +++ b/engine/scripts/setup_env.py @@ -0,0 +1,48 @@ +""" +Copies .env from .env.example if missing, and errors if any keys from the example +are absent from the actual .env file. + +Usage: + uv run scripts/setup_env.py +""" + +import os +import shutil +import sys +from pathlib import Path + +from dotenv import dotenv_values + +ROOT = Path(__file__).parent.parent +EXAMPLE_FILE = ROOT / "config" / ".env.example" +ENV_FILE = ROOT / ".env" + +print("setup-env: see engine/config/.env.example for documentation") + +if not EXAMPLE_FILE.exists(): + print(f"setup-env: {EXAMPLE_FILE.name} not found, skipping", file=sys.stderr) + sys.exit(0) + +if not ENV_FILE.exists(): + shutil.copy(EXAMPLE_FILE, ENV_FILE) + print("setup-env: created .env from .env.example") + +env_keys = set(dotenv_values(ENV_FILE).keys()) | set(os.environ.keys()) +example_keys = set(dotenv_values(EXAMPLE_FILE).keys()) +missing = sorted(example_keys - env_keys) + +if missing: + sys.exit( + "setup-env: .env is missing keys from .env.example:\n" + + "\n".join(f" {k}" for k in missing) + + "\n Add them manually or delete your local .env to re-copy from config/.env.example." + ) + +extra = sorted(k for k in dotenv_values(ENV_FILE) if k.startswith("STIRLING_") and k not in example_keys) +if extra: + print( + "setup-env: .env contains STIRLING_ keys not in config/.env.example:\n" + + "\n".join(f" {k}" for k in extra) + + "\n Add them to config/.env.example if they are intentional.", + file=sys.stderr, + ) diff --git a/engine/src/ai_generation.py b/engine/src/ai_generation.py new file mode 100644 index 0000000000..7a59234049 --- /dev/null +++ b/engine/src/ai_generation.py @@ -0,0 +1,363 @@ +from __future__ import annotations + +import json +import logging +from typing import Any + +import models +from config import SMART_MODEL, STREAMING_ENABLED +from format_prompts import get_format_prompt +from html_utils import inject_theme +from llm_utils import run_ai, stream_ai +from prompts import ( + field_values_system_prompt, + html_context_messages, + html_edit_system_prompt, + html_system_prompt, + outline_generator_system_prompt, + section_draft_system_prompt, + template_fill_html_system_prompt, +) + +logger = logging.getLogger(__name__) + + +def generate_outline_with_llm( + prompt: str, + document_type: str, + constraints: models.Constraint | None = None, +) -> models.OutlineResponse: + """ + Generate outline for a specific document type. + + document_type should already be detected - this function only generates the outline. + """ + constraint_text = "" + if constraints: + tone = constraints.tone + audience = constraints.audience + pages = constraints.page_count + constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}." + + # Try to get a format-specific extraction prompt + format_prompt, default_sections = get_format_prompt(document_type) + system_prompt = outline_generator_system_prompt(document_type, constraint_text, format_prompt, default_sections) + + messages: list[models.ChatMessage] = [ + models.ChatMessage(role="system", content=system_prompt), + models.ChatMessage(role="user", content=prompt), + ] + + parsed = run_ai( + SMART_MODEL, + messages, + models.OutlineResponse, + tag="outline", + ) + + if parsed: + logger.info( + "[OUTLINE] Generated outline doc_type=%s sections=%d filename=%s", + parsed.doc_type, + len(parsed.sections), + parsed.outline_filename, + ) + return parsed + + raise RuntimeError("AI outline generation failed.") + + +def generate_field_values( + prompt: str, + document_type: str, + fields: list[dict[str, Any]], + constraints: models.Constraint | None = None, +) -> list[dict[str, str]]: + constraint_text = "" + if constraints: + tone = constraints.tone + audience = constraints.audience + pages = constraints.page_count + constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}." + system_prompt = field_values_system_prompt(constraint_text) + messages: list[models.ChatMessage] = [ + models.ChatMessage(role="system", content=system_prompt), + models.ChatMessage(role="user", content=f"Document type: {document_type}"), + models.ChatMessage(role="user", content=f"Prompt:\n{prompt}"), + models.ChatMessage(role="user", content=f"Fields:\n{json.dumps(fields, ensure_ascii=True)}"), + ] + parsed = run_ai( + SMART_MODEL, + messages, + models.LLMFieldValuesResponse, + tag="field_values", + ) + if parsed: + return [{"label": item.label, "value": item.value} for item in parsed.fields] + + raise RuntimeError("AI field extraction failed.") + + +def generate_section_draft( + prompt: str, + document_type: str, + outline_text: str, + constraints: models.Constraint | None = None, +) -> list[models.DraftSection]: + constraint_text = "" + if constraints: + tone = constraints.tone + audience = constraints.audience + pages = constraints.page_count + constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}." + system_prompt = section_draft_system_prompt(constraint_text) + messages: list[models.ChatMessage] = [ + models.ChatMessage(role="system", content=system_prompt), + models.ChatMessage(role="user", content=f"Document type: {document_type}"), + models.ChatMessage(role="user", content=f"Outline:\n{outline_text}"), + models.ChatMessage(role="user", content=f"Prompt:\n{prompt}"), + ] + parsed = run_ai( + SMART_MODEL, + messages, + models.LLMDraftSectionsResponse, + tag="section_draft", + ) + return parsed.sections + + +# ── HTML generation ─────────────────────────────────────────────────────────── + + +def generate_template_fill_html_stream( + template_html: str, + document_type: str, + outline_text: str, + draft_sections: list[models.DraftSection] | None = None, + constraints: models.Constraint | None = None, + theme: dict[str, str] | None = None, + additional_instructions: str | None = None, + has_logo: bool = False, +): + """Fill an HTML template by replacing {{PLACEHOLDER}} tokens, streaming chunks.""" + constraints_text = "" + if constraints: + tone = constraints.tone + audience = constraints.audience + pages = constraints.page_count + constraints_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}." + if draft_sections: + constraints_text += "\nUse the section content to inform placeholder values." + + # Inject theme before sending to AI if overrides are provided + if theme: + template_html = inject_theme(template_html, theme) + + system_prompt = template_fill_html_system_prompt(constraints_text) + messages: list[models.ChatMessage] = [ + models.ChatMessage(role="system", content=system_prompt), + models.ChatMessage(role="user", content=f"Document type: {document_type}"), + models.ChatMessage( + role="user", + content=( + "CRITICAL EXAMPLE — What to preserve vs. what to change:\n" + "If template has:\n" + "
{{VENDOR_NAME}}
\n" + "You MUST output:\n" + "
Acme Corporation
← fill the token, keep the tag\n" + "\n" + "For multi-row content like {{LINEITEM_ROWS}}, generate full ... HTML.\n" + "Match ONLY the columns present in the template's — do NOT add extra columns.\n" + "Example (4-column table: #, Description, Qty, Line Total):\n" + " 1Office chairs4$480.00\n" + "\n" + "Keep ALL HTML tags, CSS, classes, and IDs EXACTLY as-is.\n" + "Do NOT modify any + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{AGREEMENT_TITLE}}
+
Advisor Agreement
+
+
+ +
+ + +
+ Reference: {{AGREEMENT_REF}} + Effective Date: {{EFFECTIVE_DATE}} + Governing Law: {{GOVERNING_LAW}} + Jurisdiction: {{JURISDICTION}} + Classification: {{CONFIDENTIALITY_LABEL}} +
+ +
+ + +
Parties
+
+
+
Company
+
{{COMPANY_LEGAL_NAME}}
+
{{COMPANY_ENTITY_TYPE}}
+
Reg. No.: {{COMPANY_REG_NO}}
+
{{COMPANY_ADDRESS}}
+
+
+
Advisor
+
{{ADVISOR_LEGAL_NAME}}
+
{{ADVISOR_ENTITY_TYPE}}
+
Reg. No.: {{ADVISOR_REG_NO}}
+
{{ADVISOR_ADDRESS}}
+
+
+ + +
Agreement
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Schedules
+ + + +
Signatures
+ +
+
+
For the Company
+
+
Signature
+
+
Name / Title: {{COMPANY_SIGNATORY}}
+
+
Date: {{SIGNATURE_DATE}}
+
+
+
For the Advisor
+
+
Signature
+
+
Name / Title: {{ADVISOR_SIGNATORY}}
+
+
Date: {{SIGNATURE_DATE}}
+
+
+ +
+ + diff --git a/engine/src/default_templates/advisor_agreement_preview.html b/engine/src/default_templates/advisor_agreement_preview.html new file mode 100644 index 0000000000..13db876141 --- /dev/null +++ b/engine/src/default_templates/advisor_agreement_preview.html @@ -0,0 +1,248 @@ + + + + + + Advisor Agreement — Preview + + + +
+ +
+ +
+
Advisor Agreement
+
Advisor Agreement
+
+ +
+ +
+ Reference: Ref: ADV-2026-01 + Effective Date: 2 February 2026 + Governing Law: England and Wales + Jurisdiction: Courts of England and Wales + Classification: Confidential +
+ +
+ +
Parties
+
+
+
Company
+
Sterling Systems Group Ltd
+
A private limited company
+
Reg. No.: 12345678
+
100 Kingfisher Way, London, EC2V 7AB, United Kingdom
+
+
+
Advisor
+
Harbourview Strategy Partners Ltd
+
A private limited company
+
Reg. No.: 87654321
+
8 Riverside Walk, London, SE1 9AB, United Kingdom
+
+
+ +
Agreement
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Schedules
+ + +
Signatures
+ +
+
+
For the Company
+
+
Signature
+
+
Name / Title: Alexandra Moore, Director
+
+
Date: 2 February 2026
+
+
+
For the Advisor
+
+
Signature
+
+
Name / Title: Priya Shah, Director
+
+
Date: 2 February 2026
+
+
+ +
+ + diff --git a/engine/src/default_templates/audit_report.html b/engine/src/default_templates/audit_report.html new file mode 100644 index 0000000000..0abec00378 --- /dev/null +++ b/engine/src/default_templates/audit_report.html @@ -0,0 +1,270 @@ + + + + + + Audit Report + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{REPORT_TITLE}}
+
{{ORG_NAME}} · Ref: {{AUDIT_REF}}
+
+
+
+ + +
+ Audit Period: {{AUDIT_PERIOD}} + Prepared By: {{PREPARED_BY}} + Report Date: {{REPORT_DATE}} +
+ +
+ + +
Executive Summary
+
{{EXECUTIVE_SUMMARY_TEXT}}
+ + +
Scope and Objectives
+
{{SCOPE_TEXT}}
+ + +
Methodology
+
{{METHODOLOGY_TEXT}}
+ + +
Findings
+ + + + + + + + + + + + {{FINDINGS_ROWS}} + +
#FindingRisk LevelRecommendationManagement Response
+ + +
Conclusion
+
{{CONCLUSION_TEXT}}
+ + +
Sign-Off
+
+ Date: {{SIGN_DATE}} +
+
+
+
Lead Auditor
+
+
{{AUDITOR_NAME}}
+
{{AUDITOR_TITLE}}
+
+
+
Reviewed By
+
+
Authorised Signatory
+
+
+ +
+ + diff --git a/engine/src/default_templates/audit_report_preview.html b/engine/src/default_templates/audit_report_preview.html new file mode 100644 index 0000000000..dde748ac52 --- /dev/null +++ b/engine/src/default_templates/audit_report_preview.html @@ -0,0 +1,149 @@ + + + + + + Audit Report + + + +
+
+
+
+
Audit Report
+
Vantage Capital Partners Ltd
+
+
+
+
+ Audit Period: 1 October 2025 – 31 January 2026 + Prepared By: Dominic Hartley, Head of Internal Audit + Report Date: 14 February 2026 +
+
+ +
Executive Summary
+
This internal audit was conducted to assess the effectiveness of Vantage Capital Partners' procurement and supplier management framework across all UK business units. The audit covered the period 1 October 2025 to 31 January 2026 and encompassed purchase order processes, supplier onboarding controls, contract management and invoice approval workflows. + +Overall, the procurement function operates with a reasonable control environment. However, the audit identified three significant findings and two areas of improvement, as detailed below. Management has acknowledged the findings and proposed remediation actions with target completion dates. The overall audit opinion is Partially Satisfactory.
+ +
Scope and Objectives
+
The audit was scoped to cover: (1) purchase order authorisation and segregation of duties; (2) supplier due diligence and onboarding procedures; (3) contract register completeness and renewal monitoring; (4) three-way matching of purchase orders, delivery notes and invoices; and (5) compliance with the Group Procurement Policy (v3.1, updated September 2025). + +Out of scope: IT systems procurement governed by the Technology Steering Committee and capital expenditure projects above £500,000 (subject to separate Board approval processes). + +The objectives were to provide assurance that procurement activities are conducted with appropriate controls, value for money is achieved, and risks of fraud and error are adequately mitigated.
+ +
Methodology
+
The audit was conducted in accordance with the Chartered Institute of Internal Auditors (CIIA) Standards. Fieldwork comprised: structured interviews with the Head of Finance Operations, two Procurement Managers and the Legal Counsel; walkthrough testing of the purchase-to-pay process; review of a random sample of 45 purchase orders raised between October 2025 and January 2026; analysis of the supplier register and contract database; and examination of invoice approval audit logs from the Oracle Fusion ERP system.
+ +
Findings
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#FindingRisk LevelRecommendationManagement Response
111 of 45 purchase orders sampled (24%) lacked a second approver for values between £25,000–£100,000, contrary to the Group Procurement Policy requirement for dual authorisation above £10,000.HighEnforce dual-authorisation controls within Oracle Fusion for all POs above £10,000 and implement automated escalation alerts for non-compliance.Agreed. Finance Operations will reconfigure Oracle Fusion approval workflows by 31 March 2026. A mandatory awareness communication will be issued to all budget holders by 28 February 2026. Owner: Head of Finance Operations.
2The supplier register contained 38 active suppliers with expired due diligence documentation (company credit checks and Modern Slavery Act questionnaires older than 24 months).HighImplement an annual supplier re-accreditation cycle and introduce automated expiry alerts within the supplier management module.Agreed. Procurement will conduct a full supplier re-accreditation exercise by 30 April 2026. Automated alerts will be configured in the ERP system by 31 March 2026. Owner: Group Procurement Manager.
317 contracts (22% of the active contract register) had no documented renewal or expiry review scheduled, creating a risk of inadvertent auto-renewal on unfavourable terms.MediumAssign a contract owner to each active contract and establish a 90-day advance review process within the contract management system.Agreed. Legal Counsel will update the contract register and assign ownership by 31 March 2026. A 90-day review process will be embedded in the contract management system by 30 April 2026. Owner: Legal Counsel.
4Invoice coding errors were identified in 6% of sampled invoices, with amounts incorrectly posted to cost centres due to the absence of a mandatory cost-centre validation rule in Oracle Fusion.LowIntroduce a mandatory cost-centre validation rule at invoice entry stage and provide refresher training to accounts payable staff.Agreed. IT will implement the validation rule by 28 February 2026. Training sessions will be arranged for accounts payable by 14 March 2026. Owner: Head of Finance Operations.
+ +
Conclusion
+
The audit concludes that the procurement and supplier management function operates with a partially satisfactory control environment. The two high-risk findings relating to purchase order authorisation and supplier due diligence represent the most significant risks and require prompt remediation. Management has responded constructively and the agreed actions, if implemented to the stated deadlines, should substantially improve the control framework. Internal Audit will conduct a follow-up review in July 2026 to verify implementation of all agreed actions.
+ +
Sign-Off
+
+ Date: 14 February 2026 +
+
+
+
Lead Auditor
+
+
Dominic Hartley
+
Head of Internal Audit
+
+
+
Reviewed By
+
+
Authorised Signatory
+
+
+
+ + diff --git a/engine/src/default_templates/board_minutes.html b/engine/src/default_templates/board_minutes.html new file mode 100644 index 0000000000..77a0041966 --- /dev/null +++ b/engine/src/default_templates/board_minutes.html @@ -0,0 +1,306 @@ + + + + + + Board Minutes + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{DOC_TITLE}}
+
{{ORG_NAME}} · Ref: {{DOC_REF}}
+
+
+
+ + +
+ Date: {{MEETING_DATE}} + Time: {{MEETING_TIME}} + Location: {{MEETING_LOCATION}} + Chair: {{CHAIR_NAME}} + Secretary: {{SECRETARY_NAME}} +
+ +
+ + +
Attendance
+ + + + + + + + + + {{PRESENT_ROWS}} + +
NameTitle / RoleOrganisation
+ + +
Apologies
+ + + + + + + + + {{APOLOGIES_ROWS}} + +
NameTitle / Role
+ + +
Matters and Business of the Meeting
+ {{AGENDA_ITEMS}} + + +
Resolutions Summary
+ + + + + + + + + + {{RESOLUTIONS_ROWS}} + +
RefResolutionCarried By
+ + +
Approval
+

These minutes are a true and accurate record of the proceedings of the above meeting.

+
+ Date Approved: {{APPROVAL_DATE}} +
+
+
+
Chair
+
+
{{CHAIR_SIGN_NAME}}
+
+
+
Secretary
+
+
{{SECRETARY_SIGN_NAME}}
+
+
+ +
+ + diff --git a/engine/src/default_templates/board_minutes_preview.html b/engine/src/default_templates/board_minutes_preview.html new file mode 100644 index 0000000000..7be82bc456 --- /dev/null +++ b/engine/src/default_templates/board_minutes_preview.html @@ -0,0 +1,171 @@ + + + + + + Board Minutes + + + +
+
+
+
+
Board Minutes
+
Hartwell Group plc
+
+
+
+
+ Date: 12 March 2026 + Time: 09:00 – 11:30 + Location: Boardroom, 14 Cavendish Square, London W1G 0PH + Chair: Dame Catherine Ashworth, Non-Executive Chair + Secretary: Richard Tewkesbury, Company Secretary +
+
+ +
Attendance
+ + + + + + + + + + + + + + + + + +
NameTitle / RoleOrganisation
Dame Catherine AshworthNon-Executive ChairHartwell Group plc
Jonathan PembertonChief Executive OfficerHartwell Group plc
Fiona CaldecottChief Financial OfficerHartwell Group plc
Marcus Osei-BonsuChief Operating OfficerHartwell Group plc
Dr Priya VenkataramanNon-Executive DirectorHartwell Group plc
Alastair DrummondSenior Independent DirectorHartwell Group plc
Richard TewkesburyCompany SecretaryHartwell Group plc
+ +
Apologies
+ + + + + + + + + + +
NameTitle / Role
Helena ForsytheNon-Executive Director
+ +
Matters and Business of the Meeting
+ +
+
1. Apologies and Conflicts of Interest
+
The Chair noted apologies received from Helena Forsythe. No conflicts of interest were declared with respect to items on the agenda. The Chair confirmed that a quorum was present and the meeting was duly constituted.
+
+ +
+
2. Minutes of Previous Meeting
+
The minutes of the Board meeting held on 12 February 2026 (Ref: BM-2026-02) were reviewed and confirmed as a true and accurate record.
+
Resolution: The Board approved the minutes of the meeting held on 12 February 2026 as a true and accurate record. Proposed by J. Pemberton; seconded by F. Caldecott. Carried unanimously.
+
+ +
+
3. Chief Executive's Report
+
Jonathan Pemberton presented the CEO's report for the period ending 28 February 2026. Revenue performance was 4.2% ahead of budget, driven by stronger-than-anticipated demand in the Infrastructure Services division. The Board noted ongoing recruitment challenges in the Technology division and requested a detailed workforce plan be presented at the April meeting. A tender submission for the Northern Rail Framework contract, valued at approximately £42m over four years, was confirmed as proceeding on schedule.
+
+ +
+
4. Financial Performance and Treasury Update
+
Fiona Caldecott presented the management accounts for February 2026. Group EBITDA stood at £8.3m against a budget of £7.9m. Net debt reduced to £14.1m, reflecting strong cash conversion. The Board approved the revised cash flow forecast for Q2 2026. Fiona noted that the revolving credit facility renewal negotiations with Barclays were progressing well and heads of terms were expected by end of March.
+
Resolution: The Board approved the revised Q2 2026 cash flow forecast as presented. Carried unanimously.
+
+ +
+
5. Any Other Business
+
Alastair Drummond raised the matter of the forthcoming AGM scheduled for 22 May 2026. Richard Tewkesbury confirmed that notice documents are on track for dispatch by 10 April 2026. The Chair thanked all Directors for their contributions and confirmed the next scheduled Board meeting.
+
+ +
Resolutions Summary
+ + + + + + + + + + + + +
RefResolutionCarried By
R-01Approval of minutes of meeting held 12 February 2026Unanimous
R-02Approval of revised Q2 2026 cash flow forecastUnanimous
+ +
Approval
+

These minutes are a true and accurate record of the proceedings of the above meeting.

+
+ Date Approved: 9 April 2026 +
+
+
+
Chair
+
+
Dame Catherine Ashworth
+
+
+
Secretary
+
+
Richard Tewkesbury
+
+
+
+ + diff --git a/engine/src/default_templates/budget_proposal.html b/engine/src/default_templates/budget_proposal.html new file mode 100644 index 0000000000..2dad47384c --- /dev/null +++ b/engine/src/default_templates/budget_proposal.html @@ -0,0 +1,274 @@ + + + + + + Budget Proposal + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{PROPOSAL_TITLE}}
+
{{COMPANY_NAME}}
+
+
+
+ + +
+
+
Budget Period
+
{{BUDGET_PERIOD}}
+
Prepared By
+
{{PREPARED_BY}}
+
+
+
Prepared Date
+
{{PREPARED_DATE}}
+
+
+ + +
Executive Summary
+
{{EXECUTIVE_SUMMARY_TEXT}}
+ + +
Revenue Projections
+ + + + + + + + + + {{REVENUE_ROWS}} + +
Revenue StreamDescriptionAmount
+
+ + + + +
Total Revenue:{{TOTAL_REVENUE}}
+
+ + +
Expense Breakdown
+ + + + + + + + + + {{EXPENSE_ROWS}} + +
CategoryDescriptionAmount
+
+ + + + + +
Total Expenses:{{TOTAL_EXPENSES}}
Net Surplus / (Deficit):{{NET_SURPLUS}}
+
+ + +
Assumptions
+
{{ASSUMPTIONS_TEXT}}
+ + +
Approval
+
+ Approver Name: {{APPROVER_NAME}} + Approver Title: {{APPROVER_TITLE}} +
+ +
+ + diff --git a/engine/src/default_templates/budget_proposal_preview.html b/engine/src/default_templates/budget_proposal_preview.html new file mode 100644 index 0000000000..2ca188b11a --- /dev/null +++ b/engine/src/default_templates/budget_proposal_preview.html @@ -0,0 +1,183 @@ + + + + + + Budget Proposal + + + +
+
+
+
+
Budget Proposal
+
Crestwood Technologies Ltd
+
+
+
+
+
+
Budget Period
+
1 April 2026 – 31 March 2027
+
Prepared By
+
Rachel Thornton, Head of Finance
+
+
+
Prepared Date
+
19 February 2026
+
+
+
Executive Summary
+
This proposal outlines the projected revenue and expenditure for the Product Engineering Division for the financial year ending 31 March 2027. Following strong performance in FY 2025/26, the division is seeking approval for an 18% increase in operating budget to accelerate product development, expand the engineering team, and invest in cloud infrastructure to support anticipated customer growth of 35%. + +The net surplus projection of £412,000 reflects disciplined cost management alongside planned investment in headcount and tooling. All figures are presented in GBP and have been reviewed by the CFO.
+
Revenue Projections
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Revenue StreamDescriptionAmount
SaaS SubscriptionsRecurring licence revenue — existing & new customers£2,800,000
Professional ServicesImplementation, training, and consultancy£480,000
Marketplace Add-onsThird-party integrations and premium modules£165,000
Support ContractsPriority and enterprise support tiers£95,000
+
+ + + + +
Total Revenue:£3,540,000
+
+
Expense Breakdown
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryDescriptionAmount
HeadcountSalaries, NI, and pension — 34 FTEs (incl. 4 new hires)£1,980,000
Cloud InfrastructureAWS hosting, storage, and data services£420,000
Software & ToolingDev tools, SaaS licences, security platforms£215,000
MarketingDemand generation, events, content production£180,000
T&EBusiness travel, client entertainment, conferences£88,000
Contingency5% operational reserve£165,000
+
+ + + + + +
Total Expenses:£3,048,000
Net Surplus / (Deficit):£492,000
+
+
Assumptions
+
1. Revenue growth of 35% year-on-year is based on confirmed pipeline and contracted renewals constituting 72% of the total revenue target. +2. Four engineering hires are assumed to be on-boarded by Q1 FY 2026/27; salaries reflect current market rates for mid-senior software engineers in the UK. +3. AWS costs include a committed-use discount of 18% negotiated under the existing enterprise agreement. +4. All figures exclude capital expenditure, which is budgeted separately under the IT CapEx programme. +5. Exchange rate risk on USD-denominated tooling licences is hedged at the treasury level and not reflected here.
+
Approval
+
+ Approver Name: Jonathan Crestwood + Approver Title: Chief Financial Officer +
+
+ + diff --git a/engine/src/default_templates/case_study.html b/engine/src/default_templates/case_study.html new file mode 100644 index 0000000000..9c6f407315 --- /dev/null +++ b/engine/src/default_templates/case_study.html @@ -0,0 +1,169 @@ + + + + + + Case Study + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{CASE_STUDY_TITLE}}
+
Case Study
+
+
+ +
+ + +
+ Client: {{CLIENT_NAME}} + Industry: {{INDUSTRY}} + Date: {{DATE}} +
+ + +
Client Overview
+
{{CLIENT_OVERVIEW_TEXT}}
+ + +
Challenge
+
{{CHALLENGE_TEXT}}
+ + +
Solution
+
{{SOLUTION_TEXT}}
+ + +
Implementation
+
{{IMPLEMENTATION_TEXT}}
+ + +
Results
+
{{RESULTS_TEXT}}
+ + +
Client Testimonial
+
+
{{TESTIMONIAL_TEXT}}
+
+ + +
Conclusion
+
{{CONCLUSION_TEXT}}
+ +
+ + diff --git a/engine/src/default_templates/case_study_preview.html b/engine/src/default_templates/case_study_preview.html new file mode 100644 index 0000000000..87ac41076e --- /dev/null +++ b/engine/src/default_templates/case_study_preview.html @@ -0,0 +1,101 @@ + + + + + + Case Study + + + +
+
+
+
+
Case Study
+
Stirling PDF
+
+
+
+
How Nexus Cloud Reduced Invoice Processing Time by 74%
+
+ Client: Nexus Cloud Ltd + Industry: Financial Technology (FinTech) + Date: February 2026 +
+
Client Overview
+
Nexus Cloud Ltd is a London-based financial technology company providing accounts-payable automation software to mid-market businesses across the UK and Ireland. Founded in 2017, the company employs 210 staff and processes over 4 million invoices annually on behalf of its 320 enterprise clients. Following a Series B funding round in 2024, Nexus Cloud entered a rapid growth phase, scaling its customer base by 45% in eighteen months.
+
Challenge
+
Rapid customer growth exposed significant bottlenecks in Nexus Cloud's internal document management and PDF generation infrastructure. The legacy system — a patchwork of third-party libraries and manual export workflows — was unable to scale with demand. Key problems included: + +- Invoice and statement generation times averaging 8.2 seconds per document, causing SLA breaches during peak periods +- No support for branded, client-configurable document templates, resulting in high-value customers receiving generic output +- A manual quality-assurance step requiring three finance staff to review approximately 600 documents per day +- An inability to generate documents programmatically via API, blocking integrations with downstream ERP systems + +The backlog had grown to 12,000 unprocessed documents at peak, and customer satisfaction scores related to document delivery had fallen to 61%.
+
Solution
+
Nexus Cloud partnered with Stirling PDF to deploy a fully integrated document generation and processing pipeline. The solution centred on Stirling PDF's AI document creation capabilities, enabling Nexus Cloud to: + +- Define and version-control a library of 24 branded document templates covering invoices, statements, remittance advice, and credit notes +- Generate PDF documents programmatically via a REST API with average end-to-end latency of 2.1 seconds +- Apply client-specific theming — including custom fonts, colour schemes, and logos — at the point of generation without manual intervention +- Route completed documents directly to Nexus Cloud's distribution service via webhook, eliminating the manual QA step for standard document types
+
Implementation
+
The implementation was completed in three phases over eight weeks: + +Phase 1 (Weeks 1–3): Template Migration. Nexus Cloud's design team worked with Stirling PDF's onboarding engineers to convert the 24 legacy document designs into Stirling PDF's HTML-based template format. All templates were validated against a sample of 500 historical documents. + +Phase 2 (Weeks 4–6): API Integration. Nexus Cloud's engineering team integrated the Stirling PDF generation API into their existing invoice processing microservice. A staging environment with synthetic load testing at 200 concurrent requests confirmed that latency remained below 2.5 seconds at the 95th percentile. + +Phase 3 (Weeks 7–8): Parallel Running and Cutover. Both systems ran in parallel for two weeks. A total of 38,000 documents were validated side-by-side. No material discrepancies were identified. The legacy system was decommissioned on 14 October 2025.
+
Results
+
Within 90 days of full deployment, Nexus Cloud reported the following outcomes: + +- Document generation time reduced from 8.2 seconds to 2.1 seconds — a 74% improvement +- Manual QA headcount reallocated from document review to higher-value tasks; estimated annual saving of £87,000 in operational cost +- Zero SLA breaches in November and December 2025 (versus 14 in the same period the prior year) +- Customer satisfaction score for document delivery increased from 61% to 89% +- 12 enterprise clients activated custom-branded templates within the first 60 days, generating an estimated £34,000 in incremental upsell revenue for Nexus Cloud +- The document backlog was eliminated within the first week of operation
+
Client Testimonial
+
+
"Switching to Stirling PDF was one of the smoothest technology transitions we have made. The onboarding team understood our constraints from day one, and the API integration took our engineers less than a week. The impact on our document throughput and customer feedback has been immediate and measurable. We are already planning to extend the platform to cover our client-facing reporting suite in Q2 2026."
+
— Marcus Webb, VP of Engineering, Nexus Cloud Ltd
+
+
Conclusion
+
Nexus Cloud's experience demonstrates how organisations experiencing rapid growth can use Stirling PDF to eliminate document processing bottlenecks without sacrificing quality or brand consistency. By replacing a fragile legacy stack with a scalable, API-first generation platform, Nexus Cloud recovered significant operational capacity, improved customer satisfaction, and created new commercial opportunities through branded document customisation. The project delivered full return on investment within eleven weeks of go-live.
+
+ + diff --git a/engine/src/default_templates/committee_agenda.html b/engine/src/default_templates/committee_agenda.html new file mode 100644 index 0000000000..acb12240d3 --- /dev/null +++ b/engine/src/default_templates/committee_agenda.html @@ -0,0 +1,224 @@ + + + + + + Committee Agenda + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
Agenda
+
{{COMMITTEE_NAME}} · {{ORG_NAME}}
+
+
+
+ + +
+ Date: {{MEETING_DATE}} + Time: {{MEETING_TIME}} + Location: {{MEETING_LOCATION}} + Chair: {{CHAIR_NAME}} +
+ +
+ + +
Expected Attendees
+ + + + + + + + + + {{ATTENDEES_ROWS}} + +
NameTitle / RoleOrganisation
+ + +
Agenda Items
+ + + + + + + + + + + {{AGENDA_ROWS}} + +
#TopicPresenterTime Allotted
+ + +
Documents and Papers Circulated
+
{{DOCUMENTS_TEXT}}
+ + +
Any Other Business (AOB)
+
{{AOB_TEXT}}
+ +
+ + diff --git a/engine/src/default_templates/committee_agenda_preview.html b/engine/src/default_templates/committee_agenda_preview.html new file mode 100644 index 0000000000..be98390519 --- /dev/null +++ b/engine/src/default_templates/committee_agenda_preview.html @@ -0,0 +1,128 @@ + + + + + + Committee Agenda + + + +
+
+
+
+
Committee Agenda
+
Meridian Healthcare NHS Foundation Trust
+
+
+
+
+ Date: 26 March 2026 + Time: 14:00 – 16:30 + Location: Committee Room 3, Trust Headquarters, Bristol BS1 5TT + Chair: Sir Geoffrey Harwood, Non-Executive Director +
+
+ +
Expected Attendees
+ + + + + + + + + + + + + + + + + +
NameTitle / RoleOrganisation
Sir Geoffrey HarwoodNon-Executive Director (Chair)Meridian Healthcare NHS FT
Dr Anita NwachukwuNon-Executive DirectorMeridian Healthcare NHS FT
Peter SloaneDirector of FinanceMeridian Healthcare NHS FT
Carolyn BridgesHead of Internal AuditMeridian Healthcare NHS FT
James FirthDeputy Director of GovernanceMeridian Healthcare NHS FT
Sarah QuilterEngagement LeadGrant Thornton UK LLP (External Auditors)
Helen McallisterCommittee SecretaryMeridian Healthcare NHS FT
+ +
Agenda Items
+ + + + + + + + + + + + + + + + + + + + + +
#TopicPresenterTime Allotted
1Welcome, Apologies and Declarations of InterestChair5 mins
2Minutes of Previous Meeting (29 January 2026) and Matters ArisingChair / Secretary10 mins
3External Audit: Progress Report and Planned Approach for 2025/26 AccountsS. Quilter20 mins
4Internal Audit: Q3 Progress Report and Tracker of Open RecommendationsC. Bridges25 mins
5Counter-Fraud Annual Update and Referral SummaryC. Bridges15 mins
6Finance Report: Month 11 Management AccountsP. Sloane20 mins
7Board Assurance Framework: Risk Register ReviewJ. Firth20 mins
8Committee Effectiveness Self-Assessment – 2025/26 ResultsChair15 mins
9Any Other BusinessChair5 mins
10Date of Next MeetingSecretary5 mins
+ +
Documents and Papers Circulated
+
The following papers have been circulated in advance of the meeting and should be read prior to attendance: + +1. Draft minutes of the Audit and Risk Committee meeting held 29 January 2026 +2. External Audit Progress Report – Grant Thornton UK LLP (March 2026) +3. Internal Audit Q3 Progress Report and Recommendations Tracker +4. Counter-Fraud Annual Summary Report 2025/26 +5. Month 11 Management Accounts (February 2026) +6. Board Assurance Framework – Quarterly Risk Register Update +7. Committee Effectiveness Self-Assessment Summary Report + +Papers are accessible via the Board portal. Hard copies available from the Committee Secretary on request.
+ +
Any Other Business (AOB)
+
Members wishing to raise items under AOB should notify the Committee Secretary (h.mcallister@meridianhealthcare.nhs.uk) no later than 24 hours before the meeting. The next scheduled meeting of the Audit and Risk Committee is Thursday, 28 May 2026 at 14:00.
+
+ + diff --git a/engine/src/default_templates/employee_handbook.html b/engine/src/default_templates/employee_handbook.html new file mode 100644 index 0000000000..dc8b91f401 --- /dev/null +++ b/engine/src/default_templates/employee_handbook.html @@ -0,0 +1,201 @@ + + + + + + Employee Handbook + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{HANDBOOK_TITLE}}
+
{{COMPANY_NAME}}
+
+
+ +
+ + +
+ Version: {{VERSION}} + Effective Date: {{EFFECTIVE_DATE}} +
+ +
+ + +
1.0   Welcome
+
{{WELCOME_TEXT}}
+ + +
2.0   Employment Policies
+
{{EMPLOYMENT_POLICIES_TEXT}}
+ + +
3.0   Code of Conduct
+
{{CODE_OF_CONDUCT_TEXT}}
+ + +
4.0   Benefits Overview
+
{{BENEFITS_TEXT}}
+ + +
5.0   Leave Policies
+
{{LEAVE_POLICIES_TEXT}}
+ + +
6.0   Disciplinary Procedure
+
{{DISCIPLINARY_TEXT}}
+ + +
7.0   Acknowledgement
+
I, the undersigned, acknowledge that I have received, read, and understood the contents of this Employee Handbook. I agree to comply with all policies and procedures set out herein.
+
+
+
Employee
+
+
Signature
+
+
Name: {{EMPLOYEE_NAME}}
+
+
Date: {{EMPLOYEE_SIGN_DATE}}
+
+
+
HR Representative
+
+
Signature
+
+
Name: {{HR_NAME}}
+
+
Date: {{HR_SIGN_DATE}}
+
+
+ +
+ + diff --git a/engine/src/default_templates/employee_handbook_preview.html b/engine/src/default_templates/employee_handbook_preview.html new file mode 100644 index 0000000000..19526ae8a1 --- /dev/null +++ b/engine/src/default_templates/employee_handbook_preview.html @@ -0,0 +1,151 @@ + + + + + + Employee Handbook + + + +
+
+
+
+
Employee Handbook
+
Northgate Engineering Services Ltd
+
+
+
+
+ Version: 3.2 + Effective Date: 1 January 2026 +
+ +
1.0   Welcome
+
Welcome to Northgate Engineering Services Ltd. We are delighted that you have joined our team and hope that your time with us will be both rewarding and fulfilling. + +This handbook sets out the key policies, procedures, and standards that govern our working environment. It is intended to give you a clear picture of what you can expect from us and what we expect from you. Please read it carefully and keep it for reference throughout your employment. + +This handbook does not form part of your contract of employment. The company reserves the right to amend any policy set out herein at any time, subject to appropriate consultation. The most current version will always be available on the HR intranet portal.
+ +
2.0   Employment Policies
+
2.1 Equal Opportunities +Northgate Engineering Services is committed to providing equal opportunities for all employees and job applicants. We do not discriminate on the grounds of age, disability, gender reassignment, marriage and civil partnership, pregnancy and maternity, race, religion or belief, sex, or sexual orientation. + +2.2 Probationary Period +All new employees are subject to a six-month probationary period. During this time, performance and conduct will be monitored closely. The company may extend the probationary period or terminate employment if performance or conduct does not meet the required standard. + +2.3 Working Hours +Core working hours are Monday to Friday, 08:30 to 17:00, with a 30-minute unpaid lunch break. Flexible working arrangements may be agreed in writing with your line manager and HR. + +2.4 Pay and Salary Review +Salaries are paid monthly by BACS transfer on the last working day of each month. Annual salary reviews take place in April each year; any increase is discretionary and subject to individual performance and business conditions.
+ +
3.0   Code of Conduct
+
All employees are expected to conduct themselves with professionalism, honesty, and integrity at all times. The following behaviours are required: + +- Treat all colleagues, clients, and business partners with courtesy and respect. +- Maintain confidentiality in relation to company information, client data, and personal data of colleagues. +- Avoid any actual or perceived conflicts of interest; disclose potential conflicts to your line manager promptly. +- Use company assets, including IT systems, vehicles, and equipment, only for legitimate business purposes. +- Comply with all applicable laws, regulations, and company policies. +- Do not make unauthorised commitments or enter into contracts on behalf of the company. + +Breaches of the code of conduct may result in disciplinary action up to and including dismissal.
+ +
4.0   Benefits Overview
+
Northgate Engineering Services offers a competitive benefits package, which includes: + +- Pension: Employer contribution of 6% into a qualifying workplace pension scheme; employee minimum contribution of 3%. +- Private Medical Insurance: Individual cover provided through AXA Health from the date of joining; family cover available at a subsidised premium. +- Life Assurance: Death-in-service benefit of four times annual base salary. +- Annual Leave: 25 days per year, rising to 28 days after five years' continuous service, plus UK public holidays. +- Cycle to Work: Interest-free salary sacrifice scheme for bicycles and cycling equipment. +- Employee Assistance Programme: Confidential 24/7 support line covering mental health, financial, and legal advice. +- Professional Development: Support for relevant professional qualifications and membership fees, subject to line manager approval.
+ +
5.0   Leave Policies
+
5.1 Annual Leave +Annual leave must be agreed in advance with your line manager and booked via the HR system. A maximum of five days may be carried over into the next leave year; carried-over days expire on 31 March. + +5.2 Sickness Absence +If you are unable to attend work due to illness, you must notify your line manager by telephone before your normal start time on the first day of absence. A self-certification form must be completed for absences of up to seven calendar days. A fit note from a GP is required for any absence exceeding seven calendar days. + +5.3 Maternity, Paternity, and Shared Parental Leave +The company provides statutory maternity pay and leave entitlements in accordance with current UK legislation. Enhanced maternity pay of 90% of average weekly earnings is provided for the first 16 weeks for employees with at least one year's continuous service. Please contact HR for full details. + +5.4 Other Leave +Compassionate leave of up to five days with full pay may be granted on the death of an immediate family member. Jury service leave will be granted; the company will make up the difference between jury service allowance and normal pay for a maximum of 10 working days.
+ +
6.0   Disciplinary Procedure
+
The company's disciplinary procedure is designed to ensure that all employees are treated fairly and consistently when concerns arise about conduct or performance. The procedure follows these stages: + +Stage 1 — Informal Discussion: For minor issues, the line manager will hold an informal discussion to address the concern and agree on corrective action. + +Stage 2 — Formal Investigation: Where informal resolution is not possible, or where the matter is sufficiently serious, a formal investigation will be conducted. The employee will be notified in writing and invited to a disciplinary hearing with appropriate notice. + +Stage 3 — Disciplinary Hearing: The employee has the right to be accompanied by a trade union representative or work colleague. Following the hearing, the outcome may be: no further action, a written warning, a final written warning, or dismissal. + +Stage 4 — Appeal: The employee may appeal against any disciplinary outcome within five working days of receiving the written decision. + +Gross misconduct, including theft, fraud, violence, and serious breaches of data protection, may result in summary dismissal without notice.
+ +
7.0   Acknowledgement
+
I, the undersigned, acknowledge that I have received, read, and understood the contents of this Employee Handbook. I agree to comply with all policies and procedures set out herein.
+
+
+
Employee
+
+
Signature
+
+
Name: Thomas Brennan
+
+
Date: 6 January 2026
+
+
+
HR Representative
+
+
Signature
+
+
Name: Claire Whitfield
+
+
Date: 6 January 2026
+
+
+
+ + diff --git a/engine/src/default_templates/executive_summary.html b/engine/src/default_templates/executive_summary.html new file mode 100644 index 0000000000..77cbc48c47 --- /dev/null +++ b/engine/src/default_templates/executive_summary.html @@ -0,0 +1,140 @@ + + + + + + Executive Summary + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{DOCUMENT_TITLE}}
+
{{COMPANY_NAME}}
+
+
+ +
+ + +
+
+
Prepared By
+
{{PREPARED_BY}}
+
+
+
Date
+
{{DATE}}
+
+
+ + +
Overview
+
{{OVERVIEW_TEXT}}
+ + +
Key Findings
+
{{FINDINGS_TEXT}}
+ + +
Recommendations
+
{{RECOMMENDATIONS_TEXT}}
+ + +
Financial Impact
+
{{FINANCIAL_IMPACT_TEXT}}
+ + +
Next Steps
+
{{NEXT_STEPS_TEXT}}
+ + +
Conclusion
+
{{CONCLUSION_TEXT}}
+ +
+ + diff --git a/engine/src/default_templates/executive_summary_preview.html b/engine/src/default_templates/executive_summary_preview.html new file mode 100644 index 0000000000..10f6ad44df --- /dev/null +++ b/engine/src/default_templates/executive_summary_preview.html @@ -0,0 +1,98 @@ + + + + + + Executive Summary + + + +
+
+
+
+
Executive Summary
+
Aldgate Capital Partners LLP
+
+
+
+
UK Retail Sector Expansion Opportunity
+
+
+
Prepared By
+
Sophia Hartley, Director of Strategy
+
+
+
Date
+
19 February 2026
+
+
+
Overview
+
This executive summary presents the findings of a three-month strategic review into potential expansion of Aldgate Capital Partners' retail sector portfolio across the Midlands and North of England. The review was commissioned by the Investment Committee in November 2025 and draws on market data, competitor analysis, and direct stakeholder interviews with eight target acquisition companies. + +The review assessed 14 candidate businesses with combined annual revenues of £320 million and identified three high-priority targets that meet the firm's return threshold of 22% IRR over a five-year horizon.
+
Key Findings
+
1. The UK independent retail sector has shown resilient 6.2% revenue growth in 2025 despite macroeconomic headwinds, driven by consumer preference for experiential shopping and local provenance brands. + +2. Three acquisition targets — Fenwick North, BrightGrocer Holdings, and Pennine Outdoor — demonstrate strong EBITDA margins of 14–19%, proven management teams, and clear scalability through supply-chain optimisation. + +3. Consolidation activity in the sector has accelerated: seven transactions were completed in the Midlands alone in H2 2025, suggesting a narrowing window for acquisitions at current valuations. + +4. Regulatory risk is low; no material competition or planning obstacles have been identified for the preferred targets. + +5. Digital transformation readiness varies: Fenwick North is category-leading; BrightGrocer and Pennine Outdoor require investment of approximately £2.8 million combined to reach baseline e-commerce capability.
+
Recommendations
+
1. Initiate formal due diligence on Fenwick North (primary target) immediately, with a target indicative offer by 15 April 2026. Estimated enterprise value: £48–54 million. + +2. Commission a separate operational review of BrightGrocer Holdings' supply chain to validate management's 23% cost-reduction claim before advancing to Heads of Terms. + +3. Place Pennine Outdoor on a six-month watchlist; monitor Q1 2026 trading results before committing further resource. A deterioration in EBITDA margin below 12% should trigger a withdrawal from consideration. + +4. Engage the firm's preferred legal counsel (Clifford Chance LLP) and financial advisers (Rothschild & Co) by end of February 2026 to prepare transaction infrastructure.
+
Financial Impact
+
Acquiring Fenwick North at the midpoint valuation of £51 million (using a 60:40 debt/equity structure) is projected to generate: +- IRR: 24.3% over five years (base case) +- Cash-on-cash multiple: 2.8x +- Projected EBITDA contribution to portfolio in Year 3: £9.2 million + +Sensitivity analysis indicates that even under a downside scenario assuming 10% revenue contraction and 200bps margin compression, the IRR remains above the 18% hurdle rate. The full financial model is available in Appendix B.
+
Next Steps
+
1. Investment Committee approval of this recommendation by 28 February 2026. +2. Execution of an NDA with Fenwick North management — week of 2 March 2026. +3. Kick-off of management presentations and site visits — week of 9 March 2026. +4. Indicative offer submission — by 15 April 2026. +5. Parallel engagement with BrightGrocer operational review team — March 2026 start.
+
Conclusion
+
The strategic review confirms that a disciplined, targeted entry into the UK Midlands and Northern retail sector presents a compelling risk-adjusted opportunity for Aldgate Capital Partners. Fenwick North represents an immediately actionable priority with strong fundamentals and a management team that has expressed openness to a structured transaction. Swift action is recommended to pre-empt competing interest from two identified trade buyers. The Investment Committee's approval to proceed is sought at the next scheduled meeting on 26 February 2026.
+
+ + diff --git a/engine/src/default_templates/expense_report.html b/engine/src/default_templates/expense_report.html new file mode 100644 index 0000000000..807cd08eba --- /dev/null +++ b/engine/src/default_templates/expense_report.html @@ -0,0 +1,313 @@ + + + + + + Expense Report + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
Expense Report
+
{{ORG_NAME}}
+
+
+ +
+ + +
+
+
Employee Name
+
{{EMPLOYEE_NAME}}
+
Department
+
{{EMPLOYEE_DEPARTMENT}}
+
+
+
Submission Date
+
{{SUBMISSION_DATE}}
+
+
+ + +
Expense Items
+ + + + + + + + + + + + + + + + {{EXPENSE_ROWS}} + +
#DateDescriptionCategoryMerchantReceiptQtyUnit CostTotal
+ + +
Mileage
+ + + + + + + + + + + + + + + {{MILEAGE_ROWS}} + +
#DatePurposeFromToMilesRateTotal
+ + +
Summary by Category
+ + + + + + + + + {{SUMMARY_ROWS}} + +
CategoryAmount
+ + +
+ + + + +
Total Amount:{{TOTAL_AMOUNT_TEXT}}
+
+ + +
Reimbursement Details
+
{{REIMBURSEMENT_TEXT}}
+ + +
Sign-Off
+
+
+
Employee
+
+
Signature
+
+
Name: {{EMPLOYEE_SIGNER_NAME}}
+
+
Date: {{EMPLOYEE_SIGN_DATE}}
+
+
+
Manager Approval
+
+
Signature
+
+
Name: {{MANAGER_SIGNER_NAME}}
+
+
Date: {{MANAGER_SIGN_DATE}}
+
+
+ +
+ + diff --git a/engine/src/default_templates/expense_report_preview.html b/engine/src/default_templates/expense_report_preview.html new file mode 100644 index 0000000000..c11af0081b --- /dev/null +++ b/engine/src/default_templates/expense_report_preview.html @@ -0,0 +1,225 @@ + + + + + + Expense Report + + + +
+
+
+
+
Expense Report
+
Thornfield Media Group plc
+
+
+
+
+
+
Employee Name
+
Daniel Fitzgerald
+
Department
+
Commercial Sales
+
+
+
Submission Date
+
31 January 2026
+
+
+
Expense Items
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#DateDescriptionCategoryMerchantReceiptQtyUnit CostTotal
114 Jan 2026Return train — London Euston to Manchester PiccadillyTravelAvanti West CoastR-0011£142.00£142.00
214 Jan 2026Client lunch — 3 attendees (Prospect meeting)MealsThe Ivy, ManchesterR-0021£87.50£87.50
315 Jan 2026Hotel accommodation — 1 nightAccommodationRadisson BluR-0031£129.00£129.00
422 Jan 2026Taxi to London Heathrow Terminal 5TravelAddison LeeR-0041£54.00£54.00
522 Jan 2026Return flight — London Heathrow to EdinburghTravelBritish AirwaysR-0051£218.00£218.00
+
Mileage
+ + + + + + + + + + + + + + + + + + + + + + + + + +
#DatePurposeFromToMilesRateTotal
119 Jan 2026Client site visitHome officeBasingstoke48£0.45£21.60
+
Summary by Category
+ + + + + + + + + + + + + +
CategoryAmount
Accommodation£129.00
Meals£87.50
Mileage£21.60
Travel£414.00
+
+ + + + +
Total Amount:£652.10
+
+
Reimbursement Details
+
Please reimburse to Daniel Fitzgerald's designated expenses account. Payment is expected within 10 working days of approval.
+
Sign-Off
+
+
+
Employee
+
+
Signature
+
+
Name: Daniel Fitzgerald
+
+
Date: 31 January 2026
+
+
+
Manager Approval
+
+
Signature
+
+
Name: Harriet Dawson
+
+
Date: 3 February 2026
+
+
+
+ + diff --git a/engine/src/default_templates/incident_report.html b/engine/src/default_templates/incident_report.html new file mode 100644 index 0000000000..7623723833 --- /dev/null +++ b/engine/src/default_templates/incident_report.html @@ -0,0 +1,294 @@ + + + + + + Incident Report + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
Incident Report
+
{{ORG_NAME}} · Report No: {{REPORT_NUMBER}}
+
+
+
+ + +
+
+
Incident Date
+
{{INCIDENT_DATE}}
+
+
+
Incident Time
+
{{INCIDENT_TIME}}
+
+
+
Location
+
{{INCIDENT_LOCATION}}
+
+
+
Incident Type
+
{{INCIDENT_TYPE}}
+
+
+
Severity Level
+
{{SEVERITY_LEVEL}}
+
+
+
Reporter
+
{{REPORTER_NAME}}, {{REPORTER_TITLE}}
+
+
+ +
+ + +
People Involved
+
+ Reported By: {{REPORTER_NAME}}, {{REPORTER_TITLE}} + Witnesses: {{WITNESSES_TEXT}} +
+ + +
Incident Description
+
{{DESCRIPTION_TEXT}}
+ + +
Immediate Actions Taken
+
{{IMMEDIATE_ACTIONS_TEXT}}
+ + +
Root Cause Analysis
+
{{ROOT_CAUSE_TEXT}}
+ + +
Corrective Actions
+
{{CORRECTIVE_ACTIONS_TEXT}}
+ + +
Attachments
+
{{ATTACHMENTS_TEXT}}
+ + +
Sign-Off and Review
+
+ Review Date: {{REVIEW_DATE}} +
+
+
+
Reviewer / Authoriser
+
+
{{REVIEWER_NAME}}
+
+
+
Reporter
+
+
{{REPORTER_NAME}}
+
+
+ +
+ + diff --git a/engine/src/default_templates/incident_report_preview.html b/engine/src/default_templates/incident_report_preview.html new file mode 100644 index 0000000000..27a042d092 --- /dev/null +++ b/engine/src/default_templates/incident_report_preview.html @@ -0,0 +1,158 @@ + + + + + + Incident Report + + + +
+
+
+
+
Incident Report
+
Ironbridge Engineering Solutions Ltd
+
+
+
Report No
+
IR-2026-041
+
+
+
+ +
+
+
Incident Date
+
4 February 2026
+
+
+
Incident Time
+
08:47
+
+
+
Location
+
Workshop Bay 3, Telford Manufacturing Site, TF1 5RQ
+
+
+
Incident Type
+
Workplace Injury – Manual Handling
+
+
+
Severity Level
+
High
+
+
+
Reporter
+
Gary Lockwood, Shift Supervisor
+
+
+ +
+ +
People Involved
+
+ Reported By: Gary Lockwood, Shift Supervisor + Witnesses: Trevor Baines (Fabrication Technician); Sandra Okafor (Health and Safety Officer) +
+ +
Incident Description
+
At approximately 08:47 on 4 February 2026, Ian Morecroft (Fabrication Operative, Grade 3) sustained an injury to his lower back while manually moving a steel fabrication jig weighing approximately 42 kg in Workshop Bay 3. Mr Morecroft had been attempting to reposition the jig from a floor-level pallet onto a workbench without mechanical lifting assistance. He did not use the overhead gantry hoist fitted in Bay 3, citing that it was in use by a colleague for a separate task. + +Mr Morecroft reported immediate onset of acute lower back pain and was unable to continue work. He was escorted to the site first-aid room by Shift Supervisor Gary Lockwood at 08:52. The on-site first aider assessed the injury and called 999 at 09:10 due to worsening symptoms. Mr Morecroft was transported to Princess Royal Hospital, Telford, by ambulance at 09:25. He was diagnosed with a lumbar muscle strain and minor disc compression and was signed off work for a minimum of four weeks.
+ +
Immediate Actions Taken
+
1. Mr Morecroft was removed from Workshop Bay 3 immediately and first aid was administered on-site. +2. Emergency services were called at 09:10 and Mr Morecroft was transported to hospital by ambulance. +3. Workshop Bay 3 was cordoned off pending investigation and the jig was secured in place. +4. The incident was verbally reported to the Site Health and Safety Manager (Sandra Okafor) at 09:00 and formally notified to the Health and Safety Executive (HSE) under RIDDOR 2013 on 4 February 2026 (Reference: HSE-RIDDOR-2026-11874). +5. All manual handling activities in Workshop Bay 3 were suspended pending a risk assessment review. +6. Mr Morecroft's next of kin were notified at 09:30.
+ +
Root Cause Analysis
+
Primary cause: Failure to follow the site Manual Handling Procedure (SHE-P-004, Rev 2) which requires mechanical lifting assistance for loads exceeding 25 kg. Mr Morecroft confirmed he was aware of the procedure but considered the move to be a quick, short-distance task. + +Contributing factors: +- The sole overhead gantry hoist in Bay 3 was in use at the time of the incident, creating a perceived operational bottleneck that encouraged unsafe ad hoc lifting. +- A toolbox talk on manual handling conducted in November 2025 did not include a practical demonstration or competency check for operatives working with heavy fabrication components. +- The risk assessment for Bay 3 workbench loading tasks (RA-BAY3-001) had not been reviewed since March 2024 and did not reflect the addition of heavier jig stock introduced in September 2025.
+ +
Corrective Actions
+
1. Install a second overhead gantry hoist in Workshop Bay 3 to eliminate single-point bottlenecks for heavy lifting tasks. Target completion: 28 February 2026. Owner: Operations Manager (D. Hartington). +2. Conduct a refresher manual handling training session for all Bay 3 operatives, including a practical competency assessment. Target completion: 14 February 2026. Owner: Health and Safety Officer (S. Okafor). +3. Review and update risk assessment RA-BAY3-001 to reflect current load weights and introduce a mandatory pre-lift checklist. Target completion: 11 February 2026. Owner: Health and Safety Officer (S. Okafor). +4. Brief all shift supervisors on enforcement of mechanical assistance requirements and the escalation procedure when equipment is unavailable. Target completion: 7 February 2026. Owner: Site Manager (P. Fairweather). +5. Review all site risk assessments for manual handling tasks to identify any others that have not been updated within the 12-month review cycle. Target completion: 31 March 2026. Owner: Health and Safety Officer (S. Okafor).
+ +
Attachments
+
1. First Aid Record – Bay 3 First Aid Room Log, 4 February 2026 +2. RIDDOR Notification Confirmation – HSE Reference HSE-RIDDOR-2026-11874 +3. Risk Assessment RA-BAY3-001 (current version, March 2024) +4. Manual Handling Procedure SHE-P-004 Rev 2 +5. Workshop Bay 3 Site Photographs (8 images, taken 4 February 2026 09:45) +6. Witness Statement – Trevor Baines, dated 4 February 2026 +7. Witness Statement – Sandra Okafor, dated 4 February 2026
+ +
Sign-Off and Review
+
+ Review Date: 6 February 2026 +
+
+
+
Reviewer / Authoriser
+
+
Paul Fairweather, Site Manager
+
+
+
Reporter
+
+
Gary Lockwood, Shift Supervisor
+
+
+
+ + diff --git a/engine/src/default_templates/independent_contractor_agreement.html b/engine/src/default_templates/independent_contractor_agreement.html new file mode 100644 index 0000000000..744ff17cfc --- /dev/null +++ b/engine/src/default_templates/independent_contractor_agreement.html @@ -0,0 +1,373 @@ + + + + + + Independent Contractor Agreement + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{AGREEMENT_TITLE}}
+
Independent Contractor Agreement
+
+
+ +
+ + +
+ Reference: {{AGREEMENT_REF}} + Effective Date: {{EFFECTIVE_DATE}} + Governing Law: {{GOVERNING_LAW}} + Jurisdiction: {{JURISDICTION}} + Classification: {{CONFIDENTIALITY_LABEL}} +
+ +
+ + +
Parties
+
+
+
Company
+
{{COMPANY_LEGAL_NAME}}
+
{{COMPANY_ENTITY_TYPE}}
+
Reg. No.: {{COMPANY_REG_NO}}
+
{{COMPANY_ADDRESS}}
+
+
+
Contractor
+
{{CONTRACTOR_LEGAL_NAME}}
+
{{CONTRACTOR_ENTITY_TYPE}}
+
Reg. No.: {{CONTRACTOR_REG_NO}}
+
{{CONTRACTOR_ADDRESS}}
+
+
+ + +
Agreement
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Schedules
+ + + +
Signatures
+ +
+
+
For the Company
+
+
Signature
+
+
Name / Title: {{COMPANY_SIGNATORY}}
+
+
Date: {{SIGNATURE_DATE}}
+
+
+
For the Contractor
+
+
Signature
+
+
Name / Title: {{CONTRACTOR_SIGNATORY}}
+
+
Date: {{SIGNATURE_DATE}}
+
+
+ +
+ + diff --git a/engine/src/default_templates/independent_contractor_agreement_preview.html b/engine/src/default_templates/independent_contractor_agreement_preview.html new file mode 100644 index 0000000000..b84054137d --- /dev/null +++ b/engine/src/default_templates/independent_contractor_agreement_preview.html @@ -0,0 +1,234 @@ + + + + + + Independent Contractor Agreement — Preview + + + +
+ +
+ +
+
Independent Contractor Agreement
+
Independent Contractor Agreement
+
+ +
+ +
+ Reference: ICA-2026-004 + Effective Date: 1 March 2026 + Governing Law: England and Wales + Jurisdiction: Courts of England and Wales + Classification: Confidential +
+ +
+ +
Parties
+
+
+
Company
+
Vantage Digital Solutions Ltd
+
A private limited company
+
Reg. No.: 11223344
+
25 Broadgate, London, EC2M 2QS, United Kingdom
+
+
+
Contractor
+
Owen Clarke Consulting Ltd
+
A private limited company
+
Reg. No.: 99887766
+
14 Canal Street, Manchester, M1 3HW, United Kingdom
+
+
+ +
Agreement
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Schedules
+ + +
Signatures
+ +
+
+
For the Company
+
+
Signature
+
+
Name / Title: Sarah Whitfield, CEO
+
+
Date: 1 March 2026
+
+
+
For the Contractor
+
+
Signature
+
+
Name / Title: Owen Clarke, Director
+
+
Date: 1 March 2026
+
+
+ +
+ + diff --git a/engine/src/default_templates/invoice.html b/engine/src/default_templates/invoice.html new file mode 100644 index 0000000000..18e6d8f343 --- /dev/null +++ b/engine/src/default_templates/invoice.html @@ -0,0 +1,297 @@ + + + + + + Invoice {{INVOICE_NUMBER}} + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
Invoice
+
+
+
Invoice Number
+
{{INVOICE_NUMBER}}
+
+
+
+ + +
+ Invoice Date: {{INVOICE_DATE}} + Due Date: {{DUE_DATE}} +
+ +
+ + +
Parties
+
+
+
From
+
{{SUPPLIER_NAME}}
+
{{SUPPLIER_ADDRESS}}
+
+
+
Bill To
+
{{CUSTOMER_NAME}}
+
{{CUSTOMER_ADDRESS}}
+
+
+ + +
Line Items
+ + + + + + + + + + + + + {{LINEITEM_ROWS}} + +
#DescriptionQtyUnitUnit PriceLine Total
+ + +
+ + + + + + + +
Subtotal:{{SUBTOTAL_TEXT}}
Tax:{{TAX_TEXT}}
Total:{{TOTAL_TEXT}}
Balance Due:{{BALANCE_DUE_TEXT}}
+
+ + +
Payment Terms
+
{{PAYMENT_TERMS_TEXT}}
+ + +
Bank Details
+
{{BANK_DETAILS_TEXT}}
+ +
+ + diff --git a/engine/src/default_templates/invoice_preview.html b/engine/src/default_templates/invoice_preview.html new file mode 100644 index 0000000000..3f8c078630 --- /dev/null +++ b/engine/src/default_templates/invoice_preview.html @@ -0,0 +1,155 @@ + + + + + + Invoice + + + +
+
+
+
+
Invoice
+
Hartwell Consulting Ltd
+
+
+
Invoice Number
+
INV-2026-0047
+
+
+
+
+ Invoice Date: 15 January 2026 + Due Date: 14 February 2026 +
+
+
Parties
+
+
+
From
+
Hartwell Consulting Ltd
+
22 Canary Wharf, London, E14 5AB, UK
+
VAT No: GB123456789
+
+
+
Bill To
+
Pemberton Industries plc
+
8 Bridge Street, Manchester, M1 2JF, UK
+
Accounts Payable Dept.
+
+
+
Line Items
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#DescriptionQtyUnitUnit PriceLine Total
1Strategic advisory — Q4 2025 retainer1month£4,500.00£4,500.00
2Market analysis report — Sector Overview 20261report£1,200.00£1,200.00
3Workshop facilitation (half-day, 12 Jan 2026)1session£950.00£950.00
+
+ + + + + + + +
Subtotal:£6,650.00
VAT (20%):£1,330.00
Total:£7,980.00
Balance Due:£7,980.00
+
+
Payment Terms
+
Payment is due within 30 days of the invoice date. Late payments may incur interest at 8% per annum above the Bank of England base rate under the Late Payment of Commercial Debts (Interest) Act 1998.
+
Bank Details
+
Account Name: Hartwell Consulting Ltd +Sort Code: 20-00-00 +Account Number: 12345678 +Bank: Lloyds Bank plc, London +Reference: INV-2026-0047
+
+ + diff --git a/engine/src/default_templates/job_description.html b/engine/src/default_templates/job_description.html new file mode 100644 index 0000000000..3db9fd9d66 --- /dev/null +++ b/engine/src/default_templates/job_description.html @@ -0,0 +1,158 @@ + + + + + + Job Description + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{JOB_TITLE}}
+
{{COMPANY_NAME}}
+
+
+
+ + +
+ Department: {{DEPARTMENT}} + Location: {{LOCATION}} + Employment Type: {{EMPLOYMENT_TYPE}} + Salary Range: {{SALARY_RANGE}} +
+
+ + +
About the Role
+
{{ABOUT_ROLE_TEXT}}
+ + +
Key Responsibilities
+ + + +
Required Qualifications
+ + + +
Preferred Qualifications
+ + + +
What We Offer
+ + + +
How to Apply
+
{{HOW_TO_APPLY_TEXT}}
+ +
+ + diff --git a/engine/src/default_templates/job_description_preview.html b/engine/src/default_templates/job_description_preview.html new file mode 100644 index 0000000000..82ec553384 --- /dev/null +++ b/engine/src/default_templates/job_description_preview.html @@ -0,0 +1,115 @@ + + + + + + Job Description + + + +
+
+
+
+
Job Description
+
Hartwell Digital Agency Ltd
+
+
+
+
Lead Software Engineer
+
+
Department: Product Engineering
+
Location: Manchester (Hybrid — 2 days in office)
+
Employment Type: Full-Time, Permanent
+
Salary Range: £75,000 – £90,000 per annum
+
+ +
About the Role
+
Hartwell Digital Agency is looking for an experienced and technically driven Lead Software Engineer to join our Product Engineering team in Manchester. In this role you will own the technical direction of one of our core SaaS product lines, leading a team of six engineers through the full software development lifecycle. + +You will work closely with the Product Manager, Head of Engineering, and key stakeholders to translate strategic objectives into well-architected, scalable, and maintainable software. This is a hands-on leadership role; we expect you to write code, review pull requests, and set the technical standards for the team.
+ +
Key Responsibilities
+ + +
Required Qualifications
+ + +
Preferred Qualifications
+ + +
What We Offer
+ + +
How to Apply
+
Please submit your CV and a covering letter (maximum one page) explaining why you are interested in this role and what you would bring to the team. Applications should be sent to careers@hartwelldigital.co.uk with the subject line "Lead Software Engineer — [Your Name]". + +The closing date for applications is 13 March 2026. First-stage interviews will take place remotely via video call during the week commencing 23 March 2026. Shortlisted candidates will be invited to a technical assessment and in-person panel interview at our Manchester office. + +Hartwell Digital Agency is an equal opportunities employer. We welcome applications from candidates of all backgrounds and are committed to creating an inclusive workplace. We are happy to discuss reasonable adjustments throughout the recruitment process.
+
+ + diff --git a/engine/src/default_templates/letter.html b/engine/src/default_templates/letter.html new file mode 100644 index 0000000000..8e133388fb --- /dev/null +++ b/engine/src/default_templates/letter.html @@ -0,0 +1,228 @@ + + + + + + Business Letter + + + +
+ + +
+
+ {{LOGO_BLOCK}} +
{{SENDER_COMPANY}}
+
{{SENDER_ADDRESS}}
+
+
{{SENDER_CONTACT}}
+
+ + +
{{LETTER_DATE}}
+ + +
+
{{RECIPIENT_NAME}}
+
{{RECIPIENT_TITLE}}
+
{{RECIPIENT_COMPANY}}
+
{{RECIPIENT_ADDRESS}}
+
+ + +
+ Re:{{SUBJECT_LINE}} +
+ + +
{{SALUTATION}}
+ + +
{{BODY_TEXT}}
+ + +
{{CLOSING}}
+ + +
+
{{SENDER_NAME}}
+
{{SENDER_TITLE}}
+
{{SENDER_COMPANY}}
+
+ + + + +
+ + diff --git a/engine/src/default_templates/letter_of_intent.html b/engine/src/default_templates/letter_of_intent.html new file mode 100644 index 0000000000..8ae48f260a --- /dev/null +++ b/engine/src/default_templates/letter_of_intent.html @@ -0,0 +1,294 @@ + + + + + + Letter of Intent + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{DOC_TITLE}}
+
{{ORG_NAME}}
+
+
+
Reference
+
{{LOI_REF}}
+
+
+ +
+ + +
+ Date: {{LOI_DATE}} + From: {{PARTY_A_NAME}} + To: {{PARTY_B_NAME}} + Confidentiality: {{CONFIDENTIALITY_LABEL}} +
+ +
+ + +
Parties
+
+
+
Party A
+
{{PARTY_A_NAME}}
+
{{PARTY_A_ADDRESS}}
+
+
+
Party B
+
{{PARTY_B_NAME}}
+
{{PARTY_B_ADDRESS}}
+
+
+ + +
Terms
+ + + + + + + + + + +
Signatures
+ +
+
+
Party A
+
+
Signature
+
+
Name: {{PARTY_A_SIGNER}}
+
+
Title: {{PARTY_A_TITLE}}
+
+
Date:
+
+
+
Party B
+
+
Signature
+
+
Name: {{PARTY_B_SIGNER}}
+
+
Title: {{PARTY_B_TITLE}}
+
+
Date:
+
+
+ +
+ + diff --git a/engine/src/default_templates/letter_of_intent_preview.html b/engine/src/default_templates/letter_of_intent_preview.html new file mode 100644 index 0000000000..c2bce5188f --- /dev/null +++ b/engine/src/default_templates/letter_of_intent_preview.html @@ -0,0 +1,154 @@ + + + + + + Letter of Intent — Preview + + + +
+ +
+
+
+
Letter of Intent
+
Meridian Capital Advisors LLP
+
+
+
Reference
+
LOI-2026-0011
+
+
+
+ +
+ Confidentiality: CONFIDENTIAL + Date: 12 February 2026 + From: Meridian Capital Advisors LLP + To: Trident Manufacturing Group Ltd +
+ +
+ +
Parties
+
+
+
Party A
+
Meridian Capital Advisors LLP
+
45 Moorgate, London, EC2R 6AQ, United Kingdom
+
+
+
Party B
+
Trident Manufacturing Group Ltd
+
Unit 12, Phoenix Industrial Estate, Sheffield, S9 2GR, United Kingdom
+
+
+ +
Terms
+ + + + + + + + + +
Signatures
+ +
+
+
Party A
+
+
Signature
+
+
Name: Jonathan Ashford
+
+
Title: Managing Partner
+
+
Date:
+
+
+
Party B
+
+
Signature
+
+
Name: Diane Trevelyan
+
+
Title: Chief Executive Officer
+
+
Date:
+
+
+ +
+ + diff --git a/engine/src/default_templates/letter_preview.html b/engine/src/default_templates/letter_preview.html new file mode 100644 index 0000000000..9e294a9ceb --- /dev/null +++ b/engine/src/default_templates/letter_preview.html @@ -0,0 +1,90 @@ + + + + + + Business Letter + + + +
+
+
+
Greenway Partners LLP
+
7 Kings Cross Road, London, WC1X 9HB
+
+
+ Tel: +44 20 7946 0123
+ info@greenwaypartners.co.uk
+ www.greenwaypartners.co.uk +
+
+
19 February 2026
+
+
Ms. Sarah Chen
+
Procurement Director
+
Titan Manufacturing Ltd
+
22 Industrial Estate, Sheffield, S1 4AB
+
+
+ Re:Proposal for Supply Chain Consulting Services — Project Horizon +
+
Dear Ms. Chen,
+
Thank you for meeting with us on 12 February 2026 to discuss your supply chain optimisation challenges. We greatly appreciated the opportunity to learn about Titan Manufacturing's operations and the pressures you are facing as you scale production to meet growing European demand. + +Following our discussions, we are pleased to submit this formal proposal for a twelve-week supply chain diagnostic and optimisation engagement. Our team will conduct an end-to-end review of your procurement, inventory, and logistics functions, benchmarking them against industry best practice and identifying measurable opportunities for cost reduction and lead-time improvement. + +Based on our initial assessment, we anticipate delivering savings of between 8 and 14 per cent on total supply chain costs, with an expected payback period of under six months. We would mobilise a dedicated team of three senior consultants with direct sector experience in precision manufacturing and European distribution networks. + +We would welcome the opportunity to present our detailed methodology and proposed workplan at your earliest convenience. Please do not hesitate to contact me directly should you require any further information or wish to arrange a follow-up meeting.
+
Yours sincerely,
+
+
Jonathan Rowe
+
Partner, Supply Chain Advisory
+
Greenway Partners LLP
+
+ +
+ + diff --git a/engine/src/default_templates/master_services_agreement.html b/engine/src/default_templates/master_services_agreement.html new file mode 100644 index 0000000000..f511114941 --- /dev/null +++ b/engine/src/default_templates/master_services_agreement.html @@ -0,0 +1,286 @@ + + + + + + Master Services Agreement + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{DOC_TITLE}}
+
{{ORG_NAME}}
+
+
+
Reference
+
{{AGREEMENT_REF}}
+
+
+ +
+ + +
+ Effective Date: {{EFFECTIVE_DATE}} + Provider: {{PROVIDER_NAME}} + Provider Address: {{PROVIDER_ADDRESS}} + Client: {{CLIENT_NAME}} + Client Address: {{CLIENT_ADDRESS}} + Confidentiality: {{CONFIDENTIALITY_LABEL}} +
+ +
+ + +
Background
+ + + +
Agreement
+ + + + + + + + + + + + + + + + + + + + +
Signatures
+ +
+
+
Provider
+
+
Signature
+
+
Name: {{PROVIDER_SIGNER}}
+
+
Title: {{PROVIDER_TITLE}}
+
+
Date:
+
+
+
Client
+
+
Signature
+
+
Name: {{CLIENT_SIGNER}}
+
+
Title: {{CLIENT_TITLE}}
+
+
Date:
+
+
+ +
+ + diff --git a/engine/src/default_templates/master_services_agreement_preview.html b/engine/src/default_templates/master_services_agreement_preview.html new file mode 100644 index 0000000000..d18fc65f94 --- /dev/null +++ b/engine/src/default_templates/master_services_agreement_preview.html @@ -0,0 +1,157 @@ + + + + + + Master Services Agreement — Preview + + + +
+ +
+
+
+
Master Services Agreement
+
Pinnacle Cloud Services Ltd
+
+
+
Reference
+
MSA-2026-0008
+
+
+
+ +
+ Confidentiality: CONFIDENTIAL + Effective Date: 1 February 2026 + Provider: Pinnacle Cloud Services Ltd + Provider Address: 22 Finsbury Square, London, EC2A 1DX, United Kingdom + Client: Caldwell Retail Group plc + Client Address: 100 Regent Street, London, W1B 5TB, United Kingdom +
+ +
+ +
Background
+ + +
Agreement
+ + + + + + + + + + + + + + + + + + + +
Signatures
+ +
+
+
Provider
+
+
Signature
+
+
Name: Marcus Webb
+
+
Title: Chief Commercial Officer
+
+
Date:
+
+
+
Client
+
+
Signature
+
+
Name: Rachel Caldwell
+
+
Title: Chief Technology Officer
+
+
Date:
+
+
+ +
+ + diff --git a/engine/src/default_templates/meeting_minutes.html b/engine/src/default_templates/meeting_minutes.html new file mode 100644 index 0000000000..a52ccd7b32 --- /dev/null +++ b/engine/src/default_templates/meeting_minutes.html @@ -0,0 +1,289 @@ + + + + + + Meeting Minutes + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{MEETING_TITLE}}
+
{{COMPANY_NAME}} · Meeting Minutes
+
+
+
+ + +
+ Date: {{MEETING_DATE}} + Time: {{MEETING_TIME}} + Location: {{MEETING_LOCATION}} + Chairperson:{{CHAIR_NAME}} + Minutes By:{{SECRETARY_NAME}} +
+ +
+ + +
Attendees
+ + + + + + + + + + + {{ATTENDEES_ROWS}} + +
NameTitle / RoleDepartmentPresent
+ + +
Discussion
+ {{DISCUSSION_ITEMS}} + + +
Action Items
+ + + + + + + + + + + {{ACTION_ROWS}} + +
Action ItemOwnerDue DateStatus
+ + +
Next Meeting
+
+ Date & Time: {{NEXT_MEETING_DATE}} + Location: {{NEXT_MEETING_LOCATION}} +
+ + +
Approval
+
+
+
Prepared By
+
+
{{MINUTES_PREPARED_BY}}
+
+
+
Approved By
+
+
{{CHAIR_NAME}}
+
+
+ +
+ + diff --git a/engine/src/default_templates/meeting_minutes_preview.html b/engine/src/default_templates/meeting_minutes_preview.html new file mode 100644 index 0000000000..8f236791bf --- /dev/null +++ b/engine/src/default_templates/meeting_minutes_preview.html @@ -0,0 +1,149 @@ + + + + + + Meeting Minutes + + + +
+
+
+
+
Meeting Minutes
+
Stirling Technologies Ltd
+
+
+
+
+ Date: 18 February 2026 + Time: 10:00 – 11:45 + Location: Conference Room B, London HQ + Chairperson:James Whitfield + Minutes By: Amelia Frost +
+
+
Attendees
+ + + + + + + + + + + + + + + + + +
NameTitle / RoleDepartmentPresent
James WhitfieldChief Technology OfficerEngineeringYes
Amelia FrostProduct ManagerProductYes
Rajan MehtaLead EngineerEngineeringYes
Sophie LangtonUX DesignerDesignYes
Oliver DrummondHead of SalesCommercialYes
Niamh O'BrienData AnalystAnalyticsNo (apologies sent)
+
Discussion
+
+
1. Q1 Roadmap Review
+
James opened with an overview of Q4 2025 delivery performance. Eight of nine planned features shipped on schedule; the smart-folder batch processing feature slipped by two weeks due to third-party API instability. The team agreed to carry this forward as a priority for Q1. Oliver noted positive customer feedback on the PDF merge redesign and requested it be highlighted in upcoming marketing materials.
+
+
+
2. New Feature Prioritisation
+
Amelia presented the backlog ranked by customer impact score. The top three items approved for Q1 development are: (1) AI-assisted document generation templates, (2) bulk processing queue with progress tracking, and (3) white-label branding options for enterprise clients. Sophie noted the design system update must be completed before white-label work begins and estimated a two-week lead time.
+
+
+
3. Infrastructure and Performance
+
Rajan presented benchmark results showing 23% improvement in PDF rendering speed following the January infrastructure upgrade. He proposed migrating the job queue to a dedicated worker cluster to support bulk processing. Estimated cost is £1,200/month; James approved provisionally pending finance sign-off. Target completion date: 14 March 2026.
+
+
+
4. Q2 Planning Preview
+
Brief discussion of Q2 themes: mobile application development and enhanced analytics dashboard. Oliver emphasised that the sales team urgently requires improved usage reporting for enterprise renewal conversations. Amelia to circulate a draft Q2 roadmap for async review by 25 February 2026.
+
+
Action Items
+ + + + + + + + + + + + + + + + +
Action ItemOwnerDue DateStatus
Finalise smart-folder batch processing spec and begin sprintRajan Mehta25 Feb 2026Open
Complete design system update to unblock white-label workSophie Langton4 Mar 2026Open
Submit infrastructure upgrade cost for finance approvalJames Whitfield21 Feb 2026Open
Circulate draft Q2 roadmap for team reviewAmelia Frost25 Feb 2026Open
Prepare PDF merge feature highlights for marketingOliver Drummond28 Feb 2026Open
+
Next Meeting
+
+ Date & Time: 18 March 2026, 10:00 + Location: Conference Room B, London HQ +
+
Approval
+
+
+
Prepared By
+
+
Amelia Frost
+
+
+
Approved By
+
+
James Whitfield
+
+
+
+ + diff --git a/engine/src/default_templates/nondisclosure_agreement.html b/engine/src/default_templates/nondisclosure_agreement.html new file mode 100644 index 0000000000..6366059180 --- /dev/null +++ b/engine/src/default_templates/nondisclosure_agreement.html @@ -0,0 +1,311 @@ + + + + + + Non-Disclosure Agreement + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{AGREEMENT_TITLE}}
+
Non-Disclosure Agreement
+
+
+ +
+ + +
+ Effective Date: {{EFFECTIVE_DATE}} + Governing Law: {{GOVERNING_LAW}} +
+ +
+ + +
Parties
+
+
+
Disclosing Party
+
{{DISCLOSING_PARTY_NAME}}
+
{{DISCLOSING_PARTY_ADDRESS}}
+
+
+
Receiving Party
+
{{RECEIVING_PARTY_NAME}}
+
{{RECEIVING_PARTY_ADDRESS}}
+
+
+ + +
Agreement
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Signatures
+
+
+
Disclosing Party
+
+
Signature
+
+
Name: {{DISCLOSING_SIGNER_NAME}}
+
+
Title: {{DISCLOSING_SIGNER_TITLE}}
+
+
Date: {{DISCLOSING_SIGN_DATE}}
+
+
+
Receiving Party
+
+
Signature
+
+
Name: {{RECEIVING_SIGNER_NAME}}
+
+
Title: {{RECEIVING_SIGNER_TITLE}}
+
+
Date: {{RECEIVING_SIGN_DATE}}
+
+
+ +
+ + diff --git a/engine/src/default_templates/nondisclosure_agreement_preview.html b/engine/src/default_templates/nondisclosure_agreement_preview.html new file mode 100644 index 0000000000..b36bab057c --- /dev/null +++ b/engine/src/default_templates/nondisclosure_agreement_preview.html @@ -0,0 +1,130 @@ + + + + + + Non-Disclosure Agreement + + + +
+
+
+
+
Non-Disclosure Agreement
+
Mutual NDA
+
+
+
+
+ Effective Date: 1 February 2026 + Governing Law: England and Wales +
+
+
Parties
+
+
+
Disclosing Party
+
Kestrel Technologies Ltd
+
14 Innovation Drive, Cambridge, CB1 3AA, UK
+
+
+
Receiving Party
+
Moorfield Capital Partners LLP
+
3 King Street, London, EC2V 8BD, UK
+
+
+
Agreement
+ + + + + + +
Signatures
+
+
+
Disclosing Party
+
+
Signature
+
+
Name: Jonathan Kestrel
+
+
Title: Chief Executive Officer
+
+
Date: 1 February 2026
+
+
+
Receiving Party
+
+
Signature
+
+
Name: Priya Moorfield
+
+
Title: Managing Partner
+
+
Date: 1 February 2026
+
+
+
+ + diff --git a/engine/src/default_templates/offer_letter.html b/engine/src/default_templates/offer_letter.html new file mode 100644 index 0000000000..af18faa911 --- /dev/null +++ b/engine/src/default_templates/offer_letter.html @@ -0,0 +1,215 @@ + + + + + + Offer of Employment + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{DOC_TITLE}}
+
{{ORG_NAME}}
+
+
+ +
+ + +
{{CANDIDATE_NAME}}
+
We are delighted to extend this offer of employment and look forward to welcoming you to our team.
+ + +
Role Details
+
+ Position: {{POSITION_TITLE}} + Department: {{DEPARTMENT}} + Start Date: {{START_DATE}} +
+ + +
Compensation
+
{{COMPENSATION_TEXT}}
+ + +
Benefits
+
{{BENEFITS_TEXT}}
+ + +
Terms and Conditions
+
{{TERMS_TEXT}}
+ + +
Please sign and return this letter by {{ACCEPTANCE_DEADLINE}} to confirm your acceptance of this offer.
+ + +
Signatures
+
+
+
On Behalf of {{ORG_NAME}}
+
+
Signature
+
+
Name: {{SIGNER_NAME}}
+
+
Title: {{SIGNER_TITLE}}
+
+
Date
+
+
+
Candidate Acceptance
+
+
Signature
+
+
Name: {{CANDIDATE_NAME}}
+
+
Date
+
+
+ +
+ + diff --git a/engine/src/default_templates/offer_letter_preview.html b/engine/src/default_templates/offer_letter_preview.html new file mode 100644 index 0000000000..c19755e81a --- /dev/null +++ b/engine/src/default_templates/offer_letter_preview.html @@ -0,0 +1,96 @@ + + + + + + Offer of Employment + + + +
+
+
+
+
Offer Letter
+
Redwood Analytics Ltd
+
+
+
+
Sophie Hargreaves
+
We are delighted to extend this offer of employment and look forward to welcoming you to our team.
+
Role Details
+
+ Position: Senior Data Analyst + Department: Product Intelligence + Start Date: 2 March 2026 +
+
Compensation
+
Your annual base salary will be £58,000, paid monthly in arrears on the last working day of each month. You will be eligible for an annual performance bonus of up to 15% of your base salary, subject to the achievement of agreed targets. A salary review will take place each April, with any adjustment effective from 1 April.
+
Benefits
+
You will be entitled to 25 days' annual leave per year, plus UK public holidays. The company operates a contributory pension scheme (employer contribution: 5%, employee minimum: 3%). Additional benefits include private health insurance (BUPA), a cycle-to-work scheme, and access to our employee assistance programme.
+
Terms and Conditions
+
Employment is subject to satisfactory completion of a three-month probationary period, receipt of two satisfactory references, and evidence of your right to work in the United Kingdom. This offer is also conditional on a satisfactory Disclosure and Barring Service (DBS) check where applicable. Full terms and conditions are set out in the contract of employment enclosed with this letter.
+
Please sign and return this letter by 28 February 2026 to confirm your acceptance of this offer.
+
Signatures
+
+
+
On Behalf of Redwood Analytics Ltd
+
+
Signature
+
+
Name: Marcus Webb
+
+
Title: Head of People & Culture
+
+
Date
+
+
+
Candidate Acceptance
+
+
Signature
+
+
Name: Sophie Hargreaves
+
+
Date
+
+
+
+ + diff --git a/engine/src/default_templates/official_memo.html b/engine/src/default_templates/official_memo.html new file mode 100644 index 0000000000..e497306bbe --- /dev/null +++ b/engine/src/default_templates/official_memo.html @@ -0,0 +1,134 @@ + + + + + + Memorandum + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
Memorandum
+
{{COMPANY_NAME}}
+
+
+ +
+ + +
+ To: {{MEMO_TO}} + From: {{MEMO_FROM}} + Date: {{MEMO_DATE}} + Subject: {{MEMO_SUBJECT}} + CC: {{MEMO_CC}} +
+ + +
{{MEMO_BODY}}
+ + +
Attachments
+
{{MEMO_ATTACHMENTS}}
+ + +
Closing
+
{{MEMO_CLOSING}}
+ +
+ + diff --git a/engine/src/default_templates/official_memo_preview.html b/engine/src/default_templates/official_memo_preview.html new file mode 100644 index 0000000000..ced5a6cd89 --- /dev/null +++ b/engine/src/default_templates/official_memo_preview.html @@ -0,0 +1,71 @@ + + + + + + Memorandum + + + +
+
+
+
+
Memorandum
+
Ashford & Partners LLP
+
+
+
+
+ To: All Senior Associates and Partners + From: Catherine Ashford, Managing Partner + Date: 19 February 2026 + Subject: Updated Remote Working Policy — Effective 1 March 2026 + CC: HR Department; Office Manager +
+
Following the firm-wide review conducted in January, we are pleased to confirm a revised remote working policy that will take effect on 1 March 2026. + +All fee-earners and support staff at Band 3 and above are eligible to work remotely for up to two days per week, provided that client-facing commitments and team meeting schedules are maintained. Requests must be agreed with your supervising partner at least one week in advance. + +Please be aware that the following expectations remain unchanged regardless of work location: billable hour targets, client response times (within four business hours), and mandatory attendance at the monthly all-hands meeting held on the first Monday of each month. + +Detailed guidance, including the updated remote-working agreement form, is available on the intranet under HR > Policies > Remote Working. Please complete and return the form to HR no later than 21 February 2026. + +Questions should be directed to Gemma Clarke in Human Resources.
+
Attachments
+
1. Remote Working Agreement Form (Rev. 3, Feb 2026) 2. IT Security Guidelines for Remote Access
+
Closing
+
We appreciate your cooperation and look forward to a smooth transition. Please do not hesitate to contact HR if you have any questions.
+
+ + diff --git a/engine/src/default_templates/pay_stub.html b/engine/src/default_templates/pay_stub.html new file mode 100644 index 0000000000..f80b4f6e9c --- /dev/null +++ b/engine/src/default_templates/pay_stub.html @@ -0,0 +1,346 @@ + + + + + + Pay Stub + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
Pay Stub
+
{{COMPANY_NAME}}
+
+
+ +
+ + +
+
+
Employee Name:{{EMPLOYEE_NAME}}
+
Pay Period:{{PAY_PERIOD_START}} – {{PAY_PERIOD_END}}
+
Employee ID:{{EMPLOYEE_ID}}
+
Pay Date:{{PAY_DATE}}
+
Department:{{DEPARTMENT}}
+
Position:{{POSITION}}
+
+
+ + +
+
+
Earnings
+ + + + + + + + + + + {{EARNINGS_ROWS}} + + + + + + + +
DescriptionHoursRateAmount
Gross Pay{{GROSS_PAY}}
+
+
+
Deductions
+ + + + + + + + + {{DEDUCTIONS_ROWS}} + + + + + + + +
DescriptionAmount
Total Deductions{{TOTAL_DEDUCTIONS}}
+
+
+ + +
+
Net Pay
+
{{NET_PAY}}
+
+ + +
Year-to-Date Summary
+ + + + + + + + + + + + + + + + + +
Gross PayTotal DeductionsNet Pay
YTD{{YTD_GROSS}}{{YTD_DEDUCTIONS}}{{YTD_NET}}
+ + +
Payment Details
+
{{PAYMENT_METHOD}}
+ +
+ + diff --git a/engine/src/default_templates/pay_stub_preview.html b/engine/src/default_templates/pay_stub_preview.html new file mode 100644 index 0000000000..71cee380f0 --- /dev/null +++ b/engine/src/default_templates/pay_stub_preview.html @@ -0,0 +1,184 @@ + + + + + + Pay Stub + + + +
+
+
+
+
Pay Stub
+
Meridian Technologies Ltd
+
+
+
+
+
+
Employee Name:Sarah J. Mitchell
+
Pay Period:1 February 2026 – 28 February 2026
+
Employee ID:EMP-0142
+
Pay Date:28 February 2026
+
Department:Engineering
+
Position:Senior Software Engineer
+
+
+
+
+
Earnings
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DescriptionHoursRateAmount
Basic Salary160£30.21£4,833.33
Overtime8£45.31£362.50
On-Call Allowance£150.00
Gross Pay£5,345.83
+
+
+
Deductions
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DescriptionAmount
Income Tax (PAYE)£1,156.60
National Insurance£455.62
Pension (Employee 5%)£267.29
Health Insurance£85.00
Total Deductions£1,964.51
+
+
+
+
Net Pay
+
£3,381.32
+
+
Year-to-Date Summary
+ + + + + + + + + + + + + + + + + +
Gross PayTotal DeductionsNet Pay
YTD£10,524.16£3,871.42£6,652.74
+
Payment Details
+
Payment Method: BACS Direct Credit
Bank: Lloyds Bank plc  |  Sort Code: 30-00-00  |  Account: ****4821
+
+ + diff --git a/engine/src/default_templates/performance_review.html b/engine/src/default_templates/performance_review.html new file mode 100644 index 0000000000..3b78492df8 --- /dev/null +++ b/engine/src/default_templates/performance_review.html @@ -0,0 +1,316 @@ + + + + + + Performance Review + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
Employee Performance Review
+
{{ORG_NAME}}
+
+
+
+ + +
Review Information
+
+
+
Employee Name
+
{{EMPLOYEE_NAME}}
+
+
+
Employee ID
+
{{EMPLOYEE_ID}}
+
+
+
Job Title
+
{{JOB_TITLE}}
+
+
+
Department
+
{{DEPARTMENT}}
+
+
+
Manager
+
{{MANAGER_NAME}}
+
+
+
Review Period
+
{{REVIEW_PERIOD_START}} to {{REVIEW_PERIOD_END}}
+
+
+ + +
{{RATING_SCALE_TEXT}}
+ + +
Goals and Outcomes
+
Goals:
{{GOALS_TEXT}}
+
Outcomes:
{{GOAL_OUTCOMES_TEXT}}
+ + +
Competencies and Ratings
+ + + + + + + + + + {{COMPETENCY_ROWS}} + +
CompetencyRating (1–5)Comments
+ + +
Key Accomplishments
+
{{ACCOMPLISHMENTS_TEXT}}
+ + +
Areas for Improvement
+
{{IMPROVEMENT_TEXT}}
+ + +
Development Plan
+
{{DEVELOPMENT_PLAN_TEXT}}
+ + +
Overall Rating
+
{{OVERALL_RATING}}
+ + +
Employee Comments
+
{{EMPLOYEE_COMMENTS_TEXT}}
+ + +
Manager Comments
+
{{MANAGER_COMMENTS_TEXT}}
+ + +
Acknowledgement
+
Signatures below acknowledge receipt and discussion of this review. Signature does not necessarily indicate agreement with all conclusions.
+
+
+
Manager
+
+
Signature
+
+
Name: {{MANAGER_NAME}}
+
+
Date: {{MANAGER_SIGN_DATE}}
+
+
+
Employee
+
+
Signature
+
+
Name: {{EMPLOYEE_NAME}}
+
+
Date: {{EMPLOYEE_SIGN_DATE}}
+
+
+ +
+ + diff --git a/engine/src/default_templates/performance_review_preview.html b/engine/src/default_templates/performance_review_preview.html new file mode 100644 index 0000000000..fc85512046 --- /dev/null +++ b/engine/src/default_templates/performance_review_preview.html @@ -0,0 +1,177 @@ + + + + + + Performance Review + + + +
+
+
+
+
Performance Review
+
Cavendish Financial Solutions Ltd
+
+
+
+
Review Information
+
+
+
Employee Name
+
Priya Nair
+
+
+
Employee ID
+
EMP-2847
+
+
+
Job Title
+
Senior Business Analyst
+
+
+
Department
+
Strategy & Operations
+
+
+
Manager
+
Jonathan Ashby
+
+
+
Review Period
+
1 January 2025 to 31 December 2025
+
+
+
Rating scale: 1 = Does Not Meet Expectations  |  2 = Partially Meets Expectations  |  3 = Meets Expectations  |  4 = Exceeds Expectations  |  5 = Outstanding
+
Goals and Outcomes
+
Goals:
1. Lead the migration of legacy reporting infrastructure to the new data platform by Q3 2025. +2. Achieve a client satisfaction score of 4.5 or above across assigned accounts. +3. Complete the Chartered Business Analysis Professional (CBAP) certification.
+
Outcomes:
The data platform migration was completed two weeks ahead of schedule in August 2025, with zero production incidents during cutover. Client satisfaction scores averaged 4.7 across all assigned accounts. Priya successfully attained her CBAP certification in October 2025.
+
Competencies and Ratings
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CompetencyRating (1–5)Comments
Problem Solving & Analysis5Consistently applies structured methodologies; proactively identifies root causes.
Communication & Stakeholder Management4Clear and concise communicator; builds strong relationships across all levels.
Delivery & Results Orientation5Delivered all major workstreams on time or ahead of schedule throughout the year.
Teamwork & Collaboration4A valued team member who readily supports colleagues and shares knowledge.
Adaptability & Learning5Embraced new tooling and methodology changes with notable enthusiasm.
+
Key Accomplishments
+
Successfully led a cross-functional team of eight in delivering the data platform migration, generating an estimated annual saving of £180,000 in licensing and maintenance costs. Authored a new requirements management framework that has since been adopted company-wide. Mentored two junior analysts, both of whom achieved promotion to Analyst II during the review period.
+
Areas for Improvement
+
Delegation: Priya occasionally takes on too much individually rather than distributing work effectively across the team. Developing stronger delegation habits will be critical as she moves towards a lead role. + +Presentation skills: While written communication is excellent, further development of executive-level presentation delivery would strengthen impact with senior stakeholders.
+
Development Plan
+
1. Attend the internal Leadership Essentials programme (Q1 2026). +2. Co-present at the annual All-Hands strategy review in March 2026 alongside the Head of Strategy. +3. Shadow the Delivery Director on at least two client pitches before Q2 2026. +4. Set up a fortnightly one-to-one with direct reports to strengthen delegation practices.
+
Overall Rating
+
4.6 — Exceeds Expectations
+
Employee Comments
+
I am proud of what the team achieved with the platform migration this year and grateful for the support and autonomy Jonathan provided throughout. I welcome the feedback on delegation and will prioritise the Leadership Essentials programme. I am excited about the development opportunities outlined for 2026.
+
Manager Comments
+
Priya has had an outstanding year and is one of the most effective contributors in the Strategy & Operations team. Her technical depth, client focus, and commitment to professional development set an excellent example. I look forward to supporting her continued growth into a senior leadership position in 2026.
+
Acknowledgement
+
Signatures below acknowledge receipt and discussion of this review. Signature does not necessarily indicate agreement with all conclusions.
+
+
+
Manager
+
+
Signature
+
+
Name: Jonathan Ashby
+
+
Date: 15 January 2026
+
+
+
Employee
+
+
Signature
+
+
Name: Priya Nair
+
+
Date: 16 January 2026
+
+
+
+ + diff --git a/engine/src/default_templates/price_sheet.html b/engine/src/default_templates/price_sheet.html new file mode 100644 index 0000000000..7d35dafb5f --- /dev/null +++ b/engine/src/default_templates/price_sheet.html @@ -0,0 +1,185 @@ + + + + + + Price Sheet + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{PRICE_SHEET_TITLE}}
+
{{COMPANY_NAME}}
+
+
+
Effective Date
+
{{EFFECTIVE_DATE}}
+
+
+
+ + +
+ Currency: {{CURRENCY}} +
+ +
+ + +
Pricing
+ + + + + + + + + + + + {{PRICE_ROWS}} + +
SKUDescriptionUnitUnit PriceNotes
+ + +
Notes
+
{{NOTES_TEXT}}
+ + +
Terms
+
{{TERMS_TEXT}}
+ +
+ + diff --git a/engine/src/default_templates/price_sheet_preview.html b/engine/src/default_templates/price_sheet_preview.html new file mode 100644 index 0000000000..0270633b84 --- /dev/null +++ b/engine/src/default_templates/price_sheet_preview.html @@ -0,0 +1,143 @@ + + + + + + Price Sheet + + + +
+
+
+
+
Price Sheet
+
Vantage Cloud Services Ltd
+
+
+
Effective Date
+
1 March 2026
+
+
+
+
+ Currency: GBP (£) — All prices exclude VAT +
+
+
Pricing
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SKUDescriptionUnitUnit PriceNotes
VCS-001Starter Plan — up to 5 usersper month£49.00Billed monthly; cancel anytime
VCS-002Growth Plan — up to 25 usersper month£149.00Includes advanced analytics
VCS-003Business Plan — up to 100 usersper month£399.00SSO & priority support included
VCS-004Enterprise Plan — unlimited usersper month£899.00Dedicated account manager
VCS-010Annual licence discount (any plan)per year−20%Pay annually, save 20%
VCS-020Professional services — implementationper day£1,200.00Min. 1 day; remote or on-site
VCS-021Training workshop (half-day, up to 10 delegates)per session£650.00Remote delivery only
VCS-030Additional data storageper 100 GB/month£18.00First 50 GB included in plan
+
Notes
+
All prices are quoted in GBP and exclude VAT at the prevailing rate. Custom enterprise pricing is available for organisations requiring more than 500 users or bespoke SLA terms — please contact sales@vantagecloud.co.uk for a tailored quote. Volume discounts of up to 15% are available for multi-year commitments.
+
Terms
+
Subscriptions are invoiced monthly in advance unless an annual plan is selected. Payment is due within 14 days of invoice. Prices are subject to annual review; customers will be notified no less than 60 days before any change takes effect. All plans are subject to Vantage Cloud Services Standard Terms and Conditions (available at vantagecloud.co.uk/terms).
+
+ + diff --git a/engine/src/default_templates/purchase_order.html b/engine/src/default_templates/purchase_order.html new file mode 100644 index 0000000000..59f9f68ff1 --- /dev/null +++ b/engine/src/default_templates/purchase_order.html @@ -0,0 +1,377 @@ + + + + + + {{DOCUMENT_TITLE}} + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{DOCUMENT_TITLE}}
+
{{BUYER_NAME}}
+
+
+
PO Number
+
{{PO_NUMBER}}
+
+
+ +
+ + +
+ PO Date: {{PO_DATE}} + Buyer Reference: {{BUYER_REFERENCE}} + Vendor Reference: {{VENDOR_REFERENCE}} + Currency: {{CURRENCY}} + Requested Delivery:{{REQUESTED_DELIVERY_DATE}} + Delivery Method: {{DELIVERY_METHOD}} + Incoterms: {{INCOTERMS}} + Payment Terms: {{PAYMENT_TERMS}} + Confidentiality: {{CONFIDENTIALITY_LABEL}} +
+ +
+ + +
Parties & Addresses
+
+
+
Buyer
+
{{BUYER_NAME}}
+
{{BUYER_ADDRESS}}
+
Contact: {{BUYER_CONTACT_NAME}}
+
Email: {{BUYER_CONTACT_EMAIL}}
+
Phone: {{BUYER_CONTACT_PHONE}}
+
+
+
Vendor
+
{{VENDOR_NAME}}
+
{{VENDOR_ADDRESS}}
+
Contact: {{VENDOR_CONTACT_NAME}}
+
Email: {{VENDOR_CONTACT_EMAIL}}
+
Phone: {{VENDOR_CONTACT_PHONE}}
+
+
+ +
+
+
Bill To
+
{{BILL_TO_NAME}}
+
{{BILL_TO_ADDRESS}}
+
+
+
Ship To
+
{{SHIP_TO_NAME}}
+
{{SHIP_TO_ADDRESS}}
+
+
+ + +
Line Items
+ + + + + + + + + + + + {{LINEITEM_ROWS}} + +
#DescriptionQtyUnit PriceLine Total
+ + +
+ + + + + + + + +
Subtotal:{{SUBTOTAL_TEXT}}
Tax:{{TAX_TEXT}}
Shipping:{{SHIPPING_TEXT}}
Discount:{{DISCOUNT_TEXT}}
Total:{{TOTAL_TEXT}}
+
+ + +
Notes
+
{{NOTES_TEXT}}
+ + +
Special Instructions
+
{{SPECIAL_INSTRUCTIONS_TEXT}}
+ + +
Terms and Conditions
+
{{TERMS_TEXT}}
+ + +
Authorisation
+ + + + + + +
Authorised by:{{AUTHORIZED_BY}}
Title:{{AUTHORIZED_TITLE}}
Date:{{AUTHORIZED_DATE}}
+ + + + +
+ + diff --git a/engine/src/default_templates/purchase_order_preview.html b/engine/src/default_templates/purchase_order_preview.html new file mode 100644 index 0000000000..561a948e08 --- /dev/null +++ b/engine/src/default_templates/purchase_order_preview.html @@ -0,0 +1,195 @@ + + + + + + Purchase Order + + + +
+
+
+
+
Purchase Order
+
Sterling Systems Group Ltd
+
+
+
PO Number
+
PO-2026-0184
+
+
+
+
+ PO Date: 12 January 2026 + Buyer Reference: REQ-A19-772 + Vendor Reference: QUOTE-Q4481 + Currency: GBP + Requested Delivery:19 January 2026 + Delivery Method: Courier + Incoterms: DAP (Delivered at Place) + Payment Terms: Net 30 +
+
+
Parties & Addresses
+
+
+
Buyer
+
Sterling Systems Group Ltd
+
100 Kingfisher Way, London, EC2V 7AB, UK
+
Contact: Operations Procurement
+
Email: procurement@example.com
+
Phone: +44 20 0000 0000
+
+
+
Vendor
+
Northbridge Office Supply Co.
+
14 Meridian Park, Reading, RG1 1AA, UK
+
Contact: Accounts Desk
+
Email: orders@example.com
+
Phone: +44 118 000 0000
+
+
+
+
+
Bill To
+
Sterling Systems Group Ltd (Accounts Payable)
+
100 Kingfisher Way, London, EC2V 7AB, UK
+
+
+
Ship To
+
Sterling Systems Group Ltd (Receiving)
+
Dock 2, 100 Kingfisher Way, London, EC2V 7AB, UK
+
+
+
Line Items
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#DescriptionQtyUnitUnit PriceLine Total
1A4 paper, 80gsm (box of 5 reams)20box£18.50£370.00
2Black toner cartridge (Model X200)6unit£74.00£444.00
3Heavy-duty archive boxes (pack of 10)8pack£21.25£170.00
+
+ + + + + + + + +
Subtotal:£984.00
Tax (20% VAT):£196.80
Shipping:£25.00
Discount:-£30.00
Total:£1,175.80
+
+
Notes
+
Deliveries accepted Mon–Fri, 09:00–16:00. Please reference the PO number on all delivery documents.
+
Special Instructions
+
Pack cartons securely; label each carton with PO number and delivery contact.
+
Terms and Conditions
+
Payment per stated terms; substitutions require buyer approval; notify buyer of delays exceeding 48 hours.
+
Authorisation
+ + + + + + +
Authorised by:Alexandra Moore
Title:Head of Operations
Date:12 January 2026
+ +
+ + diff --git a/engine/src/default_templates/quote.html b/engine/src/default_templates/quote.html new file mode 100644 index 0000000000..f3b772faad --- /dev/null +++ b/engine/src/default_templates/quote.html @@ -0,0 +1,358 @@ + + + + + + Quotation {{QUOTE_NUMBER}} + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
Quotation
+
{{VENDOR_NAME}}
+
+
+
Quote Number
+
{{QUOTE_NUMBER}}
+
+
+
+ + +
+ Quote Date: {{QUOTE_DATE}} + Valid Until: {{VALID_UNTIL}} + Reference: {{REFERENCE}} +
+ +
+ + +
Parties
+
+
+
From
+
{{VENDOR_NAME}}
+
{{VENDOR_ADDRESS}}
+
+
+
Quote To
+
{{CLIENT_NAME}}
+
{{CLIENT_ADDRESS}}
+
+
+ + +
Line Items
+ + + + + + + + + + + + + {{LINEITEM_ROWS}} + +
#DescriptionQtyUnitUnit PriceLine Total
+ + +
+ + + + + + + +
Subtotal:{{SUBTOTAL_TEXT}}
Discount:{{DISCOUNT_TEXT}}
Tax:{{TAX_TEXT}}
Total:{{TOTAL_TEXT}}
+
+ + +
Terms & Conditions
+
{{TERMS_TEXT}}
+ + +
Notes
+
{{NOTES_TEXT}}
+ + +
+
Acceptance
+
By signing below, the client accepts this quotation and authorises the work described herein. This quotation is valid until {{ACCEPTANCE_DEADLINE}}.
+
+
+
+
Authorised Signature
+
+
+
+
Date
+
+
+
+ +
+ + diff --git a/engine/src/default_templates/quote_preview.html b/engine/src/default_templates/quote_preview.html new file mode 100644 index 0000000000..7effec9b29 --- /dev/null +++ b/engine/src/default_templates/quote_preview.html @@ -0,0 +1,187 @@ + + + + + + Quotation + + + +
+
+
+
+
Quotation
+
Axiom Technology Solutions Ltd
+
+
+
Quote Number
+
QT-2026-0089
+
+
+
+
+ Quote Date: 19 February 2026 + Valid Until: 19 March 2026 + Reference: BLP-IT-2026-Q3 +
+
+
Parties
+
+
+
From
+
Axiom Technology Solutions Ltd
+
9 Silicon Way, Reading, RG1 1EH, UK
+
VAT: GB456789012 · sales@axiomtech.co.uk
+
+
+
Quote To
+
Brookfield Legal Partners LLP
+
55 Temple Row, Birmingham, B2 5LU, UK
+
Attn: Marcus Hale, IT Manager
+
+
+
Line Items
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#DescriptionQtyUnitUnit PriceLine Total
1Dell OptiPlex 7020 Desktop (i7, 32 GB RAM, 512 GB SSD)15unit£849.00£12,735.00
2Dell 27" UltraSharp Monitor U2724D15unit£389.00£5,835.00
3Microsoft 365 Business Premium (annual subscription)20licence£220.00£4,400.00
4On-site setup, imaging, and deployment (per device)15device£75.00£1,125.00
53-year on-site warranty extension (Tier 2)15unit£110.00£1,650.00
+
+ + + + + + + +
Subtotal:£25,745.00
Discount (5% volume):−£1,287.25
VAT (20%):£4,891.55
Total:£29,349.30
+
+
Terms & Conditions
+
Payment is due within 30 days of invoice date. Hardware items ship within 5–7 business days of order confirmation. Software licences are delivered electronically within 24 hours. All hardware carries a minimum 12-month manufacturer warranty. Returns accepted within 14 days of delivery for unopened items only.
+
Notes
+
Prices are inclusive of delivery to a single UK mainland address. Additional delivery locations are available at £35 per site. Volume pricing shown reflects a 5% discount applied to the hardware lines only. Quote is valid for 28 days from the date shown above. Please quote reference BLP-IT-2026-Q3 on all correspondence.
+
+
Acceptance
+
By signing below, the client accepts this quotation and authorises the work described herein. This quotation is valid until 19 March 2026.
+
+
+
+
Authorised Signature
+
+
+
+
Date
+
+
+
+
+ + diff --git a/engine/src/default_templates/receipt.html b/engine/src/default_templates/receipt.html new file mode 100644 index 0000000000..8e79548842 --- /dev/null +++ b/engine/src/default_templates/receipt.html @@ -0,0 +1,328 @@ + + + + + + Receipt {{RECEIPT_NUMBER}} + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
Receipt
+
+
+
Receipt Number
+
{{RECEIPT_NUMBER}}
+
+
+
+ + +
+ Receipt Date: {{RECEIPT_DATE}} +
+ +
+ + +
Parties
+
+
+
Issued By
+
{{ISSUER_NAME}}
+
{{ISSUER_ADDRESS}}
+
+
+
Received From
+
{{RECEIVED_FROM_NAME}}
+
{{RECEIVED_FROM_ADDRESS}}
+
+
+ + +
Items
+ + + + + + + + + + + + + {{LINEITEM_ROWS}} + +
#DescriptionQtyUnitUnit PriceLine Total
+ + +
+ + + + + + + +
Subtotal:{{SUBTOTAL_TEXT}}
Tax:{{TAX_TEXT}}
Total:{{TOTAL_TEXT}}
Amount Received:{{AMOUNT_RECEIVED_TEXT}}
+
+ + +
Payment Details
+
+ Payment Method: {{PAYMENT_METHOD_TEXT}} + Payment Reference: {{PAYMENT_REFERENCE_TEXT}} +
+ + +
Notes
+
{{NOTES_TEXT}}
+ + +
Issued By
+
+ Name: {{ISSUED_BY_NAME}} + Title: {{ISSUED_BY_TITLE}} +
+ +
+ + diff --git a/engine/src/default_templates/receipt_preview.html b/engine/src/default_templates/receipt_preview.html new file mode 100644 index 0000000000..14295d2c26 --- /dev/null +++ b/engine/src/default_templates/receipt_preview.html @@ -0,0 +1,161 @@ + + + + + + Receipt + + + +
+
+
+
+
Receipt
+
Blackthorn Digital Ltd
+
+
+
Receipt Number
+
REC-2026-0083
+
+
+
+
+ Receipt Date: 19 February 2026 +
+
+
Parties
+
+
+
Issued By
+
Blackthorn Digital Ltd
+
14 Farringdon Street, London, EC4A 4HH, UK
+
VAT No: GB987654321
+
+
+
Received From
+
Meridian Solutions plc
+
30 King Street, Birmingham, B1 2LT, UK
+
Accounts Payable
+
+
+
Items
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#DescriptionQtyUnitUnit PriceLine Total
1Software licence — Professional tier (annual)5seat£480.00£2,400.00
2Onboarding & implementation support8hour£150.00£1,200.00
3Priority support package (12 months)1package£600.00£600.00
+
+ + + + + + + +
Subtotal:£4,200.00
VAT (20%):£840.00
Total:£5,040.00
Amount Received:£5,040.00
+
+
Payment Details
+
+ Payment Method: BACS Bank Transfer + Payment Reference: REC-2026-0083 / PO-MER-4421 +
+
Notes
+
This receipt confirms full payment received for the above services. Please retain for your records. A duplicate copy has been sent to accounts@meridiansolutions.co.uk.
+
Issued By
+
+ Name: Oliver Pemberton + Title: Finance Manager +
+
+ + diff --git a/engine/src/default_templates/resume.html b/engine/src/default_templates/resume.html new file mode 100644 index 0000000000..f4ede222cd --- /dev/null +++ b/engine/src/default_templates/resume.html @@ -0,0 +1,265 @@ + + + + + + {{FULL_NAME}} — Resume + + + +
+ + +
+ {{LOGO_BLOCK}} +
+
{{FULL_NAME}}
+
{{JOB_TITLE}}
+
+ {{EMAIL}} + {{PHONE}} + {{LOCATION}} + {{LINKEDIN}} + {{WEBSITE}} +
+
+
+ + +
+
Professional Summary
+
{{SUMMARY}}
+
+ + +
+
Work Experience
+ {{EXPERIENCE_ITEMS}} +
+ + +
+
Education
+ {{EDUCATION_ITEMS}} +
+ + +
+
Skills
+
+ {{SKILLS_ITEMS}} +
+
+ + +
+
Certifications
+ {{CERTIFICATIONS_ITEMS}} +
+ + +
+
Projects
+ {{PROJECTS_ITEMS}} +
+ +
+ + diff --git a/engine/src/default_templates/resume_preview.html b/engine/src/default_templates/resume_preview.html new file mode 100644 index 0000000000..2779537fa9 --- /dev/null +++ b/engine/src/default_templates/resume_preview.html @@ -0,0 +1,159 @@ + + + + + + Alexandra Chen — Resume + + + +
+
+
+
Alexandra Chen
+
Senior Software Engineer
+
+ alex.chen@example.com + +1 (555) 234-5678 + San Francisco, CA + linkedin.com/in/alexchen +
+
+
+ +
+
Professional Summary
+
Results-driven Senior Software Engineer with 8+ years of experience designing and building scalable distributed systems. Proven track record of leading cross-functional teams, reducing infrastructure costs by 40%, and shipping products used by millions of users. Passionate about developer experience, performance optimisation, and mentoring junior engineers.
+
+ +
+
Work Experience
+ +
+
+ Senior Software Engineer + +
+
TechCorp Inc. — San Francisco, CA
+
    +
  • Led architecture redesign of core payments service, reducing p99 latency from 800ms to 120ms.
  • +
  • Managed a team of 6 engineers across 3 time zones, shipping 2 major product releases per year.
  • +
  • Reduced cloud infrastructure spend by 40% through rightsizing and caching strategies.
  • +
  • Established engineering standards and code review practices adopted company-wide.
  • +
+
+ +
+
+ Software Engineer II + +
+
StartupXYZ — New York, NY
+
    +
  • Built real-time data pipeline processing 50M events/day using Kafka and Apache Flink.
  • +
  • Designed and launched REST API serving 5M daily active users with 99.95% uptime.
  • +
  • Reduced CI/CD pipeline duration from 45 min to 8 min through parallelisation.
  • +
+
+ +
+
+ Software Engineer + +
+
DataSolutions Ltd — Boston, MA
+
    +
  • Developed ETL pipelines for Fortune 500 clients, processing terabytes of data nightly.
  • +
  • Built internal tooling that saved the analytics team 10 hours per week.
  • +
+
+
+ +
+
Education
+
+
+ B.Sc. Computer Science + +
+
Massachusetts Institute of Technology (MIT)
+
+
+ +
+
Skills
+
+
+
Languages
+
Python, Go, TypeScript, Java, SQL
+
+
+
Infrastructure
+
AWS, GCP, Kubernetes, Terraform, Docker
+
+
+
Frameworks & Tools
+
React, Node.js, FastAPI, Kafka, Redis, PostgreSQL
+
+
+
+ +
+
Certifications
+
+ AWS Certified Solutions Architect – Professional + · Amazon Web Services · 2023 +
+
+ Google Cloud Professional Data Engineer + · Google · 2022 +
+
+
+ + diff --git a/engine/src/default_templates/safe_agreement.html b/engine/src/default_templates/safe_agreement.html new file mode 100644 index 0000000000..adf9ff59e5 --- /dev/null +++ b/engine/src/default_templates/safe_agreement.html @@ -0,0 +1,323 @@ + + + + + + SAFE Agreement + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{AGREEMENT_TITLE}}
+
Simple Agreement for Future Equity
+
+
+ +
+ + +
+ Reference: {{AGREEMENT_REF}} + Effective Date: {{EFFECTIVE_DATE}} + Governing Law: {{GOVERNING_LAW}} + Classification: {{CONFIDENTIALITY_LABEL}} +
+ +
+ + +
Parties
+
+
+
Company
+
{{COMPANY_LEGAL_NAME}}
+
{{COMPANY_ADDRESS}}
+
+
+
Investor
+
{{INVESTOR_LEGAL_NAME}}
+
{{INVESTOR_ADDRESS}}
+
+
+ + +
+
Key Financial Terms
+
+ Purchase Amount: {{PURCHASE_AMOUNT}} + Valuation Cap: {{VALUATION_CAP}} + Discount Rate: {{DISCOUNT_RATE}} +
+
+ + +
Agreement
+ + + + + + + + + + + + + + + + +
Signatures
+ +
+
+
For the Company
+
+
Signature
+
+
Name / Title: {{COMPANY_SIGNATORY}}
+
+
Date: {{SIGNATURE_DATE}}
+
+
+
For the Investor
+
+
Signature
+
+
Name / Title: {{INVESTOR_SIGNATORY}}
+
+
Date: {{SIGNATURE_DATE}}
+
+
+ +
+ + diff --git a/engine/src/default_templates/safe_agreement_preview.html b/engine/src/default_templates/safe_agreement_preview.html new file mode 100644 index 0000000000..acda0f7a02 --- /dev/null +++ b/engine/src/default_templates/safe_agreement_preview.html @@ -0,0 +1,166 @@ + + + + + + SAFE Agreement — Preview + + + +
+ +
+ +
+
SAFE Agreement
+
Simple Agreement for Future Equity
+
+ +
+ +
+ Reference: SAFE-2026-003 + Effective Date: 15 February 2026 + Governing Law: England and Wales + Classification: Confidential +
+ +
+ +
Parties
+
+
+
Company
+
Helix Biotech Ltd
+
72 Science Park Road, Cambridge, CB4 0DW, United Kingdom
+
+
+
Investor
+
Beacon Ventures Fund II LP
+
8 St James's Square, London, SW1Y 4JU, United Kingdom
+
+
+ +
+
Key Financial Terms
+
+ Purchase Amount: £500,000 + Valuation Cap: £8,000,000 post-money + Discount Rate: 20% +
+
+ +
Agreement
+ + + + + + + + + + + + + + + +
Signatures
+ +
+
+
For the Company
+
+
Signature
+
+
Name / Title: Dr. Amelia Hargreaves, CEO
+
+
Date: 15 February 2026
+
+
+
For the Investor
+
+
Signature
+
+
Name / Title: Thomas Beacon, General Partner
+
+
Date: 15 February 2026
+
+
+ +
+ + diff --git a/engine/src/default_templates/sample_prompts.md b/engine/src/default_templates/sample_prompts.md new file mode 100644 index 0000000000..82d7192bd9 --- /dev/null +++ b/engine/src/default_templates/sample_prompts.md @@ -0,0 +1,158 @@ +# Sample Prompts + +One sample prompt per document template. Use these to test generation or as inspiration for users. + +--- + +## advisor_agreement +Create an advisor agreement between Helix Biotech Ltd (Company Registration Number 09876543), whose registered office is at 42 Innovation Way, Cambridge CB2 8AB, and Dr. Priya Sharma of 15 Oak Lane, London NW3 2PT, a clinical research advisor. The agreement is effective from 1 April 2026. Dr. Sharma will advise on regulatory strategy (including MHRA and EMA submissions), clinical trial design, and protocol development for the Company's lead oncology programme (Project代号 HELIX-ONC-001) for an initial term of 18 months, extendable by mutual written agreement. She commits to a minimum of 4 hours per week, with availability for up to 2 board or advisory committee meetings per quarter. Compensation: 0.3% equity options (fully diluted) vesting monthly over 24 months with a 6-month cliff from the effective date, plus a monthly retainer of £500 payable in arrears by the 15th of each month. She will receive reimbursement of reasonable out-of-pocket expenses incurred with prior approval. She confirms she has no conflicting commitments and will maintain confidentiality of all Company information. The agreement includes standard indemnification for advice given in good faith, and termination on 30 days' written notice by either party, with any unvested options lapsing on termination. Intellectual property arising from the advisory work is assigned to the Company. The agreement is governed by English law and subject to the exclusive jurisdiction of the courts of England and Wales. Include signature blocks for both parties and the date of execution. + +--- + +## audit_report +Create an internal audit report for Vantage Capital Partners Ltd. Audit reference IA-2026-11. The audit was conducted over the period 15 January to 31 March 2026 and covered payroll processing and HR data controls across the firm's London and Edinburgh offices. The audit was performed by the internal audit function; lead auditor James Hartley (CIA, CISA), with fieldwork support from Emma Chen and Oliver Wright. The scope included: payroll authorisation and segregation of duties, access rights to the HR system (PeopleHub) and payroll system (Sage Payroll), leaver process and access revocation, overtime and exception payment approvals, and completeness of personnel file documentation. Findings: (1) High risk — payroll system access was not revoked for 12 former employees within the required 24-hour window; the longest outstanding revocation was 47 days for a leaver from the Edinburgh office; management has since revoked all 12 accounts and implemented a weekly leaver-access report. (2) Medium risk — the overtime approval process was bypassed in 3 instances in February 2026, with overtime paid on manager verbal approval without documented sign-off; control has been reinforced with mandatory workflow in the system. (3) Low risk — annual salary review documentation was incomplete for 8 staff (missing signed forms or calibration notes); HR has committed to completing files by 30 April 2026. Overall opinion: Partially Satisfactory. Include an executive summary, detailed findings with criteria and recommendations, management response for each finding, and sign-off from the Head of Internal Audit. Report date: 10 April 2026. + +--- + +## board_minutes +Create board minutes for Hartwell Group plc. Meeting of the Board of Directors held on 15 April 2026 at 9:00am at the Company's offices at 100 Fleet Street, London EC4Y 1DE. Directors present: Sir Michael Hartwell (Chair), Catherine Ashworth, David Okonkwo, Fiona Reid, James Park. In attendance: Rachel Dunmore (incoming CFO, for item 2), company secretary Patricia Lowe. Apologies: none. The Chair confirmed a quorum and opened the meeting. (1) Minutes of the previous meeting (18 March 2026) were approved as a true record. (2) Q1 2026 financial results: The CFO (outgoing) presented the unaudited Q1 figures. Revenue £18.2M (up 8% YoY), EBITDA £4.2M (up 12% YoY), net debt £6.1M. The Board discussed the performance and noted strength in the UK and Nordic regions. Resolution: to approve the Q1 2026 financial results and their release to the market. Passed unanimously. (3) Appointment of CFO: The Chair proposed the appointment of Rachel Dunmore as Chief Financial Officer with effect from 1 May 2026, on terms set out in the board paper. One director abstained citing a prior professional relationship; the resolution was passed 4–1. (4) Acquisition pipeline: The CEO presented three potential acquisition targets (Meridian Analytics Ltd, and two others in confidence). The Board agreed to approve proceeding to due diligence on Meridian Analytics Ltd only, with a cap of £150K on diligence costs, and to receive an update at the May board. (5) Any other business: none. Next meeting: 20 May 2026. The meeting closed at 10:45am. Include standard wording for distribution and signing. + +--- + +## budget_proposal +Create a budget proposal for Crestwood Technologies Ltd for financial year 2026/27 (1 April 2026 to 31 March 2027). Purpose: to seek Board approval for the operating budget. Total proposed budget: £3.4M. Revenue projections: SaaS subscriptions £2.1M (assuming 18% ARR growth and two new enterprise customers signed in Q1), professional services £800K, support and maintenance contracts £500K. Major expense lines: engineering and product headcount £1.2M (current team of 12, no new hires in budget), cloud infrastructure and tooling £340K, sales and marketing £620K (including one new SDR and conference spend), G&A £480K (finance, legal, office), R&D and innovation £360K. Contingency: £100K unallocated. Key assumptions: 18% ARR growth, churn not to exceed 5% annualised, professional services margin held at 35%. Risks: delayed enterprise deal could reduce revenue by £200K; mitigation is pipeline acceleration and cost holdbacks after Q2 review. Include a one-page executive summary, revenue and cost breakdown tables, and a request for approval with date and authority. Document owner: Finance Director. Version 1.2, dated 12 March 2026. + +--- + +## case_study +Create a case study (2–3 pages) for Stirling PDF. Client: Nexus Cloud Ltd, a mid-market financial services firm based in London with 280 employees and operations in the UK and Ireland. Their challenge: manual document processing for client onboarding, KYC packs, and regulatory filings was consuming approximately 3 FTE and averaging 4 days per document, with a high error rate and poor visibility for compliance. They piloted Stirling PDF's AI document generation platform in Q3 2025 and rolled out to the operations and legal teams in Q4 2025. Solution: integrated Stirling PDF with their existing CRM and document store; used AI-assisted templates for standard document types; introduced approval workflows and audit trails. Results after 6 months: average processing time reduced from 4 days to 45 minutes for standard documents; the 3 FTE were redeployed to exception handling and client service; compliance error rate in sampled audits dropped by 94%; time to onboard a new corporate client reduced from 12 days to 4 days. Include a short quote/testimonial from their CTO, Marcus Reid: "Stirling PDF didn't just speed things up — it gave us control and traceability we never had with spreadsheets and email." Add a "Why Nexus Cloud chose Stirling PDF" section and a call-to-action for readers. Format for use on the Stirling PDF website and sales collateral. + +--- + +## committee_agenda +Create an agenda for the Audit and Risk Committee of Meridian Healthcare NHS Foundation Trust. Meeting: 26 March 2026, 10:00am–12:00pm, Trust Headquarters, Bristol (Room 4.2, and via MS Teams for remote attendees). Chair: Non-Executive Director Dr. Anne Whitfield. Attendees: all Audit and Risk Committee members (as per standing list), Director of Finance (Sarah Okonkwo), Deputy Director of Finance, Head of Internal Audit, and representatives from external auditors Grant Thornton (lead partner James Hartley, manager Emma Chen). Apologies to be notified to the Committee Secretary. Agenda: (1) Welcome and declarations of interest (5 mins). (2) Minutes of the previous meeting (14 January 2026) — for approval (5 mins). (3) External audit progress update — Grant Thornton to report on 2025/26 audit progress, key risks, and timetable (20 mins). (4) Internal audit — Q4 findings and annual report summary; Head of Internal Audit to present (25 mins). (5) Risk register review — updated strategic and operational risks; discussion of any new or escalated risks (20 mins). (6) Financial controls update — Director of Finance to update on control improvements and any incidents (15 mins). (7) Any other business (5 mins). (8) Date of next meeting: 21 May 2026. Papers circulated with the agenda: Q4 internal audit report, updated risk register (version 3.2), management accounts to February 2026, external audit progress report. Contact: Committee Secretary, ext. 4521. + +--- + +## employee_handbook +Create an employee handbook for Northgate Engineering Services Ltd, effective 1 January 2026. The company is an engineering consultancy with offices in Birmingham and Leeds; the handbook applies to all UK employees. Include: (1) Welcome and introduction from the Managing Director; company values and mission. (2) Working hours: standard 37.5 hours per week; flexible start between 7:30am and 9:30am; core hours 10am–3pm; break entitlements. (3) Annual leave: 25 days plus 8 bank holidays, rising to 28 days after 5 years' service; carry-over of up to 5 days with manager approval; booking procedure. (4) Sick leave: self-certification for up to 7 days; company sick pay after 3 months' service for up to 3 months full pay, then 3 months half pay; notification and evidence requirements. (5) Code of conduct: integrity, respect, diversity and inclusion, anti-bribery, conflicts of interest, use of social media. (6) Health and safety: responsibilities of the company and employees; reporting accidents and near misses; DSE and risk assessments. (7) IT and data security: acceptable use, passwords, data protection (UK GDPR), confidential information, bring-your-own-device policy. (8) Disciplinary and grievance procedures: informal and formal stages; right to be accompanied; appeals. (9) Other: probation (6 months), expenses, training and development, equal opportunities, maternity/paternity and other family leave (summary). State that the handbook is not contractual and may be updated; governed by UK employment law. Include a sign-off page for employees to acknowledge receipt. + +--- + +## executive_summary +Create an executive summary (2–3 pages) for the proposed acquisition of Trident Manufacturing Group Ltd by Aldgate Capital Partners LLP. Deal: Aldgate proposes to acquire 100% of the equity of Trident for £28M (enterprise value), with £3M of cash and £25M of assumed debt and seller rollover. Trident: UK-based precision engineering and manufacturing group; revenue £12M (last 12 months), EBITDA £2.1M, 3 manufacturing sites in the Midlands (Birmingham, Coventry, Wolverhampton), 180 employees. Key findings from commercial and financial due diligence: (1) Strong order book covering 14 months of production; key customers in aerospace and defence. (2) Aging plant and machinery; capex of approximately £1.8M required in year 2 for replacement and compliance. (3) Two key customer contracts (c. 30% of revenue) up for renewal in Q3 2026; renewal risk is material. (4) Management team is strong; retention packages recommended. (5) No material environmental or litigation issues identified. Recommendation: proceed with the acquisition subject to (a) capex allowance of £1.8M reflected in price or vendor warranty, and (b) earnout or deferred consideration linked to renewal of the two key contracts. Indicative timetable: exclusivity to 30 April, completion by end June 2026. Prepared by Aldgate deal team. Confidential. Date: 8 April 2026. + +--- + +## expense_report +Create an expense report for James Okafor, Sales Director at Pinnacle Cloud Services Ltd. Employee ID: EMP-10482. Department: Sales. Period: 1–28 February 2026. Expenses: (1) Client dinner at The Ivy, London on 12 February 2026 — £347.50; purpose: client entertainment with Meridian Solutions (4 attendees: James Okafor, 2 from Meridian, 1 from Pinnacle); receipt attached. (2) Train travel London Euston to Manchester Piccadilly and return on 18 February 2026 — £214.00; purpose: client meeting at Caldwell Retail Group; booking reference attached. (3) Hotel — Premier Inn Manchester Central, 18 February 2026 (one night) — £189.00; receipt attached. (4) Taxi and ground transport — various dates across February (airport transfers and client meetings) — £67.40; summary and receipts attached. Total expenses: £817.90. Currency: GBP. No advances received for this period. Declaration: I confirm that these expenses were incurred in the course of business and comply with the company expense policy. Submit for approval to: Karen Delgado, Director of Operations. Payment: reimbursement to bank account ending 8821 within 10 working days of approval. Include space for approver signature and date. + +--- + +## incident_report +Create an incident report for Ironbridge Engineering Solutions Ltd. Report reference: INC-2026-031. Date of incident: 6 March 2026. Time: 2:15pm. Location: Workshop Bay 3, Ironbridge Manufacturing Site, Telford. Reported by: Site Supervisor David Chen. Person involved: Thomas Walsh, Warehouse Operative (Employee ID 2047), 3 years' service. Summary: Mr Walsh sustained a back injury while manually lifting a 40kg component (engine housing) in Workshop Bay 3. He felt immediate pain and was unable to continue; first aid was administered on site by trained first aider Fiona Reid; he was then transported to A&E at Princess Royal Hospital, Telford. Diagnosis: lower back strain; he was discharged with advice and signed off work for 2 weeks. The incident is RIDDOR-reportable (non-fatal injury to an employee); the HSE was notified within the required timeframe. Root cause analysis: (1) No mechanical lifting aid (pallet jack or crane) was available in Bay 3; the nearest pallet jack was in Bay 1. (2) Mr Walsh had not received refresher manual handling training since 2023. (3) Risk assessment for Bay 3 had not been updated to reflect the regular movement of 40kg+ items. Corrective actions: (1) Procure and install two additional pallet jacks for Bay 3 by 20 March 2026. (2) Mandatory refresher manual handling training for all warehouse and workshop staff by 30 April 2026. (3) Update risk assessment for Workshop Bay 3 and implement a check before manual lift of items over 25kg. (4) Brief all relevant staff on the incident and lessons learned by 13 March 2026. Report prepared by: Health & Safety Manager. Approved by: Site Director. Date: 10 March 2026. + +--- + +## independent_contractor_agreement +Create an independent contractor agreement between Vantage Digital Solutions Ltd (Company Number 11223344), registered office 50 Tech Park, Edinburgh EH12 9AB (the "Client"), and Owen Clarke Consulting Ltd (Company Number 55667788), registered office 22 Consultant Lane, Glasgow G1 1AA (the "Contractor"). Commencement: 1 March 2026. Term: 6 months, unless terminated earlier. Services: the Contractor will provide senior backend engineering services (Python, AWS, system design) as directed by the Client's Engineering Manager, including code development, code review, and technical documentation. The Contractor will provide personnel as agreed in statements of work; key resource: Owen Clarke or an agreed substitute. Time commitment: up to 5 days per week; remote working with one day per week on-site at the Client's Edinburgh office (travel expenses reimbursed in line with policy). Day rate: £650 per day plus VAT; invoiced monthly in arrears; payment within 30 days. The Contractor is responsible for its own tax, NI, and insurance. Intellectual property: all IP and work product created in performing the services is assigned to the Client; the Contractor will execute any further documents required. Confidentiality: both parties will keep confidential all non-public information; obligation survives termination for 2 years. Non-solicitation: for 90 days after the end of the engagement, the Contractor will not solicit or hire the Client's employees or contractors. Termination: either party may terminate on 4 weeks' written notice; the Client may terminate for material breach with 14 days' notice to remedy. The agreement is governed by Scots law. Include signature blocks and schedule for statements of work. + +--- + +## invoice +Create an invoice from Blackthorn Digital Ltd (VAT Registration GB 123 4567 89), 15 Design Quarter, London SE1 2AB, to Meridian Solutions plc, 100 Commerce House, Manchester M2 5AB. Invoice number: INV-2026-047. Invoice date: 28 February 2026. Payment due: 29 March 2026 (30 days). Purchase order reference (if provided by customer): PO-MERI-2026-012. Line items: (1) UX design sprint — 40 hours at £125.00 per hour — £5,000.00; (2) Brand guidelines document — 1 deliverable — £1,200.00; (3) Monthly retainer February 2026 — £2,500.00. Subtotal: £8,700.00. VAT at 20%: £1,740.00. Total due: £10,440.00. Payment by bank transfer: Barclays Bank, sort code 20-00-00, account number 12345678. Reference: INV-2026-047 and your company name. Late payment: interest may be charged in accordance with the Late Payment of Commercial Debts (Interest) Act 1998. Blackthorn Digital Ltd is a company registered in England and Wales, number 09876543. Include standard terms reference (e.g. "Subject to our standard terms of business") and a thank-you message. + +--- + +## job_description +Create a job description for a Lead Software Engineer at Hartwell Digital Agency Ltd. Location: Manchester (Hybrid — 3 days per week in the office, 2 days remote). Reports to: Head of Engineering. Salary: £75,000–£90,000 per annum depending on experience. Benefits: 25 days holiday plus bank holidays, pension (5% employer contribution), private health insurance, annual training budget £2,000, cycle-to-work scheme. Role purpose: to lead a small team of backend engineers in designing, building, and maintaining scalable services and APIs for client projects and internal products; to contribute to architecture decisions and technical standards. Key responsibilities: lead and mentor a team of 3–5 engineers; own the technical design and delivery of backend features; conduct code reviews and ensure quality and security; work closely with product and design to define requirements and timelines; contribute to hiring and onboarding. Requirements: 5+ years of commercial backend engineering experience; strong proficiency in Python and Go; experience with AWS or GCP (EC2, Lambda, RDS, S3 or equivalent); experience leading or mentoring engineers; comfortable with CI/CD (e.g. GitHub Actions, Jenkins), infrastructure as code (Terraform or similar), and agile delivery. Nice to have: experience in agency or professional services; familiarity with React or front-end integration. Closing date for applications: 13 March 2026. Equal opportunities employer. Include application instructions (CV and short cover letter to careers@hartwelldigital.co.uk, quoting reference LSE-2026-01). + +--- + +## letter +Write a formal business letter from Sarah Whitfield, Head of Procurement, Pinnacle Retail Group plc, 200 Retail Way, Birmingham B12 8AB (tel. 0121 123 4567, sarah.whitfield@pinnacleretail.co.uk), to the Account Manager, Apex Logistics Ltd, Apex House, 50 Distribution Park, Northampton NN3 8AB. Date: 15 February 2026. Subject: Notice of termination — warehousing contract. The letter should formally notify Apex that Pinnacle is exercising its 90-day termination clause under the current warehousing and distribution contract (contract reference: PRG-APEX-2022-01), with effect from 31 May 2026. The reason for termination is a strategic decision by Pinnacle to bring warehousing operations in-house following the opening of their new distribution centre in Coventry. The letter should thank Apex for the 4-year business relationship and the quality of service provided, and request a transition meeting within the next 14 days to agree handover steps, including inventory reconciliation, return of any Pinnacle assets, and final account settlement. Ask for confirmation of receipt and the name of the person who will lead the transition on Apex's side. Formal tone; sign off "Yours sincerely" with Sarah Whitfield's name and title. Letterhead style (company name and address at top). + +--- + +## letter_of_intent +Create a letter of intent from Meridian Capital Advisors LLP (address: 75 Capital Square, London EC2V 7AB) to the Board of Directors of Trident Manufacturing Group Ltd (Trident House, Birmingham B6 4AB). Date: 1 April 2026. Subject: Proposed equity investment. Meridian expresses its non-binding interest in investing £3.5M in Trident in return for a 22% equity stake, on a pre-money valuation of £16M. The investment would be used primarily to fund expansion of Trident's Birmingham facility and the implementation of a new ERP system. Indicative timeline: exclusivity period of 30 days from signing of this LOI; due diligence (commercial, financial, legal) to be completed within 45 days; target completion by 30 June 2026. The LOI is non-binding except for: (1) confidentiality — both parties will keep the existence and terms of the LOI and any shared information confidential; (2) exclusivity — Trident will not solicit or entertain other offers for the duration of the exclusivity period; (3) costs — each party bears its own costs. Binding terms would be set out in definitive investment documentation. Governing law: English law. Request that Trident countersign to indicate its willingness to proceed on this basis. Include signature blocks for both parties. + +--- + +## master_services_agreement +Create a master services agreement between Pinnacle Cloud Services Ltd (Company Number 11223344), registered office 10 Cloud Street, Reading RG1 2AB ("Provider"), and Caldwell Retail Group plc (Company Number 09876543), registered office 5 High Street, Leeds LS1 1AA ("Client"). Services: the Provider will supply (1) cloud infrastructure management (AWS/Azure as agreed per order), (2) 24/7 monitoring and alerting, and (3) monthly security audits and reporting. Services will be specified in order forms or statements of work under this MSA. Fees: £18,000 per month (excluding VAT) for the baseline scope as set out in Order Form #1; additional services at agreed rates. Fees are reviewed annually with effect from each anniversary of the effective date. Term: initial term 2 years from the effective date; thereafter auto-renewing for successive 12-month periods unless either party gives at least 90 days' notice before the end of the then-current term. Data and privacy: the Provider processes personal data on behalf of the Client as a data processor under UK GDPR; a data processing addendum is incorporated. Liability: cap at 12 months of fees paid by the Client in the 12 months preceding the claim; no liability for indirect or consequential loss. Termination: for material breach (30 days to remedy); for convenience after the initial term on 30 days' written notice. Effects of termination: return of data, payment for services to date. Governing law and jurisdiction: English law, exclusive jurisdiction of the courts of England and Wales. Include entire agreement, force majeure, notices, and signature blocks. + +--- + +## meeting_minutes +Create meeting minutes for the Q1 Product Review meeting of Stirling PDF SaaS. Date: 5 March 2026. Time: 10:00am–11:30am. Location: Zoom (link sent in calendar invite). Chair: Ethan (CEO). Attendees: Ethan (CEO), Rachel (Product), Marcus (Engineering), Fiona (Design), James (Sales). Apologies: none. (1) Welcome and agenda — Ethan confirmed the focus on Q1 delivery and Q2 planning. (2) Product roadmap review — Rachel presented the updated roadmap. Key decisions: (a) Ship AI document generation v2 by 31 March 2026; scope confirmed (templates, streaming preview, PDF export). (b) Defer mobile app to Q3 2026; focus remains web and API. (c) Approve £40K budget for the planned security audit (Q2). (3) Engineering update — Marcus reported API rate limiting design is 80% complete; to be finalised and documented by 12 March. (4) Design — Fiona to deliver updated design system components (buttons, forms, modals) by 14 March for integration. (5) Sales — James noted two enterprise pilots in progress; no change to Q1 revenue forecast. (6) Action items: Marcus — finalise API rate limiting spec and share by 12 March. Rachel — update roadmap in Notion by 7 March and circulate. Fiona — design system components by 14 March. (7) Next meeting: 2 April 2026, 10:00am. Meeting closed at 11:28am. Distribution: attendees and leadership team. Include "Draft" or "Final" as appropriate and space for chair sign-off. + +--- + +## nondisclosure_agreement +Create a mutual nondisclosure agreement between Apex Renewables Ltd (Company Number 11223344), 30 Green Lane, Bristol BS1 2AB ("Party A"), and Greenfield Energy Partners Ltd (Company Number 55667788), 15 Energy Place, London EC2A 1BB ("Party B"). Purpose: both parties are sharing commercially sensitive information (including technical, financial, and commercial data) to evaluate a potential joint venture for the development and operation of a 50MW solar PV project in Somerset (the "Project"). Definition of Confidential Information: all non-public information disclosed in connection with the Project, whether in writing, orally, or otherwise, and marked or reasonably identifiable as confidential. Exclusions: information that is or becomes publicly available (other than by breach), independently developed, or rightfully received from a third party without restriction. Obligations: each party will hold the other's Confidential Information in confidence, use it only for evaluating and negotiating the Project, and not disclose it except to advisers and personnel on a need-to-know basis under equivalent obligations. Term: 3 years from the date of signing. Return/destruction of materials on request or at end of purpose. Governing law: England and Wales. No licence or obligation to proceed with the Project. Effective date: 1 March 2026. Include signature blocks for both parties. + +--- + +## offer_letter +Create a job offer letter from Hartwell Digital Agency Ltd, 25 Digital Way, Manchester M1 4BT, to James Okafor, [home address to be inserted]. Position: Lead Software Engineer. Start date: 4 May 2026. Salary: £82,000 per annum, paid monthly in arrears by BACS. Benefits: 25 days annual leave plus bank holidays; private health insurance (after 3 months); pension with 5% employer contribution (employee minimum 3%); annual training and development budget of £2,000. You will report to the Head of Engineering and your principal place of work will be our Manchester office, with hybrid working (3 days in office, 2 days remote) as per our policy. This offer is contingent on: (1) satisfactory references (we will request two, one of which should be from your current or most recent employer); (2) confirmation of your right to work in the UK; (3) no material conflict of interest. Please confirm acceptance in writing by 28 February 2026. If you accept, you will receive a contract of employment and new starter paperwork. We look forward to welcoming you to the team. Sign off from the Head of People or Managing Director. Include space for the candidate's signature and date of acceptance. + +--- + +## official_memo +Write an official memo from Ethan Scott, CEO, Stirling PDF, to all staff. Date: 1 March 2026. Subject: Company-wide AI usage policy — update effective 1 April 2026. Distribution: all employees and contractors. The memo should state that the company is updating its AI usage policy to protect our clients, our IP, and our compliance posture. Key points: (1) Approved AI tools — only the internal Stirling AI assistant (our own product) and GitHub Copilot (for code) may be used for work. No other external generative AI tools (e.g. public ChatGPT, Claude, etc.) are permitted for company or client work. (2) Customer and confidential data — you must not enter any customer data, PII, or confidential business information into external AI services. Use only approved tools in their approved, compliant configurations. (3) Mandatory training — all staff must complete the new "AI at Stirling" training module in the LMS by 31 March 2026. Incomplete training will be escalated to line managers. (4) AI usage log — for all client-facing work that involves AI-assisted output, you must log the use (tool, date, document/project) in the new AI usage register. This supports our audit and client assurance. Questions to people@stirlingpdf.com. The policy document is available on the intranet. Thank you for your cooperation. Formal memo format; sign off from Ethan Scott, CEO. + +--- + +## pay_stub +Create a pay stub for Thomas Harrison, Senior Software Engineer, Meridian Digital Ltd, 40 Software Park, Manchester M2 1AB. Employee ID: EMP-00247. Department: Engineering. Pay period: 1 January 2026 to 31 January 2026. Pay date: 31 January 2026. Earnings: basic salary (monthly) £5,200.00; overtime 5 hours at £65.00/hour £325.00; total gross £5,525.00. Deductions: income tax (PAYE) £972.50; National Insurance (employee) £471.25; pension (employee 5%) £276.25; total deductions £1,720.00. Net pay: £3,805.00. Year to date: gross £5,525.00, tax £972.50, NI £471.25, pension £276.25, net £3,805.00. Payment method: BACS to Lloyds Bank, account ending 6742. Tax reference: 123/AB45678. Leave balance: 22 days remaining (as at 31 Jan). This is not a tax certificate; P60/P45 will be issued as applicable. Include company registration number and a note that the pay stub is confidential. + +--- + +## performance_review +Create a performance review form for Priya Nair, Senior Business Analyst, Cavendish Financial Solutions Ltd. Review period: 1 January 2025 to 31 December 2025. Reviewer/Manager: David Hartley, Director of Consulting. Review date: 15 February 2026. Ratings (1–5 scale: 1 = Below Expectations, 2 = Needs Improvement, 3 = Meets Expectations, 4 = Exceeds Expectations, 5 = Outstanding): Communication — 4; Technical analysis — 5; Stakeholder management — 4; Delivery and execution — 4; Leadership — 3. Overall rating: Exceeds Expectations. Summary of accomplishments: led the delivery of the new client reporting platform, completing 2 weeks ahead of schedule and under budget; reduced standard report turnaround time by 60% through process and tooling improvements; supported 3 major client workshops with strong feedback. Areas for development: leadership and delegation; Priya has taken on more complex work but could develop by line-managing 1–2 junior analysts and leading a small workstream. Goals for 2026: (1) Build leadership skills — take on line management of two junior analysts by Q2; (2) Complete CBAP certification by end of 2026; (3) Lead at least one cross-team initiative. Employee comments (optional): [space for Priya to add]. Sign-off: employee and manager signatures and dates. Confidential — HR to retain. + +--- + +## price_sheet +Create a price sheet for Vantage Cloud Services Ltd. Document title: Vantage Cloud — Price List. Effective date: 1 March 2026. All prices are in GBP and exclude VAT unless stated. Products and services: Starter Plan (monthly, per seat) — £12; Growth Plan (monthly, per seat) — £29; Enterprise Plan (monthly, per seat) — £59; Professional Services (per day, onsite or remote) — £950; Data Migration (flat fee per migration project) — £2,500; Priority Support SLA (per month, in addition to plan) — £500; Custom Integrations — price on application (POA). Notes: minimum 12-month commitment for Growth and Enterprise; Professional Services sold in half-day blocks; Data Migration includes up to 50GB and one go-live support window. Discounts: annual prepay (10% off plan fees); volume discounts for 50+ seats on request. Contact: sales@vantagecloud.co.uk, 0800 123 4567. Company registration and VAT number at footer. "Prices subject to change; valid for new orders from the effective date." + +--- + +## purchase_order +Create a purchase order from Crestwood Technologies Ltd, 60 Business Park, Bristol BS2 8CD (Buyer: Procurement, tel. 0117 123 4567), to Apex Office Supplies Ltd, Apex House, Northampton NN3 8AB. PO number: PO-2026-0312. Date: 10 March 2026. Delivery: requested by 20 March 2026 to Crestwood Technologies Ltd, 60 Business Park, Bristol BS2 8CD; contact: Reception. Payment terms: 30 days from date of invoice. Line items: (1) Ergonomic office chairs, model ErgoFlex Pro — quantity 10, unit price £320.00, line total £3,200.00; (2) Height-adjustable standing desks — quantity 4, unit price £540.00, line total £2,160.00; (3) Monitor arms (single, VESA compatible) — quantity 20, unit price £45.00, line total £900.00. Subtotal: £6,260.00. VAT at 20%: £1,252.00. Total: £7,512.00. This PO is subject to our standard terms of purchase (available on request). Please acknowledge receipt and confirm delivery date. Include space for supplier acceptance and reference. + +--- + +## quote +Create a quote from Blackthorn Digital Ltd, 15 Design Quarter, London SE1 2AB, to Nexus Retail Solutions, 80 Retail Way, Leeds LS1 2AB. Quote number: QT-2026-089. Date: 15 February 2026. Valid for: 30 days (until 17 March 2026). Project: e-commerce redesign — discovery, design, frontend build, QA, and project management. Line items: Discovery and UX research (2 weeks, workshops and user research) — £4,800; UI design (12 key screens, responsive, design system) — £6,000; Frontend development (React, 8 weeks) — £14,400; QA and testing (2 weeks, UAT support) — £2,400; Project management (throughout) — £1,800. Total (excl. VAT): £29,400. VAT at 20%: £5,880. Total (incl. VAT): £35,280. Payment schedule: 30% on kick-off (£8,820 incl. VAT), 40% at mid-project milestone (£14,112 incl. VAT), 30% on delivery and sign-off (£10,584 incl. VAT). Assumptions: content and copy to be provided by client; backend API to be available by week 4; 2 rounds of design revisions included. Next steps: sign the quote and return to proceed; we will then send a contract and project plan. Contact: projects@blackthorndigital.co.uk. Include terms (e.g. scope change process, IP, liability) and signature/acceptance block. + +--- + +## receipt +Create a receipt from Blackthorn Digital Ltd (VAT GB 123 4567 89), 15 Design Quarter, London SE1 2AB, to Meridian Solutions plc, 100 Commerce House, Manchester M2 5AB. Receipt number: REC-2026-031. Date: 28 February 2026. This receipt confirms payment received from Meridian Solutions plc for the following: Invoice INV-2026-047 — UX design sprint, brand guidelines document, and monthly retainer February 2026. Amount received: £10,440.00 (including VAT). Payment method: BACS transfer. Payment reference: MERI-FEB26. Bank: as per our invoice. Thank you for your business. For any queries contact accounts@blackthorndigital.co.uk. Company registration: 09876543. Include "Paid in full" or similar and optional remittance advice reference. + +--- + +## resume +Create a resume (CV) for Alexandra Moore, senior product manager, based in London. Contact: alexandra.moore@email.com, 07XXX XXXXXX. Summary: 8 years of experience in B2B SaaS product management; track record of launching and scaling products and leading cross-functional teams. Employment: (1) Head of Product, Stirling PDF, 2023–present — own product strategy and roadmap for the SaaS platform; lead a team of 4 product managers; launched AI document generation feature, contributing to 40% ARR growth in 12 months. (2) Senior Product Manager, Notion, 2020–2023 — owned Notion Templates and integrations roadmap; increased template adoption by 25%. (3) Product Manager, Intercom, 2018–2020 — worked on Messenger and automation products; shipped multiple features used by 10K+ customers. Education: BSc Computer Science, University of Edinburgh, 2015 (First Class Honours). Skills: product strategy, roadmap planning, SQL, Figma, Agile/Scrum, stakeholder management, pricing and packaging. Key achievement: led the launch of Stirling PDF's AI document generation feature from concept to GA; drove adoption and feedback loops that grew ARR by 40% in the first 12 months. Professional memberships or certifications: optional. Format: clear sections, reverse chronological order, one to two pages. Tone: professional and concise. + +--- + +## safe_agreement +Create a Simple Agreement for Future Equity (SAFE) between Helix Biotech Ltd (Company Number 11223344), 42 Innovation Way, Cambridge CB2 8AB (the "Company"), and Beacon Ventures Fund II LP, 100 Venture Street, London EC2A 1BB (the "Investor"). Investment amount: £500,000 (five hundred thousand pounds). Valuation cap: £8,000,000 (pre-money). Discount rate: 20%. The SAFE will convert into equity on the next qualifying equity financing (equity round with total proceeds of at least £1M), or on a liquidity event or dissolution. MFN (most favoured nation) clause: if the Company issues SAFEs with more favourable terms before conversion, this SAFE will be amended to match. Pro rata rights: the Investor will have the right to participate in the next round on the same terms, up to its pro rata share. Governing law: England and Wales. Effective date: 15 February 2026. Signed for the Company: Dr. Amelia Hargreaves, CEO. Signed for the Investor: Thomas Beacon, Managing Partner, Beacon Ventures Fund II LP. Include definitions (e.g. "Company Capitalisation", "Conversion"), conversion mechanics, and standard boilerplate (no representations, entire agreement, notices). + +--- + +## separation_notice +Create a separation notice from Aldermoor Logistics plc, 25 Distribution Way, Bristol BS3 2AB, to Marcus Reid, Warehouse Operations Supervisor. Employee ID: EMP-30891. Department: Bristol Distribution Centre. Separation type: redundancy due to site consolidation (closure of Bristol distribution centre; operations moving to Birmingham). Effective date of termination: 28 February 2026. Final pay and entitlements: salary through 28 February 2026; 8 weeks' statutory redundancy pay (as calculated); payment in lieu of 5 days accrued untaken annual leave. Payment will be made on or before 28 February 2026 by BACS to your usual account. Benefits: private health cover will continue through 31 March 2026; you will receive separate communication regarding options thereafter. Equipment and access: you must return by 28 February 2026: company laptop, building access fob, and company vehicle keys (if applicable). Please arrange handover with your line manager Paul Fairweather. Reference: a reference letter will be provided by Paul Fairweather; you may request it from HR after your leaving date. Exit interview: HR will contact you to offer an optional exit interview. We thank you for your service and wish you well. Contact: HR at hr@aldermoorlogistics.co.uk. Include space for employee acknowledgment (optional) and company sign-off. Confidential. + +--- + +## service_agreement +Create a service agreement between Pinnacle Cloud Services Ltd (Company Number 11223344), 10 Cloud Street, Reading RG1 2AB ("Provider"), and Crestwood Technologies Ltd (Company Number 55667788), 60 Business Park, Bristol BS2 8CD ("Client"). Services: (1) Managed IT support — helpdesk available 9am–6pm Monday–Friday (excluding UK bank holidays); ticket logging, triage, and resolution; (2) Monthly security patching — OS and application patches applied in agreed maintenance windows; (3) Quarterly business reviews — review of service performance, incidents, and roadmap. Scope: up to 50 users and 80 devices as specified in the order form. Monthly fee: £3,200 plus VAT; invoiced in advance; payment within 30 days. Term: 12 months from 1 April 2026; thereafter continuing until terminated. Termination: either party may give 3 months' written notice to take effect at or after the end of the initial term. SLA: P1 (critical) — response within 1 hour, target resolution 4 hours; P2 (high) — response within 4 hours, target resolution 1 business day; P3 (normal) — response within 1 business day. Service credits for repeated SLA failures as per schedule. Data and security: Provider will process Client data in accordance with agreed security standards and UK GDPR. Liability: cap at 12 months of fees. Governing law: England and Wales. Include change control, force majeure, and signature blocks. + +--- + +## standard_operating_procedures +Create a standard operating procedure (SOP) for incoming raw materials inspection at Meridian Pharma Manufacturing Ltd. SOP number: QA-RM-007. Version: 2.1. Effective date: 1 February 2026. Previous version: 2.0 (dated 1 June 2024). Purpose: to ensure that all incoming raw materials are inspected, sampled, and assessed against approved specifications before release to production, thereby preventing non-conforming materials from entering the manufacturing process. Scope: all raw materials received at the Meridian site (Warehouse Receipt Bay 1 and 2). Responsibilities: Warehouse — receipt and logging; QC — sampling and testing; QA — review and release/reject decision. Procedure: (1) Receipt and logging — on delivery, verify identity and quantity against the purchase order and delivery note; log in the ERP system (transaction GR-RM-001); assign a unique receipt number; place materials in the quarantine area. (2) Visual inspection — check packaging for damage, contamination, or temperature deviation (where applicable); record findings on form QA-RM-007-F1. (3) Sampling — per the approved sampling plan (QA-SP-002), take representative samples; send samples to QC lab with the request form; retain reserve samples. (4) QC testing — QC performs tests per the relevant specifications; results recorded in the LIMS and CoA generated. (5) Review — QA reviews the CoA and packaging inspection against the approved specification; if conforming, approve release in the ERP system (transaction REL-RM-001); if non-conforming, reject and initiate quarantine procedure (QA-RM-008). (6) Quarantine for rejected materials — move to rejected materials area; label clearly; document and notify supplier; disposition per QA-RM-008. References: QA-SP-002 (Sampling Plan), QA-RM-008 (Quarantine and Disposition), ERP user guides. Training: all personnel performing this procedure must be trained and assessed competent; training records in the training matrix. Approved by: Dr. Sarah Okonkwo, QA Manager. Date: 15 January 2026. Next review: 1 February 2027. Include revision history table and document control footer. diff --git a/engine/src/default_templates/separation_notice.html b/engine/src/default_templates/separation_notice.html new file mode 100644 index 0000000000..05576f9207 --- /dev/null +++ b/engine/src/default_templates/separation_notice.html @@ -0,0 +1,231 @@ + + + + + + Separation Notice + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
Separation Notice
+
{{COMPANY_NAME}}
+
+
+ +
+ +
+ Company Address: {{COMPANY_ADDRESS}} + Date: {{LETTER_DATE}} +
+ +
+ + +
Employee Information
+
+ Employee Name: {{EMPLOYEE_NAME}} + Employee ID: {{EMPLOYEE_ID}} + Position: {{POSITION}} + Department: {{DEPARTMENT}} +
+ + +
Separation Details
+
+ Separation Type: {{SEPARATION_TYPE}} + Effective Date: {{EFFECTIVE_DATE}} +
+
{{SEPARATION_REASON_TEXT}}
+ + +
Final Pay
+
{{FINAL_PAY_TEXT}}
+ + +
Benefits
+
{{BENEFITS_TEXT}}
+ + +
Return of Company Property
+
{{EQUIPMENT_TEXT}}
+ + +
References Policy
+
{{REFERENCES_TEXT}}
+ + +
Closing Statement
+
+
{{CLOSING_TEXT}}
+
+
+
HR Representative
+
+
Signature
+
+
Name: {{HR_SIGNER_NAME}}
+
+
Title: {{HR_SIGNER_TITLE}}
+
+
+
Employee Acknowledgement
+
+
Signature
+
+
Name: {{EMPLOYEE_NAME}}
+
+
Date: {{EMPLOYEE_ACK_DATE}}
+
+
+
+ +
+ + diff --git a/engine/src/default_templates/separation_notice_preview.html b/engine/src/default_templates/separation_notice_preview.html new file mode 100644 index 0000000000..1666e12c03 --- /dev/null +++ b/engine/src/default_templates/separation_notice_preview.html @@ -0,0 +1,115 @@ + + + + + + Separation Notice + + + +
+
+
+
+
Separation Notice
+
Aldermoor Logistics plc
+
+
+
+
+ Company Address: 12 Wharf Street, Bristol, BS1 4RN + Date: 3 February 2026 +
+
+ +
Employee Information
+
+ Employee Name: Marcus Reid + Employee ID: EMP-1134 + Position: Warehouse Operations Supervisor + Department: Distribution & Fulfilment +
+ +
Separation Details
+
+ Separation Type: Redundancy + Effective Date: 28 February 2026 +
+
Following the completion of the company's operational restructuring review, the role of Warehouse Operations Supervisor at the Bristol Wharf site has been identified as redundant. This decision follows a full and fair consultation process conducted between 5 January and 28 January 2026. All reasonable steps to identify suitable alternative employment within the business were taken; regrettably, no suitable alternative position was available. This notice is therefore issued in accordance with your statutory and contractual rights.
+ +
Final Pay
+
Your final salary payment, including all accrued but untaken annual leave (7 days, equivalent to £1,346.15), will be processed in the February 2026 payroll run and credited to your bank account on 28 February 2026. You are entitled to a statutory redundancy payment of £4,800, calculated on the basis of your continuous service of 6 years. A written breakdown of all final payments will be provided with your payslip.
+ +
Benefits
+
Your private medical insurance cover through AXA Health will remain active until 28 February 2026. Pension contributions will cease on your effective leaving date; your pension provider, Nest, will write to you separately regarding the options available for your accumulated pension pot. Access to the employee assistance programme helpline will continue for 30 days following your leaving date.
+ +
Return of Company Property
+
You are required to return all company property in your possession before or on 28 February 2026. This includes, but is not limited to: your company-issued access fob and car park pass, any keys to company premises or vehicles, your company mobile telephone and any associated accessories, and any confidential documents or records (whether in physical or digital form). Please arrange the return of these items with your line manager, Sandra Hughes, no later than 26 February 2026.
+ +
References Policy
+
The company's policy is to provide a factual reference confirming dates of employment and job title only. References should be requested through HR at hr@aldermoorlogistics.co.uk. The company will not provide references that include subjective assessments of performance.
+ +
Closing Statement
+
+
We wish to thank Marcus for his contribution to Aldermoor Logistics over the past six years and wish him well in his future career. Should you have any questions regarding this notice or your entitlements, please contact the HR team at hr@aldermoorlogistics.co.uk or on 0117 900 4500.
+
+
+
HR Representative
+
+
Signature
+
+
Name: Rachel Thornton
+
+
Title: HR Business Partner
+
+
+
Employee Acknowledgement
+
+
Signature
+
+
Name: Marcus Reid
+
+
Date: 3 February 2026
+
+
+
+
+ + diff --git a/engine/src/default_templates/service_agreement.html b/engine/src/default_templates/service_agreement.html new file mode 100644 index 0000000000..c87ddd3a98 --- /dev/null +++ b/engine/src/default_templates/service_agreement.html @@ -0,0 +1,292 @@ + + + + + + Service Agreement {{AGREEMENT_NUMBER}} + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
Service Agreement
+
Professional Services Contract
+
+
+ +
+ + +
+ Agreement Number: {{AGREEMENT_NUMBER}} + Effective Date: {{EFFECTIVE_DATE}} + Governing Law: {{GOVERNING_LAW}} +
+ +
+ + +
Parties
+
+
+
Service Provider
+
{{SERVICE_PROVIDER_NAME}}
+
{{SERVICE_PROVIDER_ADDRESS}}
+
+
+
Client
+
{{CLIENT_NAME}}
+
{{CLIENT_ADDRESS}}
+
+
+ + +
Agreement
+ + + + + + + + + + + + + + + + + + + + + + +
Signatures
+
+
+
Service Provider
+
+
Signature
+
+
Name: {{PROVIDER_SIGNER_NAME}}
+
+
Title: {{PROVIDER_SIGNER_TITLE}}
+
+
Date: {{PROVIDER_SIGN_DATE}}
+
+
+
Client
+
+
Signature
+
+
Name: {{CLIENT_SIGNER_NAME}}
+
+
Title: {{CLIENT_SIGNER_TITLE}}
+
+
Date: {{CLIENT_SIGN_DATE}}
+
+
+ +
+ + diff --git a/engine/src/default_templates/service_agreement_preview.html b/engine/src/default_templates/service_agreement_preview.html new file mode 100644 index 0000000000..907a6c9da6 --- /dev/null +++ b/engine/src/default_templates/service_agreement_preview.html @@ -0,0 +1,144 @@ + + + + + + Service Agreement + + + +
+
+
+
Service Agreement
+
Professional Services Contract
+
+
+
+ Agreement Number: SA-2026-0012 + Effective Date: 1 February 2026 + Governing Law: England and Wales +
+
+
Parties
+
+
+
Service Provider
+
Apex Digital Solutions Ltd
+
45 Tech Park, Cambridge, CB1 3NF, UK
Company No: 09812345 · VAT: GB987654321
+
+
+
Client
+
Meridian Retail Group plc
+
12 Commerce Square, Leeds, LS1 2AB, UK
Company No: 02345678
+
+
+
Agreement
+ + + + + + + + + + +
Signatures
+
+
+
Service Provider
+
+
Signature
+
+
Name: Daniel Ashworth
+
+
Title: Managing Director
+
+
Date: 1 February 2026
+
+
+
Client
+
+
Signature
+
+
Name: Victoria Pemberton
+
+
Title: Chief Operating Officer
+
+
Date: 1 February 2026
+
+
+
+ + diff --git a/engine/src/default_templates/standard_operating_procedures.html b/engine/src/default_templates/standard_operating_procedures.html new file mode 100644 index 0000000000..321eddb25d --- /dev/null +++ b/engine/src/default_templates/standard_operating_procedures.html @@ -0,0 +1,226 @@ + + + + + + Standard Operating Procedure + + + +
+ +
+ + +
+ {{LOGO_BLOCK}} +
+
{{SOP_TITLE}}
+
{{COMPANY_NAME}} · SOP No: {{SOP_NUMBER}} · v{{VERSION}} · Effective: {{EFFECTIVE_DATE}}
+
+
+
+ + +
Purpose
+
{{PURPOSE_TEXT}}
+ + +
Scope
+
{{SCOPE_TEXT}}
+ + +
Definitions
+
{{DEFINITIONS_TEXT}}
+ + +
Roles and Responsibilities
+
{{ROLES_TEXT}}
+ + +
Procedure
+
    + {{PROCEDURE_STEPS}} +
+ + +
Quality and Safety Notes
+
{{SAFETY_NOTES_TEXT}}
+ + +
Revision History
+ + + + + + + + + + + {{REVISION_ROWS}} + +
VersionDateAuthorSummary of Changes
+ + +
Approval
+
+ Approved by: {{APPROVER_NAME}} + Title: {{APPROVER_TITLE}} + Date: {{APPROVAL_DATE}} +
+
+
Approver Signature
+ +
+ + diff --git a/engine/src/default_templates/standard_operating_procedures_preview.html b/engine/src/default_templates/standard_operating_procedures_preview.html new file mode 100644 index 0000000000..1c8a4b3713 --- /dev/null +++ b/engine/src/default_templates/standard_operating_procedures_preview.html @@ -0,0 +1,149 @@ + + + + + + Standard Operating Procedure + + + +
+
+
+
+
Standard Operating Procedure
+
Meridian Pharma Manufacturing Ltd
+
+
+
+

SOP: Incoming Raw Materials Inspection and Release

+
+ SOP No: QA-RM-007 + Version: 2.1 + Effective: 1 February 2026 +
+ +
Purpose
+
This Standard Operating Procedure defines the process for receiving, inspecting, and releasing incoming raw materials and packaging components at the Meridian Pharma Manufacturing facility. The purpose is to ensure that all materials meet defined quality specifications before use in manufacturing operations and to maintain full traceability in compliance with GMP requirements.
+ +
Scope
+
This procedure applies to all active pharmaceutical ingredients (APIs), excipients, and primary and secondary packaging materials delivered to the Swindon manufacturing site. It applies to all personnel involved in goods receipt, quality control testing, and material release activities.
+ +
Definitions
+
GMP: Good Manufacturing Practice — regulatory guidelines governing the manufacture of pharmaceutical products. +CoA: Certificate of Analysis — document provided by the supplier confirming test results for a specific batch. +Quarantine: A temporary hold status applied to materials pending inspection and approval. +Release: The act of approving a material batch for use in manufacturing following satisfactory QC testing. +Deviation: Any departure from approved procedures or specifications.
+ +
Roles and Responsibilities
+
Warehouse Operative: Receives deliveries, performs initial count and visual inspection, applies quarantine labels, and updates the ERP system. +Quality Control Analyst: Collects samples, performs or co-ordinates laboratory testing, and updates the LIMS with results. +QC Supervisor: Reviews test results against specifications and authorises material release or rejection. +Quality Assurance Manager: Approves deviations and oversees the overall integrity of the process.
+ +
Procedure
+
    +
  1. Upon delivery, the Warehouse Operative verifies the delivery note against the purchase order in the ERP system. Any discrepancies in quantity, product code, or supplier must be recorded immediately and escalated to the QC Supervisor.
  2. +
  3. The Warehouse Operative performs a visual inspection of all outer packaging for signs of damage, contamination, or tampering. Damaged or suspect items are segregated and a Non-Conformance Report (NCR) is raised.
  4. +
  5. All accepted pallets and containers are labelled with a Quarantine sticker (yellow) and moved to the designated Quarantine area. No materials may leave the Quarantine area without QC authorisation.
  6. +
  7. The Warehouse Operative creates a goods receipt record in the ERP system, recording batch number, quantity, supplier, and date of receipt.
  8. +
  9. The QC Analyst collects representative samples in accordance with the approved sampling plan (refer to SOP QA-SP-003). Samples are transferred to the QC laboratory with a completed Sample Request Form.
  10. +
  11. The QC Analyst performs identity, purity, and specification testing in accordance with the approved test method. The supplier's CoA is reviewed and compared against internal specifications.
  12. +
  13. The QC Supervisor reviews all test results and the CoA. If all results comply with specifications, the Supervisor authorises release by updating the ERP status to "Approved" and replacing the Quarantine label with a green Released label.
  14. +
  15. If any result falls outside specification, the QC Supervisor initiates a deviation and places the batch on formal Reject status. The warehouse team moves the material to the Reject area. The Purchasing team is notified to arrange return or disposal with the supplier.
  16. +
  17. The QC Analyst files all records (test results, CoA, sample forms) in the batch record folder and archives a digital copy in the LIMS.
  18. +
+ +
Quality and Safety Notes
+
All personnel handling raw materials must wear appropriate PPE as specified in the material Safety Data Sheet (SDS). + +Materials must never be moved from Quarantine without written QC authorisation. Verbal instructions are not sufficient. + +Cold-chain materials (storage below 8°C) must be inspected and transferred to temperature-controlled storage within 30 minutes of delivery. Temperature excursions must be reported immediately. + +Any suspected counterfeit or adulterated material must be quarantined immediately and the QA Manager and Responsible Person notified without delay.
+ +
Revision History
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VersionDateAuthorSummary of Changes
1.014 Mar 2023A. PatelInitial issue.
2.09 Jan 2025S. OkonkwoUpdated sampling plan reference; added cold-chain handling requirements.
2.120 Jan 2026S. OkonkwoRevised ERP steps to reflect system upgrade; added digital LIMS archiving step.
+ +
Approval
+
+ Approved by: Dr Sarah Okonkwo + Title: Quality Assurance Manager + Date: 20 January 2026 +
+
+
Approver Signature
+
+ + diff --git a/engine/src/document_types.py b/engine/src/document_types.py new file mode 100644 index 0000000000..5544ea690c --- /dev/null +++ b/engine/src/document_types.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import json +import logging +import os +from functools import cache +from typing import Any + +from config import FAST_MODEL +from llm_utils import run_ai +from models import ChatMessage, DocTypeClassification +from prompts import document_type_classification_system_prompt + +logger = logging.getLogger(__name__) + + +@cache +def _load_template_mapping() -> tuple[dict[str, Any], set[str]]: + """Load template mapping JSON and extract available doctypes.""" + # Get the directory where this file is located + current_dir = os.path.dirname(os.path.abspath(__file__)) + template_mapping_path = os.path.join(current_dir, "template_mapping.json") + + with open(template_mapping_path, encoding="utf-8") as f: + template_mapping = json.load(f) + + # Extract all doctypes from the mapping + available_doctypes = {template.get("docType") for template in template_mapping.values() if template.get("docType")} + logger.info( + "[DOCTYPE] Loaded template mapping with %d doctypes", + len(available_doctypes), + ) + + return template_mapping, available_doctypes + + +# Document types that have format prompts (for AI-based extraction) +SUPPORTED_FORMAT_TYPES = { + "invoice", + "resume", + "cover_letter", + "contract", + "nda", + "meeting_agenda", + "quote", + "receipt", + "expense_report", + "terms_of_service", + "privacy_policy", + "proposal", + "report", + "letter", + "one_pager", + "statement_of_work", + "meeting_minutes", + "press_release", + "pay_stub", +} + + +def detect_document_type( + prompt: str, + confidence_threshold: float = 0.7, +) -> tuple[str, float]: + """ + Detect document type using AI classification. + + Args: + prompt: User's text prompt + latex_code: Optional LaTeX code to analyze + confidence_threshold: Minimum confidence (0-1) to accept a match from template list + + Returns: + Tuple of (doc_type, confidence) where confidence is 0.0-1.0 + """ + ai_type, confidence = _classify_with_ai(prompt, confidence_threshold) + if ai_type and ai_type != "other" and confidence >= confidence_threshold: + return ai_type, confidence + return ai_type or "other", confidence + + +def _classify_with_ai(prompt: str, confidence_threshold: float = 0.7) -> tuple[str, float]: + """ + Use a FAST/CHEAP LLM to classify the document type from the user's prompt. + + This uses FAST_MODEL (e.g., gpt-4.1-nano) for quick, cost-effective classification. + Returns a tuple of (doc_type, confidence) or None if classification fails. + """ + # Load available doctypes from template mapping + _, available_doctypes = _load_template_mapping() + + # Build the list of available doctypes for the prompt + # Include both template mapping doctypes and legacy supported types + all_doctypes = sorted(list(available_doctypes | SUPPORTED_FORMAT_TYPES)) + doctypes_list = ", ".join(all_doctypes) + ", other" + system_prompt = document_type_classification_system_prompt(doctypes_list) + messages = [ + ChatMessage(role="system", content=system_prompt), + ChatMessage(role="user", content=prompt[:500]), + ] + + parsed = run_ai( + FAST_MODEL, + messages, + DocTypeClassification, + tag="doc_type_classify", + max_tokens=20, + ) + content = parsed.doc_type.strip().lower() + content = content.replace("-", "_").replace(" ", "_") + content = content.split()[0] if content else "" + + logger.info( + "[DOCTYPE] Fast AI classification model=%s result=%s", + FAST_MODEL, + content, + ) + # Check if the result is in available doctypes (from template mapping) + if content in available_doctypes: + # High confidence for template mapping matches + return content, 0.85 + # Check if it's in legacy supported types + elif content in SUPPORTED_FORMAT_TYPES: + # Medium confidence for legacy types + return content, 0.75 + elif content == "other": + return "other", 0.5 + else: + # Unknown type - low confidence + logger.warning(f"[DOCTYPE] AI returned unknown type '{content}', falling back to 'other'") + return "other", 0.3 + + +__all__ = [ + "detect_document_type", + "SUPPORTED_FORMAT_TYPES", +] diff --git a/engine/src/editing/__init__.py b/engine/src/editing/__init__.py new file mode 100644 index 0000000000..0bc653fa9d --- /dev/null +++ b/engine/src/editing/__init__.py @@ -0,0 +1,3 @@ +from .routes import register_edit_routes + +__all__ = ["register_edit_routes"] diff --git a/engine/src/editing/confirmation.py b/engine/src/editing/confirmation.py new file mode 100644 index 0000000000..746846536b --- /dev/null +++ b/engine/src/editing/confirmation.py @@ -0,0 +1,96 @@ +""" +Confirmation intent classification during AWAITING_CONFIRM state. +CRITICAL: Prevents misexecution when user changes mind during confirmation. +""" + +from config import FAST_MODEL +from llm_utils import run_ai +from models import ChatMessage, ConfirmationAnswer, ConfirmationIntent +from models.tool_models import OperationId +from prompts import confirmation_intent_system_prompt, confirmation_question_system_prompt + + +def classify_confirmation_intent( + message: str, + pending_plan_summary: str, + history: list[ChatMessage], + *, + session_id: str | None = None, +) -> ConfirmationIntent | None: + """ + Classify user intent during confirmation phase. + CRITICAL: This prevents misexecution when user changes mind. + + Returns: + - confirm: User agrees, execute plan + - cancel: User cancels, clear plan + - modify: User wants to change the plan (we'll clear + replan) + - new_request: User wants something different (clear + route as fresh) + - question: User asks about the plan (answer without executing) + + Examples: + "yes" → confirm + "cancel" → cancel + "actually delete page 7" → modify + "never mind, compress it" → new_request + "what will this do?" → question + + Implementation notes: + - For "modify": We implement minimal safe behavior (clear + replan) + - No complex patching needed - just ensure old plan never executes + """ + system_prompt = confirmation_intent_system_prompt(pending_plan_summary) + messages = [ChatMessage(role="system", content=system_prompt)] + messages.extend(history[-3:]) # Last few messages for context + messages.append(ChatMessage(role="user", content=message)) + + decision = run_ai( + FAST_MODEL, + messages, + ConfirmationIntent, + tag="edit_confirmation_intent", + log_label="edit-confirmation-intent", + log_exchange=True, + session_id=session_id, + ) + return decision + + +def answer_confirmation_question( + question: str, + plan_summary: str, + operations: list[OperationId], + history: list[ChatMessage], + *, + session_id: str | None = None, +) -> str: + """ + Answer user's question about pending plan without executing. + + Args: + question: User's question + plan_summary: Summary of pending plan + operations: Operation objects for details + history: Conversation history + session_id: Session ID for logging + + Returns: + Answer to user's question + """ + system_prompt = confirmation_question_system_prompt(plan_summary, operations) + messages = [ChatMessage(role="system", content=system_prompt)] + messages.extend(history[-3:]) + messages.append(ChatMessage(role="user", content=question)) + + response = run_ai( + FAST_MODEL, + messages, + ConfirmationAnswer, + tag="edit_confirmation_question", + log_label="edit-confirmation-question", + log_exchange=True, + session_id=session_id, + ) + if response and response.message: + return response.message.strip() + raise RuntimeError("AI confirmation question response failed.") diff --git a/engine/src/editing/constants.py b/engine/src/editing/constants.py new file mode 100644 index 0000000000..c266f09cbe --- /dev/null +++ b/engine/src/editing/constants.py @@ -0,0 +1,199 @@ +from models import PdfPreflight +from models.tool_models import OperationId + +REQUIRED_CLARIFICATIONS = { + "removePassword": ["password"], + "deletePages": ["pageNumbers"], +} + +DESTRUCTIVE_OPERATIONS = { + "removePassword": "This will remove all security from your PDF.", + "sanitize": "This will remove all metadata and hidden content.", + "flatten": "This will convert all form fields to static content (irreversible).", + "deletePages": "This will permanently delete the specified pages.", +} + +DEFAULT_OPERATION_OVERRIDES = { + "addPageNumbers": { + "fontType": "times", + "position": 8, + "pageNumbers": "all", + "pagesToNumber": "all", + "customMargin": "medium", + "customText": "{n}", + }, + "processPdfWithOCR": { + "languages": ["eng"], + "ocrType": "skip-text", + "ocrRenderType": "hocr", + }, + "optimizePdf": { + "optimizeLevel": 6, + "grayscale": False, + "linearize": False, + "normalize": False, + }, + "removeBlankPages": { + "threshold": 10, + "whitePercent": 95, + }, +} + + +# Risk policy table: Single source of truth for operation risk assessment +OPERATION_RISK_POLICY = [ + # High risk - destructive content removal (always confirm) + { + "op": "deletePages", + "risk": "high", + "always_confirm": True, + "reason": "destructive content removal", + "warning": "This will permanently delete the specified pages.", + }, + { + "op": "removePassword", + "risk": "high", + "always_confirm": True, + "reason": "removes all security", + "warning": "This will remove all security from your PDF.", + }, + { + "op": "sanitize", + "risk": "high", + "always_confirm": True, + "reason": "removes metadata and hidden content", + "warning": "This will remove all metadata and hidden content.", + }, + { + "op": "flatten", + "risk": "high", + "always_confirm": True, + "reason": "irreversible form field conversion", + "warning": "This will convert all form fields to static content (irreversible).", + }, + # Medium risk - lossy transformations + { + "op": "optimizePdf", + "risk": "medium", + "always_confirm": False, + "reason": "lossy compression", + "confirm_if": lambda preflight: (preflight.file_size_mb or 0) > 50, # > 50MB + }, + { + "op": "processPdfWithOCR", + "risk": "medium", + "always_confirm": False, + "reason": "may alter text layer", + }, + { + "op": "extractImages", + "risk": "medium", + "always_confirm": False, + "reason": "creates derivative content", + }, + # Low risk - non-destructive transformations + { + "op": "rotatePDF", + "risk": "low", + "always_confirm": False, + }, + { + "op": "splitPdf", + "risk": "low", + "always_confirm": False, + }, + { + "op": "mergePdfs", + "risk": "low", + "always_confirm": False, + }, + { + "op": "addPageNumbers", + "risk": "low", + "always_confirm": False, + }, + { + "op": "addWatermark", + "risk": "low", + "always_confirm": False, + }, +] + + +def get_operation_risk(operation_id: OperationId, preflight: PdfPreflight | None = None) -> dict: + """ + Get risk assessment for operation. + + Returns: + { + "level": "low" | "medium" | "high", + "reason": "...", + "should_confirm": bool, + "warning": "..." (if high risk) + } + """ + preflight = preflight or PdfPreflight() + + for policy in OPERATION_RISK_POLICY: + if policy["op"] == operation_id: + should_confirm = policy.get("always_confirm", False) + + # Check conditional confirmation + if not should_confirm and "confirm_if" in policy: + confirm_fn = policy["confirm_if"] + if callable(confirm_fn): + should_confirm = confirm_fn(preflight) + + return { + "level": policy["risk"], + "reason": policy.get("reason", ""), + "should_confirm": should_confirm, + "warning": policy.get("warning"), + } + + # Default: assume low risk + return { + "level": "low", + "reason": "", + "should_confirm": False, + "warning": None, + } + + +def assess_plan_risk(operation_ids: list[OperationId], preflight: PdfPreflight | None = None) -> dict: + """ + Assess combined risk for multiple operations. + + Args: + operation_ids: List of operation IDs in plan + preflight: File metadata + + Returns: + { + "level": "low" | "medium" | "high", + "reasons": [list of risk reasons], + "should_confirm": bool (if any op requires confirmation) + } + """ + risks = [get_operation_risk(op_id, preflight) for op_id in operation_ids] + + # Highest risk level wins + if any(r["level"] == "high" for r in risks): + level = "high" + elif any(r["level"] == "medium" for r in risks): + level = "medium" + else: + level = "low" + + # Multi-op with any high risk should confirm + should_confirm = len(operation_ids) > 1 and level == "high" + # Or any single op that always requires confirmation + should_confirm = should_confirm or any(r["should_confirm"] for r in risks) + + reasons = [r["reason"] for r in risks if r["reason"]] + + return { + "level": level, + "reasons": reasons, + "should_confirm": should_confirm, + } diff --git a/engine/src/editing/decisions.py b/engine/src/editing/decisions.py new file mode 100644 index 0000000000..3fc823733d --- /dev/null +++ b/engine/src/editing/decisions.py @@ -0,0 +1,130 @@ +import textwrap +from dataclasses import asdict + +from config import FAST_MODEL +from file_processing_agent import ToolCatalogService +from llm_utils import run_ai +from models import AskUserMessage, ChatMessage, DefaultsDecision, IntentDecision +from prompts import ( + edit_defaults_decision_system_prompt, + edit_info_system_prompt, + edit_intent_classification_system_prompt, +) + + +def wants_defaults(message: str, session_id: str | None = None) -> bool: + system_prompt = edit_defaults_decision_system_prompt() + messages = [ + ChatMessage(role="system", content=system_prompt), + ChatMessage(role="user", content=message), + ] + decision = run_ai( + FAST_MODEL, + messages, + DefaultsDecision, + tag="edit_defaults_decision", + log_label="edit-defaults-decision", + log_exchange=True, + session_id=session_id, + ) + return decision.use_defaults + + +def classify_edit_intent( + message: str, + history: list[ChatMessage], + *, + session_id: str | None = None, +) -> IntentDecision | None: + system_prompt = edit_intent_classification_system_prompt() + messages = [ChatMessage(role="system", content=system_prompt)] + messages.extend(history) + messages.append(ChatMessage(role="user", content=message)) + decision = run_ai( + FAST_MODEL, + messages, + IntentDecision, + tag="edit_intent_decision", + log_label="edit-intent-decision", + log_exchange=True, + session_id=session_id, + ) + return decision + + +def answer_conversational_info( + message: str, + history: list[ChatMessage], + tool_catalog: ToolCatalogService, + *, + session_id: str | None = None, +) -> str: + """Handle conversational queries without files (greetings, help requests, capability questions).""" + selection_index = tool_catalog.build_selection_index() + + system_instructions = textwrap.dedent("""\ + Answer the user's question about capabilities. + Be friendly, clear, and helpful. + + This system can: + 1. Edit PDF files - compress, merge, split, rotate, watermark, OCR, convert, add security, and many more operations + 2. Create new PDF documents - generate professional documents from descriptions (business proposals, reports, resumes, etc.) + 3. Create smart folders - set up automated PDF processing workflows that run on uploaded files + + If the user is greeting you (hello, hi, hey), respond warmly and briefly explain what you can help with. + If asking about capabilities (what can you do, help), provide a clear overview of all three main features. + For PDF editing questions, reference the available tools from the tool_catalog below. + Use bullets when listing multiple options. + Keep responses concise but informative. + Encourage them to upload a PDF to edit it, or ask to create a new document. + Do not mention session IDs, technical details, or backend concepts. + """).strip() + + system_payload = { + "instructions": system_instructions, + "tool_catalog": [asdict(entry) for entry in selection_index], + } + + messages = [ + ChatMessage(role="system", content=[system_payload]), + *history, + ChatMessage(role="user", content=message), + ] + response = run_ai( + FAST_MODEL, + messages, + AskUserMessage, + tag="conversational_info_response", + log_label="conversational-info-response", + log_exchange=True, + session_id=session_id, + ) + return response.message + + +def answer_edit_info( + message: str, + history: list[ChatMessage], + file_name: str, + file_type: str | None, + tool_catalog: ToolCatalogService, + *, + session_id: str | None = None, +) -> str: + catalog_text = tool_catalog.build_catalog_prompt() + system_prompt = edit_info_system_prompt(file_name, file_type, catalog_text) + messages = [ChatMessage(role="system", content=system_prompt)] + messages.extend(history) + messages.append(ChatMessage(role="user", content=message)) + response = run_ai( + FAST_MODEL, + messages, + AskUserMessage, + tag="edit_info_response", + log_label="edit-info-response", + log_exchange=True, + session_id=session_id, + ) + if response and response.message.strip(): + return response.message.strip() + raise RuntimeError("AI edit info response failed.") diff --git a/engine/src/editing/exceptions.py b/engine/src/editing/exceptions.py new file mode 100644 index 0000000000..762133bef0 --- /dev/null +++ b/engine/src/editing/exceptions.py @@ -0,0 +1,11 @@ +"""Custom exceptions for the editing module.""" + + +class InsufficientCreditsError(Exception): + """Raised when an operation is blocked due to insufficient credits.""" + + def __init__(self, status_code: int = 429, error_body: str = "", error_json: dict | None = None): + self.status_code = status_code + self.error_body = error_body + self.error_json = error_json or {} + super().__init__(f"Insufficient credits (HTTP {status_code})") diff --git a/engine/src/editing/handlers.py b/engine/src/editing/handlers.py new file mode 100644 index 0000000000..6e56fb65b2 --- /dev/null +++ b/engine/src/editing/handlers.py @@ -0,0 +1,676 @@ +import json +import logging +import mimetypes +import os +import uuid +from pathlib import Path + +from flask import jsonify +from flask.typing import ResponseReturnValue +from werkzeug.datastructures import FileStorage +from werkzeug.security import safe_join + +import analytics +import models +from config import OUTPUT_DIR +from file_processing_agent import ToolCatalogService + +from .constants import assess_plan_risk, get_operation_risk +from .decisions import ( + answer_edit_info, + classify_edit_intent, +) +from .operations import ( + answer_pdf_question, + apply_smart_defaults, + build_pdf_text_context, + build_plan_summary, + create_session_file, + format_disambiguation_question, + get_pdf_preflight, + sanitize_filename, + validate_operation_chain, +) +from .session_store import EditSession, EditSessionFile, EditSessionStore, PendingOperation, PendingPlan +from .state_router import route_message + +logger = logging.getLogger(__name__) + + +class EditService: + def __init__(self, session_store: EditSessionStore, tool_catalog: ToolCatalogService) -> None: + self.sessions = session_store + self.tool_catalog = tool_catalog + self.edit_upload_dir = os.path.join(OUTPUT_DIR, "uploads") + + def _strip_file_context_history(self, messages: list[models.ChatMessage]) -> list[models.ChatMessage]: + filtered: list[models.ChatMessage] = [] + for msg in messages: + content = msg.content + if isinstance(content, list): + new_content = [ + item for item in content if not (isinstance(item, dict) and item.get("type") == "file_context") + ] + if not new_content: + continue + if new_content == content: + filtered.append(msg) + else: + filtered.append(models.ChatMessage(role=msg.role, content=new_content)) + else: + filtered.append(msg) + return filtered + + def _build_status_response(self, session: EditSession) -> ResponseReturnValue: + if session.file_path: + filename = os.path.relpath(session.file_path, OUTPUT_DIR) + response = models.EditMessageResponse( + assistant_message="", + result_file_url=f"/output/{filename}", + result_file_name=session.file_name, + result_files=[models.EditResultFile(url=f"/output/{filename}", name=session.file_name)], + ) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + response = models.EditMessageResponse(assistant_message="") + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + def _primary_file(self, session: EditSession) -> EditSessionFile | None: + if session.files: + return session.files[0] + if session.file_path: + return EditSessionFile( + file_id="primary", + file_path=session.file_path, + file_name=session.file_name, + file_type=session.file_type, + preflight=session.preflight, + ) + return None + + def _ensure_file_context(self, session: EditSession) -> None: + if not session.file_path: + return + if session.file_context and session.file_context_path == session.file_path: + return + context = build_pdf_text_context(session.file_path) + session.file_context = context + session.file_context_path = session.file_path + message = models.ChatMessage(role="assistant", content=[context]) + for index in range(len(session.messages) - 1, -1, -1): + if session.messages[index].role == "user": + session.messages.insert(index, message) + return + session.messages.append(message) + + def create_session(self, files: list[FileStorage]) -> ResponseReturnValue: + if not files: + return jsonify({"error": "Missing file upload"}), 400 + + session_id = str(uuid.uuid4()) + session_files: list[EditSessionFile] = [] + + for index, file in enumerate(files): + original_name = sanitize_filename(file.filename or f"upload-{index + 1}.pdf") + extension = Path(original_name).suffix or ".pdf" + if extension.lower() != ".pdf": + return jsonify({"error": "Only PDF files are supported right now."}), 400 + session_dir = os.path.join(OUTPUT_DIR, session_id) + os.makedirs(session_dir, exist_ok=True) + file_path = os.path.join(session_dir, original_name) + file.save(file_path) + + # Create session file with proper type detection and preflight + session_file = create_session_file( + file_path=file_path, + file_name=original_name, + content_type=file.mimetype, + content_disposition=None, + ) + session_files.append(session_file) + + primary = session_files[0] + session = EditSession( + session_id=session_id, + file_path=primary.file_path, + file_name=primary.file_name, + file_type=primary.file_type, + preflight=primary.preflight, + files=session_files, + ) + self.sessions.set(session) + + page_counts = [ + page_count for item in session_files if isinstance((page_count := item.preflight.page_count), int) + ] + size_values = [ + file_size_mb + for item in session_files + if isinstance((file_size_mb := item.preflight.file_size_mb), (int, float)) + ] + analytics.track_event( + user_id=session_id, + event_name="edit_session_created", + properties={ + "session_id": session_id, + "file_count": len(session_files), + "total_pages": sum(page_counts), + "total_size_mb": round(sum(size_values), 2), + "has_text_layer": any(item.preflight.has_text_layer for item in session_files), + "has_encrypted": any(item.preflight.is_encrypted for item in session_files), + }, + ) + + response = models.EditSessionResponse( + session_id=session_id, + file_name=primary.file_name, + file_type=primary.file_type, + ) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + def add_attachment(self, session_id: str, name: str | None, file: FileStorage | None) -> ResponseReturnValue: + session = self.sessions.get(session_id) + if not session: + return jsonify({"error": "Edit session not found"}), 404 + if not file or not name: + return jsonify({"error": "Missing attachment or name"}), 400 + + original_name = sanitize_filename(file.filename or "attachment") + file_type = file.mimetype or mimetypes.guess_type(original_name)[0] + extension = Path(original_name).suffix or "" + attachment_id = uuid.uuid4().hex + stored_name = f"{session_id}-attachment-{attachment_id}{extension}" + os.makedirs(self.edit_upload_dir, exist_ok=True) + file_path = safe_join(self.edit_upload_dir, stored_name) + if file_path is None: + return jsonify({"error": "Invalid file path"}), 400 + file.save(file_path) + + session.attachments[name] = EditSessionFile( + file_id=attachment_id, + file_path=file_path, + file_name=original_name, + file_type=file_type, + ) + return jsonify({"name": name, "file_name": original_name}) + + def handle_message(self, session_id: str, payload: models.EditMessageRequest) -> ResponseReturnValue: + session = self.sessions.get(session_id) + if not session: + return jsonify({"error": "Edit session not found"}), 404 + + user_message = payload.message.strip() + if not user_message: + return jsonify({"error": "Message is required"}), 400 + + if payload.action == "status": + return self._build_status_response(session) + + if payload.action in {"confirm", "cancel"} and not session.pending_plan: + response = models.EditMessageResponse(assistant_message="") + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + # Add user message to history + session.messages.append(models.ChatMessage(role="user", content=user_message)) + + # NEW: Use state router for pending plan handling + if session.pending_plan: + routing_result = route_message( + session, + user_message, + self._strip_file_context_history(session.messages), + ) + + if routing_result.action == "execute": + if routing_result.plan is None: + assistant_message = "The pending plan could not be found. Please try again." + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse(assistant_message=assistant_message) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)), 500 + # Consume the plan immediately to avoid duplicate confirm requests + session.pending_plan = None + # Execute the pending plan + return self._execute_pending_plan(session, routing_result.plan) + + elif routing_result.action == "cancelled": + assistant_message = routing_result.message or "Cancelled. Let me know if you want to do something else." + # Consume the plan immediately to avoid duplicate cancel requests + session.pending_plan = None + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse(assistant_message=assistant_message) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + elif routing_result.action == "answer_question": + assistant_message = routing_result.message or "Please confirm to proceed or cancel to stop." + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse(assistant_message=assistant_message) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + elif routing_result.action == "already_executed": + assistant_message = routing_result.message or "This plan has already been executed." + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse(assistant_message=assistant_message) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + elif routing_result.action == "route_fresh": + # Clear pending and continue to fresh request handling below + session.pending_plan = None + + elif routing_result.action == "error": + assistant_message = routing_result.error or "Something went wrong. Please try again." + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse(assistant_message=assistant_message) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)), 500 + + # Repeat request handling - use atomic execution for consistency + if session.last_operation_id and self._is_repeat_request(user_message): + operation_id = session.last_operation_id + param_model = self.tool_catalog.get_operation(operation_id) + if not param_model: + session.last_operation_id = None + session.last_parameters = None + else: + parameters = apply_smart_defaults( + user_message, + session.last_parameters or param_model.model_validate({}), + ) + + # Create plan and execute atomically (same as new requests) + plan = PendingPlan( + state="AWAITING_CONFIRM", + ops=[PendingOperation(operation_id=operation_id, parameters=parameters)], + risk_level="low", + risk_reasons=[], + source_message=user_message, + ) + + return self._execute_pending_plan(session, plan) + + intent = payload.edit_intent + if not intent: + intent = classify_edit_intent( + user_message, + self._strip_file_context_history(session.messages), + session_id=session.session_id, + ) + if intent and intent.mode == "document_question": + primary = self._primary_file(session) + if not primary: + assistant_message = "I couldn't find a file in this session. Please upload a PDF first." + elif primary.preflight.has_text_layer is False: + assistant_message = "I couldn't read text in this PDF. Want me to run OCR first?" + else: + self._ensure_file_context(session) + assistant_message = answer_pdf_question(primary.file_path, user_message) + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse(assistant_message=assistant_message) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + if intent and intent.mode in {"info", "ambiguous"}: + if intent.mode == "info": + if intent.requires_file_context: + self._ensure_file_context(session) + primary = self._primary_file(session) + if primary: + assistant_message = answer_pdf_question(primary.file_path, user_message) + else: + assistant_message = "I couldn't find a file in this session. Please upload a PDF first." + else: + assistant_message = answer_edit_info( + user_message, + self._strip_file_context_history(session.messages), + session.file_name, + session.file_type, + self.tool_catalog, + session_id=session.session_id, + ) + else: + assistant_message = "Do you want me to run a tool on this file, or just explain the options?" + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse(assistant_message=assistant_message, needs_more_info=True) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + if intent and intent.requires_file_context: + self._ensure_file_context(session) + + selection_history = ( + session.messages + if intent and intent.requires_file_context + else self._strip_file_context_history(session.messages) + ) + selection = self.tool_catalog.select_edit_tool( + history=selection_history, + uploaded_files=[ + models.UploadedFileInfo(name=item.file_name, type=item.file_type) for item in session.files + ], + preflight=session.preflight, + session_id=session.session_id, + ) + + logger.info( + "[EDIT] selection action=%s operation_ids=%s", + selection.action, + selection.operation_ids, + ) + + selected_ops = self._selection_operations(session, selection, user_message, selection_history) + analytics.track_event( + user_id=session.session_id, + event_name="edit_tool_selected", + properties={ + "session_id": session.session_id, + "selection_action": selection.action, + "operation_ids": [op_id for op_id, _ in selected_ops], + "operation_count": len(selected_ops), + "intent_mode": intent.mode if intent else None, + "has_file_context": bool(intent and intent.requires_file_context), + }, + ) + if selection.action == "call_tool" and not selected_ops: + logger.warning( + "[EDIT] selection has no operations session_id=%s message=%s payload=%s", + session.session_id, + user_message, + json.dumps(selection.model_dump(), ensure_ascii=True), + ) + assistant_message = format_disambiguation_question() + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse( + assistant_message=assistant_message, + needs_more_info=True, + ) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + logger.info( + "[EDIT] selected_ops session_id=%s count=%s ops=%s", + session.session_id, + len(selected_ops), + [op_id for op_id, _ in selected_ops], + ) + + if selection.action == "ask_user": + if not selected_ops: + assistant_message = selection.response_message or "I could not find a matching tool for that request." + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse(assistant_message=assistant_message, needs_more_info=True) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + # Use _handle_selected_ops for consistency - it handles missing params and PendingPlan creation + return self._handle_selected_ops( + selected_ops, + user_message=user_message, + session=session, + response_message=selection.response_message, + ) + + if selection.action != "call_tool" or not selected_ops: + assistant_message = selection.response_message or "I could not find a matching tool for that request." + logger.info( + "[EDIT] no_tool/no_ops action=%s message=%s", + selection.action, + assistant_message[:100] if assistant_message else None, + ) + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse(assistant_message=assistant_message) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + return self._handle_selected_ops( + selected_ops, + user_message=user_message, + session=session, + response_message=selection.response_message, + ) + + def _execute_pending_plan(self, session: EditSession, plan: PendingPlan) -> ResponseReturnValue: + """ + Convert pending plan into frontend-executable tool calls. + Marks plan as executed for idempotency. + """ + # Check idempotency + if plan.plan_id in session.executed_plan_ids: + assistant_message = "This operation has already been executed." + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse(assistant_message=assistant_message) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + tool_calls: list[models.EditToolCall] = [] + for pending_op in plan.ops: + param_model = self.tool_catalog.get_operation(pending_op.operation_id) + if not param_model: + continue + tool_calls.append( + models.EditToolCall( + operation_id=pending_op.operation_id, + parameters=pending_op.parameters, + ) + ) + + if not tool_calls: + assistant_message = "I could not build a runnable tool plan. Please try rephrasing the request." + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse(assistant_message=assistant_message) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)), 500 + + session.executed_plan_ids.add(plan.plan_id) + session.pending_plan = None + + if plan.ops: + session.last_operation_id = plan.ops[-1].operation_id + session.last_parameters = plan.ops[-1].parameters + + execution_mode = "single" if len(tool_calls) == 1 else "pipeline" + pipeline_name = "AI Generated Pipeline" if execution_mode == "pipeline" else None + + analytics.track_event( + user_id=session.session_id, + event_name="edit_plan_emitted_for_frontend_execution", + properties={ + "session_id": session.session_id, + "operation_ids": [op.operation_id for op in plan.ops], + "operation_count": len(plan.ops), + "risk_level": plan.risk_level, + "risk_reasons_count": len(plan.risk_reasons), + "execution_mode": execution_mode, + }, + ) + + session.messages.append(models.ChatMessage(role="assistant", content="Prepared tool plan for frontend")) + + response = models.EditMessageResponse( + assistant_message="", + tool_calls=tool_calls, + execute_on_frontend=True, + frontend_plan=models.FrontendExecutionPlan( + mode=execution_mode, + steps=[ + models.FrontendExecutionStep( + operation_id=call.operation_id, + parameters=call.parameters, + ) + for call in tool_calls + ], + pipeline_name=pipeline_name, + ), + ) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + def _selection_operations( + self, + session: EditSession, + selection: models.EditToolSelection, + user_message: str, + history: list[models.ChatMessage], + ) -> list[tuple[models.tool_models.OperationId, models.tool_models.ParamToolModel | None]]: + ops: list[tuple[models.tool_models.OperationId, models.tool_models.ParamToolModel | None]] = [] + for operation_id in selection.operation_ids: + param_model = self.tool_catalog.get_operation(operation_id) + if not param_model: + continue + params = self.tool_catalog.extract_operation_parameters( + operation_id=operation_id, + previous_operations=ops, + user_message=user_message, + history=history, + preflight=session.preflight, + session_id=session.session_id, + ) + ops.append((operation_id, params)) + return ops + + def _is_repeat_request(self, message: str) -> bool: + value = message.strip().lower() + + # Don't treat as repeat if user is requesting new/additional actions + # E.g., "compress and rotate again", "make it smaller, and rotate again" + action_words = [ + "compress", + "optimize", + "smaller", + "larger", + "bigger", + "rotate", + "split", + "merge", + "delete", + "extract", + "add", + "remove", + "convert", + "repair", + "unlock", + "watermark", + "sign", + "flatten", + "ocr", + "searchable", + "linearize", + "grayscale", + ] + if any(action in value for action in action_words): + # If message contains action words, parse it as a new request, not a repeat + return False + + # Only treat as repeat if it's JUST asking to repeat with no new actions + return any( + phrase in value + for phrase in ( + "do that again", + "do it again", + "repeat that", + "repeat it", + "redo that", + "redo it", + "same again", + "run again", + "try again", + ) + ) + + def _handle_selected_ops( + self, + selected_ops: list[tuple[models.tool_models.OperationId, models.tool_models.ParamToolModel | None]], + user_message: str, + session: EditSession, + *, + response_message: str | None = None, + ) -> ResponseReturnValue: + """ + Handle selected operations using execution and parameter completion. + Creates PendingPlan and routes through state machine. + """ + # Refresh preflight for up-to-date metadata (only for PDFs) + if session.files and session.files[0].file_path: + if session.files[0].file_type == "application/pdf": + session.preflight = get_pdf_preflight(session.files[0].file_path) + else: + session.preflight = models.PdfPreflight() + + # Process each operation (first pass without forcing defaults) + pending_ops: list[PendingOperation] = [] + operations: list[models.tool_models.OperationId] = [] + + for operation_id, raw_parameters in selected_ops: + param_model = self.tool_catalog.get_operation(operation_id) + if not param_model: + assistant_message = "I could not find that tool. Please try another request." + session.messages.append(models.ChatMessage(role="assistant", content=assistant_message)) + response = models.EditMessageResponse(assistant_message=assistant_message) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + operations.append(operation_id) + + # Apply defaults + parameters = apply_smart_defaults( + user_message, + raw_parameters or param_model.model_validate({}), + ) + + pending_ops.append(PendingOperation(operation_id=operation_id, parameters=parameters)) + + # Validate operation chain compatibility + validation = validate_operation_chain(operations) + if not validation.is_valid: + error_msg = validation.error_message or "Incompatible operation chain" + session.messages.append(models.ChatMessage(role="assistant", content=error_msg)) + # Return structured data for frontend to format with translated names + response = models.EditMessageResponse( + assistant_message="", # Frontend will format from validation_error + result_json=( + {"validation_error": validation.error_data.model_dump(by_alias=True, mode="json")} + if validation.error_data + else None + ), + ) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)), 400 + + # No missing params - assess risk + risk_assessment = assess_plan_risk(operations, session.preflight) + + # Create pending plan or execute immediately + if risk_assessment.get("should_confirm"): + # Need confirmation - create AWAITING_CONFIRM plan + plan = PendingPlan( + state="AWAITING_CONFIRM", + ops=pending_ops, + risk_level=risk_assessment["level"], + risk_reasons=risk_assessment.get("reasons", []), + source_message=user_message, + ) + session.pending_plan = plan + + plan_summary = build_plan_summary(operations) + plan_summary += "\n\nConfirm to proceed or cancel to stop." + + session.messages.append(models.ChatMessage(role="assistant", content=plan_summary)) + + # Build tool calls for preview + tool_calls = [ + models.EditToolCall( + operation_id=op.operation_id, + parameters=op.parameters, + ) + for op in pending_ops + ] + + # Get warning if high risk + warning = None + if len(operations) == 1: + op_risk = get_operation_risk(operations[0], session.preflight) + warning = op_risk.get("warning") + + response = models.EditMessageResponse( + assistant_message=plan_summary, + confirmation_required=True, + warning=warning, + tool_calls=tool_calls, + ) + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) + + # Low risk - execute immediately using atomic execution + plan = PendingPlan( + state="AWAITING_CONFIRM", # Use confirm state but execute immediately + ops=pending_ops, + risk_level=risk_assessment["level"], + risk_reasons=risk_assessment.get("reasons", []), + source_message=user_message, + ) + + return self._execute_pending_plan(session, plan) diff --git a/engine/src/editing/operations.py b/engine/src/editing/operations.py new file mode 100644 index 0000000000..dd717a3f44 --- /dev/null +++ b/engine/src/editing/operations.py @@ -0,0 +1,309 @@ +import logging +import os +import re +import uuid +from dataclasses import dataclass +from typing import Any + +from pypdf import PdfReader + +from config import SMART_MODEL +from llm_utils import run_ai +from models import ChatMessage, IncompatibleChainError, OperationRef, PdfAnswer, PdfPreflight, tool_models +from pdf_text_editor import convert_pdf_to_text_editor_document +from prompts import pdf_qa_system_prompt + +from .session_store import EditSessionFile + +logger = logging.getLogger(__name__) + + +def sanitize_filename(filename: str) -> str: + cleaned = re.sub(r"[^a-zA-Z0-9._-]+", "_", filename or "") + return cleaned.strip("._") or "upload.pdf" + + +def infer_smart_defaults( + user_message: str, + parameters: tool_models.ParamToolModel, +) -> tool_models.ParamToolModel: + # TODO: Get rid of this function. It only works in English and shouldn't be necessary + params = parameters.model_copy() + text = user_message.lower() + + if isinstance(params, tool_models.RotateParams): + desired_angle = 90 + if any(word in text for word in ["right", "clockwise"]): + desired_angle = 90 + elif any(word in text for word in ["left", "counter", "anticlockwise", "anti-clockwise"]): + desired_angle = 270 + elif any(word in text for word in ["upside", "180"]): + desired_angle = 180 + if params.angle not in {90, 180, 270}: + params.angle = desired_angle + return params + + if isinstance(params, tool_models.OcrParams): + if any(word in text for word in ["searchable", "text layer", "text overlaid"]): + params.ocr_render_type = "sandwich" + elif any(word in text for word in ["hocr", "layout", "bounding boxes"]): + params.ocr_render_type = "hocr" + if any(word in text for word in ["spanish", "español", "espanol"]): + params.languages = ["spa"] + return params + + if isinstance(params, tool_models.WatermarkParams): + if params.watermark_type is None: + has_text = bool(params.watermark_text) + has_image = params.watermark_image is not None + if has_image and not has_text: + params.watermark_type = "image" + else: + params.watermark_type = "text" + return params + + return params + + +def format_disambiguation_question() -> str: + return ( + "I can help with rotate, OCR (make searchable), compress, split, merge, extract, and more. " + "Which change do you want?" + ) + + +# Operations that must be last in a chain — either because they produce non-PDF output, +# or because their output (e.g. an encrypted PDF) cannot be processed by subsequent operations. +TERMINAL_OPERATIONS = { + # Conversion operations (produce various file formats) + "pdfToCsv", # Produces CSV + "pdfToExcel", # Produces Excel + "pdfToHtml", # Produces HTML + "pdfToXml", # Produces XML + "pdfToText", # Produces plain text + "processPdfToRTForTXT", # Produces RTF/TXT + "convertPdfToCbr", # Produces CBR + "convertPdfToCbz", # Produces CBZ + # Analysis operations (produce JSON/Boolean responses) + "containsImage", # Returns Boolean + "containsText", # Returns Boolean + "getPdfInfo", # Returns JSON + "getBasicInfo", # Returns JSON + "getDocumentProperties", # Returns JSON + "getAnnotationInfo", # Returns JSON + "getFontInfo", # Returns JSON + "getFormFields", # Returns JSON + "getPageCount", # Returns JSON + "getPageDimensions", # Returns JSON + "getSecurityInfo", # Returns JSON + "pageCount", # Returns JSON + "pageRotation", # Returns JSON + "pageSize", # Returns JSON + "fileSize", # Returns JSON + "validateSignature", # Returns JSON + # Security — produces encrypted PDF that cannot be processed by subsequent operations + "addPassword", +} + + +@dataclass(frozen=True) +class ValidationResult: + """Result of operation chain validation.""" + + is_valid: bool + error_message: str | None = None + error_data: IncompatibleChainError | None = None + + +def validate_operation_chain(operations: list[tool_models.OperationId]) -> ValidationResult: + """ + Validate that operation chain is compatible (output of N can be input to N+1). + + Returns: + ValidationResult with is_valid, error_message, and error_data. + error_data contains structured info for frontend formatting with translated names. + """ + if len(operations) <= 1: + return ValidationResult(is_valid=True) + + for i, operation_id in enumerate(operations[:-1]): # Check all except last + if operation_id in TERMINAL_OPERATIONS: + next_op_id = operations[i + 1] + # Return structured data for frontend to format with translated names + # Include path/method so frontend can use getToolFromToolCall() for lookup + error_data = IncompatibleChainError( + type="incompatible_chain", + current_operation=OperationRef( + operation_id=operation_id, + ), + next_operation=OperationRef( + operation_id=next_op_id, + ), + ) + # Fallback message using summaries (in case frontend doesn't handle it) + current_name = operation_id + next_name = next_op_id + error_message = ( + f"Cannot chain '{current_name}' with '{next_name}'. " + f"'{current_name}' must be the last operation in a chain. " + f"Please run '{current_name}' as the final operation, or remove it from the chain." + ) + return ValidationResult( + is_valid=False, + error_message=error_message, + error_data=error_data, + ) + + return ValidationResult(is_valid=True) + + +def build_plan_summary(ops: list[tool_models.OperationId]) -> str: + if not ops: + return "I will run the requested tools." + if len(ops) == 1: + return f"I will run {ops[0]}." + return "I will run " + ", then ".join(ops) + "." + + +def get_pdf_preflight(file_path: str) -> PdfPreflight: + file_size = os.path.getsize(file_path) + + reader = PdfReader(file_path) + + is_encrypted = bool(reader.is_encrypted) + if reader.is_encrypted: + reader.decrypt("") + page_count = len(reader.pages) + text_found = False + for page in reader.pages[:2]: + extracted = page.extract_text() + if len(extracted.strip()) > 20: + text_found = True + break + return PdfPreflight( + file_size_mb=round(file_size / (1024 * 1024), 2), + is_encrypted=is_encrypted, + page_count=page_count, + has_text_layer=text_found, + ) + + +def create_session_file( + file_path: str, + file_name: str, + content_type: str | None, + content_disposition: str | None = None, +) -> EditSessionFile: + """ + Create an EditSessionFile with proper type detection and preflight handling. + + Only runs PDF preflight for actual PDF files. For non-PDF files, uses empty preflight dict. + + Args: + file_path: Path to the file on disk + file_name: Default filename to use if not in content_disposition + content_type: MIME type from response (None defaults to application/octet-stream) + content_disposition: Content-Disposition header for filename extraction + + Returns: + EditSessionFile with proper file_type and preflight data + """ + # Normalize content type (avoid defaulting to PDF) + normalized_content_type = content_type or "application/octet-stream" + file_type = normalized_content_type.split(";")[0].strip() + + # Extract filename from content_disposition if available + derived_name = file_name + if content_disposition and "filename=" in content_disposition: + derived_name = content_disposition.split("filename=")[-1].strip('"') + + # Only get PDF preflight for actual PDF files + preflight = get_pdf_preflight(file_path) if file_type == "application/pdf" else PdfPreflight() + + return EditSessionFile( + file_id=uuid.uuid4().hex, + file_path=file_path, + file_name=derived_name, + file_type=file_type, + preflight=preflight, + ) + + +def build_pdf_text_context( + file_path: str, + *, + max_pages: int = 12, + max_chars_per_page: int = 600, + max_total_chars: int = 4000, +) -> dict[str, Any]: + doc = convert_pdf_to_text_editor_document(file_path) + pages = doc.document.pages if doc else [] + context_pages: list[dict[str, Any]] = [] + total_chars = 0 + for index, page in enumerate(pages[:max_pages]): + text_chunks = [] + for elem in page.text_elements: + if elem.text: + text_chunks.append(str(elem.text)) + combined = " ".join(text_chunks) + combined = " ".join(combined.split()) + if not combined: + continue + snippet = combined[:max_chars_per_page] + total_chars += len(snippet) + if total_chars > max_total_chars: + break + context_pages.append({"page": index + 1, "text": snippet}) + + return { + "type": "file_context", + "page_count": len(pages), + "pages": context_pages, + } + + +def answer_pdf_question(file_path: str, question: str) -> str: + doc = convert_pdf_to_text_editor_document(file_path) + pages = doc.document.pages if doc else [] + snippets: list[str] = [] + for page in pages: + for elem in page.text_elements: + text = elem.text + if text: + snippets.append(str(text)) + if not snippets: + raise RuntimeError("No readable text found in PDF.") + + context = " ".join(snippets) + context = " ".join(context.split()) + max_context = 10000 + if len(context) > max_context: + context = context[:max_context] + + system_prompt = pdf_qa_system_prompt() + "\nReturn JSON matching the provided schema." + user_prompt = f"Question: {question}\n\nPDF text:\n{context}" + messages = [ + ChatMessage(role="system", content=system_prompt), + ChatMessage(role="user", content=user_prompt), + ] + response = run_ai( + SMART_MODEL, + messages, + PdfAnswer, + tag="edit_pdf_answer", + max_tokens=500, + ) + answer = response.answer.strip() + normalized_answer = re.sub(r"\s+", " ", answer).strip().lower() + normalized_context = re.sub(r"\s+", " ", context).strip().lower() + copied_context = bool(normalized_answer) and normalized_answer in normalized_context + if copied_context: + raise RuntimeError("AI answer echoed the source text.") + return answer + + +def apply_smart_defaults( + message: str, + parameters: tool_models.ParamToolModel, +) -> tool_models.ParamToolModel: + return infer_smart_defaults(message, parameters) diff --git a/engine/src/editing/params.py b/engine/src/editing/params.py new file mode 100644 index 0000000000..2cb5e69659 --- /dev/null +++ b/engine/src/editing/params.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from models import tool_models + + +def dump_params(params: tool_models.ParamToolModel | None) -> dict[str, object]: + if params is None: + return {} + return params.model_dump(by_alias=True, exclude_none=True) + + +def normalize_param_keys( + param_model: tool_models.ParamToolModelType | None, + data: dict[str, object], +) -> dict[str, object]: + if param_model is None or not data: + return data + field_map: dict[str, str] = {} + for name, field in param_model.model_fields.items(): + alias = field.alias or name + field_map[name.lower()] = alias + field_map[alias.lower()] = alias + + normalized: dict[str, object] = {} + for key, value in data.items(): + mapped = field_map.get(key.lower()) + normalized[mapped or key] = value + return normalized + + +def merge_param_updates( + param_model: tool_models.ParamToolModelType | None, + base: tool_models.ParamToolModel | None, + updates: dict[str, object], +) -> tool_models.ParamToolModel | None: + if param_model is None: + return None + data = dump_params(base) + data.update(updates) + if not data: + return None + normalized = normalize_param_keys(param_model, data) + return param_model.model_validate(normalized) diff --git a/engine/src/editing/routes.py b/engine/src/editing/routes.py new file mode 100644 index 0000000000..8eb08cd962 --- /dev/null +++ b/engine/src/editing/routes.py @@ -0,0 +1,104 @@ +import logging +import os +import subprocess +import uuid + +from flask import Blueprint, jsonify, request +from werkzeug.security import safe_join + +from config import OUTPUT_DIR +from file_processing_agent import ToolCatalogService +from llm_utils import AIProviderOverloadedError +from models import EditMessageRequest, PdfEditorUploadResponse +from pdf_text_editor import convert_pdf_to_text_editor_document + +from .service import EditService +from .session_store import EditSessionStore + +logger = logging.getLogger(__name__) + +edit_blueprint = Blueprint("edit", __name__) +_edit_service = EditService(EditSessionStore(), ToolCatalogService()) + + +def register_edit_routes(app) -> None: + app.register_blueprint(edit_blueprint) + + +def _json_body(model): + return model.model_validate(request.get_json(silent=True) or {}) + + +@edit_blueprint.route("/api/edit/sessions", methods=["POST"]) +def create_edit_session(): + files = request.files.getlist("file") + return _edit_service.create_session(files) + + +@edit_blueprint.route("/api/edit/sessions//messages", methods=["POST"]) +def edit_session_message(session_id: str): + payload = _json_body(EditMessageRequest) + try: + return _edit_service.handle_message(session_id, payload) + except AIProviderOverloadedError as exc: + logger.warning("[EDIT] AI provider overloaded session_id=%s (exc=%s)", session_id, exc) + response = { + "assistantMessage": "The AI service is temporarily unavailable. Please try again later.", + "needsMoreInfo": True, + } + return jsonify(response), 503 + + +@edit_blueprint.route("/api/edit/sessions//attachments", methods=["POST"]) +def edit_session_attachment(session_id: str): + name = request.form.get("name") + file = request.files.get("file") + return _edit_service.add_attachment(session_id, name, file) + + +@edit_blueprint.route("/api/pdf-editor/document", methods=["GET"]) +def pdf_editor_document(): + """Expose a JSON snapshot of the PDF for rich text editing.""" + pdf_url = request.args.get("pdfUrl") + if not pdf_url: + return jsonify({"error": "Missing pdfUrl"}), 400 + + filename = os.path.basename(pdf_url.split("?")[0]) + if not filename: + return jsonify({"error": "Invalid pdf file"}), 400 + if not filename.lower().endswith(".pdf"): + return jsonify({"error": "Invalid pdf file"}), 400 + + pdf_path = safe_join(OUTPUT_DIR, filename) + if pdf_path is None or not os.path.exists(pdf_path): + return jsonify({"error": "PDF not found"}), 404 + + try: + document = convert_pdf_to_text_editor_document(pdf_path) + return jsonify(document.model_dump(by_alias=True, exclude_none=True)) + except FileNotFoundError: + return jsonify({"error": "Conversion failed"}), 500 + except subprocess.CalledProcessError as exc: + logger.error("[PDF-EDITOR] Conversion failed: %s", exc) + return jsonify({"error": "Conversion failed"}), 500 + except Exception as exc: + logger.error("[PDF-EDITOR] Unexpected conversion failure: %s", exc) + return jsonify({"error": "Conversion failed"}), 500 + + +@edit_blueprint.route("/api/pdf-editor/upload", methods=["POST"]) +def pdf_editor_upload(): + """Accept an edited PDF and save it so the preview can refresh.""" + file = request.files.get("file") + if not file: + return jsonify({"error": "Missing file"}), 400 + + job_id = str(uuid.uuid4()) + filename = f"{job_id}-edited.pdf" + output_path = os.path.join(OUTPUT_DIR, filename) + os.makedirs(OUTPUT_DIR, exist_ok=True) + file.save(output_path) + + logger.info("[PDF-EDITOR] uploaded edited PDF job_id=%s -> %s", job_id, filename) + response = PdfEditorUploadResponse(pdf_url=f"/output/{filename}") + return jsonify(response.model_dump(by_alias=True, exclude_none=True)) diff --git a/engine/src/editing/service.py b/engine/src/editing/service.py new file mode 100644 index 0000000000..15fc47fdd9 --- /dev/null +++ b/engine/src/editing/service.py @@ -0,0 +1,3 @@ +from .handlers import EditService + +__all__ = ["EditService"] diff --git a/engine/src/editing/session_store.py b/engine/src/editing/session_store.py new file mode 100644 index 0000000000..e57839e1b6 --- /dev/null +++ b/engine/src/editing/session_store.py @@ -0,0 +1,81 @@ +import time +import uuid +from dataclasses import dataclass, field +from typing import Any, Literal + +from models import ChatMessage, PdfPreflight, tool_models + + +@dataclass +class EditSessionFile: + file_id: str + file_path: str + file_name: str + file_type: str | None + preflight: PdfPreflight = field(default_factory=PdfPreflight) + + +@dataclass +class PendingOperation: + """Single operation in a pending plan.""" + + operation_id: tool_models.OperationId + parameters: tool_models.ParamToolModel + + +@dataclass +class PendingPlan: + """ + Unified pending plan for both awaiting params and awaiting confirmation. + This replaces the old separate pending_operations/pending_operation_id/pending_requirements. + """ + + plan_id: str = field(default_factory=lambda: str(uuid.uuid4())) + state: Literal["AWAITING_CONFIRM"] = "AWAITING_CONFIRM" + ops: list[PendingOperation] = field(default_factory=list) + risk_level: str = "low" + risk_reasons: list[str] = field(default_factory=list) + created_at: float = field(default_factory=time.time) + source_message: str | None = None + + +@dataclass +class EditSession: + session_id: str + file_path: str + file_name: str + file_type: str | None + messages: list[ChatMessage] = field(default_factory=list) + + # Unified pending plan + pending_plan: PendingPlan | None = None + + # Last executed operation (for repeat requests) + last_operation_id: tool_models.OperationId | None = None + last_parameters: tool_models.ParamToolModel | None = None + + # File metadata + preflight: PdfPreflight = field(default_factory=PdfPreflight) + files: list[EditSessionFile] = field(default_factory=list) + attachments: dict[str, EditSessionFile] = field(default_factory=dict) + + # Document context (for Q&A) + file_context: dict[str, Any] | None = None + file_context_path: str | None = None + + # Idempotency tracking + executed_plan_ids: set[str] = field(default_factory=set) + + +class EditSessionStore: + def __init__(self) -> None: + self._sessions: dict[str, EditSession] = {} + + def get(self, session_id: str) -> EditSession | None: + return self._sessions.get(session_id) + + def set(self, session: EditSession) -> None: + self._sessions[session.session_id] = session + + def delete(self, session_id: str) -> None: + self._sessions.pop(session_id, None) diff --git a/engine/src/editing/state_router.py b/engine/src/editing/state_router.py new file mode 100644 index 0000000000..4f781e135a --- /dev/null +++ b/engine/src/editing/state_router.py @@ -0,0 +1,158 @@ +""" +Explicit state machine router for edit flow. +Routes messages based on pending_plan.state: AWAITING_CONFIRM | None (fresh request). +""" + +import logging +from dataclasses import dataclass +from typing import Any, Literal + +from models import ChatMessage + +from .confirmation import answer_confirmation_question, classify_confirmation_intent +from .operations import build_plan_summary +from .session_store import EditSession, PendingPlan + +logger = logging.getLogger(__name__) + +RoutingAction = Literal[ + "execute", + "answer_question", + "route_fresh", + "error", + "already_executed", + "cancelled", +] + + +@dataclass +class StateRoutingResult: + """Result of state routing - tells handler what to do next.""" + + action: RoutingAction + plan: PendingPlan | None = None + message: str | None = None + error: str | None = None + followup_intent: Any | None = None + keep_pending: bool | None = None + plan_id: str | None = None + + +def route_message( + session: EditSession, + user_message: str, + history: list[ChatMessage], +) -> StateRoutingResult: + """ + Route message based on pending_plan state. + + State machine: + No pending_plan → route as fresh request + AWAITING_CONFIRM → handle confirmation (confirm/cancel/modify/new_request/question) + + Returns: + StateRoutingResult with action and context + """ + if not session.pending_plan: + return StateRoutingResult(action="route_fresh") + + if session.pending_plan.state == "AWAITING_CONFIRM": + return _handle_awaiting_confirm(session, user_message, history) + + logger.error(f"[STATE_ROUTER] Unknown state: {session.pending_plan.state}") + return StateRoutingResult(action="error", error="Invalid pending plan state") + + +def _handle_awaiting_confirm( + session: EditSession, + user_message: str, + history: list[ChatMessage], +) -> StateRoutingResult: + """ + Handle message during AWAITING_CONFIRM state. + + CRITICAL: Never ignore messages. Always classify intent. + + Actions: + - confirm → execute plan + - cancel → clear plan + - modify/new_request → clear plan + route as fresh + - question → answer without executing + """ + plan = session.pending_plan + assert plan is not None + + # Build plan summary for context + operations = [op.operation_id for op in plan.ops] + plan_summary = build_plan_summary(operations) + + # Classify confirmation intent + intent = classify_confirmation_intent( + user_message, + plan_summary, + history, + session_id=session.session_id, + ) + + if not intent: + # Fallback: treat as question (safe default) + logger.warning("[STATE_ROUTER] No confirmation intent, defaulting to question") + intent_action = "question" + else: + intent_action = intent.action + + logger.info( + f"[STATE_ROUTER] confirm_state session_id={session.session_id} intent={intent_action} plan_id={plan.plan_id}" + ) + + if intent_action == "confirm": + # Check idempotency + if plan.plan_id in session.executed_plan_ids: + return StateRoutingResult( + action="already_executed", plan_id=plan.plan_id, message="This plan has already been executed." + ) + + return StateRoutingResult( + action="execute", + plan=plan, + ) + + elif intent_action == "cancel": + # Clear pending plan + session.pending_plan = None + return StateRoutingResult( + action="cancelled", message="Cancelled. Let me know if you want to do something else." + ) + + elif intent_action in ("modify", "new_request"): + # Minimal safe behavior: clear + replan + # Don't try to patch - just treat as fresh request + logger.info(f"[STATE_ROUTER] {intent_action} detected, clearing plan and routing fresh") + session.pending_plan = None + return StateRoutingResult( + action="route_fresh", + message=None, # Don't add extra message, just route + ) + + elif intent_action == "question": + # Answer question without executing + answer = answer_confirmation_question( + user_message, + plan_summary, + operations, + history, + session_id=session.session_id, + ) + return StateRoutingResult( + action="answer_question", + message=answer, + keep_pending=True, # Keep plan for later confirmation + ) + + else: + logger.warning(f"[STATE_ROUTER] Unknown confirmation intent: {intent_action}") + return StateRoutingResult( + action="answer_question", + message="I didn't understand that. Please confirm to proceed or cancel to stop.", + keep_pending=True, + ) diff --git a/engine/src/file_processing_agent.py b/engine/src/file_processing_agent.py new file mode 100644 index 0000000000..7823e0b4f2 --- /dev/null +++ b/engine/src/file_processing_agent.py @@ -0,0 +1,307 @@ +from __future__ import annotations + +import json +import logging +import time +from collections.abc import Sequence +from dataclasses import asdict, dataclass +from typing import Any + +import models +from config import FAST_MODEL, SMART_MODEL +from editing.params import dump_params +from llm_utils import run_ai +from models import tool_models +from prompts import ( + ToolParamEntry, + ToolParamIndex, + edit_followup_intent_prompt, + edit_missing_parameter_fill_prompt, + edit_tool_clarification_prompt, + edit_tool_parameter_fill_prompt, + edit_tool_selection_system_prompt, +) + +logger = logging.getLogger(__name__) + +FILE_PARAM_NAMES = {"fileInput", "fileId", "file"} +CLARIFICATION_RULES = { + "removePassword": { + "ask_for": ["password"], + "note": "If the user says there is no password or it is empty, set password to an empty string.", + } +} + + +@dataclass(frozen=True) +class ToolCatalog: + operation_ids: list[tool_models.OperationId] + + +@dataclass(frozen=True) +class ToolSelectionEntry: + operation_id: tool_models.OperationId + + +@dataclass(frozen=True) +class ToolOperationDetail: + operation_id: tool_models.OperationId + clarification: dict[str, Any] | None + + +class ToolCatalogService: + def __init__(self, *, endpoint_cache_ttl_seconds: float = 60.0) -> None: + self._endpoint_cache_ttl_seconds = endpoint_cache_ttl_seconds + self._catalog_cache = ToolCatalog(operation_ids=[]) + self._catalog_cache_ts: float = time.time() + self._catalog_cache_initialized = False + + def _build_catalog(self) -> ToolCatalog: + all_ops = sorted(tool_models.OPERATIONS.keys()) + enabled_ops = list(all_ops) + excluded_no_file = 0 + excluded_non_binary = 0 + excluded_disabled = 0 + logger.info( + "[EDIT] Catalog presort total_ops=%s file_ops=%s excluded_no_file=%s excluded_non_binary=%s excluded_disabled=%s", + len(all_ops), + len(enabled_ops), + excluded_no_file, + excluded_non_binary, + excluded_disabled, + ) + logger.info( + "[EDIT] Enabled operations: %s", + "\n".join(str(op_id) for op_id in enabled_ops), + ) + return ToolCatalog( + operation_ids=enabled_ops, + ) + + def get_catalog(self) -> ToolCatalog: + if not self._catalog_cache_initialized: + self._catalog_cache = self._build_catalog() + self._catalog_cache_ts = time.time() + self._catalog_cache_initialized = True + logger.info("[EDIT] Loaded %s file-processing operations", len(self._catalog_cache.operation_ids)) + elif time.time() - self._catalog_cache_ts >= self._endpoint_cache_ttl_seconds: + self._catalog_cache = self._build_catalog() + self._catalog_cache_ts = time.time() + logger.info("[EDIT] Refreshed file-processing operations=%s", len(self._catalog_cache.operation_ids)) + return self._catalog_cache + + def build_selection_index(self) -> list[ToolSelectionEntry]: + catalog = self.get_catalog() + payload: list[ToolSelectionEntry] = [] + for operation_id in catalog.operation_ids: + payload.append( + ToolSelectionEntry( + operation_id=operation_id, + ) + ) + return payload + + def build_catalog_prompt(self) -> str: + catalog = self.get_catalog() + payload = [self._build_operation_detail(operation_id) for operation_id in catalog.operation_ids] + return json.dumps([asdict(item) for item in payload], ensure_ascii=True) + + def _build_operation_detail( + self, + operation_id: tool_models.OperationId, + ) -> ToolOperationDetail: + return ToolOperationDetail( + operation_id=operation_id, + clarification=CLARIFICATION_RULES.get(operation_id), + ) + + def _build_operation_parameter_index( + self, + param_model: tool_models.ParamToolModelType | None, + ) -> ToolParamIndex: + if param_model is None: + return ToolParamIndex(params=[]) + params: list[ToolParamEntry] = [] + for py_name in sorted(param_model.model_fields.keys()): + field = param_model.model_fields[py_name] + params.append( + ToolParamEntry( + name=field.alias or py_name, + python_name=py_name, + required=field.is_required(), + type=str(field.annotation), + description=field.description, + ) + ) + return ToolParamIndex(params=params) + + def select_edit_tool( + self, + history: list[models.ChatMessage], + uploaded_files: list[models.UploadedFileInfo], + preflight: models.PdfPreflight | None = None, + session_id: str | None = None, + ) -> models.EditToolSelection: + selection_index = self.build_selection_index() + system_instructions = edit_tool_selection_system_prompt( + uploaded_files=uploaded_files, + preflight=preflight, + tool_catalog=[entry.operation_id for entry in selection_index], + ) + messages = [ + models.ChatMessage(role="system", content=system_instructions), + *history, + ] + response = run_ai( + SMART_MODEL, + messages, + models.EditToolSelection, + tag="edit_tool_selection", + log_label="edit-tool-selection", + session_id=session_id, + ) + return response + + def should_ask_clarification( + self, + operation_id: tool_models.OperationId, + user_message: str, + history: list[models.ChatMessage], + parameters: dict[str, Any], + session_id: str | None = None, + ) -> models.ClarificationDecision: + op_detail = self._build_operation_detail(operation_id) + system_instructions = edit_tool_clarification_prompt() + system_payload = { + "instructions": system_instructions, + "operation": asdict(op_detail), + "current_parameters": parameters, + } + messages = [ + models.ChatMessage(role="system", content=[system_payload]), + *history, + ] + response = run_ai( + FAST_MODEL, + messages, + models.ClarificationDecision, + tag="edit_tool_clarification", + log_label="edit-tool-clarification", + session_id=session_id, + ) + return response + + def extract_operation_parameters( + self, + operation_id: tool_models.OperationId, + previous_operations: Sequence[tuple[tool_models.OperationId, tool_models.ParamToolModel | None]], + user_message: str, + history: list[models.ChatMessage], + preflight: models.PdfPreflight | None = None, + session_id: str | None = None, + ) -> tool_models.ParamToolModel | None: + ai_request_model = tool_models.OPERATIONS.get(operation_id) + if ai_request_model is None: + return None + if not issubclass(ai_request_model, models.ApiModel): + raise TypeError(f"AI request model must be models.ApiModel, got: {ai_request_model}") + param_index = self._build_operation_parameter_index(ai_request_model) + system_instructions = edit_tool_parameter_fill_prompt( + operation_id=operation_id, + preflight=preflight, + parameter_catalog=param_index, + previous_operations=previous_operations, + ) + messages = [ + models.ChatMessage(role="system", content=system_instructions), + *history, + ] + result = run_ai( + SMART_MODEL, + messages, + ai_request_model, + tag="edit_tool_params", + log_label="edit-tool-params", + session_id=session_id, + ) + return result + + def fill_missing_parameters( + self, + operation_id: tool_models.OperationId, + user_message: str, + history: list[models.ChatMessage], + missing_parameters: list[str], + current_parameters: tool_models.ParamToolModel | None, + preflight: models.PdfPreflight | None = None, + session_id: str | None = None, + ) -> tool_models.ParamToolModel | None: + ai_request_model = tool_models.OPERATIONS.get(operation_id) + if ai_request_model is None: + return None + if not issubclass(ai_request_model, models.ApiModel): + raise TypeError(f"AI request model must be models.ApiModel, got: {ai_request_model}") + current_params_dump = dump_params(current_parameters) + system_instructions = edit_missing_parameter_fill_prompt() + system_payload = { + "instructions": system_instructions, + "operation_id": operation_id, + "missing_parameters": missing_parameters, + "current_parameters": current_params_dump, + "preflight": preflight.model_dump(by_alias=True, exclude_none=True) if preflight else None, + } + messages = [ + models.ChatMessage(role="system", content=[system_payload]), + *history, + ] + allowed = set(missing_parameters) + result = run_ai( + FAST_MODEL, + messages, + ai_request_model, + tag="edit_missing_fill", + log_label="edit-missing-fill", + session_id=session_id, + ) + params = result.model_dump(by_alias=True, exclude_none=True) + filtered = {name: value for name, value in params.items() if name in allowed} + return ai_request_model.model_validate(filtered) + + def decide_followup_intent( + self, + user_message: str, + history: list[models.ChatMessage], + pending_requirements: list[models.PendingRequirement], + session_id: str | None = None, + ) -> models.FollowupIntent: + pending_dump = [ + { + "operation_id": requirement.operation_id, + "parameters": dump_params(requirement.parameters), + "missing": requirement.missing, + } + for requirement in pending_requirements + ] + system_instructions = edit_followup_intent_prompt() + system_payload = { + "instructions": system_instructions, + "pending_requirements": pending_dump, + } + messages = [ + models.ChatMessage(role="system", content=[system_payload]), + *history, + models.ChatMessage(role="user", content=user_message), + ] + response = run_ai( + FAST_MODEL, + messages, + models.FollowupIntent, + tag="edit_followup_intent", + log_label="edit-followup-intent", + log_exchange=True, + session_id=session_id, + ) + return response + + def get_operation(self, operation_id: tool_models.OperationId) -> tool_models.ParamToolModelType | None: + return tool_models.OPERATIONS.get(operation_id) diff --git a/engine/src/format_prompts/__init__.py b/engine/src/format_prompts/__init__.py new file mode 100644 index 0000000000..ffc170b6c9 --- /dev/null +++ b/engine/src/format_prompts/__init__.py @@ -0,0 +1,195 @@ +""" +Format-specific outline extraction prompts. + +Each format has a dedicated prompt that instructs the AI to EXTRACT values +from the user's input rather than fabricate them. +""" + +from .advisor_agreement import ADVISOR_AGREEMENT_PROMPT, ADVISOR_AGREEMENT_SECTIONS +from .audit_report import AUDIT_REPORT_PROMPT, AUDIT_REPORT_SECTIONS +from .board_minutes import BOARD_MINUTES_PROMPT, BOARD_MINUTES_SECTIONS +from .budget_proposal import BUDGET_PROPOSAL_PROMPT, BUDGET_PROPOSAL_SECTIONS +from .case_study import CASE_STUDY_PROMPT, CASE_STUDY_SECTIONS +from .committee_agenda import COMMITTEE_AGENDA_PROMPT, COMMITTEE_AGENDA_SECTIONS +from .contract import CONTRACT_PROMPT, CONTRACT_SECTIONS +from .cover_letter import COVER_LETTER_PROMPT, COVER_LETTER_SECTIONS +from .employee_handbook import EMPLOYEE_HANDBOOK_PROMPT, EMPLOYEE_HANDBOOK_SECTIONS +from .executive_summary import EXECUTIVE_SUMMARY_PROMPT, EXECUTIVE_SUMMARY_SECTIONS +from .expense_report import EXPENSE_REPORT_PROMPT, EXPENSE_REPORT_SECTIONS +from .incident_report import INCIDENT_REPORT_PROMPT, INCIDENT_REPORT_SECTIONS +from .independent_contractor_agreement import ( + INDEPENDENT_CONTRACTOR_AGREEMENT_PROMPT, + INDEPENDENT_CONTRACTOR_AGREEMENT_SECTIONS, +) +from .invoice import INVOICE_PROMPT, INVOICE_SECTIONS +from .job_description import JOB_DESCRIPTION_PROMPT, JOB_DESCRIPTION_SECTIONS +from .letter import LETTER_PROMPT, LETTER_SECTIONS +from .letter_of_intent import LETTER_OF_INTENT_PROMPT, LETTER_OF_INTENT_SECTIONS +from .master_services_agreement import MASTER_SERVICES_AGREEMENT_PROMPT, MASTER_SERVICES_AGREEMENT_SECTIONS +from .meeting_agenda import MEETING_AGENDA_PROMPT, MEETING_AGENDA_SECTIONS +from .meeting_minutes import MEETING_MINUTES_PROMPT, MEETING_MINUTES_SECTIONS +from .nda import NDA_PROMPT, NDA_SECTIONS +from .offer_letter import OFFER_LETTER_PROMPT, OFFER_LETTER_SECTIONS +from .official_memo import OFFICIAL_MEMO_PROMPT, OFFICIAL_MEMO_SECTIONS +from .one_pager import ONE_PAGER_PROMPT, ONE_PAGER_SECTIONS +from .pay_stub import PAY_STUB_PROMPT, PAY_STUB_SECTIONS +from .performance_review import PERFORMANCE_REVIEW_PROMPT, PERFORMANCE_REVIEW_SECTIONS +from .press_release import PRESS_RELEASE_PROMPT, PRESS_RELEASE_SECTIONS +from .price_sheet import PRICE_SHEET_PROMPT, PRICE_SHEET_SECTIONS +from .privacy_policy import PRIVACY_POLICY_PROMPT, PRIVACY_POLICY_SECTIONS +from .proposal import PROPOSAL_PROMPT, PROPOSAL_SECTIONS +from .public_notice import PUBLIC_NOTICE_PROMPT, PUBLIC_NOTICE_SECTIONS +from .purchase_order import PURCHASE_ORDER_PROMPT, PURCHASE_ORDER_SECTIONS +from .quote import QUOTE_PROMPT, QUOTE_SECTIONS +from .receipt import RECEIPT_PROMPT, RECEIPT_SECTIONS +from .report import REPORT_PROMPT, REPORT_SECTIONS +from .resume import RESUME_PROMPT, RESUME_SECTIONS +from .safe_agreement import SAFE_AGREEMENT_PROMPT, SAFE_AGREEMENT_SECTIONS +from .separation_notice import SEPARATION_NOTICE_PROMPT, SEPARATION_NOTICE_SECTIONS +from .service_agreement import SERVICE_AGREEMENT_PROMPT, SERVICE_AGREEMENT_SECTIONS +from .standard_operating_procedures import STANDARD_OPERATING_PROCEDURES_PROMPT, STANDARD_OPERATING_PROCEDURES_SECTIONS +from .statement_of_work import STATEMENT_OF_WORK_PROMPT, STATEMENT_OF_WORK_SECTIONS +from .terms_of_service import TERMS_OF_SERVICE_PROMPT, TERMS_OF_SERVICE_SECTIONS + +# Fallback prompt for "other" or unknown document types +# This is GENERATIVE (not extraction-based) - allows AI to determine structure +OTHER_PROMPT = """You are an outline generator for document creation. + +The user wants to create a document but hasn't specified a standard type. Your job is to: +1. Understand what they want to create +2. Generate a sensible outline with appropriate sections + +Rules: +- Create 5-9 sections that make sense for their request +- Each section should have a title and brief description (6-12 words) +- Format: "Section Title: Brief description of what goes here" +- Be creative and tailor the outline to their specific needs +- If they ask for something unusual, do your best to structure it logically + +Output format (one section per line): +Title: [document title/name] +Section 1: Description of section 1 +Section 2: Description of section 2 +...and so on + +If the user asks to make up or fake data, you can generate appropriate placeholder content.""" + +OTHER_SECTIONS = [ + "Title", + "Introduction", + "Main Content", + "Details", + "Conclusion", +] + +# Map document types to their prompts and default sections +FORMAT_PROMPTS = { + # Popular / Legacy + "invoice": (INVOICE_PROMPT, INVOICE_SECTIONS), + "resume": (RESUME_PROMPT, RESUME_SECTIONS), + "cover_letter": (COVER_LETTER_PROMPT, COVER_LETTER_SECTIONS), + "contract": (CONTRACT_PROMPT, CONTRACT_SECTIONS), + "nda": (NDA_PROMPT, NDA_SECTIONS), + "meeting_agenda": (MEETING_AGENDA_PROMPT, MEETING_AGENDA_SECTIONS), + "agenda": (MEETING_AGENDA_PROMPT, MEETING_AGENDA_SECTIONS), + # Legal + "terms_of_service": (TERMS_OF_SERVICE_PROMPT, TERMS_OF_SERVICE_SECTIONS), + "privacy_policy": (PRIVACY_POLICY_PROMPT, PRIVACY_POLICY_SECTIONS), + # Financial + "quote": (QUOTE_PROMPT, QUOTE_SECTIONS), + "estimate": (QUOTE_PROMPT, QUOTE_SECTIONS), + "receipt": (RECEIPT_PROMPT, RECEIPT_SECTIONS), + "expense_report": (EXPENSE_REPORT_PROMPT, EXPENSE_REPORT_SECTIONS), + # Business + "proposal": (PROPOSAL_PROMPT, PROPOSAL_SECTIONS), + "report": (REPORT_PROMPT, REPORT_SECTIONS), + "letter": (LETTER_PROMPT, LETTER_SECTIONS), + "one_pager": (ONE_PAGER_PROMPT, ONE_PAGER_SECTIONS), + "statement_of_work": (STATEMENT_OF_WORK_PROMPT, STATEMENT_OF_WORK_SECTIONS), + "sow": (STATEMENT_OF_WORK_PROMPT, STATEMENT_OF_WORK_SECTIONS), + "meeting_minutes": (MEETING_MINUTES_PROMPT, MEETING_MINUTES_SECTIONS), + "minutes": (MEETING_MINUTES_PROMPT, MEETING_MINUTES_SECTIONS), + "press_release": (PRESS_RELEASE_PROMPT, PRESS_RELEASE_SECTIONS), + # Governance + "official_memo": (OFFICIAL_MEMO_PROMPT, OFFICIAL_MEMO_SECTIONS), + "board_minutes": (BOARD_MINUTES_PROMPT, BOARD_MINUTES_SECTIONS), + "committee_agenda": (COMMITTEE_AGENDA_PROMPT, COMMITTEE_AGENDA_SECTIONS), + "executive_summary": (EXECUTIVE_SUMMARY_PROMPT, EXECUTIVE_SUMMARY_SECTIONS), + "incident_report": (INCIDENT_REPORT_PROMPT, INCIDENT_REPORT_SECTIONS), + "public_notice": (PUBLIC_NOTICE_PROMPT, PUBLIC_NOTICE_SECTIONS), + # Contracts + "service_agreement": (SERVICE_AGREEMENT_PROMPT, SERVICE_AGREEMENT_SECTIONS), + "independent_contractor_agreement": ( + INDEPENDENT_CONTRACTOR_AGREEMENT_PROMPT, + INDEPENDENT_CONTRACTOR_AGREEMENT_SECTIONS, + ), + "safe_agreement": (SAFE_AGREEMENT_PROMPT, SAFE_AGREEMENT_SECTIONS), + "advisor_agreement": (ADVISOR_AGREEMENT_PROMPT, ADVISOR_AGREEMENT_SECTIONS), + "nondisclosure_agreement": (NDA_PROMPT, NDA_SECTIONS), + "master_services_agreement": (MASTER_SERVICES_AGREEMENT_PROMPT, MASTER_SERVICES_AGREEMENT_SECTIONS), + # Finance + "purchase_order": (PURCHASE_ORDER_PROMPT, PURCHASE_ORDER_SECTIONS), + "budget_proposal": (BUDGET_PROPOSAL_PROMPT, BUDGET_PROPOSAL_SECTIONS), + "audit_report": (AUDIT_REPORT_PROMPT, AUDIT_REPORT_SECTIONS), + # Sales + "case_study": (CASE_STUDY_PROMPT, CASE_STUDY_SECTIONS), + "letter_of_intent": (LETTER_OF_INTENT_PROMPT, LETTER_OF_INTENT_SECTIONS), + "price_sheet": (PRICE_SHEET_PROMPT, PRICE_SHEET_SECTIONS), + # Human Resources + "job_description": (JOB_DESCRIPTION_PROMPT, JOB_DESCRIPTION_SECTIONS), + "offer_letter": (OFFER_LETTER_PROMPT, OFFER_LETTER_SECTIONS), + "pay_stub": (PAY_STUB_PROMPT, PAY_STUB_SECTIONS), + "payslip": (PAY_STUB_PROMPT, PAY_STUB_SECTIONS), + "separation_notice": (SEPARATION_NOTICE_PROMPT, SEPARATION_NOTICE_SECTIONS), + "performance_review": (PERFORMANCE_REVIEW_PROMPT, PERFORMANCE_REVIEW_SECTIONS), + "employee_handbook": (EMPLOYEE_HANDBOOK_PROMPT, EMPLOYEE_HANDBOOK_SECTIONS), + "standard_operating_procedures": (STANDARD_OPERATING_PROCEDURES_PROMPT, STANDARD_OPERATING_PROCEDURES_SECTIONS), +} + +# Categories for frontend tabs +CATEGORIES = { + "popular": ["resume", "invoice", "cover_letter", "meeting_agenda", "contract", "nda"], + "legal": ["contract", "nda", "terms_of_service", "privacy_policy"], + "financial": ["invoice", "quote", "receipt", "expense_report"], + "business": ["proposal", "report", "letter", "one_pager", "statement_of_work", "meeting_minutes", "press_release"], +} + + +def get_format_prompt(document_type: str) -> tuple[str | None, list[str] | None]: + """ + Get the prompt and default sections for a document type. + + Returns: + tuple: (prompt, sections) - both strings/lists, or (None, None) if not found + + For "other" or unknown types, returns the generative prompt + which allows the AI to determine appropriate structure. + """ + doc_type = document_type.lower().replace(" ", "_").replace("-", "_") + + # Check if we have a specific format prompt + if doc_type in FORMAT_PROMPTS: + return FORMAT_PROMPTS[doc_type] + + # For "other", "document", or unknown types, return the generative prompt + if doc_type in ("other", "document", "miscellaneous", "unknown", ""): + return (OTHER_PROMPT, OTHER_SECTIONS) + + # Unknown type - return None to use default behavior in ai_generation.py + return (None, None) + + +def has_format_prompt(document_type: str) -> bool: + """Check if a document type has a dedicated format prompt.""" + doc_type = document_type.lower().replace(" ", "_").replace("-", "_") + return doc_type in FORMAT_PROMPTS + + +__all__ = [ + "FORMAT_PROMPTS", + "CATEGORIES", + "get_format_prompt", + "has_format_prompt", + "OTHER_PROMPT", + "OTHER_SECTIONS", +] diff --git a/engine/src/format_prompts/advisor_agreement.py b/engine/src/format_prompts/advisor_agreement.py new file mode 100644 index 0000000000..9343d52a09 --- /dev/null +++ b/engine/src/format_prompts/advisor_agreement.py @@ -0,0 +1,100 @@ +"""Advisor agreement outline extraction prompt.""" + +ADVISOR_AGREEMENT_SECTIONS = [ + "Title", + "Parties", + "Effective Date", + "Advisor Role", + "Services", + "Advisor Duties", + "Term", + "Compensation", + "Equity", + "Expenses", + "Confidentiality", + "IP Assignment", + "Termination", + "Governing Law", + "Signatures", +] + +ADVISOR_AGREEMENT_PROMPT = """You are an advisor agreement outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent party names, compensation amounts, or terms +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Title: [agreement title if mentioned] +Parties: [company name and advisor name if mentioned] +Effective Date: [start date if mentioned] +Advisor Role: [advisor's role/title if mentioned] +Services: [description of advisory services if mentioned] +Advisor Duties: [specific duties and responsibilities if mentioned] +Term: [agreement duration if mentioned] +Compensation: [cash compensation details if mentioned] +Equity: [equity compensation (stock options, percentage, vesting) if mentioned] +Expenses: [expense reimbursement terms if mentioned] +Confidentiality: [confidentiality obligations if mentioned] +IP Assignment: [intellectual property assignment terms if mentioned] +Termination: [termination conditions if mentioned] +Governing Law: [applicable law/jurisdiction if mentioned] +Signatures: [who needs to sign if mentioned] + +EXAMPLES: + +User: "advisor agreement between TechCorp and Jane Smith, 2% equity vesting over 2 years, quarterly meetings" +Title: Advisor Agreement +Parties: TechCorp, Jane Smith +Effective Date: +Advisor Role: +Services: Quarterly advisory meetings +Advisor Duties: +Term: 2 years +Compensation: +Equity: 2% equity vesting over 2 years +Expenses: +Confidentiality: +IP Assignment: +Termination: +Governing Law: +Signatures: + +User: "advisor agreement, 0.5% stock options, monthly strategy sessions, $500 per meeting" +Title: Advisor Agreement +Parties: +Effective Date: +Advisor Role: +Services: Monthly strategy sessions +Advisor Duties: +Term: +Compensation: $500 per meeting +Equity: 0.5% stock options +Expenses: +Confidentiality: +IP Assignment: +Termination: +Governing Law: +Signatures: + +User: "create advisor agreement" +Title: +Parties: +Effective Date: +Advisor Role: +Services: +Advisor Duties: +Term: +Compensation: +Equity: +Expenses: +Confidentiality: +IP Assignment: +Termination: +Governing Law: +Signatures: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/audit_report.py b/engine/src/format_prompts/audit_report.py new file mode 100644 index 0000000000..702ddb9878 --- /dev/null +++ b/engine/src/format_prompts/audit_report.py @@ -0,0 +1,85 @@ +"""Audit report outline extraction prompt.""" + +AUDIT_REPORT_SECTIONS = [ + "Title", + "Audit Period", + "Auditor", + "Auditee", + "Executive Summary", + "Scope", + "Methodology", + "Findings", + "Observations", + "Recommendations", + "Management Response", + "Conclusion", +] + +AUDIT_REPORT_PROMPT = """You are an audit report outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent findings, observations, or recommendations +- Extract exact information as stated by the user + +Return the outline in this EXACT format: + +Title: [audit title or subject if mentioned] +Audit Period: [time period audited if mentioned] +Auditor: [auditing firm or individual if mentioned] +Auditee: [entity being audited if mentioned] +Executive Summary: [brief overview if mentioned] +Scope: [what was included in the audit if mentioned] +Methodology: [audit procedures used if mentioned] +Findings: [key findings with severity levels if mentioned] +Observations: [additional observations if mentioned] +Recommendations: [recommended actions if mentioned] +Management Response: [management's response to findings if mentioned] +Conclusion: [overall assessment if mentioned] + +EXAMPLES: + +User: "audit report for XYZ Corp Q4 2023, found 3 high-risk issues in internal controls, recommend implementing new approval process" +Title: Audit Report - XYZ Corp +Audit Period: Q4 2023 +Auditor: +Auditee: XYZ Corp +Executive Summary: +Scope: +Methodology: +Findings: 3 high-risk issues in internal controls +Observations: +Recommendations: Implement new approval process +Management Response: +Conclusion: + +User: "internal audit of IT systems, identified data security gaps, management agreed to remediate within 60 days" +Title: Internal Audit - IT Systems +Audit Period: +Auditor: +Auditee: +Executive Summary: +Scope: IT systems +Methodology: +Findings: Data security gaps identified +Observations: +Recommendations: +Management Response: Agreed to remediate within 60 days +Conclusion: + +User: "create audit report" +Title: +Audit Period: +Auditor: +Auditee: +Executive Summary: +Scope: +Methodology: +Findings: +Observations: +Recommendations: +Management Response: +Conclusion: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/board_minutes.py b/engine/src/format_prompts/board_minutes.py new file mode 100644 index 0000000000..e87770246d --- /dev/null +++ b/engine/src/format_prompts/board_minutes.py @@ -0,0 +1,90 @@ +"""Board minutes outline extraction prompt.""" + +BOARD_MINUTES_SECTIONS = [ + "Title", + "Date & Time", + "Location", + "Attendees", + "Absent", + "Call to Order", + "Approval of Minutes", + "Reports", + "Old Business", + "New Business", + "Resolutions", + "Action Items", + "Adjournment", +] + +BOARD_MINUTES_PROMPT = """You are a board minutes outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent board members, decisions, or resolutions +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Title: [board name and meeting type if mentioned, e.g., "Board of Directors Regular Meeting"] +Date & Time: [when meeting was held if mentioned] +Location: [where meeting was held if mentioned] +Attendees: [board members and guests present if mentioned] +Absent: [board members absent if mentioned] +Call to Order: [who called meeting to order and when if mentioned] +Approval of Minutes: [approval of previous meeting minutes if mentioned] +Reports: [reports presented (financial, committee, executive) if mentioned] +Old Business: [ongoing matters discussed if mentioned] +New Business: [new matters introduced if mentioned] +Resolutions: [formal resolutions passed with voting results if mentioned] +Action Items: [tasks assigned with responsible parties if mentioned] +Adjournment: [when meeting was adjourned if mentioned] + +EXAMPLES: + +User: "board meeting minutes for March 15, all directors present except Smith, approved new budget $500k" +Title: Board of Directors Meeting +Date & Time: March 15 +Location: +Attendees: All directors +Absent: Director Smith +Call to Order: +Approval of Minutes: +Reports: +Old Business: +New Business: Budget discussion +Resolutions: Approved new budget of $500,000 +Action Items: +Adjournment: + +User: "quarterly board meeting, approved merger resolution unanimously, CFO presented financials" +Title: Quarterly Board Meeting +Date & Time: +Location: +Attendees: +Absent: +Call to Order: +Approval of Minutes: +Reports: CFO financial report +Old Business: +New Business: Merger proposal +Resolutions: Merger resolution passed unanimously +Action Items: +Adjournment: + +User: "create board minutes" +Title: +Date & Time: +Location: +Attendees: +Absent: +Call to Order: +Approval of Minutes: +Reports: +Old Business: +New Business: +Resolutions: +Action Items: +Adjournment: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/budget_proposal.py b/engine/src/format_prompts/budget_proposal.py new file mode 100644 index 0000000000..9149660f6b --- /dev/null +++ b/engine/src/format_prompts/budget_proposal.py @@ -0,0 +1,81 @@ +"""Budget proposal outline extraction prompt.""" + +BUDGET_PROPOSAL_SECTIONS = [ + "Title", + "Period", + "Summary", + "Revenue", + "Expenses", + "Personnel Costs", + "Operating Costs", + "Capital Expenditures", + "Net Income", + "Assumptions", + "Justification", +] + +BUDGET_PROPOSAL_PROMPT = """You are a budget proposal outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent budget figures or categories +- Extract exact amounts and categories as stated +- IMPORTANT: Extract ALL budget line items mentioned + +Return the outline in this EXACT format: + +Title: [budget title or department if mentioned] +Period: [budget period (e.g., "FY 2024", "Q1 2024") if mentioned] +Summary: [executive summary or overview if mentioned] +Revenue: [expected revenue with breakdown if mentioned] +Expenses: [total expenses if mentioned] +Personnel Costs: [salaries, benefits breakdown if mentioned] +Operating Costs: [rent, utilities, supplies breakdown if mentioned] +Capital Expenditures: [equipment, technology investments if mentioned] +Net Income: [projected net income/surplus/deficit if mentioned] +Assumptions: [key assumptions underlying the budget if mentioned] +Justification: [rationale for budget requests if mentioned] + +EXAMPLES: + +User: "budget proposal for marketing department, $500k total, $300k for personnel, $150k for campaigns, $50k for tools" +Title: Marketing Department Budget Proposal +Period: +Summary: +Revenue: +Expenses: $500,000 total +Personnel Costs: $300,000 +Operating Costs: $150,000 for campaigns +Capital Expenditures: $50,000 for tools +Net Income: +Assumptions: +Justification: + +User: "Q1 2024 budget, projected revenue $1M, expenses $750k, net income $250k" +Title: +Period: Q1 2024 +Summary: +Revenue: $1,000,000 +Expenses: $750,000 +Personnel Costs: +Operating Costs: +Capital Expenditures: +Net Income: $250,000 +Assumptions: +Justification: + +User: "create budget proposal" +Title: +Period: +Summary: +Revenue: +Expenses: +Personnel Costs: +Operating Costs: +Capital Expenditures: +Net Income: +Assumptions: +Justification: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/case_study.py b/engine/src/format_prompts/case_study.py new file mode 100644 index 0000000000..284fe6335b --- /dev/null +++ b/engine/src/format_prompts/case_study.py @@ -0,0 +1,75 @@ +"""Case study outline extraction prompt.""" + +CASE_STUDY_SECTIONS = [ + "Title", + "Client", + "Industry", + "Challenge", + "Solution", + "Implementation", + "Results", + "Metrics", + "Testimonial", + "Conclusion", +] + +CASE_STUDY_PROMPT = """You are a case study outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent client names, results, or metrics +- Extract exact information as stated by the user + +Return the outline in this EXACT format: + +Title: [case study title if mentioned] +Client: [client name if mentioned] +Industry: [client's industry if mentioned] +Challenge: [problem or challenge faced if mentioned] +Solution: [solution provided if mentioned] +Implementation: [how solution was implemented if mentioned] +Results: [outcomes achieved if mentioned] +Metrics: [quantifiable results (%, $, time saved) if mentioned] +Testimonial: [client quote or feedback if mentioned] +Conclusion: [summary or takeaway if mentioned] + +EXAMPLES: + +User: "case study: helped retail client increase sales 35% through new e-commerce platform, implemented in 3 months" +Title: +Client: Retail client +Industry: Retail +Challenge: +Solution: New e-commerce platform +Implementation: 3 months +Results: Increased sales 35% +Metrics: 35% sales increase +Testimonial: +Conclusion: + +User: "case study for TechCorp, reduced IT costs by $500k annually, client says 'best decision we made'" +Title: +Client: TechCorp +Industry: +Challenge: +Solution: +Implementation: +Results: Reduced IT costs +Metrics: $500,000 annual savings +Testimonial: "Best decision we made" +Conclusion: + +User: "create case study" +Title: +Client: +Industry: +Challenge: +Solution: +Implementation: +Results: +Metrics: +Testimonial: +Conclusion: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/committee_agenda.py b/engine/src/format_prompts/committee_agenda.py new file mode 100644 index 0000000000..558a6531ab --- /dev/null +++ b/engine/src/format_prompts/committee_agenda.py @@ -0,0 +1,92 @@ +"""Committee agenda outline extraction prompt.""" + +COMMITTEE_AGENDA_SECTIONS = [ + "Committee Name", + "Meeting Date", + "Meeting Time", + "Location", + "Call to Order", + "Roll Call", + "Approval of Minutes", + "Old Business", + "New Business", + "Discussion Items", + "Action Items", + "Announcements", + "Adjournment", +] + +COMMITTEE_AGENDA_PROMPT = """You are a committee agenda outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent committee members, topics, or times +- Extract exact information as stated by the user +- IMPORTANT: Extract ALL agenda items mentioned + +Return the outline in this EXACT format: + +Committee Name: [committee name if mentioned] +Meeting Date: [date of meeting if mentioned] +Meeting Time: [start time if mentioned] +Location: [meeting location or virtual link if mentioned] +Call to Order: [opening procedures if mentioned] +Roll Call: [attendance taking if mentioned] +Approval of Minutes: [previous meeting minutes approval if mentioned] +Old Business: [ongoing matters, use | as separator] +New Business: [new matters to discuss, use | as separator] +Discussion Items: [items for discussion, use | as separator] +Action Items: [items requiring action/voting, use | as separator] +Announcements: [announcements if mentioned] +Adjournment: [closing procedures if mentioned] + +EXAMPLES: + +User: "finance committee agenda for May 15 at 3pm, approve Q1 budget report, discuss new vendor contracts" +Committee Name: Finance Committee +Meeting Date: May 15 +Meeting Time: 3:00 PM +Location: +Call to Order: +Roll Call: +Approval of Minutes: +Old Business: +New Business: Q1 budget report approval | New vendor contracts discussion +Discussion Items: New vendor contracts +Action Items: Approve Q1 budget report +Announcements: +Adjournment: + +User: "audit committee meeting agenda, review internal controls, vote on external auditor selection" +Committee Name: Audit Committee +Meeting Date: +Meeting Time: +Location: +Call to Order: +Roll Call: +Approval of Minutes: +Old Business: +New Business: Internal controls review | External auditor selection +Discussion Items: Internal controls review +Action Items: Vote on external auditor +Announcements: +Adjournment: + +User: "create committee agenda" +Committee Name: +Meeting Date: +Meeting Time: +Location: +Call to Order: +Roll Call: +Approval of Minutes: +Old Business: +New Business: +Discussion Items: +Action Items: +Announcements: +Adjournment: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user) +REMEMBER: Extract ALL agenda items - do not skip any!""" diff --git a/engine/src/format_prompts/contract.py b/engine/src/format_prompts/contract.py new file mode 100644 index 0000000000..86d41c63f9 --- /dev/null +++ b/engine/src/format_prompts/contract.py @@ -0,0 +1,65 @@ +"""Contract outline extraction prompt.""" + +CONTRACT_SECTIONS = [ + "Title", + "Parties", + "Effective Date", + "Scope of Work", + "Payment Terms", + "Duration", + "Termination", + "Signatures", +] + +CONTRACT_PROMPT = """You are a contract outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent party names, terms, dates, or amounts +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Title: [contract type/name if mentioned] +Parties: [names of all parties involved if mentioned] +Effective Date: [start date if mentioned] +Scope of Work: [what work/services are covered if mentioned] +Payment Terms: [payment amount, schedule, method if mentioned] +Duration: [contract length/end date if mentioned] +Termination: [termination conditions if mentioned] +Signatures: [who needs to sign if mentioned] + +EXAMPLES: + +User: "freelance contract for web development, $5000 total, 3 months" +Title: Freelance Web Development Contract +Parties: +Effective Date: +Scope of Work: Web development +Payment Terms: $5000 total +Duration: 3 months +Termination: +Signatures: + +User: "contract between ABC Corp and John Smith for consulting services" +Title: Consulting Services Contract +Parties: ABC Corp, John Smith +Effective Date: +Scope of Work: Consulting services +Payment Terms: +Duration: +Termination: +Signatures: + +User: "create contract" +Title: +Parties: +Effective Date: +Scope of Work: +Payment Terms: +Duration: +Termination: +Signatures: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/cover_letter.py b/engine/src/format_prompts/cover_letter.py new file mode 100644 index 0000000000..866aa68c52 --- /dev/null +++ b/engine/src/format_prompts/cover_letter.py @@ -0,0 +1,55 @@ +"""Cover letter outline extraction prompt.""" + +COVER_LETTER_SECTIONS = [ + "Header", + "Recipient", + "Opening", + "Body", + "Qualifications", + "Closing", +] + +COVER_LETTER_PROMPT = """You are a cover letter outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent names, companies, positions, or qualifications +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Header: [applicant name and contact info if mentioned] +Recipient: [hiring manager name, company, address if mentioned] +Opening: [position applying for, how you heard about it if mentioned] +Body: [why you're interested in the role/company if mentioned] +Qualifications: [relevant experience and skills for the role if mentioned] +Closing: [call to action, availability if mentioned] + +EXAMPLES: + +User: "cover letter for software engineer position at Google" +Header: +Recipient: Google +Opening: Software Engineer position +Body: +Qualifications: +Closing: + +User: "cover letter from Jane Doe applying for marketing role, 5 years experience" +Header: Jane Doe +Recipient: +Opening: Marketing role +Body: +Qualifications: 5 years marketing experience +Closing: + +User: "create cover letter" +Header: +Recipient: +Opening: +Body: +Qualifications: +Closing: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/employee_handbook.py b/engine/src/format_prompts/employee_handbook.py new file mode 100644 index 0000000000..ac37f6ba42 --- /dev/null +++ b/engine/src/format_prompts/employee_handbook.py @@ -0,0 +1,106 @@ +"""Employee handbook outline extraction prompt.""" + +EMPLOYEE_HANDBOOK_SECTIONS = [ + "Welcome", + "Company Overview", + "Mission & Values", + "Employment Policies", + "Work Hours", + "Compensation", + "Benefits", + "Time Off", + "Code of Conduct", + "Workplace Safety", + "Anti-Discrimination", + "Technology Use", + "Confidentiality", + "Performance Reviews", + "Termination", + "Acknowledgment", +] + +EMPLOYEE_HANDBOOK_PROMPT = """You are an employee handbook outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent policies, benefits, or rules +- Extract exact information as stated by the user +- Handbooks have many sections - extract what's provided + +Return the outline in this EXACT format: + +Welcome: [welcome message if mentioned] +Company Overview: [company history and description if mentioned] +Mission & Values: [mission statement and core values if mentioned] +Employment Policies: [employment-at-will, equal opportunity if mentioned] +Work Hours: [standard hours, overtime policy if mentioned] +Compensation: [pay schedule, raises if mentioned] +Benefits: [health insurance, 401k, other benefits if mentioned] +Time Off: [vacation, sick leave, holidays if mentioned] +Code of Conduct: [expected behavior and ethics if mentioned] +Workplace Safety: [safety policies if mentioned] +Anti-Discrimination: [anti-discrimination and harassment policies if mentioned] +Technology Use: [email, internet, device policies if mentioned] +Confidentiality: [confidentiality and data protection if mentioned] +Performance Reviews: [review process and frequency if mentioned] +Termination: [termination procedures if mentioned] +Acknowledgment: [employee acknowledgment section if mentioned] + +EXAMPLES: + +User: "employee handbook, work hours 9-5, 15 days PTO annually, health insurance provided, annual performance reviews" +Welcome: +Company Overview: +Mission & Values: +Employment Policies: +Work Hours: 9:00 AM - 5:00 PM +Compensation: +Benefits: Health insurance provided +Time Off: 15 days PTO annually +Code of Conduct: +Workplace Safety: +Anti-Discrimination: +Technology Use: +Confidentiality: +Performance Reviews: Annual reviews +Termination: +Acknowledgment: + +User: "handbook policies: at-will employment, no discrimination, confidentiality required, return all property upon termination" +Welcome: +Company Overview: +Mission & Values: +Employment Policies: At-will employment | Equal opportunity employer +Work Hours: +Compensation: +Benefits: +Time Off: +Code of Conduct: +Workplace Safety: +Anti-Discrimination: Non-discrimination policy +Technology Use: +Confidentiality: Confidentiality required +Performance Reviews: +Termination: Return all property upon termination +Acknowledgment: + +User: "create employee handbook" +Welcome: +Company Overview: +Mission & Values: +Employment Policies: +Work Hours: +Compensation: +Benefits: +Time Off: +Code of Conduct: +Workplace Safety: +Anti-Discrimination: +Technology Use: +Confidentiality: +Performance Reviews: +Termination: +Acknowledgment: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/executive_summary.py b/engine/src/format_prompts/executive_summary.py new file mode 100644 index 0000000000..ee9da61985 --- /dev/null +++ b/engine/src/format_prompts/executive_summary.py @@ -0,0 +1,80 @@ +"""Executive summary outline extraction prompt.""" + +EXECUTIVE_SUMMARY_SECTIONS = [ + "Title", + "Date", + "Author", + "Purpose", + "Background", + "Key Findings", + "Recommendations", + "Financial Impact", + "Timeline", + "Next Steps", + "Conclusion", +] + +EXECUTIVE_SUMMARY_PROMPT = """You are an executive summary outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent findings, recommendations, or financial data +- Extract exact information as stated by the user + +Return the outline in this EXACT format: + +Title: [summary title or subject if mentioned] +Date: [document date if mentioned] +Author: [author or department if mentioned] +Purpose: [purpose of the document if mentioned] +Background: [context or background information if mentioned] +Key Findings: [main findings or insights, use | as separator] +Recommendations: [recommended actions, use | as separator] +Financial Impact: [costs, savings, ROI if mentioned] +Timeline: [implementation timeline if mentioned] +Next Steps: [immediate actions required if mentioned] +Conclusion: [summary conclusion if mentioned] + +EXAMPLES: + +User: "executive summary for market expansion project, recommend entering Asian market, projected ROI 25%, 18-month timeline" +Title: Market Expansion Project +Date: +Author: +Purpose: +Background: +Key Findings: +Recommendations: Enter Asian market +Financial Impact: 25% projected ROI +Timeline: 18 months +Next Steps: +Conclusion: + +User: "summary of Q4 performance, revenue up 15%, recommend increasing marketing budget by $500k, strong customer satisfaction scores" +Title: Q4 Performance Summary +Date: +Author: +Purpose: +Background: +Key Findings: Revenue increased 15% | Strong customer satisfaction +Recommendations: Increase marketing budget by $500,000 +Financial Impact: Revenue increase of 15% +Timeline: +Next Steps: +Conclusion: + +User: "create executive summary" +Title: +Date: +Author: +Purpose: +Background: +Key Findings: +Recommendations: +Financial Impact: +Timeline: +Next Steps: +Conclusion: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/expense_report.py b/engine/src/format_prompts/expense_report.py new file mode 100644 index 0000000000..155b7a2e05 --- /dev/null +++ b/engine/src/format_prompts/expense_report.py @@ -0,0 +1,67 @@ +"""Expense report outline extraction prompt.""" + +EXPENSE_REPORT_SECTIONS = [ + "Title", + "Employee", + "Department", + "Period", + "Expenses", + "Total", + "Approvals", + "Notes", +] + +EXPENSE_REPORT_PROMPT = """You are an expense report outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent expense items or amounts +- Extract exact numbers, dates, and categories as stated +- For MULTIPLE items in any section, use | as separator + +Return the outline in this EXACT format (one field per line): + +Title: [report title/purpose if mentioned] +Employee: [employee name if mentioned] +Department: [department name if mentioned] +Period: [date range covered if mentioned] +Expenses: [each expense with description and amount - use | separator for multiple expenses] +Total: [total expenses if mentioned] +Approvals: [manager/approver name - use | separator for multiple approvers] +Notes: [any additional notes if mentioned] + +EXAMPLES: + +User: "expense report for business trip to NYC, $1,200 flights, $500 hotel, $150 meals" +Title: NYC Business Trip Expenses +Employee: +Department: +Period: +Expenses: Flights - $1,200 | Hotel - $500 | Meals - $150 +Total: $1,850 +Approvals: +Notes: + +User: "January expenses for marketing team, approved by Sarah Johnson" +Title: January Marketing Expenses +Employee: +Department: Marketing +Period: January +Expenses: +Total: +Approvals: Sarah Johnson +Notes: + +User: "create expense report" +Title: +Employee: +Department: +Period: +Expenses: +Total: +Approvals: +Notes: + +DO NOT fabricate any information. Only extract what is explicitly stated. +REMEMBER: Use | separator for multiple items in any section!""" diff --git a/engine/src/format_prompts/incident_report.py b/engine/src/format_prompts/incident_report.py new file mode 100644 index 0000000000..8c5027fbd4 --- /dev/null +++ b/engine/src/format_prompts/incident_report.py @@ -0,0 +1,95 @@ +"""Incident report outline extraction prompt.""" + +INCIDENT_REPORT_SECTIONS = [ + "Incident Number", + "Date of Incident", + "Time of Incident", + "Location", + "Reported By", + "Incident Type", + "Severity", + "Description", + "Individuals Involved", + "Witnesses", + "Immediate Actions", + "Root Cause", + "Corrective Actions", + "Follow-up Required", +] + +INCIDENT_REPORT_PROMPT = """You are an incident report outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent incident details, names, or actions taken +- Extract exact information as stated by the user + +Return the outline in this EXACT format: + +Incident Number: [incident reference number if mentioned] +Date of Incident: [date when incident occurred if mentioned] +Time of Incident: [time when incident occurred if mentioned] +Location: [where incident occurred if mentioned] +Reported By: [who reported the incident if mentioned] +Incident Type: [type of incident if mentioned] +Severity: [severity level if mentioned] +Description: [detailed description of what happened if mentioned] +Individuals Involved: [people directly involved if mentioned] +Witnesses: [witnesses if mentioned] +Immediate Actions: [actions taken immediately after incident if mentioned] +Root Cause: [identified cause of incident if mentioned] +Corrective Actions: [actions to prevent recurrence if mentioned] +Follow-up Required: [follow-up actions needed if mentioned] + +EXAMPLES: + +User: "incident report for data breach on March 10, 500 records exposed, IT team notified, implementing new security protocols" +Incident Number: +Date of Incident: March 10 +Time of Incident: +Location: +Reported By: +Incident Type: Data breach +Severity: +Description: 500 records exposed +Individuals Involved: +Witnesses: +Immediate Actions: IT team notified +Root Cause: +Corrective Actions: Implementing new security protocols +Follow-up Required: + +User: "safety incident in warehouse, employee slipped on wet floor at 2pm, first aid administered, adding warning signs" +Incident Number: +Date of Incident: +Time of Incident: 2:00 PM +Location: Warehouse +Reported By: +Incident Type: Safety incident +Severity: +Description: Employee slipped on wet floor +Individuals Involved: +Witnesses: +Immediate Actions: First aid administered +Root Cause: Wet floor +Corrective Actions: Adding warning signs +Follow-up Required: + +User: "create incident report" +Incident Number: +Date of Incident: +Time of Incident: +Location: +Reported By: +Incident Type: +Severity: +Description: +Individuals Involved: +Witnesses: +Immediate Actions: +Root Cause: +Corrective Actions: +Follow-up Required: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/independent_contractor_agreement.py b/engine/src/format_prompts/independent_contractor_agreement.py new file mode 100644 index 0000000000..c4804ef6c3 --- /dev/null +++ b/engine/src/format_prompts/independent_contractor_agreement.py @@ -0,0 +1,100 @@ +"""Independent contractor agreement outline extraction prompt.""" + +INDEPENDENT_CONTRACTOR_AGREEMENT_SECTIONS = [ + "Title", + "Parties", + "Effective Date", + "Services", + "Deliverables", + "Timeline", + "Payment Terms", + "Expenses", + "Independent Contractor Status", + "IP Ownership", + "Confidentiality", + "Termination", + "Liability", + "Governing Law", + "Signatures", +] + +INDEPENDENT_CONTRACTOR_AGREEMENT_PROMPT = """You are an independent contractor agreement outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent party names, rates, or terms +- Extract exact information as stated by the user + +Return the outline in this EXACT format: + +Title: [agreement title if mentioned] +Parties: [company name and contractor name if mentioned] +Effective Date: [start date if mentioned] +Services: [description of services to be provided if mentioned] +Deliverables: [specific deliverables if mentioned] +Timeline: [project timeline or deadlines if mentioned] +Payment Terms: [rate, payment schedule, invoicing terms if mentioned] +Expenses: [expense reimbursement policy if mentioned] +Independent Contractor Status: [acknowledgment of contractor status if mentioned] +IP Ownership: [who owns intellectual property created if mentioned] +Confidentiality: [confidentiality obligations if mentioned] +Termination: [termination conditions and notice period if mentioned] +Liability: [liability and indemnification terms if mentioned] +Governing Law: [applicable law/jurisdiction if mentioned] +Signatures: [who needs to sign if mentioned] + +EXAMPLES: + +User: "independent contractor agreement for web development, $100/hour, 3 month project, contractor retains IP rights" +Title: Independent Contractor Agreement +Parties: +Effective Date: +Services: Web development +Deliverables: +Timeline: 3 months +Payment Terms: $100 per hour +Expenses: +Independent Contractor Status: +IP Ownership: Contractor retains IP rights +Confidentiality: +Termination: +Liability: +Governing Law: +Signatures: + +User: "contractor agreement between ABC Corp and Jane Doe, consulting services, net 30 payment, either party can terminate with 2 weeks notice" +Title: Contractor Agreement +Parties: ABC Corp, Jane Doe +Effective Date: +Services: Consulting services +Deliverables: +Timeline: +Payment Terms: Net 30 +Expenses: +Independent Contractor Status: +IP Ownership: +Confidentiality: +Termination: Either party, 2 weeks notice +Liability: +Governing Law: +Signatures: + +User: "create independent contractor agreement" +Title: +Parties: +Effective Date: +Services: +Deliverables: +Timeline: +Payment Terms: +Expenses: +Independent Contractor Status: +IP Ownership: +Confidentiality: +Termination: +Liability: +Governing Law: +Signatures: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/invoice.py b/engine/src/format_prompts/invoice.py new file mode 100644 index 0000000000..697c0ddc69 --- /dev/null +++ b/engine/src/format_prompts/invoice.py @@ -0,0 +1,71 @@ +"""Invoice outline extraction prompt.""" + +INVOICE_SECTIONS = [ + "Title", + "Biller", + "Payer", + "Line Items", + "Payment Details", + "Dates", + "Notes", +] + +INVOICE_PROMPT = """You are an invoice outline extractor. Your job is to EXTRACT ALL values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent names, addresses, company names, or any other details +- Extract exact numbers, currencies, and amounts as stated +- IMPORTANT: Extract EVERY line item mentioned - do not skip any! + +Return the outline in this EXACT format: + +Title: [invoice number/title if mentioned, else leave blank] +Biller: [who is SENDING the invoice - company/person name and contact if mentioned] +Payer: [who is being BILLED - client/customer name and contact if mentioned] +Line Items: [EXTRACT EVERY item with its price, use | as separator between items] +Payment Details: [subtotal, tax, total - calculate total from all line items if possible] +Dates: [invoice date, due date, payment terms if mentioned] +Notes: [any additional notes, terms, or messages mentioned] + +EXAMPLES: + +User: "invoice for £1500 software development" +Title: +Biller: +Payer: +Line Items: Software development - £1500 +Payment Details: Total: £1500 +Dates: +Notes: + +User: "invoice from ABC Corp to John Smith for $500 web design and $200 hosting, due in 30 days" +Title: +Biller: ABC Corp +Payer: John Smith +Line Items: Web design - $500 | Hosting - $200 +Payment Details: Total: $700 +Dates: Due: 30 days +Notes: + +User: "invoice for £500 software dev, £2500 website, £350 analytics, £250 upkeep for 6 months, £9.99 domain" +Title: +Biller: +Payer: +Line Items: Software development - £500 | Website - £2500 | Analytics - £350 | Website upkeep (6 months) - £250 | Domain - £9.99 +Payment Details: Total: £3609.99 +Dates: +Notes: + +User: "create invoice" +Title: +Biller: +Payer: +Line Items: +Payment Details: +Dates: +Notes: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user) +REMEMBER: Extract ALL line items - do not combine or skip any!""" diff --git a/engine/src/format_prompts/job_description.py b/engine/src/format_prompts/job_description.py new file mode 100644 index 0000000000..21674a9478 --- /dev/null +++ b/engine/src/format_prompts/job_description.py @@ -0,0 +1,87 @@ +"""Job description outline extraction prompt.""" + +JOB_DESCRIPTION_SECTIONS = [ + "Job Title", + "Department", + "Location", + "Employment Type", + "Salary Range", + "Summary", + "Responsibilities", + "Requirements", + "Preferred Qualifications", + "Benefits", + "Company Overview", + "How to Apply", +] + +JOB_DESCRIPTION_PROMPT = """You are a job description outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent responsibilities, requirements, or salary ranges +- Extract exact information as stated by the user +- IMPORTANT: Extract ALL responsibilities and requirements mentioned + +Return the outline in this EXACT format: + +Job Title: [position title if mentioned] +Department: [department or team if mentioned] +Location: [work location or remote if mentioned] +Employment Type: [full-time, part-time, contract if mentioned] +Salary Range: [salary or pay range if mentioned] +Summary: [brief job overview if mentioned] +Responsibilities: [key duties and responsibilities, use | as separator] +Requirements: [required skills, experience, education, use | as separator] +Preferred Qualifications: [nice-to-have qualifications, use | as separator] +Benefits: [benefits offered if mentioned] +Company Overview: [company description if mentioned] +How to Apply: [application instructions if mentioned] + +EXAMPLES: + +User: "senior software engineer position, remote, $120k-150k, requires 5+ years experience, React and Python, manage team of 3" +Job Title: Senior Software Engineer +Department: +Location: Remote +Employment Type: +Salary Range: $120,000 - $150,000 +Summary: +Responsibilities: Manage team of 3 developers +Requirements: 5+ years experience | React | Python +Preferred Qualifications: +Benefits: +Company Overview: +How to Apply: + +User: "marketing manager, full-time in NYC, lead campaigns, requires bachelor's degree and 3 years marketing experience" +Job Title: Marketing Manager +Department: +Location: NYC +Employment Type: Full-time +Salary Range: +Summary: +Responsibilities: Lead marketing campaigns +Requirements: Bachelor's degree | 3 years marketing experience +Preferred Qualifications: +Benefits: +Company Overview: +How to Apply: + +User: "create job description" +Job Title: +Department: +Location: +Employment Type: +Salary Range: +Summary: +Responsibilities: +Requirements: +Preferred Qualifications: +Benefits: +Company Overview: +How to Apply: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user) +REMEMBER: Extract ALL responsibilities and requirements - do not skip any!""" diff --git a/engine/src/format_prompts/letter.py b/engine/src/format_prompts/letter.py new file mode 100644 index 0000000000..c779004b05 --- /dev/null +++ b/engine/src/format_prompts/letter.py @@ -0,0 +1,65 @@ +"""Business letter outline extraction prompt.""" + +LETTER_SECTIONS = [ + "Header", + "Date", + "Recipient", + "Subject", + "Salutation", + "Body", + "Closing", + "Signature", +] + +LETTER_PROMPT = """You are a business letter outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent names, addresses, or content +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Header: [sender name, address, contact if mentioned] +Date: [letter date if mentioned] +Recipient: [recipient name, title, company, address if mentioned] +Subject: [letter subject/RE: line if mentioned] +Salutation: [greeting if mentioned] +Body: [main content/message if mentioned] +Closing: [closing phrase if mentioned] +Signature: [signer name, title if mentioned] + +EXAMPLES: + +User: "letter from John Smith to ABC Corp regarding partnership opportunity" +Header: John Smith +Date: +Recipient: ABC Corp +Subject: Partnership Opportunity +Salutation: +Body: +Closing: +Signature: John Smith + +User: "formal letter requesting a meeting with the CEO" +Header: +Date: +Recipient: CEO +Subject: Meeting Request +Salutation: +Body: +Closing: +Signature: + +User: "create letter" +Header: +Date: +Recipient: +Subject: +Salutation: +Body: +Closing: +Signature: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/letter_of_intent.py b/engine/src/format_prompts/letter_of_intent.py new file mode 100644 index 0000000000..c5dcccdcf6 --- /dev/null +++ b/engine/src/format_prompts/letter_of_intent.py @@ -0,0 +1,100 @@ +"""Letter of intent outline extraction prompt.""" + +LETTER_OF_INTENT_SECTIONS = [ + "Date", + "Sender", + "Recipient", + "Subject", + "Introduction", + "Purpose", + "Proposed Terms", + "Timeline", + "Conditions", + "Exclusivity", + "Confidentiality", + "Non-Binding Nature", + "Next Steps", + "Expiration", + "Signatures", +] + +LETTER_OF_INTENT_PROMPT = """You are a letter of intent outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent party names, terms, or conditions +- Extract exact information as stated by the user + +Return the outline in this EXACT format: + +Date: [letter date if mentioned] +Sender: [sending party if mentioned] +Recipient: [receiving party if mentioned] +Subject: [subject of LOI if mentioned] +Introduction: [introductory statement if mentioned] +Purpose: [purpose of LOI if mentioned] +Proposed Terms: [key terms being proposed if mentioned] +Timeline: [expected timeline if mentioned] +Conditions: [conditions that must be met if mentioned] +Exclusivity: [exclusivity period if mentioned] +Confidentiality: [confidentiality terms if mentioned] +Non-Binding Nature: [statement on binding/non-binding provisions if mentioned] +Next Steps: [next steps in process if mentioned] +Expiration: [LOI expiration date if mentioned] +Signatures: [who needs to sign if mentioned] + +EXAMPLES: + +User: "letter of intent to acquire TechCo for $10M, 60-day exclusivity, closing by end of Q2, subject to due diligence" +Date: +Sender: +Recipient: TechCo +Subject: Acquisition Intent +Introduction: +Purpose: Acquire TechCo +Proposed Terms: $10,000,000 purchase price +Timeline: Close by end of Q2 +Conditions: Subject to due diligence +Exclusivity: 60 days +Confidentiality: +Non-Binding Nature: +Next Steps: +Expiration: +Signatures: + +User: "LOI for partnership between ABC Corp and XYZ Inc, non-binding except confidentiality, 30-day exclusive negotiation period" +Date: +Sender: ABC Corp +Recipient: XYZ Inc +Subject: Partnership Proposal +Introduction: +Purpose: Partnership +Proposed Terms: +Timeline: +Conditions: +Exclusivity: 30 days exclusive negotiation +Confidentiality: Binding +Non-Binding Nature: Non-binding except confidentiality provisions +Next Steps: +Expiration: +Signatures: + +User: "create letter of intent" +Date: +Sender: +Recipient: +Subject: +Introduction: +Purpose: +Proposed Terms: +Timeline: +Conditions: +Exclusivity: +Confidentiality: +Non-Binding Nature: +Next Steps: +Expiration: +Signatures: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/master_services_agreement.py b/engine/src/format_prompts/master_services_agreement.py new file mode 100644 index 0000000000..3d768c29ce --- /dev/null +++ b/engine/src/format_prompts/master_services_agreement.py @@ -0,0 +1,116 @@ +"""Master services agreement outline extraction prompt.""" + +MASTER_SERVICES_AGREEMENT_SECTIONS = [ + "Title", + "Parties", + "Effective Date", + "Term", + "Services Overview", + "Statement of Work", + "Payment Terms", + "Invoicing", + "Expenses", + "IP Ownership", + "Confidentiality", + "Warranties", + "Liability", + "Indemnification", + "Termination", + "Dispute Resolution", + "Governing Law", + "Signatures", +] + +MASTER_SERVICES_AGREEMENT_PROMPT = """You are a master services agreement outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent party names, payment terms, or legal clauses +- Extract exact information as stated by the user +- MSAs are framework agreements - focus on general terms + +Return the outline in this EXACT format: + +Title: [agreement title if mentioned] +Parties: [service provider and client names if mentioned] +Effective Date: [start date if mentioned] +Term: [initial term and renewal if mentioned] +Services Overview: [general description of services if mentioned] +Statement of Work: [how SOWs will be handled if mentioned] +Payment Terms: [general payment structure if mentioned] +Invoicing: [invoicing procedures if mentioned] +Expenses: [expense handling if mentioned] +IP Ownership: [intellectual property ownership if mentioned] +Confidentiality: [confidentiality terms if mentioned] +Warranties: [service warranties if mentioned] +Liability: [liability limitations if mentioned] +Indemnification: [indemnification terms if mentioned] +Termination: [termination conditions if mentioned] +Dispute Resolution: [dispute resolution process if mentioned] +Governing Law: [applicable law/jurisdiction if mentioned] +Signatures: [who needs to sign if mentioned] + +EXAMPLES: + +User: "MSA between TechCorp and ClientCo for software services, 2 year term, net 30 payment, work defined in separate SOWs" +Title: Master Services Agreement +Parties: TechCorp (Service Provider), ClientCo (Client) +Effective Date: +Term: 2 years +Services Overview: Software services +Statement of Work: Work defined in separate SOWs +Payment Terms: Net 30 +Invoicing: +Expenses: +IP Ownership: +Confidentiality: +Warranties: +Liability: +Indemnification: +Termination: +Dispute Resolution: +Governing Law: +Signatures: + +User: "master services agreement, consulting services, $150/hour rate, auto-renews annually, company owns all IP created" +Title: Master Services Agreement +Parties: +Effective Date: +Term: Annual with auto-renewal +Services Overview: Consulting services +Statement of Work: +Payment Terms: $150 per hour +Invoicing: +Expenses: +IP Ownership: Company owns all IP created +Confidentiality: +Warranties: +Liability: +Indemnification: +Termination: +Dispute Resolution: +Governing Law: +Signatures: + +User: "create master services agreement" +Title: +Parties: +Effective Date: +Term: +Services Overview: +Statement of Work: +Payment Terms: +Invoicing: +Expenses: +IP Ownership: +Confidentiality: +Warranties: +Liability: +Indemnification: +Termination: +Dispute Resolution: +Governing Law: +Signatures: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/meeting_agenda.py b/engine/src/format_prompts/meeting_agenda.py new file mode 100644 index 0000000000..16acc38f2d --- /dev/null +++ b/engine/src/format_prompts/meeting_agenda.py @@ -0,0 +1,71 @@ +"""Meeting agenda outline extraction prompt.""" + +MEETING_AGENDA_SECTIONS = [ + "Title", + "Date & Time", + "Location", + "Attendees", + "Objectives", + "Agenda Items", + "Notes", +] + +MEETING_AGENDA_PROMPT = """You are a meeting agenda outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent topics, attendees, or times +- Extract exact information as stated by the user +- For MULTIPLE items in any section, use | as separator + +Return the outline in this EXACT format (one field per line): + +Title: [meeting name/purpose if mentioned] +Date & Time: [when the meeting is if mentioned] +Location: [where/how (in-person location or video call) if mentioned] +Attendees: [who is invited - use | separator for multiple attendees/teams] +Objectives: [meeting goals - use | separator for multiple objectives] +Agenda Items: [topics to discuss - use | separator for multiple items] +Notes: [any additional info if mentioned] + +EXAMPLES: + +User: "team standup agenda for Monday 9am, discuss blockers and progress" +Title: Team Standup +Date & Time: Monday 9am +Location: +Attendees: +Objectives: +Agenda Items: Discuss blockers | Review progress +Notes: + +User: "quarterly review meeting with sales team and marketing to discuss Q3 results and Q4 planning" +Title: Quarterly Review +Date & Time: +Location: +Attendees: Sales team | Marketing +Objectives: Discuss Q3 results | Q4 planning +Agenda Items: Q3 results review | Q4 planning discussion +Notes: + +User: "project kickoff meeting with engineering and design teams on Zoom" +Title: Project Kickoff +Date & Time: +Location: Zoom +Attendees: Engineering team | Design team +Objectives: +Agenda Items: +Notes: + +User: "create meeting agenda" +Title: +Date & Time: +Location: +Attendees: +Objectives: +Agenda Items: +Notes: + +DO NOT fabricate any information. Only extract what is explicitly stated. +REMEMBER: Use | separator for multiple items in any section!""" diff --git a/engine/src/format_prompts/meeting_minutes.py b/engine/src/format_prompts/meeting_minutes.py new file mode 100644 index 0000000000..34325e3ddf --- /dev/null +++ b/engine/src/format_prompts/meeting_minutes.py @@ -0,0 +1,75 @@ +"""Meeting minutes outline extraction prompt.""" + +MEETING_MINUTES_SECTIONS = [ + "Title", + "Date & Time", + "Location", + "Attendees", + "Absent", + "Agenda Items", + "Discussion", + "Decisions", + "Action Items", + "Next Meeting", +] + +MEETING_MINUTES_PROMPT = """You are a meeting minutes outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent attendees, decisions, or action items +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Title: [meeting name if mentioned] +Date & Time: [when meeting was held if mentioned] +Location: [where/how meeting was held if mentioned] +Attendees: [who was present if mentioned] +Absent: [who was absent if mentioned] +Agenda Items: [topics discussed if mentioned] +Discussion: [key points from discussion if mentioned] +Decisions: [decisions made if mentioned] +Action Items: [tasks assigned with owners and due dates if mentioned] +Next Meeting: [when next meeting is if mentioned] + +EXAMPLES: + +User: "minutes from board meeting on March 15, approved new budget" +Title: Board Meeting Minutes +Date & Time: March 15 +Location: +Attendees: +Absent: +Agenda Items: Budget +Discussion: +Decisions: New budget approved +Action Items: +Next Meeting: + +User: "team standup notes, John absent, next sprint planning Monday" +Title: Team Standup +Date & Time: +Location: +Attendees: +Absent: John +Agenda Items: +Discussion: +Decisions: +Action Items: +Next Meeting: Monday (sprint planning) + +User: "create meeting minutes" +Title: +Date & Time: +Location: +Attendees: +Absent: +Agenda Items: +Discussion: +Decisions: +Action Items: +Next Meeting: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/nda.py b/engine/src/format_prompts/nda.py new file mode 100644 index 0000000000..2bf88599b6 --- /dev/null +++ b/engine/src/format_prompts/nda.py @@ -0,0 +1,65 @@ +"""NDA (Non-Disclosure Agreement) outline extraction prompt.""" + +NDA_SECTIONS = [ + "Title", + "Parties", + "Effective Date", + "Confidential Information", + "Obligations", + "Duration", + "Exceptions", + "Signatures", +] + +NDA_PROMPT = """You are an NDA outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent party names, terms, or dates +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Title: [NDA type - mutual/one-way if mentioned] +Parties: [disclosing and receiving parties if mentioned] +Effective Date: [when NDA takes effect if mentioned] +Confidential Information: [what information is protected if mentioned] +Obligations: [what parties must do/not do if mentioned] +Duration: [how long confidentiality lasts if mentioned] +Exceptions: [what information is excluded if mentioned] +Signatures: [who needs to sign if mentioned] + +EXAMPLES: + +User: "mutual NDA between Startup Inc and Investor Group for 2 years" +Title: Mutual NDA +Parties: Startup Inc, Investor Group +Effective Date: +Confidential Information: +Obligations: +Duration: 2 years +Exceptions: +Signatures: + +User: "one-way NDA to protect trade secrets" +Title: One-Way NDA +Parties: +Effective Date: +Confidential Information: Trade secrets +Obligations: +Duration: +Exceptions: +Signatures: + +User: "create NDA" +Title: +Parties: +Effective Date: +Confidential Information: +Obligations: +Duration: +Exceptions: +Signatures: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/offer_letter.py b/engine/src/format_prompts/offer_letter.py new file mode 100644 index 0000000000..185a3afda3 --- /dev/null +++ b/engine/src/format_prompts/offer_letter.py @@ -0,0 +1,105 @@ +"""Offer letter outline extraction prompt.""" + +OFFER_LETTER_SECTIONS = [ + "Date", + "Candidate Name", + "Candidate Address", + "Position", + "Department", + "Start Date", + "Salary", + "Bonus", + "Benefits", + "Work Schedule", + "Reporting To", + "Location", + "At-Will Status", + "Contingencies", + "Response Deadline", + "Company Contact", +] + +OFFER_LETTER_PROMPT = """You are an offer letter outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent candidate names, salaries, or benefits +- Extract exact information as stated by the user + +Return the outline in this EXACT format: + +Date: [letter date if mentioned] +Candidate Name: [candidate's name if mentioned] +Candidate Address: [candidate's address if mentioned] +Position: [job title if mentioned] +Department: [department if mentioned] +Start Date: [employment start date if mentioned] +Salary: [annual salary or hourly rate if mentioned] +Bonus: [bonus structure if mentioned] +Benefits: [benefits package details if mentioned] +Work Schedule: [hours/days if mentioned] +Reporting To: [supervisor/manager if mentioned] +Location: [work location if mentioned] +At-Will Status: [at-will employment mention if included] +Contingencies: [background check, drug test, etc. if mentioned] +Response Deadline: [deadline to accept offer if mentioned] +Company Contact: [HR contact for questions if mentioned] + +EXAMPLES: + +User: "offer letter for John Smith as Senior Developer, $130k salary, start date January 15, reports to CTO, remote position" +Date: +Candidate Name: John Smith +Candidate Address: +Position: Senior Developer +Department: +Start Date: January 15 +Salary: $130,000 annually +Bonus: +Benefits: +Work Schedule: +Reporting To: CTO +Location: Remote +At-Will Status: +Contingencies: +Response Deadline: +Company Contact: + +User: "job offer, software engineer role, $110k per year, 10% annual bonus, full benefits, start March 1, respond by Friday" +Date: +Candidate Name: +Candidate Address: +Position: Software Engineer +Department: +Start Date: March 1 +Salary: $110,000 per year +Bonus: 10% annual bonus +Benefits: Full benefits package +Work Schedule: +Reporting To: +Location: +At-Will Status: +Contingencies: +Response Deadline: Friday +Company Contact: + +User: "create offer letter" +Date: +Candidate Name: +Candidate Address: +Position: +Department: +Start Date: +Salary: +Bonus: +Benefits: +Work Schedule: +Reporting To: +Location: +At-Will Status: +Contingencies: +Response Deadline: +Company Contact: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/official_memo.py b/engine/src/format_prompts/official_memo.py new file mode 100644 index 0000000000..458c2bb167 --- /dev/null +++ b/engine/src/format_prompts/official_memo.py @@ -0,0 +1,70 @@ +"""Official memo outline extraction prompt.""" + +OFFICIAL_MEMO_SECTIONS = [ + "To", + "From", + "Date", + "Subject", + "Purpose", + "Background", + "Details", + "Action Required", + "Deadline", +] + +OFFICIAL_MEMO_PROMPT = """You are an official memo outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent recipient names, departments, or content +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +To: [recipient(s) or department(s) if mentioned] +From: [sender or department if mentioned] +Date: [memo date if mentioned] +Subject: [memo subject/title if mentioned] +Purpose: [reason for memo if mentioned] +Background: [context or background information if mentioned] +Details: [main content, instructions, or information if mentioned] +Action Required: [what recipients need to do if mentioned] +Deadline: [due date for action if mentioned] + +EXAMPLES: + +User: "memo to all staff about new office hours starting Monday, 9am-6pm instead of 8am-5pm" +To: All Staff +From: +Date: +Subject: New Office Hours +Purpose: +Background: +Details: New office hours starting Monday: 9am-6pm (changed from 8am-5pm) +Action Required: +Deadline: + +User: "memo from HR department to managers regarding updated vacation policy, requires acknowledgment by end of week" +To: All Managers +From: HR Department +Date: +Subject: Updated Vacation Policy +Purpose: Policy update notification +Background: +Details: Updated vacation policy +Action Required: Acknowledgment required +Deadline: End of week + +User: "create official memo" +To: +From: +Date: +Subject: +Purpose: +Background: +Details: +Action Required: +Deadline: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/one_pager.py b/engine/src/format_prompts/one_pager.py new file mode 100644 index 0000000000..771dc3e0a8 --- /dev/null +++ b/engine/src/format_prompts/one_pager.py @@ -0,0 +1,70 @@ +"""One-pager outline extraction prompt.""" + +ONE_PAGER_SECTIONS = [ + "Title", + "Tagline", + "Problem", + "Solution", + "Key Features", + "Target Market", + "Traction", + "Team", + "Contact", +] + +ONE_PAGER_PROMPT = """You are a one-pager outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent company details, metrics, or team members +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Title: [company/product name if mentioned] +Tagline: [one-line description if mentioned] +Problem: [problem being solved if mentioned] +Solution: [how you solve it if mentioned] +Key Features: [main features/benefits if mentioned] +Target Market: [who it's for if mentioned] +Traction: [metrics, users, revenue if mentioned] +Team: [founders/key team if mentioned] +Contact: [contact information if mentioned] + +EXAMPLES: + +User: "one pager for startup FinanceBot, AI-powered budgeting app, 10k users" +Title: FinanceBot +Tagline: AI-powered budgeting app +Problem: +Solution: +Key Features: AI-powered budgeting +Target Market: +Traction: 10,000 users +Team: +Contact: + +User: "company overview for SaaS startup targeting small businesses" +Title: +Tagline: +Problem: +Solution: SaaS +Key Features: +Target Market: Small businesses +Traction: +Team: +Contact: + +User: "create one pager" +Title: +Tagline: +Problem: +Solution: +Key Features: +Target Market: +Traction: +Team: +Contact: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/pay_stub.py b/engine/src/format_prompts/pay_stub.py new file mode 100644 index 0000000000..bf8f87473c --- /dev/null +++ b/engine/src/format_prompts/pay_stub.py @@ -0,0 +1,66 @@ +"""Pay stub / payslip outline extraction prompt.""" + +PAY_STUB_SECTIONS = [ + "Company", + "Employee", + "Pay Period", + "Pay Date", + "Earnings", + "Deductions", + "Net Pay", + "Payment Method", +] + +PAY_STUB_PROMPT = """You are a pay stub outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent pay amounts, employee names, or company details +- Extract exact information as stated by the user +- For MULTIPLE items in any section, use | as separator + +Return the outline in this EXACT format (one field per line): + +Company: [company name and address if mentioned] +Employee: [employee name, ID, department, position if mentioned] +Pay Period: [pay period start and end dates if mentioned] +Pay Date: [payment date if mentioned] +Earnings: [each earnings item with hours/rate and amount - use | separator for multiple items] +Deductions: [each deduction with amount - use | separator for multiple deductions] +Net Pay: [net pay amount if mentioned] +Payment Method: [bank transfer details or payment method if mentioned] + +EXAMPLES: + +User: "pay stub for John Smith, salary $5,000, income tax $800, pension $250, net $3,950" +Company: +Employee: John Smith +Pay Period: +Pay Date: +Earnings: Basic Salary - $5,000 +Deductions: Income Tax - $800 | Pension - $250 +Net Pay: $3,950 +Payment Method: + +User: "payslip for Jane Doe, Engineering, January 2026, gross £4,500, NI £350, income tax £600, pension £225, net £3,325, paid by BACS" +Company: +Employee: Jane Doe, Engineering +Pay Period: January 2026 +Pay Date: +Earnings: Basic Salary - £4,500 +Deductions: National Insurance - £350 | Income Tax - £600 | Pension - £225 +Net Pay: £3,325 +Payment Method: BACS + +User: "create pay stub" +Company: +Employee: +Pay Period: +Pay Date: +Earnings: +Deductions: +Net Pay: +Payment Method: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/performance_review.py b/engine/src/format_prompts/performance_review.py new file mode 100644 index 0000000000..d540255dbf --- /dev/null +++ b/engine/src/format_prompts/performance_review.py @@ -0,0 +1,95 @@ +"""Performance review outline extraction prompt.""" + +PERFORMANCE_REVIEW_SECTIONS = [ + "Employee Name", + "Position", + "Department", + "Review Period", + "Reviewer", + "Overall Rating", + "Goals Achievement", + "Strengths", + "Areas for Improvement", + "Skills Assessment", + "Accomplishments", + "Goals for Next Period", + "Development Plan", + "Comments", +] + +PERFORMANCE_REVIEW_PROMPT = """You are a performance review outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent employee names, ratings, or feedback +- Extract exact information as stated by the user + +Return the outline in this EXACT format: + +Employee Name: [employee's name if mentioned] +Position: [employee's job title if mentioned] +Department: [department if mentioned] +Review Period: [time period being reviewed if mentioned] +Reviewer: [reviewer's name if mentioned] +Overall Rating: [overall performance rating if mentioned] +Goals Achievement: [progress on previous goals if mentioned] +Strengths: [employee strengths, use | as separator] +Areas for Improvement: [areas needing development, use | as separator] +Skills Assessment: [specific skills evaluation if mentioned] +Accomplishments: [key achievements, use | as separator] +Goals for Next Period: [goals for upcoming period, use | as separator] +Development Plan: [training or development recommendations if mentioned] +Comments: [additional comments if mentioned] + +EXAMPLES: + +User: "performance review for Jane Smith, Q4 2023, exceeded goals, strong leadership skills, needs to improve time management" +Employee Name: Jane Smith +Position: +Department: +Review Period: Q4 2023 +Reviewer: +Overall Rating: Exceeded goals +Goals Achievement: Exceeded +Strengths: Strong leadership skills +Areas for Improvement: Time management +Skills Assessment: +Accomplishments: +Goals for Next Period: +Development Plan: +Comments: + +User: "annual review, sales manager, achieved 120% of target, completed leadership training, set goal to mentor 2 team members" +Employee Name: +Position: Sales Manager +Department: +Review Period: Annual +Reviewer: +Overall Rating: +Goals Achievement: 120% of target achieved +Strengths: +Areas for Improvement: +Skills Assessment: +Accomplishments: Completed leadership training +Goals for Next Period: Mentor 2 team members +Development Plan: +Comments: + +User: "create performance review" +Employee Name: +Position: +Department: +Review Period: +Reviewer: +Overall Rating: +Goals Achievement: +Strengths: +Areas for Improvement: +Skills Assessment: +Accomplishments: +Goals for Next Period: +Development Plan: +Comments: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/press_release.py b/engine/src/format_prompts/press_release.py new file mode 100644 index 0000000000..ed6f1ea523 --- /dev/null +++ b/engine/src/format_prompts/press_release.py @@ -0,0 +1,65 @@ +"""Press release outline extraction prompt.""" + +PRESS_RELEASE_SECTIONS = [ + "Headline", + "Subheadline", + "Dateline", + "Lead Paragraph", + "Body", + "Quote", + "Boilerplate", + "Contact", +] + +PRESS_RELEASE_PROMPT = """You are a press release outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent quotes, company details, or announcements +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Headline: [main announcement if mentioned] +Subheadline: [supporting headline if mentioned] +Dateline: [city and date if mentioned] +Lead Paragraph: [who, what, when, where, why if mentioned] +Body: [additional details and context if mentioned] +Quote: [spokesperson quote and attribution if mentioned] +Boilerplate: [about the company if mentioned] +Contact: [press contact information if mentioned] + +EXAMPLES: + +User: "press release announcing Series A funding of $10M for TechStartup" +Headline: TechStartup Announces $10M Series A Funding +Subheadline: +Dateline: +Lead Paragraph: +Body: Series A funding round - $10M +Quote: +Boilerplate: TechStartup +Contact: + +User: "product launch press release for new AI tool by InnovateCo, quote from CEO Jane Smith" +Headline: InnovateCo Launches New AI Tool +Subheadline: +Dateline: +Lead Paragraph: Product launch - AI tool +Body: +Quote: Jane Smith, CEO +Boilerplate: InnovateCo +Contact: + +User: "create press release" +Headline: +Subheadline: +Dateline: +Lead Paragraph: +Body: +Quote: +Boilerplate: +Contact: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/price_sheet.py b/engine/src/format_prompts/price_sheet.py new file mode 100644 index 0000000000..13d72843f5 --- /dev/null +++ b/engine/src/format_prompts/price_sheet.py @@ -0,0 +1,92 @@ +"""Price sheet outline extraction prompt.""" + +PRICE_SHEET_SECTIONS = [ + "Title", + "Company Name", + "Date", + "Valid Until", + "Products", + "Services", + "Pricing Tiers", + "Volume Discounts", + "Payment Terms", + "Shipping Costs", + "Taxes", + "Notes", + "Contact", +] + +PRICE_SHEET_PROMPT = """You are a price sheet outline extractor. Your job is to EXTRACT ALL values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent product names, prices, or terms +- Extract exact numbers and pricing information as stated +- IMPORTANT: Extract EVERY product/service with its price - do not skip any! + +Return the outline in this EXACT format: + +Title: [price sheet title if mentioned] +Company Name: [company name if mentioned] +Date: [price sheet date if mentioned] +Valid Until: [expiration date if mentioned] +Products: [EXTRACT EVERY product with price, use | as separator] +Services: [EXTRACT EVERY service with price, use | as separator] +Pricing Tiers: [different pricing levels if mentioned, use | as separator] +Volume Discounts: [volume pricing if mentioned, use | as separator] +Payment Terms: [payment terms if mentioned] +Shipping Costs: [shipping information if mentioned] +Taxes: [tax information if mentioned] +Notes: [additional notes, terms, or conditions if mentioned] +Contact: [contact information if mentioned] + +EXAMPLES: + +User: "price sheet for software, Basic plan $29/month, Pro plan $99/month, Enterprise $299/month, annual discount 20%" +Title: Software Pricing +Company Name: +Date: +Valid Until: +Products: +Services: Basic Plan - $29/month | Pro Plan - $99/month | Enterprise Plan - $299/month +Pricing Tiers: Basic ($29/mo) | Pro ($99/mo) | Enterprise ($299/mo) +Volume Discounts: 20% discount for annual payment +Payment Terms: +Shipping Costs: +Taxes: +Notes: +Contact: + +User: "pricing for consulting services, hourly rate $150, day rate $1000, 10+ hours get 10% discount" +Title: Consulting Services Pricing +Company Name: +Date: +Valid Until: +Products: +Services: Hourly rate - $150 | Day rate - $1,000 +Pricing Tiers: +Volume Discounts: 10% discount for 10+ hours +Payment Terms: +Shipping Costs: +Taxes: +Notes: +Contact: + +User: "create price sheet" +Title: +Company Name: +Date: +Valid Until: +Products: +Services: +Pricing Tiers: +Volume Discounts: +Payment Terms: +Shipping Costs: +Taxes: +Notes: +Contact: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user) +REMEMBER: Extract ALL products/services and prices - do not skip any!""" diff --git a/engine/src/format_prompts/privacy_policy.py b/engine/src/format_prompts/privacy_policy.py new file mode 100644 index 0000000000..0818ac40b2 --- /dev/null +++ b/engine/src/format_prompts/privacy_policy.py @@ -0,0 +1,75 @@ +"""Privacy Policy outline extraction prompt.""" + +PRIVACY_POLICY_SECTIONS = [ + "Title", + "Company", + "Effective Date", + "Data Collected", + "How Data Is Used", + "Data Sharing", + "Data Security", + "User Rights", + "Cookies", + "Contact", +] + +PRIVACY_POLICY_PROMPT = """You are a Privacy Policy outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent company details or data practices +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Title: [document title if mentioned] +Company: [company/organization name if mentioned] +Effective Date: [when policy takes effect if mentioned] +Data Collected: [types of data collected if mentioned] +How Data Is Used: [purposes for data use if mentioned] +Data Sharing: [who data is shared with if mentioned] +Data Security: [security measures if mentioned] +User Rights: [user rights regarding their data if mentioned] +Cookies: [cookie usage if mentioned] +Contact: [contact for privacy questions if mentioned] + +EXAMPLES: + +User: "privacy policy for e-commerce website that collects email and payment info" +Title: Privacy Policy +Company: +Effective Date: +Data Collected: Email, payment information +How Data Is Used: +Data Sharing: +Data Security: +User Rights: +Cookies: +Contact: + +User: "GDPR compliant privacy policy for DataCorp" +Title: Privacy Policy +Company: DataCorp +Effective Date: +Data Collected: +How Data Is Used: +Data Sharing: +Data Security: +User Rights: GDPR compliant +Cookies: +Contact: + +User: "create privacy policy" +Title: +Company: +Effective Date: +Data Collected: +How Data Is Used: +Data Sharing: +Data Security: +User Rights: +Cookies: +Contact: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/proposal.py b/engine/src/format_prompts/proposal.py new file mode 100644 index 0000000000..f48a840cc2 --- /dev/null +++ b/engine/src/format_prompts/proposal.py @@ -0,0 +1,65 @@ +"""Business proposal outline extraction prompt.""" + +PROPOSAL_SECTIONS = [ + "Title", + "Executive Summary", + "Problem Statement", + "Proposed Solution", + "Scope & Deliverables", + "Timeline", + "Budget", + "Terms", +] + +PROPOSAL_PROMPT = """You are a proposal outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent project details, costs, or timelines +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Title: [proposal name/project if mentioned] +Executive Summary: [brief overview of what you're proposing if mentioned] +Problem Statement: [the problem/need you're addressing if mentioned] +Proposed Solution: [your solution/approach if mentioned] +Scope & Deliverables: [what will be delivered if mentioned] +Timeline: [project schedule/milestones if mentioned] +Budget: [costs and pricing if mentioned] +Terms: [payment terms, conditions if mentioned] + +EXAMPLES: + +User: "proposal for website redesign, $15,000 budget, 8 weeks" +Title: Website Redesign Proposal +Executive Summary: +Problem Statement: +Proposed Solution: Website redesign +Scope & Deliverables: +Timeline: 8 weeks +Budget: $15,000 +Terms: + +User: "marketing proposal for ABC Corp to increase brand awareness" +Title: Marketing Proposal +Executive Summary: +Problem Statement: Need to increase brand awareness +Proposed Solution: +Scope & Deliverables: +Timeline: +Budget: +Terms: + +User: "create proposal" +Title: +Executive Summary: +Problem Statement: +Proposed Solution: +Scope & Deliverables: +Timeline: +Budget: +Terms: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/public_notice.py b/engine/src/format_prompts/public_notice.py new file mode 100644 index 0000000000..6a858d0f79 --- /dev/null +++ b/engine/src/format_prompts/public_notice.py @@ -0,0 +1,87 @@ +"""Public notice outline extraction prompt.""" + +PUBLIC_NOTICE_SECTIONS = [ + "Notice Type", + "Title", + "Issuing Authority", + "Date", + "Effective Date", + "Summary", + "Details", + "Location", + "Public Comment Period", + "How to Respond", + "Contact Information", + "Legal Authority", +] + +PUBLIC_NOTICE_PROMPT = """You are a public notice outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent notice details, dates, or requirements +- Extract exact information as stated by the user +- Public notices must be accurate and complete + +Return the outline in this EXACT format: + +Notice Type: [type of notice if mentioned, e.g., "Public Hearing", "Zoning Change"] +Title: [notice title if mentioned] +Issuing Authority: [organization issuing notice if mentioned] +Date: [notice date if mentioned] +Effective Date: [when change/action takes effect if mentioned] +Summary: [brief summary of notice if mentioned] +Details: [detailed information if mentioned] +Location: [location relevant to notice if mentioned] +Public Comment Period: [comment period dates if mentioned] +How to Respond: [how public can respond/comment if mentioned] +Contact Information: [contact details for questions if mentioned] +Legal Authority: [legal basis for notice if mentioned] + +EXAMPLES: + +User: "public notice of zoning change for 123 Main St from residential to commercial, public hearing June 15 at City Hall, comments due by June 1" +Notice Type: Zoning Change +Title: Zoning Change Notice +Issuing Authority: +Date: +Effective Date: +Summary: Zoning change from residential to commercial +Details: Property at 123 Main Street +Location: 123 Main Street +Public Comment Period: Comments due by June 1 +How to Respond: Public hearing on June 15 at City Hall +Contact Information: +Legal Authority: + +User: "notice of road closure on Oak Avenue from May 1-15 for repairs, detour via Maple Street, call 555-0100 for questions" +Notice Type: Road Closure +Title: Road Closure Notice +Issuing Authority: +Date: +Effective Date: May 1-15 +Summary: Oak Avenue closed for repairs +Details: Repairs scheduled May 1-15 +Location: Oak Avenue +Public Comment Period: +How to Respond: Detour via Maple Street +Contact Information: 555-0100 +Legal Authority: + +User: "create public notice" +Notice Type: +Title: +Issuing Authority: +Date: +Effective Date: +Summary: +Details: +Location: +Public Comment Period: +How to Respond: +Contact Information: +Legal Authority: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user) +NOTE: Public notices must be accurate - they have legal implications.""" diff --git a/engine/src/format_prompts/purchase_order.py b/engine/src/format_prompts/purchase_order.py new file mode 100644 index 0000000000..265811c059 --- /dev/null +++ b/engine/src/format_prompts/purchase_order.py @@ -0,0 +1,92 @@ +"""Purchase order outline extraction prompt.""" + +PURCHASE_ORDER_SECTIONS = [ + "PO Number", + "Date", + "Buyer", + "Seller", + "Ship To", + "Line Items", + "Subtotal", + "Tax", + "Shipping", + "Total", + "Payment Terms", + "Delivery Date", + "Notes", +] + +PURCHASE_ORDER_PROMPT = """You are a purchase order outline extractor. Your job is to EXTRACT ALL values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent items, prices, or company names +- Extract exact numbers, quantities, and amounts as stated +- IMPORTANT: Extract EVERY line item mentioned - do not skip any! + +Return the outline in this EXACT format: + +PO Number: [purchase order number if mentioned] +Date: [PO date if mentioned] +Buyer: [purchasing company/person name and contact if mentioned] +Seller: [vendor/supplier name and contact if mentioned] +Ship To: [shipping address if mentioned] +Line Items: [EXTRACT EVERY item with quantity and price, use | as separator between items] +Subtotal: [sum of all items if calculable] +Tax: [tax amount if mentioned] +Shipping: [shipping cost if mentioned] +Total: [total amount if calculable] +Payment Terms: [payment terms if mentioned, e.g., "Net 30"] +Delivery Date: [expected delivery date if mentioned] +Notes: [special instructions or notes if mentioned] + +EXAMPLES: + +User: "purchase order for 100 units of product A at $50 each and 50 units of product B at $75 each" +PO Number: +Date: +Buyer: +Seller: +Ship To: +Line Items: Product A - Qty: 100 @ $50 each | Product B - Qty: 50 @ $75 each +Subtotal: $8,750 +Tax: +Shipping: +Total: $8,750 +Payment Terms: +Delivery Date: +Notes: + +User: "PO #12345 from Acme Corp to SupplyCo, 500 widgets at $10/unit, delivery by end of month, net 30" +PO Number: 12345 +Date: +Buyer: Acme Corp +Seller: SupplyCo +Ship To: +Line Items: Widgets - Qty: 500 @ $10 each +Subtotal: $5,000 +Tax: +Shipping: +Total: $5,000 +Payment Terms: Net 30 +Delivery Date: End of month +Notes: + +User: "create purchase order" +PO Number: +Date: +Buyer: +Seller: +Ship To: +Line Items: +Subtotal: +Tax: +Shipping: +Total: +Payment Terms: +Delivery Date: +Notes: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user) +REMEMBER: Extract ALL line items - do not combine or skip any!""" diff --git a/engine/src/format_prompts/quote.py b/engine/src/format_prompts/quote.py new file mode 100644 index 0000000000..66ee09eb08 --- /dev/null +++ b/engine/src/format_prompts/quote.py @@ -0,0 +1,72 @@ +"""Quote/Estimate outline extraction prompt.""" + +QUOTE_SECTIONS = [ + "Title", + "From", + "To", + "Quote Number", + "Date", + "Valid Until", + "Line Items", + "Total", + "Terms", +] + +QUOTE_PROMPT = """You are a quote/estimate outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent prices, services, or company details +- Extract exact numbers and descriptions as stated +- For MULTIPLE items in any section, use | as separator + +Return the outline in this EXACT format (one field per line): + +Title: [quote title/project name if mentioned] +From: [your company/name if mentioned] +To: [client company/name if mentioned] +Quote Number: [quote/estimate number if mentioned] +Date: [quote date if mentioned] +Valid Until: [expiry date if mentioned] +Line Items: [each item/service with price - use | separator for multiple items] +Total: [total amount if mentioned] +Terms: [payment terms, conditions - use | separator for multiple terms] + +EXAMPLES: + +User: "quote for bathroom renovation, $5,000 for labor, $3,500 for materials" +Title: Bathroom Renovation Quote +From: +To: +Quote Number: +Date: +Valid Until: +Line Items: Labor - $5,000 | Materials - $3,500 +Total: $8,500 +Terms: + +User: "estimate from Smith Contractors to Johnson family for roofing $12,000 and gutters $2,000, valid 30 days" +Title: Roofing & Gutters Estimate +From: Smith Contractors +To: Johnson family +Quote Number: +Date: +Valid Until: 30 days +Line Items: Roofing - $12,000 | Gutters - $2,000 +Total: $14,000 +Terms: + +User: "create quote" +Title: +From: +To: +Quote Number: +Date: +Valid Until: +Line Items: +Total: +Terms: + +DO NOT fabricate any information. Only extract what is explicitly stated. +REMEMBER: Use | separator for multiple items in any section!""" diff --git a/engine/src/format_prompts/receipt.py b/engine/src/format_prompts/receipt.py new file mode 100644 index 0000000000..9c415a6b6a --- /dev/null +++ b/engine/src/format_prompts/receipt.py @@ -0,0 +1,70 @@ +"""Receipt outline extraction prompt.""" + +RECEIPT_SECTIONS = [ + "Header", + "Receipt Number", + "Date", + "Customer", + "Items", + "Subtotal", + "Tax", + "Total", + "Payment Method", +] + +RECEIPT_PROMPT = """You are a receipt outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent transaction details or amounts +- Extract exact numbers and items as stated + +Return the outline in this EXACT format (one field per line): + +Header: [business name, address, contact if mentioned] +Receipt Number: [receipt/transaction number if mentioned] +Date: [transaction date if mentioned] +Customer: [customer name if mentioned] +Items: [each item with quantity and price if mentioned] +Subtotal: [subtotal before tax if mentioned] +Tax: [tax amount if mentioned] +Total: [total amount paid if mentioned] +Payment Method: [cash, card, etc. if mentioned] + +EXAMPLES: + +User: "receipt for $45.99 coffee subscription paid by credit card" +Header: +Receipt Number: +Date: +Customer: +Items: Coffee subscription - $45.99 +Subtotal: +Tax: +Total: $45.99 +Payment Method: Credit card + +User: "receipt from Joe's Cafe for 2 lattes at $5 each" +Header: Joe's Cafe +Receipt Number: +Date: +Customer: +Items: Latte x2 - $5 each +Subtotal: +Tax: +Total: $10 +Payment Method: + +User: "create receipt" +Header: +Receipt Number: +Date: +Customer: +Items: +Subtotal: +Tax: +Total: +Payment Method: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/report.py b/engine/src/format_prompts/report.py new file mode 100644 index 0000000000..1c6df783f9 --- /dev/null +++ b/engine/src/format_prompts/report.py @@ -0,0 +1,70 @@ +"""Report outline extraction prompt.""" + +REPORT_SECTIONS = [ + "Title", + "Author", + "Date", + "Executive Summary", + "Introduction", + "Findings", + "Analysis", + "Recommendations", + "Conclusion", +] + +REPORT_PROMPT = """You are a report outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent data, findings, or conclusions +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Title: [report title/subject if mentioned] +Author: [who is writing the report if mentioned] +Date: [report date if mentioned] +Executive Summary: [brief overview if mentioned] +Introduction: [background/context if mentioned] +Findings: [key data points or discoveries if mentioned] +Analysis: [interpretation of findings if mentioned] +Recommendations: [suggested actions if mentioned] +Conclusion: [summary/closing thoughts if mentioned] + +EXAMPLES: + +User: "quarterly sales report for Q3 2024, revenue up 15%" +Title: Q3 2024 Sales Report +Author: +Date: Q3 2024 +Executive Summary: +Introduction: +Findings: Revenue up 15% +Analysis: +Recommendations: +Conclusion: + +User: "market research report on AI adoption in healthcare by Research Team" +Title: AI Adoption in Healthcare Market Research +Author: Research Team +Date: +Executive Summary: +Introduction: +Findings: +Analysis: +Recommendations: +Conclusion: + +User: "create report" +Title: +Author: +Date: +Executive Summary: +Introduction: +Findings: +Analysis: +Recommendations: +Conclusion: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/resume.py b/engine/src/format_prompts/resume.py new file mode 100644 index 0000000000..22b5add399 --- /dev/null +++ b/engine/src/format_prompts/resume.py @@ -0,0 +1,62 @@ +"""Resume/CV outline extraction prompt.""" + +RESUME_SECTIONS = [ + "Header", + "Summary", + "Experience", + "Education", + "Skills", + "Certifications", + "Projects", +] + +RESUME_PROMPT = """You are a resume outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent job titles, companies, dates, or qualifications +- Extract exact information as stated by the user +- For MULTIPLE items in a section, use | as separator + +Return the outline in this EXACT format (one field per line): + +Header: [name, contact info, location, LinkedIn/portfolio if mentioned] +Summary: [professional summary or objective if mentioned] +Experience: [each job - use | separator for multiple jobs] +Education: [each degree - use | separator for multiple degrees] +Skills: [each skill - use | separator for multiple skills] +Certifications: [each certification - use | separator for multiple certs] +Projects: [each project - use | separator for multiple projects] + +EXAMPLES: + +User: "resume for John Smith, software engineer with 5 years Python experience, knows JavaScript and React" +Header: John Smith +Summary: Software engineer with 5 years Python experience +Experience: +Education: +Skills: Python (5 years) | JavaScript | React +Certifications: +Projects: + +User: "CV for marketing manager, worked at Google 2019-2023 and Facebook 2017-2019, MBA from Harvard, BA from Yale" +Header: +Summary: Marketing manager +Experience: Google - 2019-2023 | Facebook - 2017-2019 +Education: MBA - Harvard | BA - Yale +Skills: +Certifications: +Projects: + +User: "create resume" +Header: +Summary: +Experience: +Education: +Skills: +Certifications: +Projects: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user) +REMEMBER: Use | separator for multiple items in any section!""" diff --git a/engine/src/format_prompts/safe_agreement.py b/engine/src/format_prompts/safe_agreement.py new file mode 100644 index 0000000000..19064395b8 --- /dev/null +++ b/engine/src/format_prompts/safe_agreement.py @@ -0,0 +1,92 @@ +"""SAFE agreement outline extraction prompt.""" + +SAFE_AGREEMENT_SECTIONS = [ + "Title", + "Company Name", + "Investor Name", + "Issue Date", + "Purchase Amount", + "Valuation Cap", + "Discount Rate", + "Conversion Trigger", + "Pro Rata Rights", + "Most Favored Nation", + "Termination", + "Governing Law", + "Signatures", +] + +SAFE_AGREEMENT_PROMPT = """You are a SAFE (Simple Agreement for Future Equity) outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent company names, investment amounts, or terms +- Extract exact numbers and percentages as stated +- SAFE agreements are specific investment instruments - maintain accuracy + +Return the outline in this EXACT format: + +Title: [agreement title if mentioned] +Company Name: [company receiving investment if mentioned] +Investor Name: [investor name if mentioned] +Issue Date: [date of agreement if mentioned] +Purchase Amount: [investment amount if mentioned] +Valuation Cap: [valuation cap if mentioned] +Discount Rate: [discount percentage if mentioned] +Conversion Trigger: [when SAFE converts to equity if mentioned] +Pro Rata Rights: [investor's right to participate in future rounds if mentioned] +Most Favored Nation: [MFN provision if mentioned] +Termination: [termination conditions if mentioned] +Governing Law: [applicable law/jurisdiction if mentioned] +Signatures: [who needs to sign if mentioned] + +EXAMPLES: + +User: "SAFE agreement for $100k investment in StartupCo, $5M valuation cap, 20% discount" +Title: SAFE Agreement +Company Name: StartupCo +Investor Name: +Issue Date: +Purchase Amount: $100,000 +Valuation Cap: $5,000,000 +Discount Rate: 20% +Conversion Trigger: +Pro Rata Rights: +Most Favored Nation: +Termination: +Governing Law: +Signatures: + +User: "safe for $250k with $10M cap, converts on Series A, includes pro-rata rights" +Title: SAFE Agreement +Company Name: +Investor Name: +Issue Date: +Purchase Amount: $250,000 +Valuation Cap: $10,000,000 +Discount Rate: +Conversion Trigger: Series A funding round +Pro Rata Rights: Included +Most Favored Nation: +Termination: +Governing Law: +Signatures: + +User: "create SAFE agreement" +Title: +Company Name: +Investor Name: +Issue Date: +Purchase Amount: +Valuation Cap: +Discount Rate: +Conversion Trigger: +Pro Rata Rights: +Most Favored Nation: +Termination: +Governing Law: +Signatures: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user) +NOTE: SAFE agreements are legal documents - accuracy is critical.""" diff --git a/engine/src/format_prompts/separation_notice.py b/engine/src/format_prompts/separation_notice.py new file mode 100644 index 0000000000..b909ac203c --- /dev/null +++ b/engine/src/format_prompts/separation_notice.py @@ -0,0 +1,102 @@ +"""Separation notice outline extraction prompt.""" + +SEPARATION_NOTICE_SECTIONS = [ + "Date", + "Employee Name", + "Position", + "Department", + "Separation Date", + "Separation Type", + "Reason", + "Final Pay", + "Benefits Status", + "Severance", + "Unused PTO", + "Return of Property", + "Non-Compete", + "References", + "Contact Information", +] + +SEPARATION_NOTICE_PROMPT = """You are a separation notice outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent employee names, dates, or termination reasons +- Extract exact information as stated by the user +- Handle sensitive information appropriately + +Return the outline in this EXACT format: + +Date: [notice date if mentioned] +Employee Name: [employee's name if mentioned] +Position: [employee's position if mentioned] +Department: [department if mentioned] +Separation Date: [last day of employment if mentioned] +Separation Type: [voluntary, involuntary, layoff, retirement if mentioned] +Reason: [reason for separation if mentioned] +Final Pay: [final paycheck information if mentioned] +Benefits Status: [status of health insurance, etc. if mentioned] +Severance: [severance package details if mentioned] +Unused PTO: [vacation/PTO payout if mentioned] +Return of Property: [company property to return if mentioned] +Non-Compete: [non-compete obligations if mentioned] +References: [reference policy if mentioned] +Contact Information: [HR contact if mentioned] + +EXAMPLES: + +User: "separation notice for John Smith, last day March 31, position eliminated, 2 weeks severance, benefits continue 60 days" +Date: +Employee Name: John Smith +Position: +Department: +Separation Date: March 31 +Separation Type: Position eliminated +Reason: Position eliminated +Final Pay: +Benefits Status: Continue for 60 days +Severance: 2 weeks +Unused PTO: +Return of Property: +Non-Compete: +References: +Contact Information: + +User: "termination notice, effective immediately, final paycheck includes accrued vacation, return laptop and badge" +Date: +Employee Name: +Position: +Department: +Separation Date: Immediate +Separation Type: Termination +Reason: +Final Pay: Includes accrued vacation +Benefits Status: +Severance: +Unused PTO: Paid out in final paycheck +Return of Property: Laptop and badge +Non-Compete: +References: +Contact Information: + +User: "create separation notice" +Date: +Employee Name: +Position: +Department: +Separation Date: +Separation Type: +Reason: +Final Pay: +Benefits Status: +Severance: +Unused PTO: +Return of Property: +Non-Compete: +References: +Contact Information: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user) +NOTE: Separation notices are sensitive - accuracy is critical.""" diff --git a/engine/src/format_prompts/service_agreement.py b/engine/src/format_prompts/service_agreement.py new file mode 100644 index 0000000000..ba121938ff --- /dev/null +++ b/engine/src/format_prompts/service_agreement.py @@ -0,0 +1,90 @@ +"""Service agreement outline extraction prompt.""" + +SERVICE_AGREEMENT_SECTIONS = [ + "Title", + "Parties", + "Effective Date", + "Services", + "Deliverables", + "Timeline", + "Payment Terms", + "Expenses", + "Warranties", + "Liability", + "Termination", + "Governing Law", + "Signatures", +] + +SERVICE_AGREEMENT_PROMPT = """You are a service agreement outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent party names, services, or payment terms +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Title: [agreement title if mentioned] +Parties: [service provider and client names if mentioned] +Effective Date: [start date if mentioned] +Services: [description of services to be provided if mentioned] +Deliverables: [specific deliverables if mentioned] +Timeline: [project timeline or milestones if mentioned] +Payment Terms: [payment amount, schedule, method if mentioned] +Expenses: [expense handling if mentioned] +Warranties: [service warranties or guarantees if mentioned] +Liability: [liability limitations if mentioned] +Termination: [termination conditions if mentioned] +Governing Law: [applicable law/jurisdiction if mentioned] +Signatures: [who needs to sign if mentioned] + +EXAMPLES: + +User: "service agreement for web development, $10,000 total, 3 month timeline, monthly payments" +Title: Web Development Service Agreement +Parties: +Effective Date: +Services: Web development +Deliverables: +Timeline: 3 months +Payment Terms: $10,000 total, monthly payments +Expenses: +Warranties: +Liability: +Termination: +Governing Law: +Signatures: + +User: "services agreement between ABC Services and XYZ Corp, consulting services $150/hour, net 30 payment" +Title: Services Agreement +Parties: ABC Services, XYZ Corp +Effective Date: +Services: Consulting services +Deliverables: +Timeline: +Payment Terms: $150/hour, net 30 payment terms +Expenses: +Warranties: +Liability: +Termination: +Governing Law: +Signatures: + +User: "create service agreement" +Title: +Parties: +Effective Date: +Services: +Deliverables: +Timeline: +Payment Terms: +Expenses: +Warranties: +Liability: +Termination: +Governing Law: +Signatures: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/standard_operating_procedures.py b/engine/src/format_prompts/standard_operating_procedures.py new file mode 100644 index 0000000000..ceb0fcebf3 --- /dev/null +++ b/engine/src/format_prompts/standard_operating_procedures.py @@ -0,0 +1,97 @@ +"""Standard operating procedures outline extraction prompt.""" + +STANDARD_OPERATING_PROCEDURES_SECTIONS = [ + "Title", + "Document Number", + "Effective Date", + "Department", + "Purpose", + "Scope", + "Responsibilities", + "Definitions", + "Procedure Steps", + "Safety Considerations", + "Quality Standards", + "Documentation", + "References", + "Revision History", +] + +STANDARD_OPERATING_PROCEDURES_PROMPT = """You are a standard operating procedures (SOP) outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent procedure steps or requirements +- Extract exact information as stated by the user +- IMPORTANT: Extract ALL procedure steps in order + +Return the outline in this EXACT format: + +Title: [SOP title/name if mentioned] +Document Number: [SOP document number if mentioned] +Effective Date: [when SOP takes effect if mentioned] +Department: [responsible department if mentioned] +Purpose: [purpose of this SOP if mentioned] +Scope: [what this SOP covers if mentioned] +Responsibilities: [who is responsible for what if mentioned] +Definitions: [key terms and definitions if mentioned] +Procedure Steps: [EXTRACT ALL steps in order, use | as separator] +Safety Considerations: [safety requirements or warnings if mentioned] +Quality Standards: [quality requirements if mentioned] +Documentation: [required documentation if mentioned] +References: [related documents or regulations if mentioned] +Revision History: [revision information if mentioned] + +EXAMPLES: + +User: "SOP for customer onboarding, step 1: verify identity, step 2: create account, step 3: send welcome email, documentation required for compliance" +Title: Customer Onboarding SOP +Document Number: +Effective Date: +Department: +Purpose: +Scope: +Responsibilities: +Definitions: +Procedure Steps: 1. Verify customer identity | 2. Create customer account | 3. Send welcome email +Safety Considerations: +Quality Standards: +Documentation: Required for compliance +References: +Revision History: + +User: "standard operating procedure for equipment maintenance, weekly inspections required, safety goggles must be worn, document all findings" +Title: Equipment Maintenance SOP +Document Number: +Effective Date: +Department: +Purpose: +Scope: +Responsibilities: +Definitions: +Procedure Steps: Weekly equipment inspections +Safety Considerations: Safety goggles must be worn +Quality Standards: +Documentation: Document all inspection findings +References: +Revision History: + +User: "create SOP" +Title: +Document Number: +Effective Date: +Department: +Purpose: +Scope: +Responsibilities: +Definitions: +Procedure Steps: +Safety Considerations: +Quality Standards: +Documentation: +References: +Revision History: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user) +REMEMBER: Extract ALL procedure steps in order - do not skip any!""" diff --git a/engine/src/format_prompts/statement_of_work.py b/engine/src/format_prompts/statement_of_work.py new file mode 100644 index 0000000000..39d9c78560 --- /dev/null +++ b/engine/src/format_prompts/statement_of_work.py @@ -0,0 +1,75 @@ +"""Statement of Work (SOW) outline extraction prompt.""" + +STATEMENT_OF_WORK_SECTIONS = [ + "Title", + "Parties", + "Project Overview", + "Scope of Work", + "Deliverables", + "Timeline", + "Milestones", + "Budget", + "Acceptance Criteria", + "Signatures", +] + +STATEMENT_OF_WORK_PROMPT = """You are a Statement of Work outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent project details, costs, or timelines +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Title: [project name if mentioned] +Parties: [client and contractor/vendor if mentioned] +Project Overview: [brief project description if mentioned] +Scope of Work: [what work is included if mentioned] +Deliverables: [specific items to be delivered if mentioned] +Timeline: [project duration/dates if mentioned] +Milestones: [key checkpoints if mentioned] +Budget: [total cost and payment schedule if mentioned] +Acceptance Criteria: [how work will be approved if mentioned] +Signatures: [who needs to sign if mentioned] + +EXAMPLES: + +User: "SOW for mobile app development, 6 months, $120,000" +Title: Mobile App Development +Parties: +Project Overview: +Scope of Work: Mobile app development +Deliverables: +Timeline: 6 months +Milestones: +Budget: $120,000 +Acceptance Criteria: +Signatures: + +User: "statement of work between Acme Corp and DevTeam Inc for website rebuild with 3 milestones" +Title: Website Rebuild +Parties: Acme Corp, DevTeam Inc +Project Overview: Website rebuild +Scope of Work: +Deliverables: +Timeline: +Milestones: 3 milestones +Budget: +Acceptance Criteria: +Signatures: + +User: "create statement of work" +Title: +Parties: +Project Overview: +Scope of Work: +Deliverables: +Timeline: +Milestones: +Budget: +Acceptance Criteria: +Signatures: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/format_prompts/terms_of_service.py b/engine/src/format_prompts/terms_of_service.py new file mode 100644 index 0000000000..36bc309f2b --- /dev/null +++ b/engine/src/format_prompts/terms_of_service.py @@ -0,0 +1,75 @@ +"""Terms of Service outline extraction prompt.""" + +TERMS_OF_SERVICE_SECTIONS = [ + "Title", + "Company", + "Service", + "Effective Date", + "User Obligations", + "Prohibited Uses", + "Payment Terms", + "Liability", + "Termination", + "Contact", +] + +TERMS_OF_SERVICE_PROMPT = """You are a Terms of Service outline extractor. Your job is to EXTRACT values from the user's prompt. + +CRITICAL RULES: +- Only fill in fields that the user EXPLICITLY mentioned +- Leave fields BLANK if the user didn't mention them +- DO NOT invent company details, terms, or conditions +- Extract exact information as stated by the user + +Return the outline in this EXACT format (one field per line): + +Title: [document title if mentioned] +Company: [company/service provider name if mentioned] +Service: [what service/product the terms cover if mentioned] +Effective Date: [when terms take effect if mentioned] +User Obligations: [what users must do if mentioned] +Prohibited Uses: [what users cannot do if mentioned] +Payment Terms: [pricing, billing, refunds if mentioned] +Liability: [liability limitations if mentioned] +Termination: [how service can be terminated if mentioned] +Contact: [contact information for questions if mentioned] + +EXAMPLES: + +User: "terms of service for SaaS platform with monthly subscription" +Title: Terms of Service +Company: +Service: SaaS platform +Effective Date: +User Obligations: +Prohibited Uses: +Payment Terms: Monthly subscription +Liability: +Termination: +Contact: + +User: "TOS for mobile app by TechCo, no refunds policy" +Title: Terms of Service +Company: TechCo +Service: Mobile app +Effective Date: +User Obligations: +Prohibited Uses: +Payment Terms: No refunds +Liability: +Termination: +Contact: + +User: "create terms of service" +Title: +Company: +Service: +Effective Date: +User Obligations: +Prohibited Uses: +Payment Terms: +Liability: +Termination: +Contact: + +DO NOT fabricate any information. Only extract what is explicitly stated. (unless asked to fake or make up the data in the prompt by the user)""" diff --git a/engine/src/header_styles.py b/engine/src/header_styles.py new file mode 100644 index 0000000000..b415c3497c --- /dev/null +++ b/engine/src/header_styles.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +# Shared header component CSS injected via {{HEADER_CSS}}. +# Includes both the base header (top-bar, doc-header, doc-title, doc-subtitle) +# and the number-badge variant (doc-number-area, doc-number-label, doc-number) +# used by invoice, receipt, quote, and price-sheet templates. +SHARED_HEADER_CSS = """ + /* ── Top accent bar ── */ + .top-bar { + height: 3pt; + background: var(--theme-accent, #2563eb); + margin-bottom: 10pt; + } + + /* ── Header (logo left, title centred) ── */ + .doc-header { + display: flex; + align-items: center; + gap: 12pt; + margin-bottom: 8pt; + } + .doc-header-text { + flex: 1; + text-align: center; + } + .doc-header .company-logo { flex-shrink: 0; } + + .doc-title { + font-size: 16pt; + font-weight: 700; + color: var(--theme-primary, #1e3a5f); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 3pt; + } + + .doc-subtitle { + font-size: 9pt; + color: var(--theme-text-muted, #6b7280); + } + + /* Number badge variant (invoice, receipt, quote, price sheet) */ + .doc-number-area { + text-align: right; + flex-shrink: 0; + } + + .doc-number-label { + font-size: 7.5pt; + color: var(--theme-text-muted, #6b7280); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 2pt; + } + + .doc-number { + font-size: 11pt; + font-weight: 700; + color: var(--theme-primary, #1e3a5f); + }""" + + +# CSS-only header layout — works for every template regardless of whether it uses +# {{HEADER_CSS}}. Injected as a """ + + +def inject_header_css(html_content: str) -> str: + """Replace {{HEADER_CSS}} with the shared header component CSS. + + Templates that use the standard document header include this placeholder + in their " + if "" in html_content: + logger.info("[inject_theme] injecting