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
11 changes: 9 additions & 2 deletions src/a2a_servers/.env.template
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# Azure AI Foundry configuration template. Copy to `.env` and populate.
#
# Required values
AZURE_AI_PROJECT_ENDPOINT=https://<your-ai-services>.services.ai.azure.com/api/projects/<your-project>
# Required values:
# For each endpoint alias used in your `agents/*_agent.toml` files, define:
# AZURE_AI_PROJECT_ENDPOINT_<ALIAS_UPPER>=https://<your-ai-services>.services.ai.azure.com/api/projects/<your-project>
#
# Example for `endpoint_alias = "contoso_main"`:
AZURE_AI_PROJECT_ENDPOINT_CONTOSO_MAIN=https://<your-ai-services>.services.ai.azure.com/api/projects/<your-project>
#
# 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
Expand Down
2 changes: 1 addition & 1 deletion src/a2a_servers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `/<slug>/`.
- 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

Expand Down
6 changes: 6 additions & 0 deletions src/a2a_servers/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from urllib.parse import urlparse

import click
import uvicorn
Expand Down Expand Up @@ -68,6 +69,11 @@ 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 "<invalid endpoint URL>"
)
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])
Expand Down
52 changes: 47 additions & 5 deletions src/a2a_servers/agent_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, ...]
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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"]
),
Expand All @@ -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:
Expand All @@ -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
1 change: 1 addition & 0 deletions src/a2a_servers/agents/agent.template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ streaming = false

[foundry]
agent_name = "Your-Foundry-Agent-Name"
endpoint_alias = "your_endpoint_alias"

[smoke_tests]
prompts = [
Expand Down
1 change: 1 addition & 0 deletions src/a2a_servers/agents/email_agent.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ streaming = false

[foundry]
agent_name = "email"
endpoint_alias = "contoso_main"

[smoke_tests]
prompts = [
Expand Down
1 change: 1 addition & 0 deletions src/a2a_servers/agents/math_agent.sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ streaming = false

[foundry]
agent_name = "Math-Agent"
endpoint_alias = "math_lab"

[smoke_tests]
prompts = [
Expand Down
1 change: 1 addition & 0 deletions src/a2a_servers/agents/purchase_order_agent.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ streaming = false

[foundry]
agent_name = "userPurchaseOrder"
endpoint_alias = "contoso_main"

[smoke_tests]
prompts = [
Expand Down
1 change: 1 addition & 0 deletions src/a2a_servers/agents/quote_agent.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ streaming = false

[foundry]
agent_name = "userQuote"
endpoint_alias = "contoso_main"

[smoke_tests]
prompts = [
Expand Down
5 changes: 1 addition & 4 deletions src/a2a_servers/app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 17 additions & 2 deletions src/a2a_servers/docs/agent-definition-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<ENDPOINT_ALIAS_UPPER>`

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

Expand Down Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/a2a_servers/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -157,7 +157,7 @@ There are two layers of configuration:

Examples:

- `AZURE_AI_PROJECT_ENDPOINT`
- `AZURE_AI_PROJECT_ENDPOINT_<ALIAS_UPPER>` (per `foundry.endpoint_alias`)
- `A2A_HOST`
- `A2A_PORT`
- `A2A_URL_MODE`
Expand Down
4 changes: 2 additions & 2 deletions src/a2a_servers/docs/deployment-azure.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Optional but recommended:
These settings must be present in the deployed app:

```dotenv
AZURE_AI_PROJECT_ENDPOINT=https://<your-ai-services>.services.ai.azure.com/api/projects/<your-project>
AZURE_AI_PROJECT_ENDPOINT_<ALIAS_UPPER>=https://<your-ai-services>.services.ai.azure.com/api/projects/<your-project>
A2A_AGENT_CONFIG_DIR=agents
A2A_HOST=0.0.0.0
A2A_PORT=8000
Expand Down Expand Up @@ -156,7 +156,7 @@ Set the required app settings listed earlier.

Most importantly:

- `AZURE_AI_PROJECT_ENDPOINT`
- `AZURE_AI_PROJECT_ENDPOINT_<ALIAS_UPPER>` for every alias used by deployed agents
- `A2A_URL_MODE=forwarded`
- `A2A_FORWARDED_BASE_URL=https://<app-hostname>`

Expand Down
8 changes: 7 additions & 1 deletion src/a2a_servers/docs/developer-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ cp .env.template .env
Minimum required value:

```dotenv
AZURE_AI_PROJECT_ENDPOINT=https://<your-ai-services>.services.ai.azure.com/api/projects/<your-project>
AZURE_AI_PROJECT_ENDPOINT_<ALIAS_UPPER>=https://<your-ai-services>.services.ai.azure.com/api/projects/<your-project>
```

Example:

```dotenv
AZURE_AI_PROJECT_ENDPOINT_CONTOSO_MAIN=https://<your-ai-services>.services.ai.azure.com/api/projects/<your-project>
```

Recommended development values:
Expand Down
33 changes: 20 additions & 13 deletions src/a2a_servers/docs/foundry-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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_<ALIAS_UPPER>=https://<your-ai-services>.services.ai.azure.com/api/projects/<your-project>
```

Example:

```dotenv
AZURE_AI_PROJECT_ENDPOINT_CONTOSO_MAIN=https://<your-ai-services>.services.ai.azure.com/api/projects/<your-project>
```

Optional backward-compatible fallback:

```dotenv
AZURE_AI_PROJECT_ENDPOINT=https://<your-ai-services>.services.ai.azure.com/api/projects/<your-project>
Expand Down Expand Up @@ -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_<ALIAS_UPPER>` 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

Expand Down
3 changes: 2 additions & 1 deletion src/a2a_servers/docs/runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ When something seems wrong, check these in order:
2. `GET /<slug>/health` returns the configured health message.
3. `GET /<slug>/.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_<ALIAS_UPPER>` environment variable.
6. local Azure credentials are valid for that project.

## Shutdown Behavior
Expand Down
5 changes: 3 additions & 2 deletions src/a2a_servers/docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@

Symptom:

- startup raises a `ValueError` about `AZURE_AI_PROJECT_ENDPOINT`
- startup raises a `ValueError` about missing `AZURE_AI_PROJECT_ENDPOINT_<ALIAS>`

Cause:

- the required environment variable is missing or empty

Fix:

- set `AZURE_AI_PROJECT_ENDPOINT` in `.env` or the deployment environment
- set `AZURE_AI_PROJECT_ENDPOINT_<ALIAS_UPPER>` for each `foundry.endpoint_alias`
used by mounted agents (or set `AZURE_AI_PROJECT_ENDPOINT` fallback)

## Startup Fails Because No Agent Configs Were Found

Expand Down
Loading
Loading