Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ from agent_framework_anthropic import (
RawAnthropicFoundryClient,
RawAnthropicVertexClient,
)
from agent_framework_claude import ClaudeAgent, ClaudeAgentOptions
from agent_framework_claude import ClaudeAgent, ClaudeAgentOptions, RawClaudeAgent

__all__ = [
"AnthropicBedrockClient",
Expand All @@ -25,4 +25,5 @@ __all__ = [
"RawAnthropicClient",
"RawAnthropicFoundryClient",
"RawAnthropicVertexClient",
"RawClaudeAgent",
]
2 changes: 2 additions & 0 deletions python/packages/core/agent_framework/foundry/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ from agent_framework_azure_contentunderstanding import (
from agent_framework_foundry import (
FOUNDRY_HOSTED_AGENT_SESSION_ID_KEY,
FoundryAgent,
FoundryAgentOptions,
FoundryChatClient,
FoundryChatOptions,
FoundryEmbeddingClient,
Expand Down Expand Up @@ -66,6 +67,7 @@ __all__ = [
"FileSearchBackend",
"FileSearchConfig",
"FoundryAgent",
"FoundryAgentOptions",
"FoundryAgentSessionStore",
"FoundryChatClient",
"FoundryChatOptions",
Expand Down
19 changes: 19 additions & 0 deletions python/packages/core/agent_framework/hyperlight/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.

from agent_framework_hyperlight import (
AllowedDomain,
AllowedDomainInput,
FileMount,
FileMountInput,
HyperlightCodeActProvider,
HyperlightExecuteCodeTool,
)

__all__ = [
"AllowedDomain",
"AllowedDomainInput",
"FileMount",
"FileMountInput",
"HyperlightCodeActProvider",
"HyperlightExecuteCodeTool",
]
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ from agent_framework_orchestrations import (
AgentBasedGroupChatOrchestrator,
AgentOrchestrationOutput,
AgentRequestInfoResponse,
BaseGroupChatOrchestrator,
ConcurrentBuilder,
GroupChatBuilder,
GroupChatOrchestrator,
GroupChatRequestMessage,
GroupChatRequestSentEvent,
GroupChatResponseReceivedEvent,
GroupChatSelectionFunction,
GroupChatState,
HandoffAgentExecutor,
Expand All @@ -33,8 +35,12 @@ from agent_framework_orchestrations import (
MagenticProgressLedger,
MagenticProgressLedgerItem,
MagenticResetSignal,
OrchestrationState,
SequentialBuilder,
StandardMagenticManager,
TerminationCondition,
clean_conversation_for_handoff,
create_completion_message,
)

__all__ = [
Expand All @@ -46,11 +52,13 @@ __all__ = [
"AgentBasedGroupChatOrchestrator",
"AgentOrchestrationOutput",
"AgentRequestInfoResponse",
"BaseGroupChatOrchestrator",
"ConcurrentBuilder",
"GroupChatBuilder",
"GroupChatOrchestrator",
"GroupChatRequestMessage",
"GroupChatRequestSentEvent",
"GroupChatResponseReceivedEvent",
"GroupChatSelectionFunction",
"GroupChatState",
"HandoffAgentExecutor",
Expand All @@ -70,6 +78,10 @@ __all__ = [
"MagenticProgressLedger",
"MagenticProgressLedgerItem",
"MagenticResetSignal",
"OrchestrationState",
"SequentialBuilder",
"StandardMagenticManager",
"TerminationCondition",
"clean_conversation_for_handoff",
"create_completion_message",
]
80 changes: 80 additions & 0 deletions python/packages/core/tests/core/test_namespace_stubs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright (c) Microsoft. All rights reserved.

"""Keep the lazy ``agent_framework.<namespace>`` modules and their type stubs in sync.

Each namespace re-exports connector classes lazily through ``_IMPORTS`` and declares
the same names in ``__init__.pyi`` for type checkers. A name missing from the stub
imports fine at runtime but is rejected by type checkers, so compare them statically.
"""

import ast
from pathlib import Path

import pytest

_PACKAGE_ROOT = Path(__file__).parents[2] / "agent_framework"


def _lazy_namespaces() -> list[Path]:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left these in the test module. My reading of that section is that it's about where shared helpers go: fixtures in conftest.py, other reusable helpers in an imported module rather than conftest.py. These three functions are only used by this file. That matches the existing pattern in this directory: 30 test_*.py modules under packages/core/tests/core define module-private helpers, including the closest sibling, test_docstrings.py (6 helpers). Happy to move them into a helper module if maintainers read the rule more strictly.

namespaces: list[Path] = []
for init in sorted(_PACKAGE_ROOT.glob("*/__init__.py")):
if _lazy_imports(init) is not None:
namespaces.append(init.parent)
return namespaces


def _lazy_imports(init: Path) -> dict[str, str | None] | None:
"""Return the lazily exported names mapped to their source module, if declared."""
module = ast.parse(init.read_text(encoding="utf-8"))
import_path: str | None = None
for node in module.body:
if isinstance(node, ast.Assign) and len(node.targets) == 1:
target, assigned = node.targets[0], node.value
elif isinstance(node, ast.AnnAssign) and node.value is not None:
target, assigned = node.target, node.value
else:
continue
if not isinstance(target, ast.Name):
continue
if target.id == "IMPORT_PATH":
import_path = ast.literal_eval(assigned)
elif target.id == "_IMPORTS":
value = ast.literal_eval(assigned)
if isinstance(value, dict):
return {name: source[0] for name, source in value.items()}
return dict.fromkeys(value, import_path)
return None


def _stub_exports(stub: Path) -> tuple[dict[str, str | None], set[str]]:
module = ast.parse(stub.read_text(encoding="utf-8"))
imported: dict[str, str | None] = {}
exported: set[str] = set()
for node in module.body:
if isinstance(node, ast.ImportFrom):
for alias in node.names:
imported[alias.asname or alias.name] = node.module
elif isinstance(node, ast.Assign) and any(
isinstance(target, ast.Name) and target.id == "__all__" for target in node.targets
):
exported = set(ast.literal_eval(node.value))
return imported, exported


@pytest.mark.parametrize("namespace", _lazy_namespaces(), ids=lambda path: path.name)
def test_namespace_stub_declares_every_lazy_export(namespace: Path) -> None:
lazy_imports = _lazy_imports(namespace / "__init__.py")
assert lazy_imports is not None
stub = namespace / "__init__.pyi"
assert stub.exists(), f"agent_framework.{namespace.name} has lazy exports but no __init__.pyi"

stub_imports, stub_all = _stub_exports(stub)

assert set(stub_imports) == set(lazy_imports)
assert stub_all == set(lazy_imports)
for name, source in lazy_imports.items():
stub_source = stub_imports[name]
assert source is not None
assert stub_source is not None
# The stub may import from a submodule of the same distribution package.
assert stub_source.split(".")[0] == source.split(".")[0], name
Loading