From 9330ffacccda8d5704480ada346d3fc585957cd6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 03:43:44 +0000 Subject: [PATCH 1/4] Initial plan From 0836ea83dcafe229d21e3003fc812ccdca62f80d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 03:50:59 +0000 Subject: [PATCH 2/4] Support per-agent Foundry endpoint aliases Co-authored-by: judacas <67698498+judacas@users.noreply.github.com> Agent-Logs-Url: https://github.com/judacas/LLM-Automated-Inventory-Management/sessions/23831b2f-7f88-4cfd-bf69-af9249880a5f --- src/a2a_servers/.env.template | 11 ++- src/a2a_servers/README.md | 2 +- src/a2a_servers/__main__.py | 4 + src/a2a_servers/agent_definition.py | 52 +++++++++- src/a2a_servers/agents/agent.template.toml | 1 + src/a2a_servers/agents/email_agent.toml | 1 + src/a2a_servers/agents/math_agent.sample.toml | 1 + .../agents/purchase_order_agent.toml | 1 + src/a2a_servers/agents/quote_agent.toml | 1 + src/a2a_servers/app_factory.py | 5 +- .../docs/agent-definition-reference.md | 19 +++- src/a2a_servers/docs/architecture.md | 4 +- src/a2a_servers/docs/deployment-azure.md | 4 +- src/a2a_servers/docs/developer-setup.md | 8 +- src/a2a_servers/docs/foundry-integration.md | 33 ++++--- src/a2a_servers/docs/runbook.md | 3 +- src/a2a_servers/docs/troubleshooting.md | 5 +- src/a2a_servers/settings.py | 9 -- src/a2a_servers/test_client.py | 2 +- .../tests/test_agent_definition.py | 94 ++++++++++++++++++- 20 files changed, 214 insertions(+), 46 deletions(-) diff --git a/src/a2a_servers/.env.template b/src/a2a_servers/.env.template index f1af634..fb48111 100644 --- a/src/a2a_servers/.env.template +++ b/src/a2a_servers/.env.template @@ -1,7 +1,14 @@ # Azure AI Foundry configuration template. Copy to `.env` and populate. # -# Required values -AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +# Required values: +# For each endpoint alias used in your `agents/*_agent.toml` files, define: +# AZURE_AI_PROJECT_ENDPOINT_=https://.services.ai.azure.com/api/projects/ +# +# Example for `endpoint_alias = "contoso_main"`: +AZURE_AI_PROJECT_ENDPOINT_CONTOSO_MAIN=https://.services.ai.azure.com/api/projects/ +# +# Optional backward-compatible shared fallback (used when alias-specific var is missing): +AZURE_AI_PROJECT_ENDPOINT= # Directory containing `*_agent.toml` definitions. Defaults to `agents/` if omitted. A2A_AGENT_CONFIG_DIR=agents diff --git a/src/a2a_servers/README.md b/src/a2a_servers/README.md index 6817705..8bf5245 100644 --- a/src/a2a_servers/README.md +++ b/src/a2a_servers/README.md @@ -41,7 +41,7 @@ In this branch, `src/a2a_servers` is primarily infrastructure for exposing Found - Sample configs should use a name like `agents/*_agent.sample.toml` so they are kept in-repo but not loaded at startup. - Each config becomes a route prefix like `//`. - The published agent card URL changes based on `A2A_URL_MODE`. -- Duplicate slugs or duplicate Foundry agent names fail startup. +- Duplicate slugs or duplicate Foundry agent targets (same endpoint alias + agent name) fail startup. - The A2A server process does not create Foundry agents for you; it expects them to already exist. - adding or changing an agent in production requires a redeploy diff --git a/src/a2a_servers/__main__.py b/src/a2a_servers/__main__.py index 927cbd8..a6c5e9e 100644 --- a/src/a2a_servers/__main__.py +++ b/src/a2a_servers/__main__.py @@ -1,4 +1,5 @@ import logging +from urllib.parse import urlparse import click import uvicorn @@ -68,6 +69,9 @@ def _log_agent_startup(mounted_agent: MountedAgent, settings: ServerSettings) -> logger.info("Agent slug: %s", definition.slug) logger.info("Loaded agent config from %s", definition.source_path) logger.info("Foundry agent name: %s", definition.foundry_agent_name) + endpoint_host = urlparse(definition.foundry_project_endpoint).netloc or "" + logger.info("Foundry endpoint alias: %s", definition.foundry_endpoint_alias) + logger.info("Foundry endpoint host: %s", endpoint_host) logger.info("Agent card: %s", mounted_agent.agent_card.name) logger.info("Agent card URL: %s", mounted_agent.agent_card.url) logger.info("Skills: %s", [skill.name for skill in definition.skills]) diff --git a/src/a2a_servers/agent_definition.py b/src/a2a_servers/agent_definition.py index ad5e5bb..f731842 100644 --- a/src/a2a_servers/agent_definition.py +++ b/src/a2a_servers/agent_definition.py @@ -21,6 +21,8 @@ class AgentDefinition: version: str health_message: str foundry_agent_name: str + foundry_endpoint_alias: str + foundry_project_endpoint: str default_input_modes: tuple[str, ...] default_output_modes: tuple[str, ...] skills: tuple[AgentSkill, ...] @@ -65,6 +67,36 @@ def _derive_agent_slug(path: Path) -> str: return _normalize_agent_slug(stem) +def _normalize_foundry_endpoint_alias(raw_alias: str) -> str: + alias = re.sub(r"[^a-z0-9]+", "_", raw_alias.strip().lower()).strip("_") + if not alias: + raise ValueError( + "Foundry endpoint alias must contain at least one letter or number" + ) + return alias + + +def _resolve_foundry_project_endpoint(foundry: dict[str, object]) -> tuple[str, str]: + endpoint_alias = _normalize_foundry_endpoint_alias( + _read_required_string(foundry, "endpoint_alias", "foundry") + ) + endpoint_env_var = f"AZURE_AI_PROJECT_ENDPOINT_{endpoint_alias.upper()}" + endpoint = (os.getenv(endpoint_env_var) or "").strip() + if endpoint: + return endpoint_alias, endpoint + + # Backward compatibility: a shared endpoint can still be provided. + fallback_endpoint = (os.getenv("AZURE_AI_PROJECT_ENDPOINT") or "").strip() + if fallback_endpoint: + return endpoint_alias, fallback_endpoint + + raise ValueError( + "Missing required environment variable for Foundry endpoint alias " + f"`{endpoint_alias}`: set `{endpoint_env_var}` " + "(or `AZURE_AI_PROJECT_ENDPOINT` as a shared fallback)." + ) + + def resolve_agent_config_dir(config_dir: str | None = None) -> Path: raw_path = (config_dir or os.getenv("A2A_AGENT_CONFIG_DIR") or "").strip() if raw_path: @@ -117,6 +149,9 @@ def load_agent_definition(config_path: str | Path) -> AgentDefinition: raise ValueError("`[smoke_tests]` must be a table if provided") foundry_agent_name = _read_required_string(foundry, "agent_name", "foundry") + foundry_endpoint_alias, foundry_project_endpoint = _resolve_foundry_project_endpoint( + foundry + ) skills: list[AgentSkill] = [] for index, skill_data in enumerate(skills_table, start=1): @@ -156,6 +191,8 @@ def load_agent_definition(config_path: str | Path) -> AgentDefinition: version=_read_required_string(a2a, "version", "a2a"), health_message=_read_required_string(a2a, "health_message", "a2a"), foundry_agent_name=foundry_agent_name, + foundry_endpoint_alias=foundry_endpoint_alias, + foundry_project_endpoint=foundry_project_endpoint, default_input_modes=_read_string_list( a2a, "default_input_modes", default=["text"] ), @@ -177,7 +214,7 @@ def load_agent_definitions( ) seen_slugs: dict[str, Path] = {} - seen_foundry_names: dict[str, Path] = {} + seen_foundry_targets: dict[tuple[str, str], Path] = {} seen_paths: set[Path] = set() for definition in definitions: @@ -195,13 +232,18 @@ def load_agent_definitions( ) seen_slugs[definition.slug] = definition.source_path - previous_foundry_path = seen_foundry_names.get(definition.foundry_agent_name) + foundry_target = ( + definition.foundry_endpoint_alias, + definition.foundry_agent_name, + ) + previous_foundry_path = seen_foundry_targets.get(foundry_target) if previous_foundry_path is not None: raise ValueError( - "Duplicate Foundry agent name " - f"`{definition.foundry_agent_name}` in " + "Duplicate Foundry agent target " + f"`{definition.foundry_agent_name}` on endpoint alias " + f"`{definition.foundry_endpoint_alias}` in " f"{previous_foundry_path} and {definition.source_path}" ) - seen_foundry_names[definition.foundry_agent_name] = definition.source_path + seen_foundry_targets[foundry_target] = definition.source_path return definitions diff --git a/src/a2a_servers/agents/agent.template.toml b/src/a2a_servers/agents/agent.template.toml index f07a004..336cf3d 100644 --- a/src/a2a_servers/agents/agent.template.toml +++ b/src/a2a_servers/agents/agent.template.toml @@ -9,6 +9,7 @@ streaming = false [foundry] agent_name = "Your-Foundry-Agent-Name" +endpoint_alias = "your_endpoint_alias" [smoke_tests] prompts = [ diff --git a/src/a2a_servers/agents/email_agent.toml b/src/a2a_servers/agents/email_agent.toml index b18b29c..88e10ec 100644 --- a/src/a2a_servers/agents/email_agent.toml +++ b/src/a2a_servers/agents/email_agent.toml @@ -9,6 +9,7 @@ streaming = false [foundry] agent_name = "email" +endpoint_alias = "contoso_main" [smoke_tests] prompts = [ diff --git a/src/a2a_servers/agents/math_agent.sample.toml b/src/a2a_servers/agents/math_agent.sample.toml index 10756d0..1394394 100644 --- a/src/a2a_servers/agents/math_agent.sample.toml +++ b/src/a2a_servers/agents/math_agent.sample.toml @@ -9,6 +9,7 @@ streaming = false [foundry] agent_name = "Math-Agent" +endpoint_alias = "math_lab" [smoke_tests] prompts = [ diff --git a/src/a2a_servers/agents/purchase_order_agent.toml b/src/a2a_servers/agents/purchase_order_agent.toml index 909b4c7..6f419fc 100644 --- a/src/a2a_servers/agents/purchase_order_agent.toml +++ b/src/a2a_servers/agents/purchase_order_agent.toml @@ -9,6 +9,7 @@ streaming = false [foundry] agent_name = "userPurchaseOrder" +endpoint_alias = "contoso_main" [smoke_tests] prompts = [ diff --git a/src/a2a_servers/agents/quote_agent.toml b/src/a2a_servers/agents/quote_agent.toml index c93be29..07434a5 100644 --- a/src/a2a_servers/agents/quote_agent.toml +++ b/src/a2a_servers/agents/quote_agent.toml @@ -9,6 +9,7 @@ streaming = false [foundry] agent_name = "userQuote" +endpoint_alias = "contoso_main" [smoke_tests] prompts = [ diff --git a/src/a2a_servers/app_factory.py b/src/a2a_servers/app_factory.py index d30a8fe..3c10d45 100644 --- a/src/a2a_servers/app_factory.py +++ b/src/a2a_servers/app_factory.py @@ -45,15 +45,12 @@ def create_agent_app( definition: AgentDefinition, settings: ServerSettings, ) -> tuple[Starlette, AgentCard, FoundryAgentExecutor]: - if settings.project_endpoint is None: - raise ValueError("Server settings must include project_endpoint") - agent_card = build_agent_card( definition, settings.agent_card_url_for(definition.slug) ) backend_factory = partial( create_foundry_agent_backend, - endpoint=settings.project_endpoint, + endpoint=definition.foundry_project_endpoint, agent_name=definition.foundry_agent_name, ) agent_executor = create_foundry_agent_executor(agent_card, backend_factory) diff --git a/src/a2a_servers/docs/agent-definition-reference.md b/src/a2a_servers/docs/agent-definition-reference.md index 9d022ca..3a428ab 100644 --- a/src/a2a_servers/docs/agent-definition-reference.md +++ b/src/a2a_servers/docs/agent-definition-reference.md @@ -55,8 +55,23 @@ Behavior notes: Required key: - `agent_name` +- `endpoint_alias` -This must exactly match the portal-managed Azure AI Foundry agent name the server should call. +`agent_name` must exactly match the portal-managed Azure AI Foundry agent name the +server should call. + +`endpoint_alias` is a short nickname (for example `contoso_main` or +`sales_eastus`) used to resolve the Foundry project endpoint from environment +variables: + +- `AZURE_AI_PROJECT_ENDPOINT_` + +Example: + +- `endpoint_alias = "contoso_main"` -> `AZURE_AI_PROJECT_ENDPOINT_CONTOSO_MAIN` + +This avoids placing full endpoint URLs in TOML files while still allowing +different agents to target different Foundry projects/endpoints. ## `[[skills]]` Blocks @@ -117,7 +132,7 @@ Startup fails if: - `streaming` is not a boolean - `slug` is invalid - there are duplicate slugs -- there are duplicate Foundry agent names +- there are duplicate Foundry targets (same `foundry.agent_name` and same `foundry.endpoint_alias`) ## Conventions For New Agents diff --git a/src/a2a_servers/docs/architecture.md b/src/a2a_servers/docs/architecture.md index c0e194c..4745f3d 100644 --- a/src/a2a_servers/docs/architecture.md +++ b/src/a2a_servers/docs/architecture.md @@ -68,7 +68,7 @@ Key responsibilities: - bind host and port - resolve whether published URLs are local or proxy-facing -- require `AZURE_AI_PROJECT_ENDPOINT` +- require Foundry endpoint env vars matching configured aliases - generate per-agent base URLs and card URLs The published URL model is important because A2A agent cards must advertise a URL that external callers can actually reach. @@ -157,7 +157,7 @@ There are two layers of configuration: Examples: -- `AZURE_AI_PROJECT_ENDPOINT` +- `AZURE_AI_PROJECT_ENDPOINT_` (per `foundry.endpoint_alias`) - `A2A_HOST` - `A2A_PORT` - `A2A_URL_MODE` diff --git a/src/a2a_servers/docs/deployment-azure.md b/src/a2a_servers/docs/deployment-azure.md index 1a87552..587ac7f 100644 --- a/src/a2a_servers/docs/deployment-azure.md +++ b/src/a2a_servers/docs/deployment-azure.md @@ -64,7 +64,7 @@ Optional but recommended: These settings must be present in the deployed app: ```dotenv -AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_PROJECT_ENDPOINT_=https://.services.ai.azure.com/api/projects/ A2A_AGENT_CONFIG_DIR=agents A2A_HOST=0.0.0.0 A2A_PORT=8000 @@ -156,7 +156,7 @@ Set the required app settings listed earlier. Most importantly: -- `AZURE_AI_PROJECT_ENDPOINT` +- `AZURE_AI_PROJECT_ENDPOINT_` for every alias used by deployed agents - `A2A_URL_MODE=forwarded` - `A2A_FORWARDED_BASE_URL=https://` diff --git a/src/a2a_servers/docs/developer-setup.md b/src/a2a_servers/docs/developer-setup.md index d458111..398bb85 100644 --- a/src/a2a_servers/docs/developer-setup.md +++ b/src/a2a_servers/docs/developer-setup.md @@ -45,7 +45,13 @@ cp .env.template .env Minimum required value: ```dotenv -AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_PROJECT_ENDPOINT_=https://.services.ai.azure.com/api/projects/ +``` + +Example: + +```dotenv +AZURE_AI_PROJECT_ENDPOINT_CONTOSO_MAIN=https://.services.ai.azure.com/api/projects/ ``` Recommended development values: diff --git a/src/a2a_servers/docs/foundry-integration.md b/src/a2a_servers/docs/foundry-integration.md index 8d0676a..3818600 100644 --- a/src/a2a_servers/docs/foundry-integration.md +++ b/src/a2a_servers/docs/foundry-integration.md @@ -14,9 +14,10 @@ For the step-by-step workflow for adding a new mounted agent, use [adding-agents ## Core Mapping -Each mounted A2A agent maps to one Foundry agent name: +Each mounted A2A agent maps to one Foundry target: - TOML field: `foundry.agent_name` +- TOML field: `foundry.endpoint_alias` - runtime call target: `agent_reference.name` Examples in this branch: @@ -26,9 +27,21 @@ Examples in this branch: If the name is wrong, the runtime will fail when it verifies or calls that Foundry agent. -## Required Environment Variable +## Required Environment Variables -The package requires: +For each endpoint alias used in agent TOML, set: + +```dotenv +AZURE_AI_PROJECT_ENDPOINT_=https://.services.ai.azure.com/api/projects/ +``` + +Example: + +```dotenv +AZURE_AI_PROJECT_ENDPOINT_CONTOSO_MAIN=https://.services.ai.azure.com/api/projects/ +``` + +Optional backward-compatible fallback: ```dotenv AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ @@ -125,21 +138,15 @@ When another team member or agent asks "how do I call the Foundry model?", the c That keeps A2A identity, skills metadata, and route conventions in one place. -## Important Design Constraint - -All mounted A2A agents in one process share the same `AZURE_AI_PROJECT_ENDPOINT`. - -Implications: - -- you can map different slugs to different Foundry agents -- you cannot, in the current implementation, map different slugs to different Foundry projects without changing the code +## Multi-Endpoint Behavior -If that becomes necessary, the configuration model will need to move project endpoint selection down to the per-agent level. +Mounted A2A agents can use different Foundry project endpoints by assigning +different `foundry.endpoint_alias` values and configuring each alias with its own +`AZURE_AI_PROJECT_ENDPOINT_` secret. ## Known Gaps - No persistent conversation store -- No per-agent Foundry project override - No built-in validation of whether Foundry tools are configured correctly beyond agent lookup - No automated provisioning of Foundry agents from this package diff --git a/src/a2a_servers/docs/runbook.md b/src/a2a_servers/docs/runbook.md index 3012785..81845b0 100644 --- a/src/a2a_servers/docs/runbook.md +++ b/src/a2a_servers/docs/runbook.md @@ -116,7 +116,8 @@ When something seems wrong, check these in order: 2. `GET //health` returns the configured health message. 3. `GET //.well-known/agent-card.json` returns the expected public metadata. 4. the TOML `foundry.agent_name` matches a real Foundry agent. -5. `AZURE_AI_PROJECT_ENDPOINT` points to the correct Foundry project. +5. each agent's `foundry.endpoint_alias` has a matching + `AZURE_AI_PROJECT_ENDPOINT_` environment variable. 6. local Azure credentials are valid for that project. ## Shutdown Behavior diff --git a/src/a2a_servers/docs/troubleshooting.md b/src/a2a_servers/docs/troubleshooting.md index 9a44373..defdf71 100644 --- a/src/a2a_servers/docs/troubleshooting.md +++ b/src/a2a_servers/docs/troubleshooting.md @@ -4,7 +4,7 @@ Symptom: -- startup raises a `ValueError` about `AZURE_AI_PROJECT_ENDPOINT` +- startup raises a `ValueError` about missing `AZURE_AI_PROJECT_ENDPOINT_` Cause: @@ -12,7 +12,8 @@ Cause: Fix: -- set `AZURE_AI_PROJECT_ENDPOINT` in `.env` or the deployment environment +- set `AZURE_AI_PROJECT_ENDPOINT_` for each `foundry.endpoint_alias` + used by mounted agents (or set `AZURE_AI_PROJECT_ENDPOINT` fallback) ## Startup Fails Because No Agent Configs Were Found diff --git a/src/a2a_servers/settings.py b/src/a2a_servers/settings.py index 2d553bd..e4764eb 100644 --- a/src/a2a_servers/settings.py +++ b/src/a2a_servers/settings.py @@ -11,7 +11,6 @@ class ServerSettings: url_mode: str forwarded_base_url: str log_level_name: str - project_endpoint: str | None = None @property def public_base_url(self) -> str: @@ -37,7 +36,6 @@ def load_server_settings( port: int | None = None, url_mode: str | None = None, forwarded_base_url: str | None = None, - require_project_endpoint: bool = True, ) -> ServerSettings: resolved_url_mode = ( (url_mode or os.getenv("A2A_URL_MODE") or "local").strip().lower() @@ -45,12 +43,6 @@ def load_server_settings( if resolved_url_mode not in {"local", "forwarded"}: raise ValueError("A2A_URL_MODE must be either 'local' or 'forwarded'") - project_endpoint = (os.getenv("AZURE_AI_PROJECT_ENDPOINT") or "").strip() or None - if require_project_endpoint and project_endpoint is None: - raise ValueError( - "Missing required environment variable: AZURE_AI_PROJECT_ENDPOINT" - ) - return ServerSettings( host=(host if host is not None else os.getenv("A2A_HOST", "localhost")).strip(), port=port if port is not None else int(os.getenv("A2A_PORT", "10007")), @@ -59,5 +51,4 @@ def load_server_settings( forwarded_base_url or os.getenv("A2A_FORWARDED_BASE_URL") or "" ).strip(), log_level_name=(os.getenv("LOG_LEVEL", "INFO")).strip().upper(), - project_endpoint=project_endpoint, ) diff --git a/src/a2a_servers/test_client.py b/src/a2a_servers/test_client.py index d921ff5..1e5b451 100644 --- a/src/a2a_servers/test_client.py +++ b/src/a2a_servers/test_client.py @@ -320,7 +320,7 @@ async def main() -> None: logger = logging.getLogger(__name__) definitions = load_agent_definitions(args.agent_config_dir) - settings = load_server_settings(require_project_endpoint=False) + settings = load_server_settings() if args.base_url: logger.info("Using explicit base URL override: %s", args.base_url.rstrip("/")) selected_definitions = definitions diff --git a/src/a2a_servers/tests/test_agent_definition.py b/src/a2a_servers/tests/test_agent_definition.py index eb8c6e1..ee8eea8 100644 --- a/src/a2a_servers/tests/test_agent_definition.py +++ b/src/a2a_servers/tests/test_agent_definition.py @@ -29,6 +29,7 @@ [foundry] agent_name = "math-foundry-agent" + endpoint_alias = "math" [[skills]] id = "math" @@ -113,6 +114,11 @@ def test_load_valid_definition(tmp_path: Path) -> None: assert defn.slug == "math" # derived from filename assert defn.public_name == "Math Agent" assert defn.foundry_agent_name == "math-foundry-agent" + assert defn.foundry_endpoint_alias == "math" + assert ( + defn.foundry_project_endpoint + == "https://math.services.ai.azure.com/api/projects/project-math" + ) assert defn.version == "1.0.0" assert len(defn.skills) == 1 assert defn.skills[0].id == "math" @@ -132,6 +138,7 @@ def test_load_explicit_slug_overrides_filename(tmp_path: Path) -> None: [foundry] agent_name = "math-foundry-agent" + endpoint_alias = "math" [[skills]] id = "math" @@ -236,6 +243,52 @@ def test_missing_foundry_agent_name_raises(tmp_path: Path) -> None: load_agent_definition(p) +def test_missing_foundry_endpoint_alias_raises(tmp_path: Path) -> None: + content = _VALID_TOML.replace('endpoint_alias = "math"\n', "") + p = _write_toml(tmp_path, "math_agent.toml", content) + with pytest.raises(ValueError, match="foundry.endpoint_alias"): + load_agent_definition(p) + + +def test_missing_endpoint_env_for_alias_raises( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + content = _VALID_TOML.replace('endpoint_alias = "math"', 'endpoint_alias = "quote"') + p = _write_toml(tmp_path, "math_agent.toml", content) + monkeypatch.delenv("AZURE_AI_PROJECT_ENDPOINT", raising=False) + with pytest.raises(ValueError, match="AZURE_AI_PROJECT_ENDPOINT_QUOTE"): + load_agent_definition(p) + + +def test_alias_uses_global_endpoint_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("AZURE_AI_PROJECT_ENDPOINT_MATH", raising=False) + monkeypatch.setenv( + "AZURE_AI_PROJECT_ENDPOINT", + "https://shared.services.ai.azure.com/api/projects/shared-project", + ) + p = _write_toml(tmp_path, "math_agent.toml", _VALID_TOML) + defn = load_agent_definition(p) + assert defn.foundry_endpoint_alias == "math" + assert ( + defn.foundry_project_endpoint + == "https://shared.services.ai.azure.com/api/projects/shared-project" + ) + + +def test_endpoint_alias_is_normalized(tmp_path: Path) -> None: + content = _VALID_TOML.replace('endpoint_alias = "math"', 'endpoint_alias = " Prod East "') + p = _write_toml(tmp_path, "math_agent.toml", content) + defn = load_agent_definition(p) + assert defn.foundry_endpoint_alias == "prod_east" + + +def test_invalid_endpoint_alias_raises(tmp_path: Path) -> None: + content = _VALID_TOML.replace('endpoint_alias = "math"', 'endpoint_alias = "---"') + p = _write_toml(tmp_path, "math_agent.toml", content) + with pytest.raises(ValueError, match="endpoint alias"): + load_agent_definition(p) + + # --------------------------------------------------------------------------- # load_agent_definition – type validation # --------------------------------------------------------------------------- @@ -281,6 +334,7 @@ def test_duplicate_slugs_raises(tmp_path: Path) -> None: [foundry] agent_name = "{foundry}" + endpoint_alias = "math" [[skills]] id = "s" @@ -304,6 +358,7 @@ def test_duplicate_foundry_names_raises(tmp_path: Path) -> None: [foundry] agent_name = "shared-foundry-agent" + endpoint_alias = "shared" [[skills]] id = "s" @@ -312,10 +367,34 @@ def test_duplicate_foundry_names_raises(tmp_path: Path) -> None: """) _write_toml(tmp_path, name, toml_content) - with pytest.raises(ValueError, match="Duplicate Foundry agent name"): + with pytest.raises(ValueError, match="Duplicate Foundry agent target"): load_agent_definitions(str(tmp_path)) +def test_duplicate_foundry_names_allowed_across_endpoint_aliases(tmp_path: Path) -> None: + for name, alias in (("alpha_agent.toml", "east"), ("beta_agent.toml", "west")): + toml_content = textwrap.dedent(f"""\ + [a2a] + name = "Agent {alias}" + description = "desc" + version = "1.0.0" + health_message = "ok" + + [foundry] + agent_name = "shared-foundry-agent" + endpoint_alias = "{alias}" + + [[skills]] + id = "s" + name = "S" + description = "desc" + """) + _write_toml(tmp_path, name, toml_content) + + definitions = load_agent_definitions(str(tmp_path)) + assert len(definitions) == 2 + + def test_load_multiple_valid_definitions(tmp_path: Path) -> None: for slug, foundry in (("math", "math-agent"), ("quote", "quote-agent")): toml_content = textwrap.dedent(f"""\ @@ -327,6 +406,7 @@ def test_load_multiple_valid_definitions(tmp_path: Path) -> None: [foundry] agent_name = "{foundry}" + endpoint_alias = "math" [[skills]] id = "skill-{slug}" @@ -349,3 +429,15 @@ def test_empty_config_dir_raises(tmp_path: Path) -> None: def test_nonexistent_config_dir_raises(tmp_path: Path) -> None: with pytest.raises(FileNotFoundError, match="not found"): load_agent_definitions(str(tmp_path / "does_not_exist")) + + +@pytest.fixture(autouse=True) +def _set_default_foundry_endpoint_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv( + "AZURE_AI_PROJECT_ENDPOINT_MATH", + "https://math.services.ai.azure.com/api/projects/project-math", + ) + monkeypatch.setenv( + "AZURE_AI_PROJECT_ENDPOINT", + "https://shared.services.ai.azure.com/api/projects/shared-project", + ) From 9f7d2c354bc097c3989daa0820ee349c12423350 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 03:51:16 +0000 Subject: [PATCH 3/4] Format endpoint alias updates and revalidate Co-authored-by: judacas <67698498+judacas@users.noreply.github.com> Agent-Logs-Url: https://github.com/judacas/LLM-Automated-Inventory-Management/sessions/23831b2f-7f88-4cfd-bf69-af9249880a5f --- src/a2a_servers/agent_definition.py | 4 ++-- src/a2a_servers/tests/test_agent_definition.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/a2a_servers/agent_definition.py b/src/a2a_servers/agent_definition.py index f731842..5b73c39 100644 --- a/src/a2a_servers/agent_definition.py +++ b/src/a2a_servers/agent_definition.py @@ -149,8 +149,8 @@ def load_agent_definition(config_path: str | Path) -> AgentDefinition: raise ValueError("`[smoke_tests]` must be a table if provided") foundry_agent_name = _read_required_string(foundry, "agent_name", "foundry") - foundry_endpoint_alias, foundry_project_endpoint = _resolve_foundry_project_endpoint( - foundry + foundry_endpoint_alias, foundry_project_endpoint = ( + _resolve_foundry_project_endpoint(foundry) ) skills: list[AgentSkill] = [] diff --git a/src/a2a_servers/tests/test_agent_definition.py b/src/a2a_servers/tests/test_agent_definition.py index ee8eea8..07c3013 100644 --- a/src/a2a_servers/tests/test_agent_definition.py +++ b/src/a2a_servers/tests/test_agent_definition.py @@ -260,7 +260,9 @@ def test_missing_endpoint_env_for_alias_raises( load_agent_definition(p) -def test_alias_uses_global_endpoint_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_alias_uses_global_endpoint_fallback( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.delenv("AZURE_AI_PROJECT_ENDPOINT_MATH", raising=False) monkeypatch.setenv( "AZURE_AI_PROJECT_ENDPOINT", @@ -276,7 +278,9 @@ def test_alias_uses_global_endpoint_fallback(tmp_path: Path, monkeypatch: pytest def test_endpoint_alias_is_normalized(tmp_path: Path) -> None: - content = _VALID_TOML.replace('endpoint_alias = "math"', 'endpoint_alias = " Prod East "') + content = _VALID_TOML.replace( + 'endpoint_alias = "math"', 'endpoint_alias = " Prod East "' + ) p = _write_toml(tmp_path, "math_agent.toml", content) defn = load_agent_definition(p) assert defn.foundry_endpoint_alias == "prod_east" @@ -371,7 +375,9 @@ def test_duplicate_foundry_names_raises(tmp_path: Path) -> None: load_agent_definitions(str(tmp_path)) -def test_duplicate_foundry_names_allowed_across_endpoint_aliases(tmp_path: Path) -> None: +def test_duplicate_foundry_names_allowed_across_endpoint_aliases( + tmp_path: Path, +) -> None: for name, alias in (("alpha_agent.toml", "east"), ("beta_agent.toml", "west")): toml_content = textwrap.dedent(f"""\ [a2a] From a5a27797f7fed11ff2b7a00e1de5a16dccd87584 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 03:53:14 +0000 Subject: [PATCH 4/4] Address review feedback and finalize validations Co-authored-by: judacas <67698498+judacas@users.noreply.github.com> Agent-Logs-Url: https://github.com/judacas/LLM-Automated-Inventory-Management/sessions/23831b2f-7f88-4cfd-bf69-af9249880a5f --- src/a2a_servers/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/a2a_servers/__main__.py b/src/a2a_servers/__main__.py index a6c5e9e..2a3fb24 100644 --- a/src/a2a_servers/__main__.py +++ b/src/a2a_servers/__main__.py @@ -69,7 +69,9 @@ def _log_agent_startup(mounted_agent: MountedAgent, settings: ServerSettings) -> logger.info("Agent slug: %s", definition.slug) logger.info("Loaded agent config from %s", definition.source_path) logger.info("Foundry agent name: %s", definition.foundry_agent_name) - endpoint_host = urlparse(definition.foundry_project_endpoint).netloc or "" + endpoint_host = ( + urlparse(definition.foundry_project_endpoint).netloc or "" + ) logger.info("Foundry endpoint alias: %s", definition.foundry_endpoint_alias) logger.info("Foundry endpoint host: %s", endpoint_host) logger.info("Agent card: %s", mounted_agent.agent_card.name)