Skip to content
Merged
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 @@ -52,6 +52,8 @@
Unset,
)

from .source_scenario import SourceScenario

from .aiohttp_scenario import (
AgentEnvironment,
AiohttpScenario,
Expand Down Expand Up @@ -107,5 +109,6 @@
"TranscriptFormatter",
"print_activities",
"print_conversation",
"print_json"
"print_json",
"SourceScenario"
]
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
# Import commands
from .environment import env_group
from .scenario import scenario_group
from .init import init_group

# Add commands to this list to register them with the CLI
COMMANDS: list[Command] = [
env_group,
scenario_group,
init_group,
]

__all__ = ["COMMANDS"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

"""Init CLI command — scaffold a test harness from a preset template."""

import shutil
from pathlib import Path
from importlib.resources import files as _pkg_files

import click

from microsoft_agents.testing.cli.core import pass_output, Output

# ---------------------------------------------------------------------------
# Preset discovery — runs once at import time
# ---------------------------------------------------------------------------
_PRESETS_ROOT = _pkg_files("microsoft_agents.testing") / "presets"


def _discover_presets() -> dict:
try:
return {
entry.name: entry
for entry in _PRESETS_ROOT.iterdir()
if entry.is_dir()
}
except (FileNotFoundError, NotADirectoryError):
return {}


PRESETS: dict = _discover_presets()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _copy_traversable(src, dest: Path) -> None:
"""Recursively copy a Traversable tree into an existing dest directory."""
for item in src.iterdir():
target = dest / item.name
if item.is_dir():
target.mkdir(parents=True, exist_ok=True)
_copy_traversable(item, target)
else:
target.write_bytes(item.read_bytes())


# ---------------------------------------------------------------------------
# Command
# ---------------------------------------------------------------------------
@click.command(name="init")
@click.argument("preset", required=False, default=None)
@click.option("--force", is_flag=True, default=False, help="Overwrite any existing files from the preset.")
@pass_output
def init_group(out: Output, preset: str, force: bool) -> None:
"""Scaffold a test harness from a preset template.

PRESET is the name of the harness template to use.
Omit PRESET to list available presets.
"""
if not preset:
if not PRESETS:
out.warning("No presets found.")
else:
out.info("Available presets:")
for name in PRESETS:
out.info(f"\t{name}")
return

if preset not in PRESETS:
available = ", ".join(PRESETS) or "none"
out.error(f"Unknown preset '{preset}'. Available: {available}")
raise SystemExit(1)

cwd = Path.cwd()
preset_traversable = PRESETS[preset]

conflicts = [
cwd / item.name
for item in preset_traversable.iterdir()
if (cwd / item.name).exists()
]

if conflicts and not force:
names = ", ".join(p.name for p in conflicts)
out.error(f"Already exists: {names}. Use --force to overwrite.")
raise SystemExit(1)

if force:
for conflict in conflicts:
if conflict.is_dir():
shutil.rmtree(conflict)
else:
conflict.unlink()

out.info(f"Initializing preset '{preset}' ...")
_copy_traversable(preset_traversable, cwd)
out.success(f"Done. Files written to {cwd}")

out.info("")
out.info("Next step: create .env files for each component from their env.TEMPLATE:")
env_templates = [
cwd / item.name / "env.TEMPLATE"
for item in preset_traversable.iterdir()
if item.is_dir() and (cwd / item.name / "env.TEMPLATE").exists()
]
for template in env_templates:
out.info(f"\t{template.parent.name}/env.TEMPLATE → {template.parent.name}/.env")
out.info("")
out.info("Populate each .env with your app credentials and configuration before running.")
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

ENTRY_POINT_NAME = "run_agent.ps1"
DEFAULT_LOCAL_AGENT_ENDPOINT = "http://localhost:3978/api/messages"

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"agents": {
"my_agent": {
"path": "../my_agent",
"setup": "uv sync",
"run": "uv run python -m src.main"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
name = "e2e-tests"
version = "0.0.0"
requires-python = ">=3.13"
Comment thread
rodrigobr-msft marked this conversation as resolved.
dependencies = [
"pytest",
"pytest-asyncio",
"microsoft-agents-testing @ git+https://github.com/microsoft/Agents-for-python.git@main#subdirectory=dev/testing/microsoft-agents-testing",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[pytest]
# pytest config for the environments/local test suite.
# Run locally: uv run pytest tests/
# Or via Docker: ./scripts/run_local.ps1

filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
ignore::aiohttp.web.NotAppKeyWarning

asyncio_mode = auto

addopts =
--strict-markers
--strict-config
--verbose
--tb=short
--durations=10

minversion = 6.0

markers =
unit: Unit tests
integration: Integration tests
slow: Slow tests that may take longer to run
requires_network: Tests that require network access
requires_auth: Tests that require authentication

asyncio_default_fixture_loop_scope = class
asyncio_default_test_loop_scope = class
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from microsoft_agents.testing import scenario_registry

scenario_registry.load_json("config.json")
Comment thread
rodrigobr-msft marked this conversation as resolved.
Comment thread
rodrigobr-msft marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import pytest

from microsoft_agents.testing import AgentClient

@pytest.mark.agent_test("my_agent")
async def test_my_agent(agent_client: AgentClient):

await agent_client.send("Hello, World!", wait=5.0)

# assert that the agent replied with a message Activity
agent_client.expect().that_for_any(type="message")
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[project]
name = "my-agent"
version = "0.0.0"
requires-python = ">=3.13"
Comment thread
rodrigobr-msft marked this conversation as resolved.
dependencies = [
"python-dotenv",
"aiohttp",
"microsoft-agents-hosting-aiohttp",
"microsoft-agents-hosting-core",
"microsoft-agents-authentication-msal",
"microsoft-agents-activity"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import logging
from os import environ, path
Comment thread
rodrigobr-msft marked this conversation as resolved.
from dotenv import load_dotenv
from microsoft_agents.activity import load_configuration_from_env
from microsoft_agents.authentication.msal import (
MsalConnectionManager,
)
from microsoft_agents.hosting.core import (
AgentApplication,
TurnState,
TurnContext,
MemoryStorage,
)
from microsoft_agents.hosting.aiohttp import CloudAdapter
from microsoft_agents.hosting.core.app.oauth.authorization import Authorization

from .start_server import start_server

logging.basicConfig(level=logging.INFO)
load_dotenv()

agents_sdk_config = load_configuration_from_env(environ)

STORAGE = MemoryStorage()
CONNECTION_MANAGER = MsalConnectionManager(**agents_sdk_config)
ADAPTER = CloudAdapter(connection_manager=CONNECTION_MANAGER)
AUTHORIZATION = Authorization(STORAGE, CONNECTION_MANAGER, **agents_sdk_config)

AGENT_APP = AgentApplication[TurnState](
storage=STORAGE, adapter=ADAPTER, authorization=AUTHORIZATION, **agents_sdk_config
)


async def _help(context: TurnContext, _state: TurnState):
await context.send_activity(
"Welcome to the Echo Agent sample 🚀. "
"Type /help for help or send a message to see the echo feature in action."
)


AGENT_APP.conversation_update("membersAdded")(_help)

AGENT_APP.message("/help")(_help)


@AGENT_APP.activity("message")
async def on_message(context: TurnContext, _):
await context.send_activity(f"you said: {context.activity.text}")


if __name__ == "__main__":
try:
start_server(
agent_application=AGENT_APP,
auth_configuration=CONNECTION_MANAGER.get_default_connection_configuration(),
)
except Exception as error:
raise error
Comment thread
rodrigobr-msft marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from os import environ
from microsoft_agents.hosting.core import AgentApplication, AgentAuthConfiguration
from microsoft_agents.hosting.aiohttp import (
start_agent_process,
jwt_authorization_middleware,
CloudAdapter,
)
from aiohttp.web import Request, Response, Application, run_app


def start_server(
agent_application: AgentApplication, auth_configuration: AgentAuthConfiguration
):
async def entry_point(req: Request) -> Response:
agent: AgentApplication = req.app["agent_app"]
adapter: CloudAdapter = req.app["adapter"]
return await start_agent_process(
req,
agent,
adapter,
)

APP = Application(middlewares=[jwt_authorization_middleware])
APP.router.add_post("/api/messages", entry_point)
APP.router.add_get("/api/messages", lambda _: Response(status=200))
APP["agent_configuration"] = auth_configuration
APP["agent_app"] = agent_application
APP["adapter"] = agent_application.adapter

try:
run_app(APP, host="localhost", port=environ.get("PORT", 3978))
except Exception as error:
raise error
Comment thread
rodrigobr-msft marked this conversation as resolved.
Comment thread
rodrigobr-msft marked this conversation as resolved.
Comment thread
rodrigobr-msft marked this conversation as resolved.
Loading
Loading