Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
10dd4a5
📦 (adapters): Create empty adapters package with __init__.py
Chisanan232 Apr 27, 2026
0b6c598
📦 (adapters): Create empty base adapter module
Chisanan232 Apr 27, 2026
b5e904d
📦 (tests): Create empty adapters unit-test package with __init__.py
Chisanan232 Apr 27, 2026
4ffe4a8
📦 (tests): Create empty base adapter test module
Chisanan232 Apr 27, 2026
9d78655
🏗️ (adapters): Add GovernanceInterceptor protocol shell
Chisanan232 Apr 27, 2026
b8a33ec
🏗️ (adapters): Add FrameworkAdapter ABC shell
Chisanan232 Apr 27, 2026
d80e5af
✨ (adapters): Add abstract method get_framework_name()
Chisanan232 Apr 27, 2026
6b4a022
✨ (adapters): Add abstract method get_supported_versions()
Chisanan232 Apr 27, 2026
367a9eb
✨ (adapters): Add abstract method register_hooks()
Chisanan232 Apr 27, 2026
b481d4a
✨ (adapters): Add abstract method unregister_hooks()
Chisanan232 Apr 27, 2026
277de6e
✨ (adapters): Add default is_available() implementation
Chisanan232 Apr 27, 2026
0f93abc
✨ (adapters): Add default get_active_version() implementation
Chisanan232 Apr 27, 2026
e8f2461
✨ (exceptions): Add AdapterValidationError class
Chisanan232 Apr 27, 2026
ccdb780
♻️ (exceptions): Export AdapterValidationError from exceptions package
Chisanan232 Apr 27, 2026
bfaed11
♻️ (adapters): Export GovernanceInterceptor in adapters package
Chisanan232 Apr 27, 2026
0dbc66d
♻️ (adapters): Export FrameworkAdapter in adapters package
Chisanan232 Apr 27, 2026
f48b249
♻️ (sdk): Re-export adapter base types from root package
Chisanan232 Apr 27, 2026
8e9e755
📝 (adapters): Add docstring for GovernanceInterceptor
Chisanan232 Apr 27, 2026
57b9c60
📝 (adapters): Add docstring for FrameworkAdapter
Chisanan232 Apr 27, 2026
46a8ca7
📝 (adapters): Add docstring for get_framework_name()
Chisanan232 Apr 27, 2026
99d6a2c
📝 (adapters): Add docstring for get_supported_versions()
Chisanan232 Apr 27, 2026
0c32370
📝 (adapters): Add docstring for register_hooks()
Chisanan232 Apr 27, 2026
a9c30df
📝 (adapters): Add docstring for unregister_hooks()
Chisanan232 Apr 27, 2026
e8cd0b2
📝 (adapters): Add docstring for is_available()
Chisanan232 Apr 27, 2026
bf194cc
📝 (adapters): Add docstring for get_active_version()
Chisanan232 Apr 27, 2026
6865d1b
✅ (tests): Add test for abstract contract enforcement
Chisanan232 Apr 27, 2026
6688624
✅ (tests): Add test for is_available() when framework exists
Chisanan232 Apr 27, 2026
e945d11
✅ (tests): Add test for is_available() when framework missing
Chisanan232 Apr 27, 2026
a409bbf
✅ (tests): Add test for get_active_version() with __version__
Chisanan232 Apr 27, 2026
22f2d68
✅ (tests): Add test for get_active_version() without __version__
Chisanan232 Apr 27, 2026
86f1fbc
🔧 (typing): Enforce strict type-check coverage for new adapter base m…
Chisanan232 Apr 27, 2026
4c158ab
✨ (adapters): Add registration-time contract validation flow
Chisanan232 Apr 27, 2026
91ee05e
✅ (tests): Add LangChain coverage for adapter default helpers
Chisanan232 Apr 27, 2026
1f61cf8
📝 (adapters): Clarify contract docstrings with error conditions
Chisanan232 Apr 27, 2026
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
7 changes: 6 additions & 1 deletion agent_assembly/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Agent Assembly Python SDK."""

from agent_assembly.adapters import FrameworkAdapter, GovernanceInterceptor
from agent_assembly.core import init_assembly
from agent_assembly.exceptions import (
AdapterValidationError,
AgentError,
AssemblyError,
ConfigurationError,
Expand All @@ -14,9 +16,12 @@
__all__ = [
"__version__",
"init_assembly",
"GovernanceInterceptor",
"FrameworkAdapter",
"AssemblyError",
"AgentError",
"PolicyError",
"GatewayError",
"ConfigurationError",
]
"AdapterValidationError",
]
3 changes: 3 additions & 0 deletions agent_assembly/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor

__all__ = ["GovernanceInterceptor", "FrameworkAdapter"]
131 changes: 131 additions & 0 deletions agent_assembly/adapters/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
from __future__ import annotations

from abc import ABC, abstractmethod
import importlib
from typing import Protocol

from agent_assembly.exceptions import AdapterValidationError


class GovernanceInterceptor(Protocol):
"""Protocol implemented by governance interceptors used by adapters."""

pass


class FrameworkAdapter(ABC):
"""Abstract contract implemented by every framework adapter.

Adapters should be registered through `register()` so contract validation
errors are raised before framework hooks are attached.
"""

@abstractmethod
def get_framework_name(self) -> str:
"""Return the canonical importable framework package name.

Error conditions:
- Empty or whitespace-only names trigger `AdapterValidationError`
during `register()`.
"""

...

@abstractmethod
def get_supported_versions(self) -> list[str]:
"""Return supported semantic version ranges for the framework.

Error conditions:
- Empty lists or empty range strings trigger `AdapterValidationError`
during `register()`.
"""

...

@abstractmethod
def register_hooks(self, interceptor: GovernanceInterceptor) -> None:
"""Attach framework hooks to a governance interceptor instance.

Error conditions:
- Framework-specific hook wiring failures should raise adapter-specific
exceptions for the caller to handle.
"""

...

@abstractmethod
def unregister_hooks(self) -> None:
"""Detach all framework hooks in an idempotent way.

Error conditions:
- This method should avoid raising when no hooks are currently active.
"""

...

def validate_registration(self) -> None:
"""Validate adapter contract values before hook registration.

Raises:
AdapterValidationError: If the framework name or version ranges
violate the base adapter contract.
"""
framework_name = self.get_framework_name()
if not framework_name.strip():
raise AdapterValidationError(
"Adapter contract invalid: framework name must be non-empty."
)

supported_versions = self.get_supported_versions()
if not supported_versions:
raise AdapterValidationError(
"Adapter contract invalid: supported versions must not be empty."
)

for version_range in supported_versions:
if not version_range.strip():
raise AdapterValidationError(
"Adapter contract invalid: version ranges must be non-empty strings."
)

def register(self, interceptor: GovernanceInterceptor) -> None:
"""Validate contract values and then attach framework hooks.

Raises:
AdapterValidationError: If `validate_registration()` fails.
"""
self.validate_registration()
self.register_hooks(interceptor)

def is_available(self) -> bool:
"""Return True when the framework package can be imported.

Error conditions:
- Import failures are handled internally and return `False`.
"""

try:
importlib.import_module(self.get_framework_name())
except ImportError:
return False

return True

def get_active_version(self) -> str | None:
"""Return framework `__version__` when present, otherwise `None`.

Error conditions:
- Import failures and missing/non-string `__version__` values return
`None`.
"""

try:
module = importlib.import_module(self.get_framework_name())
except ImportError:
return None

version = getattr(module, "__version__", None)
if isinstance(version, str):
return version

return None
14 changes: 14 additions & 0 deletions agent_assembly/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

from __future__ import annotations

__all__ = [
"AssemblyError",
"AgentError",
"PolicyError",
"GatewayError",
"ConfigurationError",
"AdapterValidationError",
]


class AssemblyError(Exception):
"""Base exception for Agent Assembly SDK errors."""
Expand All @@ -26,3 +35,8 @@ class GatewayError(AssemblyError):
class ConfigurationError(AssemblyError):
"""Exception raised for configuration errors."""
pass


class AdapterValidationError(AssemblyError):
"""Exception raised when an adapter contract is invalid."""
pass
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ warn_unused_ignores = True
;no_implicit_reexport = True
strict_equality = True
strict_concatenate = True

[mypy-agent_assembly.adapters.base]
strict = True
Empty file.
172 changes: 172 additions & 0 deletions test/unit/adapters/test_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import pytest
from types import SimpleNamespace

from agent_assembly.adapters import FrameworkAdapter, GovernanceInterceptor
from agent_assembly.exceptions import AdapterValidationError


class IncompleteAdapter(FrameworkAdapter):
def get_framework_name(self) -> str:
return "math"


def test_framework_adapter_requires_all_abstract_methods() -> None:
with pytest.raises(TypeError):
IncompleteAdapter()


class AvailableFrameworkAdapter(FrameworkAdapter):
def get_framework_name(self) -> str:
return "math"

def get_supported_versions(self) -> list[str]:
return []

def register_hooks(self, interceptor: GovernanceInterceptor) -> None:
return None

def unregister_hooks(self) -> None:
return None


def test_is_available_returns_true_when_framework_exists() -> None:
assert AvailableFrameworkAdapter().is_available() is True


class MissingFrameworkAdapter(FrameworkAdapter):
def get_framework_name(self) -> str:
return "_agent_assembly_missing_framework_"

def get_supported_versions(self) -> list[str]:
return []

def register_hooks(self, interceptor: GovernanceInterceptor) -> None:
return None

def unregister_hooks(self) -> None:
return None


def test_is_available_returns_false_when_framework_is_missing() -> None:
assert MissingFrameworkAdapter().is_available() is False


class VersionedFrameworkAdapter(FrameworkAdapter):
def get_framework_name(self) -> str:
return "pytest"

def get_supported_versions(self) -> list[str]:
return []

def register_hooks(self, interceptor: GovernanceInterceptor) -> None:
return None

def unregister_hooks(self) -> None:
return None


def test_get_active_version_returns_module_version() -> None:
assert VersionedFrameworkAdapter().get_active_version() is not None


class NonVersionedFrameworkAdapter(FrameworkAdapter):
def get_framework_name(self) -> str:
return "math"

def get_supported_versions(self) -> list[str]:
return []

def register_hooks(self, interceptor: GovernanceInterceptor) -> None:
return None

def unregister_hooks(self) -> None:
return None


def test_get_active_version_returns_none_without_version() -> None:
assert NonVersionedFrameworkAdapter().get_active_version() is None


class InvalidRegistrationAdapter(FrameworkAdapter):
def get_framework_name(self) -> str:
return " "

def get_supported_versions(self) -> list[str]:
return []

def register_hooks(self, interceptor: GovernanceInterceptor) -> None:
return None

def unregister_hooks(self) -> None:
return None


def test_register_raises_validation_error_for_invalid_contract() -> None:
with pytest.raises(AdapterValidationError):
InvalidRegistrationAdapter().register(object())


class ValidRegistrationAdapter(FrameworkAdapter):
def __init__(self) -> None:
self.hooks_registered = False

def get_framework_name(self) -> str:
return "pytest"

def get_supported_versions(self) -> list[str]:
return [">=8.0.0,<9.0.0"]

def register_hooks(self, interceptor: GovernanceInterceptor) -> None:
self.hooks_registered = True

def unregister_hooks(self) -> None:
return None


def test_register_calls_register_hooks_when_contract_is_valid() -> None:
adapter = ValidRegistrationAdapter()
adapter.register(object())
assert adapter.hooks_registered is True


class LangChainFrameworkAdapter(FrameworkAdapter):
def get_framework_name(self) -> str:
return "langchain"

def get_supported_versions(self) -> list[str]:
return [">=0.1.0,<0.4.0"]

def register_hooks(self, interceptor: GovernanceInterceptor) -> None:
return None

def unregister_hooks(self) -> None:
return None


def test_is_available_returns_true_for_langchain(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[str] = []

def fake_import_module(module_name: str) -> object:
calls.append(module_name)
if module_name == "langchain":
return SimpleNamespace(__version__="0.3.0")
raise ImportError

monkeypatch.setattr("agent_assembly.adapters.base.importlib.import_module", fake_import_module)

adapter = LangChainFrameworkAdapter()
assert adapter.is_available() is True
assert calls == ["langchain"]


def test_get_active_version_returns_langchain_version(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def fake_import_module(module_name: str) -> object:
if module_name == "langchain":
return SimpleNamespace(__version__="0.3.0")
raise ImportError

monkeypatch.setattr("agent_assembly.adapters.base.importlib.import_module", fake_import_module)

assert LangChainFrameworkAdapter().get_active_version() == "0.3.0"