From bac3efee89fe0b6b0c34f01e91fbe036b1aa0baf Mon Sep 17 00:00:00 2001 From: Bernd Worsch Date: Thu, 26 Mar 2026 22:06:09 +0000 Subject: [PATCH] =?UTF-8?q?feat(activities):=20resolve=5Fcontext=20stub=20?= =?UTF-8?q?+=20evaluate=5Ftemplates=20=E2=80=94=20T15/T16?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit activities.py — resolve_context (T15): - dispatches on source.type: 'static' returns config["value"] - 'http_get' / 'db_query' raise ApplicationError(non_retryable=True) - unknown types raise ApplicationError(non_retryable=True) template_engine.py — evaluate_templates (T16, pure function): - evaluates optional condition expressions against context snapshot (restricted eval, no builtins) - interpolates {context..} placeholders via str.format_map - returns list[{task_type, params}] with falsy-condition rows omitted Co-Authored-By: Claude Sonnet 4.6 --- src/activity_core/activities.py | 30 +++++- src/activity_core/template_engine.py | 101 ++++++++++++++++++ .../custodian-WP-0001-temporal-backbone.md | 4 +- 3 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 src/activity_core/template_engine.py diff --git a/src/activity_core/activities.py b/src/activity_core/activities.py index a184373..c85ddba 100644 --- a/src/activity_core/activities.py +++ b/src/activity_core/activities.py @@ -83,11 +83,35 @@ async def load_activity_definition(activity_id: str) -> dict: @activity.defn async def resolve_context(context_sources: list[dict]) -> dict: - """Fetch and merge all context sources into a single snapshot dict. + """Resolve each context source and merge into a snapshot dict. - Implemented in T15. + Returns: {source.name: resolved_value, ...} + + Supported source types: + static — returns config["value"] directly + http_get — not yet implemented + db_query — not yet implemented """ - raise NotImplementedError("T15") + snapshot: dict = {} + for source in context_sources: + name = source["name"] + source_type = source["type"] + config = source.get("config", {}) + + if source_type == "static": + snapshot[name] = config.get("value") + elif source_type in ("http_get", "db_query"): + raise ApplicationError( + f"Context source type {source_type!r} is not yet implemented", + non_retryable=True, + ) + else: + raise ApplicationError( + f"Unknown context source type {source_type!r}", + non_retryable=True, + ) + + return snapshot @activity.defn diff --git a/src/activity_core/template_engine.py b/src/activity_core/template_engine.py new file mode 100644 index 0000000..b1d3b2c --- /dev/null +++ b/src/activity_core/template_engine.py @@ -0,0 +1,101 @@ +"""Pure-function template evaluation for activity-core. + +evaluate_templates() is called directly inside RunActivityWorkflow (not as +an activity) because it is purely deterministic and has no I/O. + +Template interpolation syntax: + params_template values may contain {context..} placeholders, + resolved against the context snapshot via Python's str.format_map. + + Example: + context_snapshot = {"stub": {"ping": "pong"}} + params_template = {"message": "hello {context.stub.ping}"} + result = {"message": "hello pong"} + +Condition syntax: + A Python expression string evaluated against {"context": context_snapshot}. + The task is included only when the expression is truthy. + Builtins are intentionally stripped — keep conditions simple comparisons. + + Example: "context['user']['is_active'] == True" +""" + +from __future__ import annotations + +import copy + + +class _DotAccessDict(dict): + """Dict subclass that allows attribute-style access for str.format_map. + + Enables {context.name.key} in format strings by making nested dicts + accessible as attributes. + """ + + def __getattr__(self, name: str) -> object: + try: + value = self[name] + except KeyError: + raise AttributeError(name) from None + if isinstance(value, dict): + return _DotAccessDict(value) + return value + + +def _interpolate(value: object, context_snapshot: dict) -> object: + """Recursively interpolate {context.*} placeholders in a params value.""" + if isinstance(value, str): + return value.format_map({"context": _DotAccessDict(context_snapshot)}) + if isinstance(value, dict): + return {k: _interpolate(v, context_snapshot) for k, v in value.items()} + if isinstance(value, list): + return [_interpolate(item, context_snapshot) for item in value] + return value + + +def _eval_condition(condition: str, context_snapshot: dict) -> bool: + """Evaluate a condition expression against the context snapshot. + + Raises: + ValueError: if the expression raises or returns a non-bool-able value. + """ + try: + result = eval( # noqa: S307 + condition, + {"__builtins__": {}}, + {"context": context_snapshot}, + ) + except Exception as exc: + raise ValueError(f"Condition {condition!r} raised: {exc}") from exc + return bool(result) + + +def evaluate_templates( + task_templates: list[dict], + context_snapshot: dict, +) -> list[dict]: + """Expand task templates against a context snapshot. + + Args: + task_templates: list of TaskTemplate dicts (task_type, condition, + params_template). + context_snapshot: merged context dict produced by resolve_context. + + Returns: + List of concrete task specs: [{"task_type": str, "params": dict}, ...] + Templates whose condition evaluates falsy are omitted. + """ + results: list[dict] = [] + + for template in task_templates: + condition = template.get("condition") + if condition is not None and not _eval_condition(condition, context_snapshot): + continue + + params = _interpolate( + copy.deepcopy(template.get("params_template", {})), + context_snapshot, + ) + results.append({"task_type": template["task_type"], "params": params}) + + return results diff --git a/workplans/custodian-WP-0001-temporal-backbone.md b/workplans/custodian-WP-0001-temporal-backbone.md index 2d829dc..a883dfe 100644 --- a/workplans/custodian-WP-0001-temporal-backbone.md +++ b/workplans/custodian-WP-0001-temporal-backbone.md @@ -64,11 +64,11 @@ tasks: state_hub_task_id: b05f046f-a6ba-4d96-a298-a0bbea067427 - id: T15 title: Implement resolve_context activity (stub) - status: todo + status: done state_hub_task_id: 2417912f-845d-489a-ace4-fb9280d3b679 - id: T16 title: Implement evaluate_templates (pure function) - status: todo + status: done state_hub_task_id: b7decbb6-ad2b-4fa5-8efc-05a7eb435d76 - id: T17 title: Implement log_run activity