Module Interface Definition¶
Canonical Definition - This document is the authoritative definition of the Module interface
Module is the core interface of apcore, and all modules must implement this interface.
1. Interface Overview¶
from abc import ABC, abstractmethod
from typing import Any, ClassVar, Type
from pydantic import BaseModel
class Module(ABC):
"""apcore module base class"""
# ============ Required Definitions ============
# Input Schema (required)
input_schema: ClassVar[Type[BaseModel]]
# Output Schema (required)
output_schema: ClassVar[Type[BaseModel]]
# Short description (required, ≤200 characters)
# From docstring or explicit definition
description: ClassVar[str]
# Detailed documentation (optional, ≤5000 characters, supports Markdown)
documentation: ClassVar[str | None] = None
@abstractmethod
def execute(self, inputs: dict[str, Any], context: "Context") -> dict[str, Any]:
"""
Execute module logic (must be implemented)
Args:
inputs: Input parameters (validated against input_schema)
context: Execution context
Returns:
Output result (validated against output_schema)
Raises:
ModuleError: Module execution error
"""
...
# ============ Optional Definitions ============
# Module name (optional, generated from class name by default)
name: ClassVar[str | None] = None
# Module tags (optional)
tags: ClassVar[list[str]] = []
# Module version (optional)
version: ClassVar[str] = "1.0.0"
# Behavior annotations (optional, helps AI make invocation decisions)
annotations: ClassVar["ModuleAnnotations | None"] = None
# Usage examples (optional, helps AI understand complex modules)
examples: ClassVar[list["ModuleExample"]] = []
# Extended metadata (optional, free-form dict)
metadata: ClassVar[dict[str, Any]] = {}
# ============ Lifecycle Hooks (optional) ============
def on_load(self) -> None:
"""Called when module is loaded"""
pass
def on_unload(self) -> None:
"""Called when module is unloaded"""
pass
# ============ Optional Methods ============
def validate(self, inputs: dict[str, Any]) -> "ValidationResult":
"""
Validate input only, without execution (optional implementation)
Args:
inputs: Input parameters
Returns:
Validation result
"""
...
# Note: Modules only need to define one execute() method, using either def or async def.
# The framework automatically detects and selects the appropriate invocation method (sync or async), no need to define separately.
2. Required Attributes¶
2.1 input_schema¶
Defines the structure of module input parameters.
from pydantic import BaseModel, Field
class SendEmailInput(BaseModel):
"""Input parameters for sending email"""
to: str = Field(
...,
description="Recipient email address",
pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$",
examples=["[email protected]"]
)
subject: str = Field(
...,
description="Email subject",
max_length=200
)
body: str = Field(
...,
description="Email body"
)
cc: list[str] = Field(
default=[],
description="CC list"
)
class SendEmailModule(Module):
input_schema = SendEmailInput # Must be defined
...
input_schema Requirements:
| Requirement | Description |
|---|---|
| Type | Must be a subclass of pydantic.BaseModel |
| Field descriptions | Every field must have a description |
| Constraint definitions | Recommended to define pattern, min/max constraints |
| Default values | Optional fields must have default values |
2.2 output_schema¶
Defines the structure of module output.
class SendEmailOutput(BaseModel):
"""Output result for sending email"""
success: bool = Field(
...,
description="Whether the email was sent successfully"
)
message_id: str | None = Field(
None,
description="Message ID (returned when successfully sent)"
)
error: str | None = Field(
None,
description="Error message (returned when sending failed)"
)
class SendEmailModule(Module):
output_schema = SendEmailOutput # Must be defined
...
output_schema Requirements:
| Requirement | Description |
|---|---|
| Type | Must be a subclass of pydantic.BaseModel |
| Field descriptions | Every field must have a description |
| Consistency | execute() return value must conform to this Schema |
2.3 description and documentation¶
Module functionality description, used by AI/LLM to understand the module.
apcore uses two fields to organize module documentation:
| Field | Required | Length Limit | Markdown | Purpose |
|---|---|---|---|---|
description |
Required | ≤200 characters | No | Short description of module functionality for AI quick matching and understanding |
documentation |
Optional | ≤5000 characters | Yes | Detailed documentation including usage scenarios, constraints, configuration requirements |
description Field (Required)¶
# Method 1: Via docstring (recommended)
class SendEmailModule(Module):
"""Send email to specified recipient. Uses SMTP protocol, non-idempotent operation, requires email server configuration."""
...
# Method 2: Explicit definition
class SendEmailModule(Module):
description = "Send email to specified recipient. Uses SMTP protocol, non-idempotent operation, requires email server configuration."
...
description Requirements:
| Requirement | Description |
|---|---|
| Must exist | Either docstring or explicit definition |
| Length limit | ≤200 characters (approximately 100 Chinese characters) |
| Content requirements | Describe "what it does" + "when to use" + "key features" |
| Clear and precise | AI/LLM needs to understand module functionality |
documentation Field (Optional)¶
class SendEmailModule(Module):
description = "Send email to specified recipient. Uses SMTP protocol, non-idempotent operation, requires email server configuration."
documentation = """
# Features
Send email via SMTP protocol, supports plain text and HTML format.
## Configuration Requirements
- SMTP server information must be configured in apcore.yaml
- Valid SMTP authentication credentials must be provided
## Usage Scenarios
- Send notification emails, verification codes, reports
## Limitations
- Gmail: 500 emails/day
- Attachment size: ≤25MB
"""
When to use documentation:
| Scenario | Need documentation |
|---|---|
| Simple modules (e.g., validation functions) | No - only description needed |
| Complex configuration requirements | Yes |
| Important usage constraints | Yes |
| Multiple usage scenarios | Yes |
Relationship with docstring¶
| Location | Purpose | Audience | Format |
|---|---|---|---|
description |
Quick understanding of module functionality | AI module discovery phase | Plain text, ≤200 characters |
documentation |
Detailed usage documentation | AI invocation decision phase | Markdown, ≤5000 characters |
| Python docstring | Code-level documentation | Developers, IDE | reStructuredText/Markdown |
2.4 execute()¶
The core execution logic of the module.
class SendEmailModule(Module):
input_schema = SendEmailInput
output_schema = SendEmailOutput
def execute(self, inputs: dict[str, Any], context: Context) -> dict[str, Any]:
"""
Execute email sending
Args:
inputs: Input parameters, validated against input_schema
context: Execution context, includes trace_id, caller_id, etc.
Returns:
Output result, must conform to output_schema
"""
# 1. Can directly use inputs (already validated)
to = inputs["to"]
subject = inputs["subject"]
body = inputs["body"]
# 2. Or convert to Pydantic object
params = self.input_schema(**inputs)
# 3. Execute business logic
try:
message_id = self._send_email(params.to, params.subject, params.body)
return {"success": True, "message_id": message_id, "error": None}
except Exception as e:
return {"success": False, "message_id": None, "error": str(e)}
def _send_email(self, to: str, subject: str, body: str) -> str:
"""Internal method: actually send email"""
# ... implementation ...
return "msg_123"
execute() Specifications:
| Specification | Description |
|---|---|
| Parameters | inputs: dict and context: Context |
| Return value | Must be dict, conforming to output_schema |
| Input validated | Framework has validated against input_schema, can use directly |
| Output validated | Return value validated against output_schema |
| Exception handling | Can throw ModuleError, framework handles uniformly |
2.5 Interface Contract Summary¶
| Interface | Level | Contract |
|---|---|---|
input_schema |
MUST | Must be defined, must be a valid Schema type |
output_schema |
MUST | Must be defined, must be a valid Schema type |
description |
MUST | Must be provided via docstring or explicit attribute, ≤200 characters |
documentation |
MAY | Optional; ≤5000 characters, supports Markdown |
execute() |
MUST | Must be implemented (def or async def), framework auto-detects sync/async |
validate() |
MAY | Optional implementation; should have no side effects when called |
on_load() / on_unload() |
MAY | Optional implementation; exceptions should not block other module loading |
name |
MAY | Optional; generated from class name by default |
tags |
MAY | Optional; empty list by default |
version |
MAY | Optional; defaults to "1.0.0", must conform to semver |
annotations |
MAY | Optional; all values use defaults by default |
examples |
MAY | Optional; inputs must conform to input_schema |
metadata |
MAY | Optional; empty dict by default |
Timeout Semantics:
- Module execution should complete within configured timeout (resources.timeout default 30000ms; global executor.timeout default 60000ms)
- After timeout, framework must throw MODULE_TIMEOUT error
- Module should support graceful cancellation (by checking cancellation signal in context)
Thread Safety Specifications:
- Module instances must support concurrent calls to execute() by multiple threads/coroutines
- Modules must not modify instance-level state (ClassVar attributes) in execute()
- If shared state is needed, must use thread-safe data structures
Return Value Constraints:
- execute() must return dict (or language-equivalent Map type)
- Return value must pass output_schema validation
- Return value must not include non-serializable objects (functions, connections, etc.)
3. Optional Attributes¶
3.1 name¶
class SendEmailModule(Module):
name = "Send Email" # Human-readable name
# If not defined, generated from class name by default:
# SendEmailModule → "Send Email Module"
3.2 tags¶
class SendEmailModule(Module):
tags = ["email", "notification", "communication"]
# Used for categorization and search
# registry.list(tags=["email"]) can filter
3.3 version¶
class SendEmailModule(Module):
version = "2.0.0"
# Used for version management
# Defaults to "1.0.0"
3.4 annotations¶
Canonical Definition - This section is the authoritative definition of ModuleAnnotations
Behavior annotations, help AI/LLM make invocation decisions.
from dataclasses import dataclass
@dataclass
class ModuleAnnotations:
"""Module behavior annotations"""
readonly: bool = False # Read-only, no side effects
destructive: bool = False # Has destructive operations
idempotent: bool = False # Idempotent, safe to call repeatedly
requires_approval: bool = False # Requires human confirmation
open_world: bool = True # Involves external systems
| Field | Default | Meaning | AI Behavior |
|---|---|---|---|
readonly |
False |
Does not modify any state | True → Safe to call |
destructive |
False |
May delete/overwrite data | True → Warn before calling |
idempotent |
False |
Repeated calls have no additional side effects | True → Safe to retry |
requires_approval |
False |
Requires human confirmation | True → Seek consent |
open_world |
True |
Connects to external systems | True → May be slow |
# Query module - read-only, safe
class GetUserModule(Module):
"""Query user information"""
annotations = ModuleAnnotations(readonly=True, idempotent=True, open_world=False)
# Send module - has side effects, connects to external system
class SendEmailModule(Module):
"""Send email"""
annotations = ModuleAnnotations(open_world=True)
# Delete module - destructive, requires confirmation
class DeleteUserModule(Module):
"""Delete user"""
annotations = ModuleAnnotations(destructive=True, requires_approval=True, open_world=False)
3.5 examples¶
Usage examples, help AI understand complex modules.
SHOULD recommendation: When a module's input_schema contains any of the following features, SHOULD provide at least one
ModuleExample: - UsesoneOf/anyOf(Union types) - More than 5 required fields (requiredarray length > 5) - Contains nestedobject(2+ levels)These features significantly increase the difficulty for AI to correctly construct inputs, examples can greatly improve AI invocation accuracy.
from dataclasses import dataclass
from typing import Any
@dataclass
class ModuleExample:
"""Module usage example"""
title: str # Example title
inputs: dict[str, Any] # Example input
output: dict[str, Any] | None = None # Example output (optional)
description: str | None = None # Description (optional)
class SendEmailModule(Module):
"""Send email"""
examples = [
ModuleExample(
title="Send plain text email",
description="Send plain text email via SMTP",
inputs={
"to": "[email protected]",
"subject": "Hello",
"body": "World"
},
output={
"success": True,
"message_id": "msg_123"
}
),
ModuleExample(
title="Send HTML email",
description="Send HTML format email to multiple recipients via SMTP",
inputs={
"to": ["[email protected]", "[email protected]"],
"subject": "Notification",
"html": "<h1>Hello</h1>",
"smtp_host": "smtp.example.com",
"smtp_port": 587
}
)
]
Mapping to AI Protocols:
| Protocol | Mapped Field |
|---|---|
| Anthropic | input_examples |
| A2A | AgentSkill.examples |
| MCP | Not directly supported, can be placed in _meta |
3.6 metadata¶
Free-form extended metadata, framework does not validate content.
class SendEmailModule(Module):
"""Send email"""
metadata = {
# Performance hints
"cost_per_call": 0.001,
"avg_latency_ms": 500,
# Data sensitivity
"data_sensitivity": ["PII"],
# Operations info
"owner": "email-team",
"documentation_url": "https://docs.example.com/send-email"
}
Usage Scenarios:
| Purpose | Example |
|---|---|
| Middleware reading | Rate limiting middleware reads metadata["rate_limit"] |
| Monitoring dashboard | Display metadata["owner"] |
| Documentation generation | Link to metadata["documentation_url"] |
| Other needs | When new standards emerge, test in metadata first |
4. Lifecycle Hooks¶
4.1 on_load()¶
class DatabaseModule(Module):
def on_load(self) -> None:
"""Called when module is loaded, used for resource initialization"""
self.connection = create_db_connection()
self.pool = create_connection_pool()
def on_unload(self) -> None:
"""Called when module is unloaded, used for resource cleanup"""
self.pool.close()
self.connection.close()
Lifecycle:
Registry.discover()
↓
Module class loading
↓
Module instantiation
↓
on_load() called ← Initialize resources
↓
[Module available]
↓
on_unload() called ← Cleanup resources (on application exit)
5. Optional Methods¶
5.1 validate()¶
Validate input only, without execution.
class SendEmailModule(Module):
def validate(self, inputs: dict[str, Any]) -> ValidationResult:
"""
Validate input parameters
Returns:
ValidationResult: Validation result
"""
errors = []
# Custom validation logic
if inputs.get("to", "").endswith("@blocked.com"):
errors.append({
"field": "to",
"code": "BLOCKED_DOMAIN",
"message": "This domain is blocked"
})
return ValidationResult(
valid=len(errors) == 0,
errors=errors
)
Purpose: - Pre-validate before execution - Provide more friendly error messages - Perform custom business validation
5.2 Async Execution¶
Modules only need to define one execute() method, either def or async def. The framework automatically detects the function type and selects the appropriate invocation method.
# Sync module
class SyncModule(Module):
def execute(self, inputs: dict[str, Any], context: Context) -> dict[str, Any]:
"""Synchronous execution"""
result = self._do_work(inputs)
return {"result": result}
# Async module
class AsyncModule(Module):
async def execute(self, inputs: dict[str, Any], context: Context) -> dict[str, Any]:
"""Asynchronous execution — framework auto-detects async def and uses async invocation"""
async with aiohttp.ClientSession() as session:
resp = await session.post("https://api.example.com", json=inputs)
data = await resp.json()
return {"result": data}
Rules:
- Modules do not need to define a separate execute_async() method
- Framework auto-detects via inspect.iscoroutinefunction() or language-equivalent mechanism
- Both Executor.call() and Executor.call_async() can correctly handle both types of modules
6. Complete Example¶
from typing import Any, ClassVar, Type
from pydantic import BaseModel, Field
from apcore import Module, Context, ModuleError, ModuleAnnotations, ModuleExample
# ============ Schema Definitions ============
class SendEmailInput(BaseModel):
"""Send email input"""
to: str = Field(..., description="Recipient email address", pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")
subject: str = Field(..., description="Email subject", max_length=200)
body: str = Field(..., description="Email body")
cc: list[str] = Field(default=[], description="CC list")
html: bool = Field(default=False, description="Whether HTML format")
class SendEmailOutput(BaseModel):
"""Send email output"""
success: bool = Field(..., description="Whether sending was successful")
message_id: str | None = Field(None, description="Message ID")
error: str | None = Field(None, description="Error message")
# ============ Module Definition ============
class SendEmailModule(Module):
"""Send email module (Python docstring, for developers to read)
This is the Python docstring, for developers to view in IDE.
Can include more detailed technical implementation notes, source code comments, etc.
"""
# Required definitions
input_schema: ClassVar[Type[BaseModel]] = SendEmailInput
output_schema: ClassVar[Type[BaseModel]] = SendEmailOutput
description: ClassVar[str] = "Send email to specified recipient. Uses SMTP protocol, non-idempotent operation, requires email server configuration."
# Optional: detailed documentation
documentation: ClassVar[str] = """
# Features
Send email via SMTP protocol, supports plain text and HTML format.
## Configuration Requirements
- SMTP server information must be configured in apcore.yaml
- Valid SMTP authentication credentials must be provided
## Usage Scenarios
- Send notification emails, verification codes, reports
## Limitations
- Gmail: 500 emails/day
- Attachment size: ≤25MB
"""
# Optional definitions
name: ClassVar[str] = "Send Email"
tags: ClassVar[list[str]] = ["email", "notification"]
version: ClassVar[str] = "1.0.0"
# Behavior annotations
annotations = ModuleAnnotations(
readonly=False, # Has side effects (sends email)
destructive=False, # Non-destructive
idempotent=False, # Non-idempotent (each call sends one email)
requires_approval=False,
open_world=True # Connects to external SMTP/API
)
# Usage examples
examples = [
ModuleExample(
title="Send plain text email",
inputs={
"to": "[email protected]",
"subject": "Hello",
"body": "World"
},
output={"success": True, "message_id": "msg_123", "error": None}
),
ModuleExample(
title="Send HTML email",
inputs={
"to": "[email protected]",
"subject": "Welcome",
"body": "<h1>Hello</h1>",
"html": True
},
output={"success": True, "message_id": "msg_456", "error": None}
)
]
# Extended metadata
metadata = {
"cost_per_call": 0.001,
"avg_latency_ms": 500,
"owner": "email-team"
}
# Instance attributes
_smtp_client: Any = None
def on_load(self) -> None:
"""Initialize SMTP client"""
self._smtp_client = self._create_smtp_client()
def on_unload(self) -> None:
"""Close SMTP client"""
if self._smtp_client:
self._smtp_client.close()
def execute(self, inputs: dict[str, Any], context: Context) -> dict[str, Any]:
"""Execute email sending"""
params = SendEmailInput(**inputs)
try:
message_id = self._send(params)
return {
"success": True,
"message_id": message_id,
"error": None
}
except Exception as e:
# Log error (using trace_id from context)
self._log_error(context.trace_id, e)
return {
"success": False,
"message_id": None,
"error": str(e)
}
def _send(self, params: SendEmailInput) -> str:
"""Internal method: actual sending logic"""
# ... implementation ...
return "msg_123"
def _create_smtp_client(self) -> Any:
"""Create SMTP client"""
# ... implementation ...
pass
def _log_error(self, trace_id: str, error: Exception) -> None:
"""Log error"""
# ... implementation ...
pass
7. Interface Validation¶
apcore validates interface implementation correctness when loading modules:
# Framework internal validation logic
def validate_module(module_class: Type[Module]) -> list[str]:
errors = []
# ============ Core Layer Validation (Required) ============
# Check input_schema
if not hasattr(module_class, 'input_schema'):
errors.append("Missing input_schema")
elif not issubclass(module_class.input_schema, BaseModel):
errors.append("input_schema must be a subclass of BaseModel")
# Check output_schema
if not hasattr(module_class, 'output_schema'):
errors.append("Missing output_schema")
elif not issubclass(module_class.output_schema, BaseModel):
errors.append("output_schema must be a subclass of BaseModel")
# Check description
description = getattr(module_class, 'description', None) or module_class.__doc__
if not description:
errors.append("Missing description (docstring or explicit definition)")
elif len(description) > 200:
errors.append("description exceeds 200 character limit")
# Check documentation (optional)
documentation = getattr(module_class, 'documentation', None)
if documentation is not None and len(documentation) > 5000:
errors.append("documentation exceeds 5000 character limit")
# Check execute method
if not hasattr(module_class, 'execute'):
errors.append("Missing execute method")
# ============ Annotation Layer Validation (Optional, type checking) ============
annotations = getattr(module_class, 'annotations', None)
if annotations is not None and not isinstance(annotations, ModuleAnnotations):
errors.append("annotations must be of type ModuleAnnotations")
examples = getattr(module_class, 'examples', [])
if examples:
for i, example in enumerate(examples):
if not isinstance(example, ModuleExample):
errors.append(f"examples[{i}] must be of type ModuleExample")
elif not example.title or not example.inputs:
errors.append(f"examples[{i}] must have title and inputs")
metadata = getattr(module_class, 'metadata', {})
if not isinstance(metadata, dict):
errors.append("metadata must be of type dict")
return errors
8. Function-based Module Interface¶
In addition to the Class-based approach of inheriting the Module base class, apcore also supports function-based module definitions. See PROTOCOL_SPEC §5.11 for detailed specifications.
8.1 module() Unified Concept¶
module() is the single registration mechanism, supporting two forms:
- Decorator form: Suitable for new code or modifiable code
- Function call form: Suitable for unmodifiable existing code (class methods, third-party library functions, etc.)
Modules produced by both forms are completely equivalent to Class-based Modules in Registry/Executor/Schema behavior.
8.2 Parameter Signature¶
# Decorator form
@module(
id: str = None, # Module ID (optional, auto-generated)
description: str = None, # Module description (optional, extracted from docstring)
documentation: str = None, # Detailed documentation (optional, ≤5000 characters, Markdown)
annotations: ModuleAnnotations = None, # Behavior annotations
tags: list[str] = None, # Tags
version: str = "1.0.0", # Version number
metadata: dict = None # Extended metadata
)
def my_function(...):
...
# Function call form
module(
callable, # Target function or method
id: str = None,
description: str = None,
documentation: str = None, # Detailed documentation (optional, ≤5000 characters, Markdown)
annotations: ModuleAnnotations = None,
tags: list[str] = None,
version: str = "1.0.0",
metadata: dict = None
)
8.3 Equivalence Mapping with Class-based Module¶
| Class-based Attribute | Function-based Equivalent |
|---|---|
input_schema |
Auto-generated from function parameter type annotations |
output_schema |
Auto-generated from return type annotation |
description |
Extracted from docstring first line, or description parameter |
documentation |
documentation parameter |
execute() |
Function (def or async def, framework auto-detects) |
name |
Generated from function name |
tags |
tags parameter |
version |
version parameter |
annotations |
annotations parameter |
metadata |
metadata parameter |
on_load() / on_unload() |
Not supported (function-based modules have no lifecycle hooks) |
8.4 Context Injection Mechanism¶
When a function parameter declares context: Context, the framework automatically injects the Context object:
@module(id="email.send")
def send_email(to: str, subject: str, context: Context) -> dict:
"""Send email"""
print(f"trace_id: {context.trace_id}")
return {"success": True}
- The
contextparameter will not appear in the generatedinput_schema - If the function doesn't need Context, the parameter can be omitted
8.5 Underlying Module Object Access¶
Functions registered via module() produce an underlying Module object, accessible via Registry:
@module(id="email.send")
def send_email(to: str, subject: str) -> dict:
"""Send email"""
return {"success": True}
# Get underlying Module object via Registry
registry = Registry()
mod = registry.get("email.send")
# Can access all standard Module attributes
print(mod.description) # "Send email"
print(mod.input_schema) # Auto-generated JSON Schema
print(mod.output_schema) # Auto-generated JSON Schema
8.6 Interface Contract Summary¶
| Interface | Level | Contract |
|---|---|---|
module() parameter type annotations |
MUST | All parameters must have type annotations |
| Return type annotation | MUST | Function must have return type annotation |
id parameter |
MAY | Optional; auto-generated from function path if not provided |
description |
MAY | Optional; extracted from docstring if not provided |
| Context injection | MAY | Optional; auto-injected when context: Context parameter declared |
| Sync/async | MUST | Both def and async def map to execute(), framework auto-detects and selects invocation method |
| Schema equivalence | MUST | Generated Schema equivalent to Class-based definition |
Next Steps¶
- Creating Modules Guide - Complete module creation tutorial
- Context Object - Detailed explanation of Context
- Registry API - Module registration and discovery