diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a2d8062 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: ci + +# The wrapper's quality gates (pytest, ruff, basedpyright) plus the strict +# documentation build, then gh-pages publication on push to main - the same +# verify/deploy split as the engine repo's docs workflow. +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' # the supported floor + - name: Install package with dev extras + run: pip install -e '.[dev]' basedpyright + - name: Lint (ruff) + run: ruff check . + - name: Type check (basedpyright) + run: basedpyright + - name: Unit tests + run: pytest -q + + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Install MkDocs + run: pip install mkdocs-material + - name: Build docs (strict) + run: mkdocs build --strict + + deploy-docs: + # Publish to gh-pages only on push to main or manual dispatch, after both gates pass. + if: github.event_name != 'pull_request' + needs: [test, docs] + runs-on: ubuntu-latest + permissions: + contents: write + concurrency: + group: docs-deploy + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Install MkDocs + run: pip install mkdocs-material + - name: Build (strict) and deploy to gh-pages + run: mkdocs gh-deploy --force --strict diff --git a/.gitignore b/.gitignore index 626ebb2..4fff2c6 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ uv.lock .aider.tags.cache* review-scratch/ *.py[cod] + +# mkdocs build output +site/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 9853b47..469d91a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## 0.1.0 (unreleased) +- Documentation site (mkdocs-material, the engine repo's theme): the three-layer theme + reference, rationale/design foundations, function-writing patterns, flow and + knowledge-graph join chapters, a one-page AI agent guide with llms.txt, and + configuration/HTTP references - published to + https://accenture.github.io/mercury-python/ by the new CI workflow, which also runs + the three quality gates (pytest, ruff, basedpyright) on every push and pull request. - Host polish for engine parity: `GET /` serves the engines' minimal index page linking the actuator endpoints (embedded - no static file service by design); actuator JSON responses are pretty-printed (the engines' default-serializer presentation); unknown diff --git a/README.md b/README.md index eaaf710..325ac6f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ Orchestration deliberately stays in the engines. Functions written here are addr route name through the engines' declarative `yaml.event.over.http` map, so a flow or a graph task calls a Python function exactly as if it were local. +**Documentation:** — including the +[AI Agent Guide](https://accenture.github.io/mercury-python/guides/ai-agent-guide/) +for deterministic function generation. + > **Status: pre-release.** This repository was repurposed in August 2026 for the polyglot > initiative. The legacy Mercury language-pack implementation remains available in the git > history. diff --git a/docs/css/extra.css b/docs/css/extra.css new file mode 100644 index 0000000..c8f470c --- /dev/null +++ b/docs/css/extra.css @@ -0,0 +1,20 @@ +/* Mercury Composable docs — small overrides on the Material theme. + * + * Reference tables should wrap long inline-code tokens (package and type + * names like org.platformlambda.core.annotations) instead of forcing the + * whole table to scroll horizontally. + */ + +.md-typeset table:not([class]) td, +.md-typeset table:not([class]) th { + vertical-align: top; +} + +/* Break only genuinely long tokens, and only as a last resort. Unlike + * word-break / overflow-wrap:anywhere, this does NOT shrink a column below + * its longest word — short words like "String" are never split mid-word. */ +.md-typeset table:not([class]) td code, +.md-typeset table:not([class]) th code { + overflow-wrap: break-word; + white-space: normal; +} diff --git a/docs/guides/ai-agent-guide.md b/docs/guides/ai-agent-guide.md new file mode 100644 index 0000000..42ec0ae --- /dev/null +++ b/docs/guides/ai-agent-guide.md @@ -0,0 +1,128 @@ +--- +title: AI Agent Guide +summary: The complete authoring grammar for Python polyglot functions on one page - + contract, registration, config keys, endpoints and error rules, for deterministic generation. +audience: [ai-agent] +keywords: [ai agent, grammar, contract, deterministic, preload, postoffice] +--- + +# AI Agent Guide + +**Purpose: generate a correct Python polyglot function from this page alone.** +Humans: the narrative versions live in [Function Writing Patterns](function-patterns.md) +and the [join chapters](join-event-script.md). Orchestration (flows, graphs) is +authored on the engine — use the +[engine AI guides](https://accenture.github.io/mercury-composable/guides/ai-developer-guide/). + +## Pre-write checklist + +1. Route name: lowercase `[a-z0-9._-]`, at least one period. Example: `order.enrich`. +2. Handler style: wraps blocking library (`requests`, NumPy, DB driver) → plain `def`; + asyncio I/O or calls sibling functions → `async def`. +3. Function is stateless. State belongs to the calling flow/graph. +4. Orchestration-shaped logic (sequencing, retries, branching) → STOP; author an + Event Script flow or MiniGraph graph on the engine instead. + +## The contract + +```python +from mercury_composable import AppException, Body, PostOffice, annotate_trace, \ + get_logger, get_trace, platform, preload + +log = get_logger(__name__) + +@preload(route="order.enrich", instances=10) # private=True -> in-app only +def handler(headers: dict[str, str], body: Body): # or: async def + # 1. validate; intentional errors = AppException(status, message) + if not isinstance(body, dict) or not isinstance(body.get("id"), str): + raise AppException(400, "missing 'id'") + # 2. work (blocking is safe in plain def - executor thread) + # 3. optional telemetry + annotate_trace("source", "python") # rides back on the reply + # 4. return the reply body (or an EventEnvelope for status/header control) + return {"id": body["id"], "enriched": True} + +if __name__ == "__main__": + platform.run() +``` + +Rules: + +- `headers: dict[str, str]`; `body: Body` = `None|bool|int|float|str|bytes|list|dict`. +- Return value = reply body. Return `EventEnvelope` only when setting status/headers. +- `raise AppException(status, message)` → envelope status + message (portable error). + Unexpected exception → 500 + message + stack. Never return HTTP-shaped dicts. +- `get_trace()` → `TraceInfo(trace_id, trace_path, cid)` or `None`. +- Reserved inbound header `my_correlation_id` = the caller's business correlation id + (read-only). Never send headers named `my_*` or `x-event-api`. + +## Composition (calling other functions) + +```python +# async handler: +reply = await PostOffice().request("other.route", body={...}, timeout_ms=5000) +# plain-def handler (sync bridge; blocks only this worker thread): +reply = PostOffice().request_sync("other.route", body={...}, timeout_ms=5000) +# drop-n-forget twins: send / send_sync -> 202 ack envelope +# remote peer or engine: +async with PostOffice(endpoint="http://host:8085/api/event") as po: ... +``` + +- `request_sync` on the event loop → RuntimeError (use `await request()`). +- `request_sync` outside a hosted function → RuntimeError (use `asyncio.run(...)`). +- Always check `reply.get_status()`; errors are envelopes, not exceptions. +- Local calls reach `private=True` routes; the wire cannot (403). + +## Run + configure + +```bash +mercury-serve app.py # config: resources/application.yml +mercury-serve app.py -Dkey=value # runtime override (engine syntax) +``` + +Well-known keys (full table: [Configuration Reference](configuration-reference.md)): +`application.name`, `rest.server.port` (default 8085), `log.format` +(text|json|compact), `log.level`, `info.app.version`, `info.app.description`, +`show.env.variables`, `show.application.properties`, +`mandatory.health.dependencies`, `optional.health.dependencies`. + +## Health check function (engine interface contract) + +```python +@preload(route="my.health", instances=5, private=True) +async def health(headers: dict[str, str], _body: Body): + if headers.get("type") == "info": + return {"service": "my.dependency", "href": "http://backend"} + return "my.dependency is running fine" # non-200 reply marks it DOWN +``` + +List the route in `mandatory.health.dependencies` (or `optional.…`). + +## HTTP surface (served by the host, no code needed) + +`POST /api/event` (envelope wire) · `GET /` `/info` `/info/routes` `/env` `/health` +`/livenessprobe`. Shapes: [HTTP Surface Reference](http-surface-reference.md). + +## Engine-side wiring (for completeness; authored on the engine) + +```yaml +# application.properties: yaml.event.over.http=classpath:/event-over-http.yaml +event.http: + - route: 'order.enrich' + target: 'http://python-host:8086/api/event' +``` + +Flow task `process: 'order.enrich'` or graph node +`{"skill": "graph.task", "task": "order.enrich", ...}` (engines ≥ v4.11.11 for +graph.task). Details: [Join an Event Script Flow](join-event-script.md) · +[Join a Knowledge Graph](join-knowledge-graph.md). + +## DO / DON'T + +| DO | DON'T | +|----|-------| +| plain `def` for blocking libraries | block inside `async def` | +| `AppException` for intentional errors | return `{"status": 400, ...}` dicts | +| keep functions stateless | cache business state in module globals | +| compose one or two leaf helpers | re-implement flows/retries in Python | +| let deadlines fail fast (408 envelope) | swallow timeouts and hoard work | diff --git a/docs/guides/config-logging-actuators.md b/docs/guides/config-logging-actuators.md new file mode 100644 index 0000000..0c5bec0 --- /dev/null +++ b/docs/guides/config-logging-actuators.md @@ -0,0 +1,94 @@ +--- +title: Configuration, Logging & Actuators +summary: The engines' operational conventions in Python - resources folder, -D overrides, + three log formats, actuator endpoints and Kubernetes probes. +audience: [developer, operator] +keywords: [configuration, resources, log format, actuator, health, kubernetes, livenessprobe] +--- + +# Configuration, Logging & Actuators + +*Write functions: run them the way engine apps run.* + +> **At a glance** +> +> - **What** — one configuration style, one log presentation, one operational surface +> across Java, Rust, Python and Node.js apps. +> - **For** developers wiring an app and operators monitoring a polyglot estate. + +## Configuration — the engines' conventions + +Configuration lives in the `resources` folder (`resources/application.yml`, `.yaml` +or `.properties`), in the working directory or next to the application file. Values +support `${ENV_VAR:default}` substitution; `-Dkey=value` command-line arguments are +runtime overrides checked first on every read — the same syntax as the Java engine's +JVM system properties and the Rust port's `-D` arguments: + +```bash +mercury-serve app.py -Drest.server.port=8090 -Dlog.format=compact +``` + +See the worked sample +[`examples/resources/application.yml`](https://github.com/Accenture/mercury-python/blob/main/examples/resources/application.yml) +and the full key table in the [Configuration Reference](configuration-reference.md). + +## Logging — one aggregation, three presentations + +Log lines follow the Java reference engine's pattern, so a polyglot installation reads +one way in the aggregator: + +```text +2026-08-24 10:15:30.123 INFO my_app:42 - Loaded PUBLIC hello.python, instances=10 +``` + +`log.format` carries the engines' three presentations: `text` (default), `json` +(pretty-printed) and `compact` (single-line JSONL for log aggregators). The level +comes from the `LOG_LEVEL` environment variable when set, else `log.level`. + +## Actuators — the engines' operational surface + +The host serves the engines' endpoints on the same port as `/api/event`: + +| Endpoint | Purpose | +|----------|---------| +| `GET /` | minimal index page linking the endpoints below | +| `GET /info` | app identity, runtime, origin id, start time, uptime | +| `GET /info/routes` | registered routes split by visibility, with instance counts | +| `GET /env` | selected environment variables and configuration parameters (opt-in lists) | +| `GET /health` | dependency health checks — `UP` (HTTP 200) or `DOWN` (HTTP 400) | +| `GET /livenessprobe` | `OK` while the last health outcome was good, else HTTP 400 | + +JSON responses are pretty-printed (the engines' default-serializer presentation) with +`application/json; charset=utf-8`; unknown paths answer the engines' error shape — +see the [HTTP Surface Reference](http-surface-reference.md). + +### Health check functions — the engines' interface contract + +A health check is a normal registered function (usually private) listed in +`mandatory.health.dependencies` / `optional.health.dependencies`. The actuator calls +it through the event bus, first with header `type=info` (an advisory identity map +merged into its dependency entry), then with `type=health` (a status text or map; a +non-200 reply marks the dependency down): + +```python +@preload(route="demo.health", instances=5, private=True) +async def health_check(headers: dict[str, str], _body: Body): + if headers.get("type") == "info": + return {"service": "demo.service", "href": "http://127.0.0.1"} + return "demo.service is running fine" +``` + +Optional dependencies never change the overall status; mandatory ones decide +`UP`/`DOWN`, and the most recent outcome drives `/livenessprobe`. + +## Kubernetes wiring + +```yaml +livenessProbe: + httpGet: { path: /livenessprobe, port: 8086 } +readinessProbe: + httpGet: { path: /health, port: 8086 } +``` + +The pod presents exactly like an engine pod — one dashboard shape for the whole +polyglot estate. diff --git a/docs/guides/configuration-reference.md b/docs/guides/configuration-reference.md new file mode 100644 index 0000000..9696daa --- /dev/null +++ b/docs/guides/configuration-reference.md @@ -0,0 +1,47 @@ +--- +title: Configuration Reference +summary: Every well-known configuration key, the resolution order, and the substitution syntax. +audience: [developer, operator, ai-agent] +keywords: [configuration, reference, keys, substitution, overrides] +--- + +# Configuration Reference + +*Reference: the complete key table.* + +## Resolution + +1. `-Dkey=value` command-line overrides (and programmatic `AppConfig.set`) — checked + first on every read, the engines' `f:setConfig` analog. +2. The configuration file: `resources/application.yml` | `.yaml` | `.properties`, in + the working directory or next to the application file, or `--config `. +3. `${ENV_VAR:default}` substitution inside values: environment first, then a base + configuration key of that name, then the default. + +## Well-known keys (shared with the engines) + +| Key | Meaning | Default | +|-----|---------|---------| +| `application.name` | application identity in logs, `/info` and `/health` | `application` | +| `rest.server.port` | Event API + actuator port | `8085` | +| `log.format` | `text`, `json` (pretty-printed) or `compact` (single-line JSONL) | `text` | +| `log.level` | log level; the `LOG_LEVEL` environment variable wins | `INFO` | +| `info.app.version` | version reported by `/info` | package version | +| `info.app.description` | description reported by `/info` | `application.name` | +| `show.env.variables` | opt-in list of environment variables shown by `/env` | (empty) | +| `show.application.properties` | opt-in list of configuration keys shown by `/env` | (empty) | +| `mandatory.health.dependencies` | routes of health check functions that decide `/health` | (empty) | +| `optional.health.dependencies` | health check routes reported but never affecting status | (empty) | + +List-valued keys accept a comma/space-separated string (engine syntax) or a YAML list. + +## Programmatic access + +```python +from mercury_composable import app_config + +config = app_config() +port = config.get("rest.server.port", 8085) +name = config.get_property("application.name", "application") +config.set("feature.flag", "on") # runtime override, checked first +``` diff --git a/docs/guides/design.md b/docs/guides/design.md new file mode 100644 index 0000000..b220294 --- /dev/null +++ b/docs/guides/design.md @@ -0,0 +1,92 @@ +--- +title: Design +summary: The wrapper anatomy - five small components, each mirroring an engine convention, + and the minimalist rulings that keep the package a leaf. +audience: [architect, developer] +keywords: [design, event bus, anycast, postoffice, actuator, envelope, wire format] +--- + +# Design — The Wrapper Anatomy + +*Foundations: what is inside, and why each piece earns its place.* + +> **At a glance** +> +> - **What** — the five components of the function host and the design rulings behind +> them: faithful `instances`/`private`, an anycast event bus, fail-fast deadlines, +> and engine-identical operations. +> - **For** developers and reviewers who want the mental model before the API. + +## Anatomy + +```mermaid +flowchart TB + subgraph Host [Python function host] + direction TB + A["/api/event
(Event API host)"] --> B[EventBus
per-route FIFO mailboxes] + C[PostOffice
local mode] --> B + B --> W1[worker 1..N] --> F["@preload function"] + D[Actuator
/info /health ...] -.probes via bus.-> B + end + E[Engine or peer] -- envelope bytes --> A + F -- PostOffice remote mode --> X[another host or engine] +``` + +Five components, one dispatch pipeline: + +| Component | Engine convention it mirrors | +|-----------|------------------------------| +| **Envelope codec** (`EventEnvelope`) | the [standard wire format](https://accenture.github.io/mercury-composable/guides/event-envelope-wire-format/), verified against golden vectors shared with both engines | +| **Event API host** (`POST /api/event`) | the engines' `event.api.service` semantics: `x-ttl` bounds execution, `x-async` is drop-n-forget, handler errors ride HTTP 200 inside the envelope | +| **Primitive event bus** | the engines' in-memory bus semantics: per-route FIFO, `instances` worker tasks, deliver (RPC) and publish (drop-n-forget) — nothing else | +| **PostOffice** | the engines' `po.request`/`po.send` vocabulary — remote to any peer's `/api/event`, local through the same bus | +| **Actuator + index page** | the engines' operational surface: `/`, `/info`, `/info/routes`, `/env`, `/health`, `/livenessprobe`, pretty JSON, the same error signature | + +## The rulings, and why + +**`instances` and `private` are faithful, not decorative.** Each route has one FIFO +mailbox consumed by exactly `instances` worker tasks — the parameter really is the +concurrency limit, as in the engines. `private=True` means what it means there too: +callable in-app through PostOffice, while the wire answers 403. A developer who reads +the engine documentation forms expectations this host meets. + +**The bus is an anycast work queue, deliberately hand-built.** Each delivery goes to +exactly one of N workers and waits its FIFO turn while all are busy. That contract is a +work queue, not a broadcast — which is why the implementation is a small mailbox on +asyncio primitives rather than a pub/sub construct. Two operations only: + +- `deliver` — RPC bounded by the caller's ttl; a queued call whose caller already + timed out is skipped, never wastefully executed (the dead-work check). +- `publish` — drop-n-forget, acknowledged with the engines' 202 shape. + +**No spill tier, no queue cap, fail fast by deadline.** Back-pressure belongs to the +tier that owns recovery — the engines' flows and graphs ([Rationale](rationale.md)). +A breach produces the standard `408` envelope (`Timeout for N ms`), identical to an +engine timeout, so flows handle both the same way. + +**Blocking code cannot hurt the host.** Plain `def` handlers run in a thread-pool +executor with trace context carried across; the event loop that serves every other +route is never blocked. This is the Python analog of the Java engine's virtual +threads: *write sequential blocking-style code; the platform makes it safe.* + +**Sync functions compose through a bridge, not a second API.** +`PostOffice.request_sync()`/`send_sync()` submit the same coroutines onto the host +loop while blocking only the handler's own worker thread — trace chain unbroken, +identical envelope shaping. Misuse teaches: calling the bridge on the event loop, or +outside a hosted function, raises a descriptive error instead of deadlocking. + +**In-memory only.** In-flight events die with the process, exactly like the engines' +own in-memory bus; at-least-once behavior comes from flow-level retries, not from a +leaf journal. + +## The scope fence + +The package intentionally contains **no flows, no graphs, no persistence and no +pub/sub broadcast**. What it carries is deliberately minimal: functions, the primitive +bus, the thin client, and the engine-consistent utilities (configuration, logging, +trace). Divergence from an engine convention is treated as a bug, not a style choice. + +## Where to go next + +[Function Writing Patterns](function-patterns.md) turns this anatomy into day-to-day +code. diff --git a/docs/guides/function-patterns.md b/docs/guides/function-patterns.md new file mode 100644 index 0000000..ce970fd --- /dev/null +++ b/docs/guides/function-patterns.md @@ -0,0 +1,153 @@ +--- +title: Function Writing Patterns +summary: The coding patterns for externalized functions - the handler contract, sync vs + async, errors, trace context, private functions and composition. +audience: [developer, ai-agent] +keywords: [preload, handler, sync, async, AppException, trace, private, request_sync] +--- + +# Function Writing Patterns + +*Write functions: the day-to-day patterns, with the reasons attached.* + +> **At a glance** +> +> - **What** — the `(headers, body)` contract, both handler styles, the portable error +> contract, trace context, private functions, and composition through PostOffice. +> - **Rule of thumb** — wrapping a blocking library → plain `def`; composing functions +> or async I/O → `async def`. + +## The contract + +A function is a handler registered under a route name: + +```python +from mercury_composable import Body, preload + +@preload(route="my.function", instances=10) +def handler(headers: dict[str, str], body: Body): + return {"ok": True} +``` + +- **Input** — the same two-part input as an engine `TypedLambdaFunction`: + `headers: dict[str, str]` and `body: Body` (any MsgPack value: `None | bool | int | + float | str | bytes | list | dict`). +- **Output** — return the reply body, or an `EventEnvelope` for full control of status + and reply headers. +- **Route names** — lowercase letters, digits, period, hyphen, underscore, with at + least one period (`hello.python`, not `HelloPython`). +- **Statelessness** — anything a handler must keep belongs to the caller's flow model + or graph state machine, never to module globals. + +## Sync or async — both are first-class + +Python has two library ecosystems, and a polyglot function must be able to wrap +either: + +=== "plain def — the blocking world" + + ```python + import requests # or NumPy, pandas, an ML runtime, a DB driver + + @preload(route="quote.fetch", instances=10) + def fetch_quote(_headers: dict[str, str], body: Body): + assert isinstance(body, dict) + response = requests.get(body["url"], timeout=5) # blocking is SAFE here + return {"status": response.status_code, "text": response.text[:200]} + ``` + + Plain `def` handlers run in a thread-pool executor, so a blocking call can never + stall the event loop that hosts every other function. This is the Python analog of + the Java engine's virtual threads. + +=== "async def — the asyncio world" + + ```python + from mercury_composable import PostOffice + + @preload(route="hello.chain", instances=10) + async def chain(_headers: dict[str, str], body: Body): + reply = await PostOffice().request("demo.suffix.helper", body=body, + timeout_ms=5000) + return reply.body + ``` + + `async def` handlers run on the event loop — the natural fit for asyncio-native + I/O and for composing sibling functions. + +Detection is automatic (`inspect.iscoroutinefunction`); trace context, `instances`, +envelopes and telemetry behave identically in both styles. + +## Errors — one portable contract + +Raise `AppException(status, message)` for intentional errors: + +```python +from mercury_composable import AppException + +raise AppException(400, "missing 'text'") +``` + +On the wire this becomes a normal envelope with status 400 and the message as body — +the flow's exception handler or the graph's `error.*` contract receives it exactly as +it would from an engine function. An unexpected exception becomes status 500 with the +message and a stack trace, mirroring the engines. Handler-level errors always ride +HTTP 200; only transport-level failures (unknown route, private target, timeout, +undecodable envelope) surface as HTTP status codes. + +## Trace context + +Every delivery runs under its caller's trace: + +```python +from mercury_composable import annotate_trace, get_trace + +info = get_trace() # trace_id, trace_path, cid - or None +annotate_trace("model", "v3") # rides back on the reply envelope +``` + +Outside a hosted function (batch jobs, tests), establish context explicitly: + +```python +from mercury_composable import trace_context + +with trace_context("trace-1", "BATCH /nightly", cid="order-42"): + reply = await po.request("my.function", body={...}) +``` + +## Private functions and composition + +`private=True` marks a function callable **in-app only** — the HTTP host answers 403 +for it, while a local `PostOffice` (no endpoint) reaches it through the bus: + +```python +@preload(route="demo.suffix.helper", instances=10, private=True) +async def suffix_helper(_headers: dict[str, str], body: Body): ... + +# async composition +reply = await PostOffice().request("demo.suffix.helper", body=body, timeout_ms=5000) + +# sync composition (from a plain-def handler): blocks this worker thread only +reply = PostOffice().request_sync("demo.suffix.helper", body=body, timeout_ms=5000) +``` + +The sync bridge refuses misuse with teaching errors: on the event loop it says +*await request() instead*; outside a hosted function it points at +`asyncio.run(po.request(...))`. The trace chain rides across the bridge unbroken. + +!!! warning "Composition is for leaf-side helpers" + A public function calling a private formatter is healthy. A function that + sequences three other functions with retries is a flow wearing a disguise — + write it as Event Script or a graph instead ([Rationale](rationale.md)). + +## Calling remote peers + +The same PostOffice, given an endpoint, calls any engine or peer host with the +engines' relay contract (octet-stream envelope, `x-ttl`, trace headers): + +```python +async with PostOffice(endpoint="http://peer:8085/api/event") as po: + reply = await po.request("hello.node", body={"text": "hi"}, timeout_ms=5000) +``` + +The reply envelope is authoritative in every mode: inspect `reply.get_status()`. diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md new file mode 100644 index 0000000..d324803 --- /dev/null +++ b/docs/guides/getting-started.md @@ -0,0 +1,126 @@ +--- +title: Getting Started +summary: A running Python function in five minutes - hosted, probed, and called from a + Mercury engine flow. +audience: [developer] +keywords: [quick start, preload, mercury-serve, event over http] +--- + +# Getting Started + +*Guide: from zero to a Python function an engine can orchestrate.* + +> **At a glance** +> +> - **What** — install the package, write one function, serve it, call it — first with +> `curl`, then from a real engine flow. +> - **Time** — about five minutes. + +## 1. Install + +```bash +git clone https://github.com/Accenture/mercury-python.git +cd mercury-python +pip install -e '.[dev]' +``` + +*(Pre-release: the package installs from source until the PyPI release.)* + +## 2. Write a function + +A function is a plain handler registered under a **route name** — the only address the +rest of the system will ever know it by. + +```python +# app.py +from mercury_composable import AppException, Body, platform, preload + +@preload(route="hello.python", instances=10) +def handle_event(headers: dict[str, str], body: Body): + if not isinstance(body, dict) or not isinstance(body.get("text"), str): + raise AppException(400, "missing 'text'") + return {"text": body["text"].upper(), "language": "python"} + +if __name__ == "__main__": + platform.run() +``` + +Plain `def` is fine — blocking code (a `requests` call, a NumPy computation) runs in a +thread pool and can never stall the host. `async def` works too. The +[Function Writing Patterns](function-patterns.md) guide covers when to use which. + +## 3. Configure (the engines' convention) + +```yaml +# resources/application.yml +application.name: 'hello-app' +rest.server.port: 8086 +``` + +Configuration lives in a `resources` folder, exactly like the engines, and any key can +be overridden at run time with the engines' `-D` syntax. + +## 4. Serve it + +```bash +mercury-serve app.py +``` + +```text +2026-08-24 10:15:30.123 INFO mercury.server:124 - Loaded PUBLIC hello.python, instances=10 +2026-08-24 10:15:30.124 INFO mercury.server:126 - hello-app - Event API service started on port 8086 +``` + +Open — the host serves the engines' familiar index page, and +the same actuator endpoints (`/info`, `/health`, `/livenessprobe`, …) your operations +team already monitors on engine apps. + +## 5. Call it from an engine + +One declarative entry in the engine application tells it where the route lives — +`application.properties`: + +```properties +yaml.event.over.http=classpath:/event-over-http.yaml +``` + +`event-over-http.yaml`: + +```yaml +event.http: + - route: 'hello.python' + target: 'http://127.0.0.1:8086/api/event' +``` + +Any Event Script task or MiniGraph `graph.task` node that names `hello.python` now +executes your Python function — trace context, correlation id and error contract +carried end to end. [Join an Event Script Flow](join-event-script.md) walks through a +complete flow; [Join a Knowledge Graph](join-knowledge-graph.md) does the same for a +graph. + +## 6. Or just curl it + +The host speaks the engines' Event API protocol (envelope bytes over +`POST /api/event`), so the natural ad-hoc client is the package itself: + +```python +import asyncio +from mercury_composable import PostOffice + +async def main(): + async with PostOffice(endpoint="http://127.0.0.1:8086/api/event") as po: + reply = await po.request("hello.python", body={"text": "polyglot"}, timeout_ms=5000) + print(reply.get_status(), reply.body) + +asyncio.run(main()) +``` + +```text +200 {'text': 'POLYGLOT', 'language': 'python'} +``` + +## Next + +- The **why**: [Rationale — Externalized Functions](rationale.md) +- The **how, in depth**: [Function Writing Patterns](function-patterns.md) +- The **wiring**: [Join an Event Script Flow](join-event-script.md) diff --git a/docs/guides/http-surface-reference.md b/docs/guides/http-surface-reference.md new file mode 100644 index 0000000..b08cff8 --- /dev/null +++ b/docs/guides/http-surface-reference.md @@ -0,0 +1,62 @@ +--- +title: HTTP Surface Reference +summary: The exact protocol and response shapes of the function host - Event API, actuators, + error signature and content types. +audience: [developer, operator, ai-agent] +keywords: [http, reference, event api, actuator, error shape, content type] +--- + +# HTTP Surface Reference + +*Reference: every byte the host serves.* + +## POST /api/event — the Event API (envelope wire) + +Mirrors the engines' `event.api.service`: + +| Aspect | Behavior | +|--------|----------| +| Request body | event envelope bytes ([standard wire format](https://accenture.github.io/mercury-composable/guides/event-envelope-wire-format/)) | +| `x-ttl` header | execution bound in ms (floor 1000) | +| `x-async: true` | drop-n-forget → HTTP 202 with ack envelope `{type: async, delivered: true, time}` | +| Reply | always envelope bytes, `content-type: application/octet-stream` | +| Handler outcome | rides **HTTP 200** with the status inside the envelope (including AppException and unexpected errors) | +| Transport failures | set the HTTP status too: 400 undecodable / missing route field, 403 private target, 404 unknown route (`Route X not found`), 408 timeout (`Timeout for N ms`) | +| Header hygiene | inbound `x-event-api` and `my_*` removed; the `my_cid` tag becomes the read-only `my_correlation_id` header | + +## Actuator endpoints + +All JSON responses are pretty-printed with `content-type: application/json; +charset=utf-8` (the engines' default-serializer presentation). + +| Endpoint | Content type | Shape | +|----------|--------------|-------| +| `GET /` | `text/html` | minimal index page linking the endpoints | +| `GET /info` | JSON | `{app{name,version,description}, runtime{language,python,mercury_composable}, origin, time{start,current}, up_time}` | +| `GET /info/routes` | JSON | `{app, routing{public{route: instances}, private{...}}}` | +| `GET /env` | JSON | `{app, env{environment{...}, properties{...}}}` (opt-in lists) | +| `GET /health` | JSON | `{dependency[...], status: UP\|DOWN, origin, name}` — HTTP 200 when UP, 400 when DOWN | +| `GET /livenessprobe` | `text/plain` | `OK`, or HTTP 400 `Unhealthy. Please check '/health' endpoint.` | + +Each `/health` dependency entry: `{route, required, ...info-map, status_code, +message}` — the info map comes from the function's `type=info` reply; a missing route +reports `status_code: 404` with `Please check - Route X not found`. + +- `origin` — unique instance id, minted once per process: UTC `yyyyMMdd` + 32-hex + uuid (the Java reference engine's format). +- `up_time` — the engines' rendering (`59 seconds`, `1 minute 1 second`, …). + +## Error signature (host-level) + +Unknown paths and non-GET methods on known paths answer the engines' shape — +pretty-printed JSON: + +```json +{ + "status": 404, + "message": "Resource not found", + "type": "error" +} +``` + +Handler-level errors never use this shape — they ride the envelope on `/api/event`. diff --git a/docs/guides/join-event-script.md b/docs/guides/join-event-script.md new file mode 100644 index 0000000..1e47149 --- /dev/null +++ b/docs/guides/join-event-script.md @@ -0,0 +1,120 @@ +--- +title: Join an Event Script Flow +summary: Wire a Python function into a flow - the declarative map, the task that names it, + data mapping from the function's seat, and the exception path end to end. +audience: [developer] +keywords: [event script, flow, yaml.event.over.http, declarative, task, exception handler] +--- + +# Join an Event Script Flow + +*Join the engines: your function as a first-class flow task.* + +> **At a glance** +> +> - **What** — one YAML entry on the engine makes a route remote; the flow itself does +> not change at all. +> - **Worked demo** — the engine's `composable-example` ships this exact wiring and +> has executed the Python demo function unchanged. + +## The one moving part: the declarative map + +On the engine application, enable the map and point the route at your host — +`application.properties`: + +```properties +yaml.event.over.http=classpath:/event-over-http.yaml +``` + +`event-over-http.yaml`: + +```yaml +event.http: + - route: 'hello.declarative' + target: 'http://${peer.demo.host:127.0.0.1}:${peer.demo.port}/api/event' + # optional security headers, e.g. an authorization token the host or a + # gateway validates: + # headers: + # authorization: '${DEMO_PEER_TOKEN:demo}' +``` + +That is the entire integration surface. Every Event Script task (and MiniGraph +`graph.task`) that names `hello.declarative` now calls your Python host. The full map +grammar lives in the engine's +[Event over HTTP guide](https://accenture.github.io/mercury-composable/guides/event-over-http/). + +## The flow does not know, and must not care + +This is the engine's shipped demo flow — note that nothing in it says "remote" or +"python": + +```yaml +flow: + id: 'event-over-http-declarative' + description: 'Demonstrate Event-over-Http protocol using declarative means' + ttl: 10s + exception: 'v1.hello.exception' + +first.task: 'event-over-http-declarative' + +tasks: + - name: 'event-over-http-declarative' + input: + - 'input.header -> header' + - 'input.body -> *' + process: 'hello.declarative' + output: + - 'text(application/json) -> output.header.content-type' + - 'result -> output.body' + execution: end +``` + +Register the route on the Python side and the flow executes it: + +```python +@preload(route="hello.declarative", instances=10) +async def declarative_echo(headers: dict[str, str], body: Body): + return {"body": body, "headers": headers, "language": "python"} +``` + +## What your function sees + +```mermaid +sequenceDiagram + participant C as REST client + participant E as Engine (flow) + participant H as Python host + participant F as hello.declarative + C->>E: GET /api/event/http/declarative + E->>H: POST /api/event (envelope bytes, x-ttl, trace headers) + H->>F: (headers, body) on the bus + F-->>H: reply body + H-->>E: reply envelope (status, exec_time, annotations) + E-->>C: flow output mapping +``` + +- **Headers** — the task's `input.header -> header` mapping arrives as your + `headers` dict. Reserved keys are cleaned at ingress, and the flow's business + correlation id arrives as the read-only `my_correlation_id` header. +- **Body** — whatever the task's input mapping sends (`input.body -> *` passes the + whole body through). +- **Trace** — the engine's trace id and path ride the wire; `get_trace()` sees them, + and `annotate_trace()` entries return on the reply envelope into the engine's + telemetry. + +## Errors flow into the flow + +- `AppException(400, "missing 'text'")` in Python → a 400 envelope → the flow's + `exception:` task fires with `error.code=400` and `error.message` exactly as for an + engine function. +- An unexpected Python exception → 500 with message and stack. +- The flow's `ttl` bounds the call: on breach the engine receives the standard 408, + and the exception path decides what happens next — retries and compensation stay in + the flow, never in Python ([Rationale](rationale.md)). + +## Checklist + +1. Host running and healthy (`/livenessprobe` → `OK`). +2. Route registered (`/info/routes` lists it as `public`). +3. Engine `application.properties` names the map; the map names the route and target. +4. The flow task's `process:` names the route. Nothing else changes. diff --git a/docs/guides/join-knowledge-graph.md b/docs/guides/join-knowledge-graph.md new file mode 100644 index 0000000..0e96663 --- /dev/null +++ b/docs/guides/join-knowledge-graph.md @@ -0,0 +1,83 @@ +--- +title: Join a Knowledge Graph +summary: Wire a Python function into a MiniGraph model - graph.task to a declarative target, + deadlines, and the error.* contract from the function's seat. +audience: [developer, architect] +keywords: [minigraph, knowledge graph, graph.task, declarative target, error contract, x-ttl] +--- + +# Join a Knowledge Graph + +*Join the engines: your function as a graph task in an Active Knowledge Graph.* + +> **At a glance** +> +> - **What** — a `graph.task` node names your route; the same declarative map from the +> [flow chapter](join-event-script.md) points it at your host. +> - **Requires** — engine **v4.11.11 or later** (the release that taught the +> deployed-graph guard about the declarative map). + +## The graph names the route — nothing more + +In MiniGraph, behavior lives on nodes as **skills**. The `graph.task` skill invokes a +composable function by route name — and since v4.11.11 that route may resolve through +`yaml.event.over.http` to a Python host. A minimal node: + +```json +{ + "types": ["Task"], + "alias": "python-step", + "properties": { + "skill": "graph.task", + "task": "hello.python", + "input": ["input.body -> *"], + "output": ["result -> output.body"] + } +} +``` + +The graph model carries no URL and no language — deployment stays in the engine's +configuration, exactly as the composable theme demands. Everything about authoring +graphs (nodes, skills, the compile gate, the Playground) is the engine's domain: +start at [Build your first graph](https://accenture.github.io/mercury-composable/guides/knowledge-graph/build-your-first-graph/) +and the [built-in skills reference](https://accenture.github.io/mercury-composable/guides/knowledge-graph/skills-reference/). + +## Deadlines: two distinct clocks + +- The graph's **ttl** bounds the *event call* to your function — on breach the engine + sees the standard 408 envelope. +- If your function itself calls onward over HTTP, that client call carries its own + timeout — the same decoupling the engines document for `async.http.request` + (`headers.x-ttl` in milliseconds). + +Design functions so their own outbound work respects the deadline they were given: +read the caller's intent from your ttl-bounded world and fail fast rather than +overstay ([Design](design.md)). + +## Errors: the graph's error.* contract + +When your function raises `AppException(code, message)` — or fails unexpectedly with +a 500 — the walker stages the graph's generic error context: +`error.source` (the failing node), `error.code`, `error.message`, and `error.stack` +when one traveled. A graph can therefore route around a Python failure with the same +IF/THEN patterns it uses for any other task — see the engine's +[knowledge-graph guides](https://accenture.github.io/mercury-composable/guides/knowledge-graph/) +for the retry and recovery idioms. + +## Worked demo + +The quickest end-to-end proof mirrors the engines' own conformance test: a graph with +one `graph.task` node whose `task` names a route served by the Python demo app +(`hello.python` in [Getting Started](getting-started.md)), with the declarative map on +the engine pointing at `http://127.0.0.1:8086/api/event`. Deploy the graph (or dry-run +it in the Playground), `POST /api/graph/{graph-id}`, and the reply body is your +function's output — trace id visible in the Python host's log line. + +## Checklist + +1. Engine at v4.11.11+ with `yaml.event.over.http` configured. +2. The map entry's route == the node's `task` value == your `@preload` route. +3. The graph passes the compile gate and is listed in the manifest (deployed lane) — + "compiled or 404" is the engine's rule. +4. Your host is healthy; `/info/routes` lists the route as `public` (a private route + answers 403 on the wire by design). diff --git a/docs/guides/rationale.md b/docs/guides/rationale.md new file mode 100644 index 0000000..0a8a124 --- /dev/null +++ b/docs/guides/rationale.md @@ -0,0 +1,90 @@ +--- +title: Rationale +summary: Why externalized functions exist - the composable theme extended across language + boundaries, and the boundaries that keep it honest. +audience: [architect, developer] +keywords: [rationale, polyglot, event over http, orchestration, distributed monolith] +--- + +# Rationale — Externalized Functions + +*Foundations: the thinking before the how.* + +> **At a glance** +> +> - **What** — why Python functions join Mercury flows and graphs over Event-over-HTTP, +> and why the wrapper refuses to become a second orchestrator. +> - **For** architects deciding where a piece of logic should live. + +## The composable theme does not stop at the JVM boundary + +Mercury's core promise is that **functions are self-contained and coupled only by +route names and event envelopes**. A flow that reads + +```yaml +process: 'hello.python' +``` + +does not know — and must not care — whether that route resolves to a Java method on a +virtual thread, a Rust function, or a Python handler in another process. The route +name is the contract; everything else is deployment detail. + +Externalized functions take that promise literally. The +[Event-over-HTTP seam](https://accenture.github.io/mercury-composable/guides/event-over-http/) +already existed for engine-to-engine calls: the same event envelope, serialized in a +language-neutral wire format, carried over HTTP. A Python function host is simply +another peer speaking that protocol — nothing new crosses the wire. + +## Why you would externalize a function + +**Ecosystem gravity.** Python's strength is its libraries: `requests`-style +integrations, NumPy/pandas, scikit-learn and the ML inference stacks, domain SDKs. +Porting that logic into Java rarely adds value; wrapping it as a route does. The +function stays where its ecosystem lives; the orchestration stays where the +architecture lives. + +**Team reality.** Enterprise installations are polyglot for the long haul. A data +science team ships Python; the platform team runs engine applications. Route names are +the only interface the two must agree on — a one-line YAML entry, not a shared +codebase. + +**Independent lifecycles.** A Python host deploys, scales and restarts on its own +cadence as an ordinary Kubernetes pod — with the engines' own actuator surface, so +operations sees one shape everywhere. + +## Why the wrapper owns nothing but functions + +The tempting mistake is to grow the wrapper into a mini-engine — local flows, retries, +queues, persistence. Mercury's architecture deliberately says no: + +- **Orchestration lives in the engines.** Event Script and MiniGraph carry the retry + logic, branching, exception routing and state. A leaf that re-implements recovery + fights the tier that owns it. +- **Back-pressure belongs to the tier that owns recovery.** The host's internal event + bus has *no spill tier and no queue cap*: a call that cannot be served by its + deadline fails fast with the standard 408 envelope, and the engine's flow decides + what happens next. A leaf that hoards work merely hides congestion from the layer + designed to handle it. +- **Sync-over-async across services is an anti-pattern by default.** The engines' own + documentation warns that superimposing synchronous coupling on distributed parts + builds a *distributed monolith*. The wrapper inherits that judgment: local + composition is for simple leaf-side helpers; workflow processing belongs in flows + and graphs. + +The result is a package small enough to audit in an afternoon — an envelope codec, a +host, a primitive bus, a thin client, and the engines' operational conventions — with +the entire orchestration brain kept where it already is. + +## When *not* to externalize + +- **Orchestration-shaped logic** — sequencing, branching, compensation: write a flow + or a graph, not a Python function that calls other functions in a loop. +- **Latency-critical hot paths** — an in-engine function call is an in-memory event; + an externalized one is an HTTP round trip. Microseconds versus milliseconds. +- **Logic that is really data mapping** — Event Script's data mapping and MiniGraph's + built-in skills often eliminate the function entirely. + +## Where to go next + +[Design — The Wrapper Anatomy](design.md) shows how these principles become the five +small components of the package. diff --git a/docs/guides/testing.md b/docs/guides/testing.md new file mode 100644 index 0000000..c3e012d --- /dev/null +++ b/docs/guides/testing.md @@ -0,0 +1,96 @@ +--- +title: Testing Your Functions +summary: The test harness patterns this package uses on itself - registry fixtures, real-HTTP + host tests, and the golden wire-format vectors. +audience: [developer] +keywords: [testing, pytest, pytest-asyncio, fixtures, golden vectors, wire format] +--- + +# Testing Your Functions + +*Write functions: test them the way this package tests itself.* + +> **At a glance** +> +> - **What** — three proven layers: direct handler tests, in-process bus tests, and +> real-HTTP host tests; plus the golden vectors that pin wire compatibility. + +## Layer 1 — the handler is just a function + +The cheapest test needs no framework at all: + +```python +def test_uppercase_contract(): + reply = handle_event({}, {"text": "polyglot"}) + assert reply == {"text": "POLYGLOT", "language": "python"} +``` + +## Layer 2 — through the bus, with a fresh registry + +Register into a **fresh** `FunctionRegistry` per test (never the default one) and +close its bus on teardown — the pattern from this package's own `tests/test_bus.py`: + +```python +import pytest_asyncio +from collections.abc import AsyncIterator +from mercury_composable import FunctionRegistry, PostOffice, trace_context + +@pytest_asyncio.fixture +async def registry() -> AsyncIterator[FunctionRegistry]: + fresh = FunctionRegistry() + yield fresh + await fresh.bus.close() + +async def test_trace_rides_through(registry: FunctionRegistry): + registry.register("my.function", handler) + po = PostOffice(registry=registry) + with trace_context("trace-1", "TEST /unit", cid="cid-1"): + reply = await po.request("my.function", body={"text": "x"}, timeout_ms=5000) + assert reply.get_status() == 200 +``` + +This exercises `instances`, private routes, deadlines (assert the 408 envelope) and +trace propagation exactly as production will. + +## Layer 3 — over real HTTP + +Boot the host on an ephemeral port and speak the actual protocol +(`tests/test_server.py` pattern): + +```python +from aiohttp import web +from mercury_composable.server import EventApiServer + +server = EventApiServer(registry) +runner = web.AppRunner(server.create_app()) +await runner.setup() +site = web.TCPSite(runner, "127.0.0.1", 0) +await site.start() +port = runner.addresses[0][1] +# ... aiohttp client posts envelope bytes to /api/event, or PostOffice(endpoint=...) +await runner.cleanup() +``` + +Use this layer to pin transport behavior: 403 for private routes, 404 messages, the +`x-async` 202 acknowledgement, reserved-header hygiene, actuator shapes. + +## Wire compatibility — the golden vectors + +The codec is verified against the **golden conformance vectors shared with the Java +and Rust engines** (`tests/vectors/vectors.json`). If you extend envelope handling, +run the vector suite — it is the cross-language contract: + +```bash +pytest -q tests/test_envelope.py +``` + +## The project gates + +```bash +pytest -q # all tests +ruff check . # lint +basedpyright # types (tests included) +``` + +The CI workflow runs all three plus a strict documentation build on every push and +pull request. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..5ad12be --- /dev/null +++ b/docs/index.md @@ -0,0 +1,74 @@ +--- +title: Composable for Python +summary: Externalized functions for Mercury Composable - write decoupled functions in Python + and let the Java/Rust engines orchestrate them from Event Script flows and knowledge graphs. +audience: [developer, architect, ai-agent] +keywords: [polyglot, python, event over http, composable, minigraph, externalized functions] +--- + +# Externalized functions for Mercury Composable + +*Write the function in Python. Let the engine orchestrate it.* + +> **At a glance** +> +> - **What** — a lightweight Event-over-HTTP **function host and client**: your Python +> functions become routes a [Mercury Composable](https://accenture.github.io/mercury-composable/) +> engine calls from Event Script flows and MiniGraph knowledge graphs, exactly as if +> they were local. +> - **For** teams whose business logic lives in Python — `requests`-style integrations, +> NumPy/pandas, ML inference - inside a composable, event-driven architecture. +> - **Not** an orchestrator. Flows, graphs, retries and state live in the engines; +> this package deliberately provides functions only. + +## The main theme, in one screen + +Mercury Composable builds applications from **self-contained functions wired by +configuration** — functions never call each other directly; they couple only through +route names and event envelopes. The platform ascends three layers, each documented in +the [engine documentation](https://accenture.github.io/mercury-composable/): + +| Layer | Idea | Authority | +|-------|------|-----------| +| **1 — Event-driven** | Functions + route names + `EventEnvelope` over an in-memory event bus | [Event-driven Foundation](https://accenture.github.io/mercury-composable/guides/event-driven/) | +| **2 — Composable** | **Event Script**: YAML flows choreograph functions — orchestration is configuration, not code | [Composable Orchestration](https://accenture.github.io/mercury-composable/guides/event-script/) | +| **3 — Knowledge Graph** | **MiniGraph**: an Active Knowledge Graph *is* the application — graphs execute behavior through skills | [Knowledge Graph](https://accenture.github.io/mercury-composable/guides/knowledge-graph/) | + +This site documents the **fourth seat at that table**: functions that live *outside* +the engine — in a Python process — yet participate in layers 2 and 3 as first-class +tasks. The seam is [Event over HTTP](https://accenture.github.io/mercury-composable/guides/event-over-http/): +the same event envelope, carried over HTTP, addressed by the same route names. + +```mermaid +flowchart LR + subgraph Engine [Java or Rust engine] + F[Event Script flow] --> R[route: hello.python] + G[MiniGraph graph.task] --> R + end + R -- "Event-over-HTTP
(one envelope, MsgPack)" --> H + subgraph Host [Python function host] + H[POST /api/event] --> B[event bus] --> P["@preload('hello.python')"] + end +``` + +An engine flow or graph names the route `hello.python`; a one-entry declarative map +points that route at your Python host; your function runs with trace context carried +end to end. **No engine code changes. No orchestration in Python.** + +## Where to go next + +- **New here?** [Getting Started](guides/getting-started.md) — a running function in + five minutes, called from an engine flow. +- **Why this design?** [Rationale](guides/rationale.md) and + [Design](guides/design.md) — the thinking before the how. +- **Writing functions?** [Function Writing Patterns](guides/function-patterns.md). +- **Wiring the engine?** [Join an Event Script Flow](guides/join-event-script.md) · + [Join a Knowledge Graph](guides/join-knowledge-graph.md). +- **You are an AI agent?** Start at the [AI Agent Guide](guides/ai-agent-guide.md) — + the complete authoring grammar on one page — and the machine index + [llms.txt](llms.txt). + +!!! note "Engine versions" + Event Script flows call declarative Event-over-HTTP targets on any 4.x engine. + **MiniGraph `graph.task` targets require engine v4.11.11 or later** (the release + that taught the deployed-graph guard about the declarative map). diff --git a/docs/llms.txt b/docs/llms.txt new file mode 100644 index 0000000..7b7140a --- /dev/null +++ b/docs/llms.txt @@ -0,0 +1,33 @@ +# Composable for Python (mercury-python) + +> A lightweight Event-over-HTTP function host and client: write decoupled functions in +> Python and let Mercury Composable engines (Java, Rust) orchestrate them from Event +> Script flows and MiniGraph knowledge graphs. Orchestration stays in the engines; this +> package provides functions plus the engines' operational conventions (configuration, +> logging, trace, actuators). The engines' documentation lives at +> https://accenture.github.io/mercury-composable/ (its llms.txt maps the three layers). + +> This file is the machine-readable map of this documentation site. + +## Start here +- [Home](https://accenture.github.io/mercury-python/): the three-layer theme and where a Python function sits (a leaf peer over Event-over-HTTP). +- [AI Agent Guide](https://accenture.github.io/mercury-python/guides/ai-agent-guide/): **start here if you are an AI agent** — the complete authoring grammar on one page: contract, registration, composition, config keys, error rules. +- [Getting Started](https://accenture.github.io/mercury-python/guides/getting-started/): a running function in five minutes, called from an engine flow. + +## Foundations +- [Rationale](https://accenture.github.io/mercury-python/guides/rationale/): why externalized functions — ecosystem gravity, the Event-over-HTTP seam, why the wrapper owns nothing but functions. +- [Design](https://accenture.github.io/mercury-python/guides/design/): the anatomy — envelope codec, Event API host, primitive anycast bus (faithful instances/private), PostOffice, actuators — and the minimalist rulings. + +## Write functions +- [Function Writing Patterns](https://accenture.github.io/mercury-python/guides/function-patterns/): the (headers, body) contract, plain-def vs async-def, AppException, trace context, private functions, composition incl. the sync bridge. +- [Configuration, Logging & Actuators](https://accenture.github.io/mercury-python/guides/config-logging-actuators/): resources convention, -D overrides, three log formats, actuator endpoints, health-function contract, Kubernetes probes. +- [Testing Your Functions](https://accenture.github.io/mercury-python/guides/testing/): handler tests, fresh-registry bus tests, real-HTTP host tests, golden wire vectors. + +## Join the engines +- [Join an Event Script Flow](https://accenture.github.io/mercury-python/guides/join-event-script/): yaml.event.over.http, the flow task, what the function sees, the exception path. +- [Join a Knowledge Graph](https://accenture.github.io/mercury-python/guides/join-knowledge-graph/): graph.task to a declarative target (engines >= v4.11.11), deadlines, the error.* contract. + +## Reference +- [Configuration Reference](https://accenture.github.io/mercury-python/guides/configuration-reference/): every well-known key, resolution order, substitution syntax. +- [HTTP Surface Reference](https://accenture.github.io/mercury-python/guides/http-surface-reference/): /api/event protocol, actuator shapes, error signature, content types. +- [Release Notes](https://github.com/Accenture/mercury-python/blob/main/CHANGELOG.md) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..24b46cd --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,126 @@ +site_name: Composable for Python +site_description: >- + Mercury Composable polyglot functions for Python — a lightweight + Event-over-HTTP function host and client: write functions here, let the + Java/Rust engines orchestrate them from Event Script flows and MiniGraph + knowledge graphs. +site_url: https://accenture.github.io/mercury-python/ +docs_dir: docs + +repo_name: Accenture/mercury-python +repo_url: https://github.com/Accenture/mercury-python +edit_uri: edit/main/docs/ + +copyright: >- + Apache-2.0 · a Mercury Composable polyglot wrapper — engines: + Java and + Rust. + +theme: + name: material + icon: + repo: fontawesome/brands/github + logo: material/language-python + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: deep purple + accent: cyan + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: deep purple + accent: cyan + toggle: + icon: material/brightness-4 + name: Switch to system preference + features: + - navigation.tabs + - navigation.sections + - navigation.top + - navigation.footer + - navigation.indexes + - toc.follow + - search.suggest + - search.highlight + - content.code.copy + - content.tabs.link + +markdown_extensions: + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets: + base_path: ["."] + check_paths: true + # NOTE: pymdownx.emoji is deliberately NOT enabled (engine convention) - + # colon-delimited technical patterns would render as emoji. + +plugins: + - search: + lang: en + separator: '[\s\-\.]+' + +# Machine-readable artifacts linked from the AI docs, not part of the nav. +not_in_nav: | + llms.txt + +validation: + nav: + omitted_files: info + links: + not_found: warn + absolute_links: ignore + +extra_css: + - css/extra.css + +nav: + - Home: index.md + + - Orientation: + - Getting Started: guides/getting-started.md + + - Foundations: + - Rationale — Externalized Functions: guides/rationale.md + - Design — The Wrapper Anatomy: guides/design.md + + - Write functions: + - Function Writing Patterns: guides/function-patterns.md + - Configuration, Logging & Actuators: guides/config-logging-actuators.md + - Testing Your Functions: guides/testing.md + + - Join the engines: + - Join an Event Script Flow: guides/join-event-script.md + - Join a Knowledge Graph: guides/join-knowledge-graph.md + + - AI: + - AI Agent Guide: guides/ai-agent-guide.md + + - Reference: + - Configuration Reference: guides/configuration-reference.md + - HTTP Surface Reference: guides/http-surface-reference.md + - Release Notes: https://github.com/Accenture/mercury-python/blob/main/CHANGELOG.md + - Contributing: https://github.com/Accenture/mercury-python/blob/main/CONTRIBUTING.md