mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
fix(docker): ship tmux and the Office/PDF libraries; document skills keep a persistent side env
Hosted Docker instances could not run the hermes-agent skill's tmux spawning recipe (no tmux) or create Office/PDF files: the docx/xlsx/powerpoint/pdf skills said `pip install ...`, but the image's sealed venv has no pip and the root fs resets on restart. - Dockerfile: tmux in the runtime apt layer (+3 MB); bake a new [documents] extra (python-docx, openpyxl, python-pptx, reportlab, pypdf) into the sealed venv (+14 MB compressed, ~+38 MB on disk). - pyproject/uv.lock: the [documents] extra, exact pins, outside [all]; pm/extras ANCHORS gets its import anchors. - docx/xlsx/powerpoint/pdf (+ optional excel-author/pptx-author): use `python` when it already imports the libs, otherwise a persistent side venv at $HERMES_HOME/environments/documents (recreated when its Python is gone); pdf script hints and the OCR reference point there, never at pip into Hermes's interpreter. - hermes-agent skill: send text and Enter as separate tmux keystrokes (one burst lands as a newline, verified live), plus a no-tmux fallback via terminal(background=true, pty=true) + process_manage write. Found on Hermes Cloud staging.
This commit is contained in:
1 parent
93286ffd87
commit
b357ebbfc4
24 files changed
+259
-37
No files matched your search
+8
-2
@@ -74,9 +74,11 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/tools
|
||||
# must be declared here. The list is the `ldd ... | grep "not found"` set of
|
||||
# the pinned chrome binary in this base image, mapped to trixie package
|
||||
# names (see .hermes/plans/termux-removal-commit-spec.md).
|
||||
# tmux backs the hermes-agent skill's interactive spawning recipe; the runtime
|
||||
# user cannot apt-get it later (no root, and the root fs resets on restart).
|
||||
RUN apt-get -o Acquire::Retries=3 update && \
|
||||
apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
|
||||
ca-certificates curl iputils-ping python3 python-is-python3 gcc g++ make cmake python3-dev python3-venv libffi-dev libolm-dev libatomic1 procps git openssh-client docker-cli xz-utils \
|
||||
ca-certificates curl iputils-ping python3 python-is-python3 gcc g++ make cmake python3-dev python3-venv libffi-dev libolm-dev libatomic1 procps git openssh-client docker-cli xz-utils tmux \
|
||||
libasound2t64 libatk-bridge2.0-0t64 libatk1.0-0t64 libatspi2.0-0t64 libcairo2 libcups2t64 libdbus-1-3 libgbm1 libglib2.0-0t64 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -284,6 +286,10 @@ FROM runtime_base AS python_deps
|
||||
# baked here (aiohttp, so no API server). stage2's dependency refresh
|
||||
# restores this baseline into volumes that already lost it.
|
||||
#
|
||||
# The [documents] extra (python-docx, openpyxl, python-pptx, reportlab, pypdf;
|
||||
# ~25 MB) is baked so the docx/xlsx/powerpoint/pdf skills work out of the box:
|
||||
# this venv is sealed and has no pip, so the agent could not add them itself.
|
||||
#
|
||||
# Source binding is created after the source copy below.
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN touch ./README.md
|
||||
@@ -296,7 +302,7 @@ RUN python3 -m pm.build_env --source /opt/hermes --python /usr/local/bin/python3
|
||||
--cache /opt/hermes/uv-cache \
|
||||
--out /opt/hermes/.venv --no-install-project --sealed --record-selection \
|
||||
--extra all --extra messaging --extra otlp --extra anthropic --extra bedrock \
|
||||
--extra azure-identity --extra matrix --extra google-chat --extra fal
|
||||
--extra azure-identity --extra matrix --extra google-chat --extra fal --extra documents
|
||||
|
||||
# Icons render on the runtime environment: Pillow and resvg-py are core
|
||||
# dependencies. A stage of its own so the frontend stage keeps building its
|
||||
|
||||
@@ -25,10 +25,22 @@ Adapted from Anthropic's `xlsx-author` and `audit-xls` skills in the [anthropics
|
||||
|
||||
## Setup
|
||||
|
||||
Use `python` when it already imports `openpyxl` (the Hermes Docker image
|
||||
ships it). Otherwise install into a persistent side environment under the
|
||||
Hermes home, never into the interpreter that runs Hermes:
|
||||
|
||||
```bash
|
||||
pip install "openpyxl>=3.0"
|
||||
export PY=python
|
||||
if ! "$PY" -c "import openpyxl" 2>/dev/null; then
|
||||
DOCS_ENV="${HERMES_HOME:-$HOME/.hermes}/environments/documents"
|
||||
PY="$DOCS_ENV/bin/python"
|
||||
"$PY" -c "" 2>/dev/null || python -m venv --clear "$DOCS_ENV"
|
||||
"$PY" -c "import openpyxl" 2>/dev/null || "$PY" -m pip install -q "openpyxl>=3.0"
|
||||
fi
|
||||
```
|
||||
|
||||
Run the model-building Python with `"$PY"`.
|
||||
|
||||
## Core conventions (non-negotiable)
|
||||
|
||||
### Blue / black / green cell color
|
||||
|
||||
@@ -26,10 +26,22 @@ For the broader, already-shipped PowerPoint authoring skill (slides, speaker not
|
||||
|
||||
## Setup
|
||||
|
||||
Use `python` when it already imports `pptx` (the Hermes Docker image ships
|
||||
it). Otherwise install into a persistent side environment under the Hermes
|
||||
home, never into the interpreter that runs Hermes:
|
||||
|
||||
```bash
|
||||
pip install "python-pptx>=0.6"
|
||||
export PY=python
|
||||
if ! "$PY" -c "import pptx" 2>/dev/null; then
|
||||
DOCS_ENV="${HERMES_HOME:-$HOME/.hermes}/environments/documents"
|
||||
PY="$DOCS_ENV/bin/python"
|
||||
"$PY" -c "" 2>/dev/null || python -m venv --clear "$DOCS_ENV"
|
||||
"$PY" -c "import pptx" 2>/dev/null || "$PY" -m pip install -q "python-pptx>=0.6"
|
||||
fi
|
||||
```
|
||||
|
||||
Run the deck-building Python with `"$PY"`.
|
||||
|
||||
## Core conventions
|
||||
|
||||
### One idea per slide
|
||||
|
||||
@@ -59,6 +59,7 @@ ANCHORS: dict[str, str | tuple[str, ...]] = {
|
||||
"acp": "acp",
|
||||
"web": "fastapi",
|
||||
"doc-extract": "anydoc",
|
||||
"documents": ("docx", "openpyxl", "pptx", "reportlab", "pypdf"),
|
||||
"computer-use": "mcp",
|
||||
"trace-upload": "huggingface_hub",
|
||||
# Pillow resize recovery for vision tools (the `vision` extra is a no-op
|
||||
|
||||
@@ -373,6 +373,17 @@ google-chat = [
|
||||
# bundles firecrawl-anydoc==0.2.4; the extra exists so a lean/broken install
|
||||
# can re-sync exactly the pin from core.
|
||||
doc-extract = ["firecrawl-anydoc==0.2.4"]
|
||||
# Office/PDF authoring libraries for skills/productivity/{docx,xlsx,powerpoint,pdf}.
|
||||
# Baked into the Docker image (Dockerfile `--extra documents`), whose sealed
|
||||
# venv has no pip and whose root fs resets on restart; ~25 MB there. Not in
|
||||
# [all]: plain installs keep the skills' persistent side environment instead.
|
||||
documents = [
|
||||
"python-docx==1.2.0",
|
||||
"openpyxl==3.1.5",
|
||||
"python-pptx==1.0.2",
|
||||
"reportlab==5.1.0",
|
||||
"pypdf==6.21.2",
|
||||
]
|
||||
trace-upload = ["huggingface-hub==1.33.0"]
|
||||
# Cloud memory providers are opt-in PM extras. They stay outside [all]
|
||||
# so a quarantined release cannot break fresh installs.
|
||||
|
||||
@@ -141,23 +141,34 @@ terminal(command="hermes chat -q 'Set up CI/CD for ~/myapp'", background=true)
|
||||
|
||||
### Interactive PTY Mode (via tmux)
|
||||
|
||||
Hermes uses prompt_toolkit, which requires a real terminal. Use tmux for interactive spawning:
|
||||
Hermes uses prompt_toolkit, which requires a real terminal. Use tmux for interactive spawning. Send the text and `Enter` as two `send-keys` calls: text and Enter in one call reach Hermes as one burst, and the Enter lands as a newline inside the message instead of submitting it.
|
||||
|
||||
```
|
||||
# Start
|
||||
terminal(command="tmux new-session -d -s agent1 -x 120 -y 40 'hermes'", timeout=10)
|
||||
|
||||
# Wait for startup, then send a message
|
||||
terminal(command="sleep 8 && tmux send-keys -t agent1 'Build a FastAPI auth service' Enter", timeout=15)
|
||||
terminal(command="sleep 8 && tmux send-keys -t agent1 'Build a FastAPI auth service' && sleep 0.5 && tmux send-keys -t agent1 Enter", timeout=15)
|
||||
|
||||
# Read output
|
||||
terminal(command="sleep 20 && tmux capture-pane -t agent1 -p", timeout=5)
|
||||
|
||||
# Send follow-up
|
||||
terminal(command="tmux send-keys -t agent1 'Add rate limiting middleware' Enter", timeout=5)
|
||||
terminal(command="tmux send-keys -t agent1 'Add rate limiting middleware' && sleep 0.5 && tmux send-keys -t agent1 Enter", timeout=5)
|
||||
|
||||
# Exit
|
||||
terminal(command="tmux send-keys -t agent1 '/exit' Enter && sleep 2 && tmux kill-session -t agent1", timeout=10)
|
||||
terminal(command="tmux send-keys -t agent1 '/exit' && sleep 0.5 && tmux send-keys -t agent1 Enter && sleep 2 && tmux kill-session -t agent1", timeout=10)
|
||||
```
|
||||
|
||||
**No tmux** (`command -v tmux` prints nothing, e.g. an older Hermes Docker image): run Hermes as a PTY background process and drive it with `process_manage`. Stdin only exists with `pty=true`. Send the text and the Enter key (`"\r"`) as two separate `write` calls, for the same reason as with tmux; `submit` appends `"\n"`, which Hermes reads as a newline, never as Enter.
|
||||
|
||||
```
|
||||
terminal(command="hermes", background=true, pty=true) # returns session_id
|
||||
process_manage(action="write", session_id="<id>", data="Build a FastAPI auth service")
|
||||
process_manage(action="write", session_id="<id>", data="\r")
|
||||
process_manage(action="log", session_id="<id>", limit=40) # read the screen
|
||||
process_manage(action="write", session_id="<id>", data="/exit")
|
||||
process_manage(action="write", session_id="<id>", data="\r")
|
||||
```
|
||||
|
||||
### Multi-Agent Coordination
|
||||
@@ -165,15 +176,15 @@ terminal(command="tmux send-keys -t agent1 '/exit' Enter && sleep 2 && tmux kill
|
||||
```
|
||||
# Agent A: backend
|
||||
terminal(command="tmux new-session -d -s backend -x 120 -y 40 'hermes -w'", timeout=10)
|
||||
terminal(command="sleep 8 && tmux send-keys -t backend 'Build REST API for user management' Enter", timeout=15)
|
||||
terminal(command="sleep 8 && tmux send-keys -t backend 'Build REST API for user management' && sleep 0.5 && tmux send-keys -t backend Enter", timeout=15)
|
||||
|
||||
# Agent B: frontend
|
||||
terminal(command="tmux new-session -d -s frontend -x 120 -y 40 'hermes -w'", timeout=10)
|
||||
terminal(command="sleep 8 && tmux send-keys -t frontend 'Build React dashboard for user management' Enter", timeout=15)
|
||||
terminal(command="sleep 8 && tmux send-keys -t frontend 'Build React dashboard for user management' && sleep 0.5 && tmux send-keys -t frontend Enter", timeout=15)
|
||||
|
||||
# Check progress, relay context between them
|
||||
terminal(command="tmux capture-pane -t backend -p | tail -30", timeout=5)
|
||||
terminal(command="tmux send-keys -t frontend 'Here is the API schema from the backend agent: ...' Enter", timeout=5)
|
||||
terminal(command="tmux send-keys -t frontend 'Here is the API schema from the backend agent: ...' && sleep 0.5 && tmux send-keys -t frontend Enter", timeout=5)
|
||||
```
|
||||
|
||||
### Session Resume
|
||||
@@ -192,7 +203,7 @@ terminal(command="tmux new-session -d -s resumed 'hermes --resume 20260225_14305
|
||||
- **Use `-w` (worktree mode)** when spawning agents that edit code — prevents git conflicts
|
||||
- **Set timeouts** for one-shot mode — complex tasks can take 5-10 minutes
|
||||
- **Use `hermes chat -q` for fire-and-forget** — no PTY needed
|
||||
- **Use tmux for interactive sessions** — raw PTY mode has `\r` vs `\n` issues with prompt_toolkit
|
||||
- **Prefer tmux for interactive sessions when it is installed** — `capture-pane` reads the rendered screen; the PTY-background fallback only has the raw output log
|
||||
- **For scheduled tasks**, use the `cronjob` tool instead of spawning — handles delivery and retry
|
||||
- **"delegate_task is capped at N" reports** — see `references/delegate-task-concurrency-diagnosis.md`. Three real cap paths in Hermes; if none fired, the model is self-limiting and rationalising it as "the runtime caps."
|
||||
- **"Can $external_app use my Nous Portal subscription / OAuth?"** — see `references/portal-auth-for-third-party-apps.md`. Walk the user through three layers (plugin-vs-app, what Portal actually exposes, local-broker-proxy option).
|
||||
|
||||
@@ -36,8 +36,26 @@ fields, and package health checks. It does not render documents itself
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+ with `python-docx` installed:
|
||||
`pip install python-docx` (import name is `docx`; lxml comes with it).
|
||||
- Python 3.10+ with `python-docx` (import name `docx`; lxml comes with it).
|
||||
- Pick the interpreter with `terminal`. Use `python` when it already imports
|
||||
the libraries (the Hermes Docker image ships them); otherwise use a
|
||||
persistent side environment under the Hermes home, which survives container
|
||||
restarts and never touches the Hermes install itself:
|
||||
|
||||
```bash
|
||||
export PY=python
|
||||
if ! "$PY" -c "import docx" 2>/dev/null; then
|
||||
DOCS_ENV="${HERMES_HOME:-$HOME/.hermes}/environments/documents"
|
||||
PY="$DOCS_ENV/bin/python"
|
||||
"$PY" -c "" 2>/dev/null || python -m venv --clear "$DOCS_ENV" # new, or broken by a Python upgrade
|
||||
"$PY" -c "import docx" 2>/dev/null || "$PY" -m pip install -q python-docx
|
||||
fi
|
||||
"$PY" scripts/docx_create.py --help
|
||||
```
|
||||
|
||||
Run every helper below with `"$PY"`. On Windows the side interpreter is
|
||||
`%HERMES_HOME%\environments\documents\Scripts\python.exe`. Never
|
||||
`pip install` into the interpreter that runs Hermes.
|
||||
- Comments `add` uses the native API on python-docx >= 1.2 and an XML
|
||||
fallback on older versions — both are automatic.
|
||||
- For image blocks: the image files must exist locally (PNG/JPEG).
|
||||
|
||||
@@ -31,9 +31,27 @@ Create PDFs from structured specs, build and fill AcroForm forms (with layout li
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+ with `pypdf`, `reportlab`, `pdfplumber`:
|
||||
`python -m pip install pypdf reportlab pdfplumber`
|
||||
- Optional, for page rasterization (`pdf_page_image.py`, overlay rendering): `python -m pip install pypdfium2`, or poppler's `pdftoppm` on PATH. Scripts fall back pypdfium2 → pdftoppm and report `{"rendered": false, "missing": [...]}` (exit 0) when neither exists.
|
||||
- Python 3.10+ with `pypdf`, `reportlab`, `pdfplumber`.
|
||||
- Pick the interpreter with `terminal`. Use `python` when it already imports
|
||||
the libraries (the Hermes Docker image ships them); otherwise use a
|
||||
persistent side environment under the Hermes home, which survives container
|
||||
restarts and never touches the Hermes install itself:
|
||||
|
||||
```bash
|
||||
export PY=python
|
||||
if ! "$PY" -c "import pypdf, reportlab, pdfplumber" 2>/dev/null; then
|
||||
DOCS_ENV="${HERMES_HOME:-$HOME/.hermes}/environments/documents"
|
||||
PY="$DOCS_ENV/bin/python"
|
||||
"$PY" -c "" 2>/dev/null || python -m venv --clear "$DOCS_ENV" # new, or broken by a Python upgrade
|
||||
"$PY" -c "import pypdf, reportlab, pdfplumber" 2>/dev/null || "$PY" -m pip install -q pypdf reportlab pdfplumber
|
||||
fi
|
||||
"$PY" scripts/pdf_create.py --help
|
||||
```
|
||||
|
||||
Run every helper below with `"$PY"`. On Windows the side interpreter is
|
||||
`%HERMES_HOME%\environments\documents\Scripts\python.exe`. Never
|
||||
`pip install` into the interpreter that runs Hermes.
|
||||
- Optional, for page rasterization (`pdf_page_image.py`, overlay rendering): `pypdfium2` installed into that side environment (`"$DOCS_ENV/bin/python" -m pip install pypdfium2`, then use it as `$PY`), or poppler's `pdftoppm` on PATH. Scripts fall back pypdfium2 → pdftoppm and report `{"rendered": false, "missing": [...]}` (exit 0) when neither exists.
|
||||
- Each helper script checks imports lazily and prints an install hint if a dependency is missing.
|
||||
|
||||
## How to Run
|
||||
|
||||
@@ -51,8 +51,12 @@ If the user needs marker capabilities but the system lacks ~5GB free disk:
|
||||
|
||||
## pymupdf (lightweight)
|
||||
|
||||
Install into the persistent documents environment from the pdf skill's
|
||||
Prerequisites (`$DOCS_ENV`, ~110 MB with its deps; never into Hermes's own
|
||||
interpreter), then run the helper with `"$PY"`:
|
||||
|
||||
```bash
|
||||
pip install pymupdf pymupdf4llm
|
||||
"$DOCS_ENV/bin/python" -m pip install -q pymupdf pymupdf4llm && PY="$DOCS_ENV/bin/python"
|
||||
```
|
||||
|
||||
**Via helper script**:
|
||||
@@ -83,7 +87,7 @@ for page in doc:
|
||||
# Check disk space first
|
||||
python scripts/extract_marker.py --check
|
||||
|
||||
pip install marker-pdf
|
||||
"$DOCS_ENV/bin/python" -m pip install marker-pdf # same side environment as pymupdf
|
||||
```
|
||||
|
||||
**Via helper script**:
|
||||
|
||||
@@ -26,7 +26,7 @@ def available_backends() -> list[str]:
|
||||
def missing_hints() -> list[str]:
|
||||
"""Install hints for when no backend is available."""
|
||||
return [
|
||||
"python3 -m pip install pypdfium2",
|
||||
"pypdfium2 in the persistent documents environment (pdf skill, Prerequisites)",
|
||||
"poppler-utils (provides pdftoppm), e.g. apt-get install poppler-utils",
|
||||
]
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ def build_pdf(spec: dict, out_path: str) -> int:
|
||||
TableStyle,
|
||||
)
|
||||
except ImportError:
|
||||
print("Missing dependency: install with 'python3 -m pip install reportlab'", file=sys.stderr)
|
||||
print("Missing dependency: 'reportlab' is not importable here; install it into the persistent documents environment (pdf skill, Prerequisites) and rerun with that interpreter", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
page_size = letter if str(spec.get("page_size", "A4")).lower() == "letter" else A4
|
||||
|
||||
@@ -35,7 +35,7 @@ def main() -> int:
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
from pypdf.generic import BooleanObject, NameObject
|
||||
except ImportError:
|
||||
print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
|
||||
print("Missing dependency: 'pypdf' is not importable here; install it into the persistent documents environment (pdf skill, Prerequisites) and rerun with that interpreter", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
with open(args.fields_json, encoding="utf-8") as fh:
|
||||
|
||||
@@ -54,7 +54,7 @@ def build_form(spec: dict, out_path: str) -> int:
|
||||
from reportlab.lib import colors
|
||||
from reportlab.pdfgen import canvas
|
||||
except ImportError:
|
||||
print("Missing dependency: install with 'python3 -m pip install reportlab'", file=sys.stderr)
|
||||
print("Missing dependency: 'reportlab' is not importable here; install it into the persistent documents environment (pdf skill, Prerequisites) and rerun with that interpreter", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
width, height = _page_size(spec)
|
||||
|
||||
@@ -24,7 +24,7 @@ def main() -> int:
|
||||
try:
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
except ImportError:
|
||||
print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
|
||||
print("Missing dependency: 'pypdf' is not importable here; install it into the persistent documents environment (pdf skill, Prerequisites) and rerun with that interpreter", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
writer = PdfWriter()
|
||||
|
||||
@@ -46,7 +46,7 @@ def main() -> int:
|
||||
try:
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
except ImportError:
|
||||
print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
|
||||
print("Missing dependency: 'pypdf' is not importable here; install it into the persistent documents environment (pdf skill, Prerequisites) and rerun with that interpreter", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
reader = PdfReader(args.pdf)
|
||||
|
||||
@@ -62,7 +62,7 @@ def main() -> int:
|
||||
try:
|
||||
from pypdf import PdfReader
|
||||
except ImportError:
|
||||
print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
|
||||
print("Missing dependency: 'pypdf' is not importable here; install it into the persistent documents environment (pdf skill, Prerequisites) and rerun with that interpreter", file=sys.stderr)
|
||||
return 2
|
||||
reader = PdfReader(args.pdf)
|
||||
if reader.is_encrypted:
|
||||
|
||||
@@ -21,7 +21,7 @@ def _need(module: str, package: str):
|
||||
try:
|
||||
return __import__(module)
|
||||
except ImportError:
|
||||
print(f"Missing dependency: install with 'python3 -m pip install {package}'", file=sys.stderr)
|
||||
print(f"Missing dependency: '{package}' is not importable here; install it into the persistent documents environment (pdf skill, Prerequisites) and rerun with that interpreter", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ def main() -> int:
|
||||
try:
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
except ImportError:
|
||||
print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
|
||||
print("Missing dependency: 'pypdf' is not importable here; install it into the persistent documents environment (pdf skill, Prerequisites) and rerun with that interpreter", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
reader = PdfReader(args.pdf)
|
||||
|
||||
@@ -51,7 +51,7 @@ def main() -> int:
|
||||
try:
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
except ImportError:
|
||||
print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
|
||||
print("Missing dependency: 'pypdf' is not importable here; install it into the persistent documents environment (pdf skill, Prerequisites) and rerun with that interpreter", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
reader = PdfReader(args.pdf)
|
||||
|
||||
@@ -103,7 +103,7 @@ def main() -> int:
|
||||
try:
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
except ImportError:
|
||||
print("Missing dependency: install with 'python3 -m pip install pypdf reportlab'",
|
||||
print("Missing dependency: 'pypdf reportlab' is not importable here; install it into the persistent documents environment (pdf skill, Prerequisites) and rerun with that interpreter",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ def main() -> int:
|
||||
try:
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
except ImportError:
|
||||
print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
|
||||
print("Missing dependency: 'pypdf' is not importable here; install it into the persistent documents environment (pdf skill, Prerequisites) and rerun with that interpreter", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
reader = PdfReader(args.pdf)
|
||||
|
||||
@@ -33,15 +33,33 @@ and slide rendering — all offline, no PowerPoint installation required.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+ with `python-pptx` installed
|
||||
(`pip install python-pptx`).
|
||||
- Python 3.10+ with `python-pptx` (import name `pptx`).
|
||||
- Pick the interpreter with `terminal`. Use `python` when it already imports
|
||||
the libraries (the Hermes Docker image ships them); otherwise use a
|
||||
persistent side environment under the Hermes home, which survives container
|
||||
restarts and never touches the Hermes install itself:
|
||||
|
||||
```bash
|
||||
export PY=python
|
||||
if ! "$PY" -c "import pptx" 2>/dev/null; then
|
||||
DOCS_ENV="${HERMES_HOME:-$HOME/.hermes}/environments/documents"
|
||||
PY="$DOCS_ENV/bin/python"
|
||||
"$PY" -c "" 2>/dev/null || python -m venv --clear "$DOCS_ENV" # new, or broken by a Python upgrade
|
||||
"$PY" -c "import pptx" 2>/dev/null || "$PY" -m pip install -q python-pptx
|
||||
fi
|
||||
"$PY" scripts/pptx_create.py --help
|
||||
```
|
||||
|
||||
Run every helper below with `"$PY"`. On Windows the side interpreter is
|
||||
`%HERMES_HOME%\environments\documents\Scripts\python.exe`. Never
|
||||
`pip install` into the interpreter that runs Hermes.
|
||||
- Optional: LibreOffice (`soffice`) plus poppler (`pdftoppm` or
|
||||
`pdftocairo`) for rendering slides to PNGs and for PDF export.
|
||||
`pptx_render.py` detects both with `shutil.which` and degrades
|
||||
gracefully (reports `{"rendered": false, "missing": [...]}`, exit 0)
|
||||
when absent — all create/read/edit operations work without them.
|
||||
- Check availability via `terminal`:
|
||||
`python -c "import pptx; print(pptx.__version__)"` and `which soffice pdftoppm`.
|
||||
`"$PY" -c "import pptx; print(pptx.__version__)"` and `which soffice pdftoppm`.
|
||||
|
||||
## How to Run
|
||||
|
||||
|
||||
@@ -38,8 +38,27 @@ scripts are argparse CLIs that print JSON and use explicit UTF-8 I/O.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+ with `openpyxl` (`pip install openpyxl`). No other
|
||||
third-party packages are needed; everything else is stdlib.
|
||||
- Python 3.10+ with `openpyxl`. No other third-party packages are
|
||||
needed; everything else is stdlib.
|
||||
- Pick the interpreter with `terminal`. Use `python` when it already imports
|
||||
the libraries (the Hermes Docker image ships them); otherwise use a
|
||||
persistent side environment under the Hermes home, which survives container
|
||||
restarts and never touches the Hermes install itself:
|
||||
|
||||
```bash
|
||||
export PY=python
|
||||
if ! "$PY" -c "import openpyxl" 2>/dev/null; then
|
||||
DOCS_ENV="${HERMES_HOME:-$HOME/.hermes}/environments/documents"
|
||||
PY="$DOCS_ENV/bin/python"
|
||||
"$PY" -c "" 2>/dev/null || python -m venv --clear "$DOCS_ENV" # new, or broken by a Python upgrade
|
||||
"$PY" -c "import openpyxl" 2>/dev/null || "$PY" -m pip install -q openpyxl
|
||||
fi
|
||||
"$PY" scripts/xlsx_create.py --help
|
||||
```
|
||||
|
||||
Run every helper below with `"$PY"`. On Windows the side interpreter is
|
||||
`%HERMES_HOME%\environments\documents\Scripts\python.exe`. Never
|
||||
`pip install` into the interpreter that runs Hermes.
|
||||
- Optional: LibreOffice (`soffice`) for headless recalculation or
|
||||
format conversion.
|
||||
|
||||
|
||||
@@ -1738,6 +1738,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/29/64/0b75bc50ec53b4e000bac913625511215aa96124adf5dba8c4baa17c02cd/espeakng_loader-0.2.4-py3-none-win_arm64.whl", hash = "sha256:d7a2928843eaeb2df82f99a370f44e8a630f59b02f9b0d1f168a03c4eeb76b89", size = 9426841, upload-time = "2025-01-17T01:23:21.766Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "et-xmlfile"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eval-type-backport"
|
||||
version = "0.4.0"
|
||||
@@ -2394,6 +2403,13 @@ discord = [
|
||||
doc-extract = [
|
||||
{ name = "firecrawl-anydoc" },
|
||||
]
|
||||
documents = [
|
||||
{ name = "openpyxl" },
|
||||
{ name = "pypdf" },
|
||||
{ name = "python-docx" },
|
||||
{ name = "python-pptx" },
|
||||
{ name = "reportlab" },
|
||||
]
|
||||
edge-tts = [
|
||||
{ name = "edge-tts" },
|
||||
]
|
||||
@@ -2673,6 +2689,7 @@ requires-dist = [
|
||||
{ name = "numpy", marker = "extra == 'voice'", specifier = "==2.5.4" },
|
||||
{ name = "numpy", marker = "extra == 'wake'", specifier = "==2.5.4" },
|
||||
{ name = "openai", marker = "python_full_version >= '3.14'", specifier = "==2.54.0" },
|
||||
{ name = "openpyxl", marker = "extra == 'documents'", specifier = "==3.1.5" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'otlp'", specifier = "==1.45.1" },
|
||||
{ name = "opentelemetry-sdk", marker = "extra == 'otlp'", specifier = "==1.45.1" },
|
||||
{ name = "packaging", marker = "python_full_version >= '3.14'", specifier = "==26.3" },
|
||||
@@ -2694,11 +2711,14 @@ requires-dist = [
|
||||
{ name = "pyjwt", extras = ["crypto"], marker = "python_full_version >= '3.14'", specifier = "==2.15.1" },
|
||||
{ name = "pyopen-wakeword", marker = "(platform_machine != 'ARM64' and platform_machine != 'x86_64' and extra == 'wake') or (platform_machine == 'ARM64' and sys_platform == 'darwin' and extra == 'wake') or (platform_machine == 'x86_64' and sys_platform == 'win32' and extra == 'wake') or (sys_platform != 'darwin' and sys_platform != 'win32' and extra == 'wake')", specifier = "==1.1.0" },
|
||||
{ name = "pyopen-wakeword", marker = "(platform_machine != 'ARM64' and platform_machine != 'x86_64' and extra == 'wake-openwakeword') or (platform_machine == 'ARM64' and sys_platform == 'darwin' and extra == 'wake-openwakeword') or (platform_machine == 'x86_64' and sys_platform == 'win32' and extra == 'wake-openwakeword') or (sys_platform != 'darwin' and sys_platform != 'win32' and extra == 'wake-openwakeword')", specifier = "==1.1.0" },
|
||||
{ name = "pypdf", marker = "extra == 'documents'", specifier = "==6.21.2" },
|
||||
{ name = "pypinyin", marker = "extra == 'wake'", specifier = "==0.55.0" },
|
||||
{ name = "pypinyin", marker = "extra == 'wake-sherpa'", specifier = "==0.55.0" },
|
||||
{ name = "python-docx", marker = "extra == 'documents'", specifier = "==1.2.0" },
|
||||
{ name = "python-dotenv", marker = "python_full_version >= '3.14'", specifier = "==1.2.4" },
|
||||
{ name = "python-multipart", marker = "python_full_version >= '3.14'", specifier = ">=0.0.9,<1" },
|
||||
{ name = "python-multipart", marker = "extra == 'web'", specifier = "==0.0.32" },
|
||||
{ name = "python-pptx", marker = "extra == 'documents'", specifier = "==1.0.2" },
|
||||
{ name = "python-telegram-bot", extras = ["webhooks"], marker = "extra == 'messaging'", specifier = "==22.8" },
|
||||
{ name = "python-telegram-bot", extras = ["webhooks"], marker = "extra == 'telegram'", specifier = "==22.8" },
|
||||
{ name = "python-telegram-bot", extras = ["webhooks"], marker = "extra == 'termux'", specifier = "==22.8" },
|
||||
@@ -2707,6 +2727,7 @@ requires-dist = [
|
||||
{ name = "qrcode", marker = "extra == 'dingtalk'", specifier = "==7.4.2" },
|
||||
{ name = "qrcode", marker = "extra == 'feishu'", specifier = "==7.4.2" },
|
||||
{ name = "qrcode", marker = "extra == 'messaging'", specifier = "==7.4.2" },
|
||||
{ name = "reportlab", marker = "extra == 'documents'", specifier = "==5.1.0" },
|
||||
{ name = "requests", marker = "python_full_version >= '3.14'", specifier = "==2.34.2" },
|
||||
{ name = "resvg-py", marker = "python_full_version >= '3.14'", specifier = "==0.5.0" },
|
||||
{ name = "rich", marker = "python_full_version >= '3.14'", specifier = "==14.3.4" },
|
||||
@@ -2748,7 +2769,7 @@ requires-dist = [
|
||||
{ name = "winrt-windows-services-store", marker = "python_full_version >= '3.14' and sys_platform == 'win32'", specifier = ">=3.2.1,<4" },
|
||||
{ name = "youtube-transcript-api", marker = "extra == 'youtube'", specifier = "==1.2.4" },
|
||||
]
|
||||
provides-extras = ["uvloop", "anthropic", "exa", "firecrawl", "parallel-web", "ddgs", "fal", "edge-tts", "neutts", "kittentts", "piper", "modal", "daytona", "vercel", "google-meet", "messaging", "cron", "slack", "matrix", "wecom", "keychain", "tts-premium", "voice", "wake", "telegram", "discord", "stt-whisper", "audio-io", "silk", "wake-openwakeword", "wake-sherpa", "wake-porcupine", "google-chat", "doc-extract", "trace-upload", "vision", "pty", "mcp", "nemo-relay", "sms", "teams", "computer-use", "acp", "mistral", "otlp", "bedrock", "vertex", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"]
|
||||
provides-extras = ["uvloop", "anthropic", "exa", "firecrawl", "parallel-web", "ddgs", "fal", "edge-tts", "neutts", "kittentts", "piper", "modal", "daytona", "vercel", "google-meet", "messaging", "cron", "slack", "matrix", "wecom", "keychain", "tts-premium", "voice", "wake", "telegram", "discord", "stt-whisper", "audio-io", "silk", "wake-openwakeword", "wake-sherpa", "wake-porcupine", "google-chat", "doc-extract", "documents", "trace-upload", "vision", "pty", "mcp", "nemo-relay", "sms", "teams", "computer-use", "acp", "mistral", "otlp", "bedrock", "vertex", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
@@ -4330,6 +4351,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/a8/bb76c7356de8ad57f59d5ff993d434df0607f07f08bcc9c9a5c275e399c0/openai-2.54.0-py3-none-any.whl", hash = "sha256:89089789197ccdb87f173a03145ed1598d00795220c93e96cf712b1cbf5e5f2b", size = 1660351, upload-time = "2026-08-11T18:46:56.684Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openpyxl"
|
||||
version = "3.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "et-xmlfile" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.45.1"
|
||||
@@ -5324,6 +5357,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl", hash = "sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4", size = 126420, upload-time = "2026-09-20T20:59:04.025Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pypdf"
|
||||
version = "6.21.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/28/081f1cbcca7042441dafc8de00f48ba60853f89e5bff92c5f26477346d81/pypdf-6.21.2.tar.gz", hash = "sha256:0a3e1c5c886a1be03646c16a04827cc166c2a0575c971b26a4bf89686551c2d4", size = 7144571, upload-time = "2026-10-21T16:44:55.272Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/4d/c9d2d67f204eff463237a1a8f5e25a6923b7b1043982b91fcaf104c8ef60/pypdf-6.21.2-py3-none-any.whl", hash = "sha256:1d44d1a3a200f25062adff0085b02040e4646431ac2ccac22e42679e91466084", size = 407766, upload-time = "2026-10-21T16:44:53.626Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pypinyin"
|
||||
version = "0.55.0"
|
||||
@@ -5382,6 +5424,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-docx"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "lxml" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.4"
|
||||
@@ -5429,6 +5484,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/93/f6729f10149305262194774d6c8b438c0b084740cf239f48ab97b4df02fa/python_olm-3.2.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a5e68a2f4b5a2bfa5fdb5dbfa22396a551730df6c4a572235acaa96e997d3f", size = 297000, upload-time = "2023-11-28T19:25:31.045Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-pptx"
|
||||
version = "1.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "lxml" },
|
||||
{ name = "pillow" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "xlsxwriter" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload-time = "2024-08-07T17:33:37.772Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-socketio"
|
||||
version = "5.17.0"
|
||||
@@ -5706,6 +5776,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/01/aa/074e2cfb3d8101a6a764aba5f7c5d1e21de087483e35bdc0c4ce2eb60364/regex-2026.9.29-cp314-cp314t-win_arm64.whl", hash = "sha256:9173db3be74a35cb6731701094b98120f7ee4876a287882a59cdea1fa7da342f", size = 285954, upload-time = "2026-09-29T00:48:38.901Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reportlab"
|
||||
version = "5.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "pillow" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/22/c18474be40b9bbb52fdb7051177d4d25f1ea59fada17eb181026411e93b1/reportlab-5.1.0.tar.gz", hash = "sha256:6878200464527e87672d19e9f7cfc6bb890e3a6931e74ac6a1a8d1754aa55671", size = 4109612, upload-time = "2026-10-22T07:26:43.307Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/3c/bf68d44f160b44685fc714f746a954c71d0091b3dc4fd3c8670154b44841/reportlab-5.1.0-py3-none-any.whl", hash = "sha256:59fdbaec5efe437095ad1d7292ec9a271c8ca2d3ec75e889c6e61400929b90df", size = 2018647, upload-time = "2026-10-22T07:26:40.754Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.2"
|
||||
@@ -7468,6 +7551,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xlsxwriter"
|
||||
version = "3.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/f2/98b45e52bc0330256bd4bb039cc44b28f838ad85602ce4f358f355d501bb/xlsxwriter-3.3.0.tar.gz", hash = "sha256:0a6bf2e4dcec8b29dd9b86d1e622132134c48bb6fd10f925fdfb229f0447199a", size = 221380, upload-time = "2026-10-22T00:26:03.317Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/9d/3d30eaaa7e06bdb6172f938b2661d64f8d899bdca54120384ecf3759488a/xlsxwriter-3.3.0-py3-none-any.whl", hash = "sha256:5f04772d43281da5109af66f39b65153359a203e896847c416476e709b024469", size = 179548, upload-time = "2026-10-22T00:26:02.009Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.25.1"
|
||||
|
||||
Reference in new issue
Block a user