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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ uv.lock
.aider.tags.cache*
review-scratch/
*.py[cod]

# mkdocs build output
site/
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:** <https://accenture.github.io/mercury-python/> — 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.
Expand Down
20 changes: 20 additions & 0 deletions docs/css/extra.css
Original file line number Diff line number Diff line change
@@ -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;
}
128 changes: 128 additions & 0 deletions docs/guides/ai-agent-guide.md
Original file line number Diff line number Diff line change
@@ -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 |
94 changes: 94 additions & 0 deletions docs/guides/config-logging-actuators.md
Original file line number Diff line number Diff line change
@@ -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.
47 changes: 47 additions & 0 deletions docs/guides/configuration-reference.md
Original file line number Diff line number Diff line change
@@ -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 <path>`.
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
```
Loading
Loading