Skip to content

Commit 7a5076e

Browse files
committed
feat: Introduce new observability components, event subscribers, and system module registration with updated API documentation.
1 parent e259e2f commit 7a5076e

File tree

10 files changed

+135
-16
lines changed

10 files changed

+135
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ Built-in `system.*` modules that allow AI agents to query, monitor
483483

484484
---
485485

486+
[0.12.0]: https://github.com/aipartnerup/apcore-python/compare/v0.11.0...v0.12.0
486487
[0.11.0]: https://github.com/aipartnerup/apcore-python/compare/v0.10.0...v0.11.0
487488
[0.10.0]: https://github.com/aipartnerup/apcore-python/compare/v0.9.0...v0.10.0
488489
[0.9.0]: https://github.com/aipartnerup/apcore-python/compare/v0.8.0...v0.9.0

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,66 @@ Schema-driven module development framework for AI-perceivable interfaces.
2929

3030
## API Overview
3131

32+
**Core**
33+
3234
| Class | Description |
3335
|-------|-------------|
3436
| `APCore` | High-level client -- register modules, call, stream, validate |
3537
| `Registry` | Module storage -- discover, register, get, list, watch |
3638
| `Executor` | Execution engine -- call with middleware pipeline, ACL, approval |
3739
| `Context` | Request context -- trace ID, identity, call chain, cancel token |
3840
| `Config` | Configuration -- load from YAML, get/set values |
41+
| `Identity` | Caller identity -- id, type, roles, attributes |
42+
| `FunctionModule` | Wrapped function module created by `@module` decorator |
43+
44+
**Access Control & Approval**
45+
46+
| Class | Description |
47+
|-------|-------------|
3948
| `ACL` | Access control -- rule-based caller/target authorization |
49+
| `ApprovalHandler` | Pluggable approval gate protocol |
50+
| `AlwaysDenyHandler` / `AutoApproveHandler` / `CallbackApprovalHandler` | Built-in approval handlers |
51+
52+
**Middleware**
53+
54+
| Class | Description |
55+
|-------|-------------|
4056
| `Middleware` | Pipeline hooks -- before/after/on_error interception |
57+
| `BeforeMiddleware` / `AfterMiddleware` | Single-phase middleware adapters |
58+
| `LoggingMiddleware` | Structured logging middleware |
59+
| `RetryMiddleware` | Automatic retry with backoff |
60+
| `ErrorHistoryMiddleware` | Records errors into ErrorHistory |
61+
| `PlatformNotifyMiddleware` | Emits events on error rate/latency spikes |
62+
63+
**Schema**
64+
65+
| Class | Description |
66+
|-------|-------------|
67+
| `SchemaLoader` | Load schemas from YAML or native types |
68+
| `SchemaValidator` | Validate data against schemas |
69+
| `SchemaExporter` | Export schemas for MCP, OpenAI, Anthropic, generic |
70+
| `RefResolver` | Resolve `$ref` references in JSON Schema |
71+
72+
**Observability**
73+
74+
| Class | Description |
75+
|-------|-------------|
76+
| `TracingMiddleware` | Distributed tracing with span export |
77+
| `MetricsMiddleware` / `MetricsCollector` | Call count, latency, error rate metrics |
78+
| `ContextLogger` | Context-aware structured logging |
79+
| `ErrorHistory` | Ring buffer of recent errors with deduplication |
80+
| `UsageCollector` | Per-module usage statistics and trends |
81+
82+
**Events & Extensions**
83+
84+
| Class | Description |
85+
|-------|-------------|
4186
| `EventEmitter` | Event system -- subscribe, emit, flush |
87+
| `WebhookSubscriber` / `A2ASubscriber` | Built-in event subscribers |
88+
| `ExtensionManager` | Unified extension point management |
89+
| `AsyncTaskManager` | Background module execution with status tracking |
90+
| `CancelToken` | Cooperative cancellation token |
91+
| `BindingLoader` | Load modules from YAML binding files |
4292

4393
## Documentation
4494

src/apcore/__init__.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from __future__ import annotations
44

55
from collections.abc import AsyncIterator
6+
from importlib.metadata import PackageNotFoundError
7+
from importlib.metadata import version as _get_version
68
from typing import Any
79

810
# Core
@@ -91,10 +93,12 @@
9193
from apcore.middleware import (
9294
AfterMiddleware,
9395
BeforeMiddleware,
96+
ErrorHistoryMiddleware,
9497
LoggingMiddleware,
9598
Middleware,
9699
MiddlewareChainError,
97100
MiddlewareManager,
101+
PlatformNotifyMiddleware,
98102
RetryConfig,
99103
RetryMiddleware,
100104
)
@@ -137,6 +141,8 @@
137141
# Observability
138142
from apcore.observability import (
139143
ContextLogger,
144+
ErrorEntry,
145+
ErrorHistory,
140146
InMemoryExporter,
141147
MetricsCollector,
142148
MetricsMiddleware,
@@ -146,15 +152,25 @@
146152
SpanExporter,
147153
StdoutExporter,
148154
TracingMiddleware,
155+
UsageCollector,
156+
UsageMiddleware,
149157
create_span,
150158
)
151159

152160
# Events
153-
from apcore.events import ApCoreEvent, EventEmitter, EventSubscriber
161+
from apcore.events import A2ASubscriber, ApCoreEvent, EventEmitter, EventSubscriber, WebhookSubscriber
154162

155163
# Trace Context
156164
from apcore.trace_context import TraceContext, TraceParent
157165

166+
# System Modules
167+
from apcore.sys_modules.registration import (
168+
register_subscriber_type,
169+
register_sys_modules,
170+
reset_subscriber_registry,
171+
unregister_subscriber_type,
172+
)
173+
158174
# ---------------------------------------------------------------------------
159175
# Default client for simplified global access
160176
# ---------------------------------------------------------------------------
@@ -285,7 +301,10 @@ def enable(module_id: str, reason: str = "Enabled via APCore client") -> dict[st
285301
return _default_client.enable(module_id, reason)
286302

287303

288-
__version__ = "0.11.0"
304+
try:
305+
__version__ = _get_version("apcore")
306+
except PackageNotFoundError:
307+
__version__ = "unknown"
289308

290309
__all__ = [
291310
# Core
@@ -393,6 +412,8 @@ def enable(module_id: str, reason: str = "Enabled via APCore client") -> dict[st
393412
"MiddlewareChainError",
394413
"RetryConfig",
395414
"RetryMiddleware",
415+
"ErrorHistoryMiddleware",
416+
"PlatformNotifyMiddleware",
396417
# Decorators
397418
"module",
398419
"FunctionModule",
@@ -435,14 +456,25 @@ def enable(module_id: str, reason: str = "Enabled via APCore client") -> dict[st
435456
"OTLPExporter",
436457
"SpanExporter",
437458
"create_span",
459+
"ErrorEntry",
460+
"ErrorHistory",
461+
"UsageCollector",
462+
"UsageMiddleware",
438463
# Events
439464
"ApCoreEvent",
440465
"EventEmitter",
441466
"EventSubscriber",
467+
"WebhookSubscriber",
468+
"A2ASubscriber",
442469
# Trace Context
443470
"TraceContext",
444471
"TraceParent",
445472
# Version
446473
"VersionIncompatibleError",
447474
"negotiate_version",
475+
# System Modules
476+
"register_sys_modules",
477+
"register_subscriber_type",
478+
"unregister_subscriber_type",
479+
"reset_subscriber_registry",
448480
]

src/apcore/events/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
"""
77

88
from apcore.events.emitter import ApCoreEvent, EventEmitter, EventSubscriber
9+
from apcore.events.subscribers import A2ASubscriber, WebhookSubscriber
910

1011
__all__ = [
1112
"ApCoreEvent",
1213
"EventEmitter",
1314
"EventSubscriber",
15+
"A2ASubscriber",
16+
"WebhookSubscriber",
1417
]

src/apcore/executor.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,9 @@ def call(
375375
errors=_convert_validation_errors(e),
376376
) from e
377377

378-
ctx.data["_apcore.executor.redacted_output"] = redact_sensitive(output, module.output_schema.model_json_schema())
378+
ctx.data["_apcore.executor.redacted_output"] = redact_sensitive(
379+
output, module.output_schema.model_json_schema()
380+
)
379381

380382
# Step 10 -- Middleware After
381383
output = self._middleware_manager.execute_after(module_id, inputs, output, ctx)
@@ -497,18 +499,24 @@ def validate(
497499
try:
498500
preflight_warnings = module.preflight(inputs, ctx)
499501
if isinstance(preflight_warnings, list) and preflight_warnings:
500-
checks.append(PreflightCheckResult(
501-
check="module_preflight", passed=True, warnings=preflight_warnings,
502-
))
502+
checks.append(
503+
PreflightCheckResult(
504+
check="module_preflight",
505+
passed=True,
506+
warnings=preflight_warnings,
507+
)
508+
)
503509
else:
504510
checks.append(PreflightCheckResult(check="module_preflight", passed=True))
505511
except Exception as exc:
506512
# preflight() should not raise, but handle gracefully if it does
507-
checks.append(PreflightCheckResult(
508-
check="module_preflight",
509-
passed=True,
510-
warnings=[f"preflight() raised {type(exc).__name__}: {exc}"],
511-
))
513+
checks.append(
514+
PreflightCheckResult(
515+
check="module_preflight",
516+
passed=True,
517+
warnings=[f"preflight() raised {type(exc).__name__}: {exc}"],
518+
)
519+
)
512520

513521
valid = all(c.passed for c in checks)
514522
return PreflightResult(valid=valid, checks=checks, requires_approval=requires_approval)
@@ -873,7 +881,9 @@ async def call_async(
873881
errors=_convert_validation_errors(e),
874882
) from e
875883

876-
ctx.data["_apcore.executor.redacted_output"] = redact_sensitive(output, module.output_schema.model_json_schema())
884+
ctx.data["_apcore.executor.redacted_output"] = redact_sensitive(
885+
output, module.output_schema.model_json_schema()
886+
)
877887

878888
# Step 10 -- Middleware After (async-aware)
879889
output = await self._middleware_manager.execute_after_async(module_id, inputs, output, ctx)
@@ -997,7 +1007,9 @@ async def stream(
9971007
errors=_convert_validation_errors(e),
9981008
) from e
9991009

1000-
ctx.data["_apcore.executor.redacted_output"] = redact_sensitive(output, module.output_schema.model_json_schema())
1010+
ctx.data["_apcore.executor.redacted_output"] = redact_sensitive(
1011+
output, module.output_schema.model_json_schema()
1012+
)
10011013

10021014
# Step 10 -- Middleware After (async-aware)
10031015
output = await self._middleware_manager.execute_after_async(module_id, effective_inputs, output, ctx)

src/apcore/middleware/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
from apcore.middleware.adapters import AfterMiddleware, BeforeMiddleware
44
from apcore.middleware.base import Middleware
5+
from apcore.middleware.error_history import ErrorHistoryMiddleware
56
from apcore.middleware.logging import LoggingMiddleware
67
from apcore.middleware.manager import MiddlewareChainError, MiddlewareManager
8+
from apcore.middleware.platform_notify import PlatformNotifyMiddleware
79
from apcore.middleware.retry import RetryConfig, RetryMiddleware
810

911
__all__ = [
@@ -15,4 +17,6 @@
1517
"LoggingMiddleware",
1618
"RetryConfig",
1719
"RetryMiddleware",
20+
"ErrorHistoryMiddleware",
21+
"PlatformNotifyMiddleware",
1822
]

src/apcore/observability/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
TracingMiddleware,
2727
create_span,
2828
)
29+
from apcore.observability.usage import UsageCollector, UsageMiddleware
2930

3031
__all__ = [
3132
"ContextLogger",
@@ -40,5 +41,7 @@
4041
"SpanExporter",
4142
"StdoutExporter",
4243
"TracingMiddleware",
44+
"UsageCollector",
45+
"UsageMiddleware",
4346
"create_span",
4447
]

src/apcore/schema/strict.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,7 @@ def _strip_extensions(node: Any, *, strip_defaults: bool = True) -> None:
5858
return
5959

6060
keys_to_remove = [
61-
k
62-
for k in node
63-
if (isinstance(k, str) and k.startswith("x-")) or (strip_defaults and k == "default")
61+
k for k in node if (isinstance(k, str) and k.startswith("x-")) or (strip_defaults and k == "default")
6462
]
6563
for k in keys_to_remove:
6664
del node[k]

tests/observability/test_observability_package.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ def test_all_contains_all_public_names(self):
8282
"SpanExporter",
8383
"StdoutExporter",
8484
"TracingMiddleware",
85+
"UsageCollector",
86+
"UsageMiddleware",
8587
"create_span",
8688
}
8789
assert set(obs.__all__) == expected

tests/test_public_api.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,8 @@ class TestPublicAPIAll:
466466
"EventEmitter",
467467
"EventSubscriber",
468468
"ApCoreEvent",
469+
"WebhookSubscriber",
470+
"A2ASubscriber",
469471
"on",
470472
"off",
471473
# Toggle
@@ -477,6 +479,18 @@ class TestPublicAPIAll:
477479
# Additional errors
478480
"ModuleDisabledError",
479481
"ReloadFailedError",
482+
# Observability (added in 0.11.0, exported in 0.12.0)
483+
"ErrorEntry",
484+
"ErrorHistory",
485+
"ErrorHistoryMiddleware",
486+
"UsageCollector",
487+
"UsageMiddleware",
488+
"PlatformNotifyMiddleware",
489+
# System Modules
490+
"register_sys_modules",
491+
"register_subscriber_type",
492+
"unregister_subscriber_type",
493+
"reset_subscriber_registry",
480494
}
481495

482496
def test_all_contains_all_expected_names(self):

0 commit comments

Comments
 (0)