Compare commits

..
Author SHA1 Message Date
f76a6c8436 Update python/samples/02-agents/observability/agent_observability.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 08:53:45 -07:00
3c8ffac336 Update python/samples/02-agents/observability/agent_observability.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 08:53:29 -07:00
24d87a7789 Update python/samples/02-agents/observability/configure_otel_providers_with_parameters.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 08:52:21 -07:00
Tao Chen bb7b7fa625 Fix observability samples 2026-03-31 16:55:08 -07:00
3a49b1d6dd Python: [BREAKING] Remove deprecated Python OpenAI/Azure AI surfaces (#4990)
* [BREAKING] Remove deprecated Python OpenAI/Azure AI surfaces

Also clean up follow-on docs, environment guidance, package metadata, and lab test stability.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix deleted semantic-kernel sample links

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* improve foundry language

* Fix A2A Foundry sample regression

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 20:36:21 +00:00
a5eacbbe65 Python: Add Python A2A agent-as-function-tools sample (#4889)
* Add Python A2A agent-as-function-tools sample

Port of the .NET A2AAgent_AsFunctionTools sample to Python.
Resolves a remote A2A agent card, converts each skill to a
FunctionTool via as_tool(), and registers them with a host agent
using AzureOpenAIResponsesClient.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Sanitize A2A skill names before passing to as_tool()

as_tool() only auto-sanitizes when name is omitted. Since we pass
skill.name explicitly, we need to strip special characters ourselves.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 20:00:40 +00:00
55b6e7a9f4 Python: Add Python feature lifecycle decorators for released APIs (#4975)
* Add Python feature lifecycle decorators

Introduce reusable experimental and release-candidate decorators for released packages, migrate the Skills APIs to the new staged metadata and warning system, and add lifecycle guidance plus samples.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Python CI follow-ups

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Preserve protocol runtime checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 19:40:08 +00:00
9c9d81d8b6 .NET: Improve README: architecture overview, troubleshooting, and sample links (#5002)
* Fix README issues: simplify Azure quickstart, add missing sample links, fix typo

- Replace BearerTokenPolicy Azure snippet with simpler AzureOpenAIClient + DefaultAzureCredential pattern
- Add missing sample links for Python (04-hosting, 05-end-to-end) and .NET (01-get-started, 04-hosting, 05-end-to-end)
- Fix 'infererence' typo in dotnet/samples/README.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add architecture overview and troubleshooting sections to README

- Add ASCII architecture diagram showing AIAgent and Workflow pipelines
- Add agent-vs-workflow decision table with 8 common scenarios
- Add troubleshooting section for authentication issues and environment variables
- Fix 'infererence' typo in dotnet/samples/README.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix architecture diagram alignment

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix diagram

* Update architecture diagram and rename Azure AI Foundry to Microsoft Foundry

- Add A2AAgent and Skills to the architecture diagram
- Rename Azure AI Foundry references to Microsoft Foundry
- Add A2AAgent to agent type descriptions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* adderss comments

* address PR review comments

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 18:44:27 +00:00
westeyandGitHub 47a8a305d2 Fix environment variable set statement in py DEV_SETUP (#5006) 2026-03-31 18:34:04 +00:00
westeyandGitHub 6e7254bba7 .NET: [BREAKING] Rename from ServiceStoredSimulatingChatClient to PerServiceCallChatHistoryPersistingChatClient (#4993)
* Rename from ServiceStoredSimulatingChatClient to PerServiceCallChatHistoryPersistingChatClient

* Address PR comment
2026-03-31 17:32:05 +00:00
9c57680f00 Python: Add header_provider to Streamable HTTP MCP servers (#4849)
* Python: Add header_provider to MCPStreamableHTTPTool (#4808)

Add a header_provider callback parameter to MCPStreamableHTTPTool that
enables injecting dynamic per-request HTTP headers from runtime kwargs
(originating from FunctionInvocationContext.kwargs set in agent middleware).

The implementation uses contextvars and httpx event hooks to ensure headers
are task-local and safe for concurrent tool calls:

- header_provider receives the runtime kwargs dict and returns headers
- call_tool sets a ContextVar before delegating to MCPTool.call_tool
- An httpx request event hook reads from the ContextVar and injects headers

Example usage:
    mcp_tool = MCPStreamableHTTPTool(
        name="web-api",
        url="https://api.example.com/mcp",
        header_provider=lambda kwargs: {
            "X-Auth-Token": kwargs.get("auth_token", ""),
        },
    )

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4808: Python: [Bug]: Unable to pass AgentContext to MCPStreamableHTTPTool

* Add test for header_provider via FunctionTool.invoke with FunctionInvocationContext

Addresses PR review comment: exercises the full pipeline from
FunctionInvocationContext.kwargs through FunctionTool.invoke to
MCPStreamableHTTPTool.call_tool and header_provider, rather than
testing call_tool in isolation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4808: review comment fixes

* Fix streamable MCP transport defaults

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Azure AI test client mocks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix MCP runtime kwarg regressions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Stabilize MCP tool runtime kwargs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Use context kwargs in MCP wrappers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated mcp samples

* fix link

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 17:23:49 +00:00
7c2dae8855 Python: Fix sample bugs: incorrect API params, wrong client types, and invalid options (#4983)
* Fix sample bugs: incorrect API params, wrong client types, and invalid options

- typed_options.py: Fix AnthropicClient model->model_id, wrap raw strings in Message objects for get_response(), fix reasoning_effort->reasoning dict, fix budget_tokens minimum (1024), use OpenAIChatClient not FoundryChatClient, remove unused import

- client_reasoning.py: Fix deprecated model_id to model param

- client_with_hosted_mcp.py: Remove invalid store=True kwarg from Agent.run()

- code_defined_skill.py: Fix precision kwarg to use function_invocation_kwargs

- Various other samples: Fix deprecated API usage and incorrect params

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review comments

- client_with_hosted_mcp.py: Fix remaining store=True kwarg on line 68 to use options dict

- client_with_session.py: Change store=True to store=False to match in-memory persistence demo intent

- typed_options.py: Remove non-existent import and model key from docstring example

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* new sample fixes

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 16:58:51 +00:00
Peter IbekweandGitHub 3d09337446 Update project name from 'Semantic Kernel' to 'Agent Framework' (#5001) 2026-03-31 16:52:44 +00:00
3c727b5b71 Improve CONTRIBUTING.md with dev setup links and docs guidance (#5000)
* Improve CONTRIBUTING.md with dev setup links and docs guidance

- Consolidate Development Scripts into a Development Setup section with
  quick links to language-specific dev guides and coding standards
- Add Python build/test/lint commands alongside existing .NET commands
- Add Documentation Contributions section with link checker, writing
  guidelines, and style guidance

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Use directory note for .NET commands, matching Python style

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Split test commands into unit vs. integration for both Python and .NET

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove Documentation Contributions section

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 16:42:52 +00:00
35adfdb318 Python: Foundry Evals integration for Python (#4750)
* Foundry Evals integration for Python

Merged and refactored eval module per Eduard's PR review:

- Merge _eval.py + _local_eval.py into single _evaluation.py
- Convert EvalItem from dataclass to regular class
- Rename to_dict() to to_eval_data()
- Convert _AgentEvalData to TypedDict
- Simplify check system: unified async pattern with isawaitable
- Parallelize checks and evaluators with asyncio.gather
- Add all/any mode to tool_called_check
- Fix bool(passed) truthy bug in _coerce_result
- Remove deprecated function_evaluator/async_function_evaluator aliases
- Remove _MinimalAgent, tighten evaluate_agent signature
- Set self.name in __init__ (LocalEvaluator, FoundryEvals)
- Limit FoundryEvals to AsyncOpenAI only
- Type project_client as AIProjectClient
- Remove NotImplementedError continuous eval code
- Add evaluation samples in 02-agents/ and 03-workflows/
- Update all imports and tests (167 passing)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: resolve mypy redundant-cast errors while keeping pyright happy

Use cast(list[Any], x) with type: ignore[redundant-cast] comments to
satisfy both mypy (which considers casting Any redundant) and pyright
strict mode (which needs explicit casts to narrow Unknown types).

Also fix evaluator decorator check_name type annotation to be
explicitly str, resolving mypy str|Any|None mismatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: CI failures — pyupgrade, evaluator overloads, sample API, reset attr

- Apply pyupgrade: Sequence from collections.abc, remove forward-ref quotes
- Add @overload signatures to evaluator() for proper @evaluator usage
- Fix evaluate_workflow sample to use WorkflowBuilder(start_executor=) API
- Fix _workflow.py executor.reset() to use getattr pattern for pyright
- Remove unused EvalResults forward-ref string in default_factory lambda

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: skip gRPC-dependent observability test

The test_configure_otel_providers_with_env_file_and_vs_code_port test
triggers gRPC OTLP exporter creation, but the grpc dependency is
optional and not installed by default. Add skipif decorator matching
the pattern used by all other gRPC exporter tests in the same file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add nosec B101 for bandit assert check

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* style: align eval samples with repo conventions

- Move module docstrings before imports (after copyright header)
- Add -> None return type to all main() and helper functions
- Fix line-too-long in multiturn sample conversation data
- Add Workflow import for typed return in all_patterns_sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback: async fixes, sample bugs, deprecation warnings

- Simplify _ensure_async_result to direct await (async-only clients)
- Replace get_event_loop() with get_running_loop()
- Narrow _fetch_output_items exception handling to specific types
- Add warning log when _filter_tool_evaluators falls back to defaults
- Add DeprecationWarning to options alias in Agent.__init__
- Add DeprecationWarning to evaluate_response()
- Rename raw key to _raw_arguments in convert_message fallback
- Fix evaluate_agent_sample.py: replace evals.select() with FoundryEvals()
- Fix evaluate_multiturn_sample.py: use Message/Content/FunctionTool types
- Fix evaluate_workflow_sample.py: replace evals.select() with FoundryEvals()
- Update test mocks to use AsyncMock for awaited API calls

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add test coverage for review feedback items

- Add num_repetitions=2 positive test verifying 2Ă—items and 4 agent calls
- Add _poll_eval_run tests: timeout, failed, and canceled paths
- Add evaluate_traces tests: validation error, response_ids path, trace_ids path
- Add evaluate_foundry_target happy-path test with target/query verification

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix ruff ISC004 lint error and apply formatter

- Wrap implicit string concatenation in parens in evaluate_multiturn_sample.py
- Apply ruff formatter to 6 other files with minor formatting drift

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove core type changes (extracted to fix/workflow-stale-session branch)

Reverts changes to _agents.py, _agent_executor.py, and _workflow.py
back to upstream/main. These fixes are now in a separate PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review round 2: bugs, tests, and architecture

Code fixes:
- Fix _normalize_queries inverted condition (single query now replicates
  to match expected_count)
- Fix substring match bug: 'end' in 'backend' matched; use exact set
  lookup for executor ID filtering
- Fix used_available_tools sample: tool_definitions→tools param, use
  FunctionTool attribute access instead of dict .get()
- Add None-check in _resolve_openai_client for misconfigured project
- Add Returns section to evaluate_workflow docstring
- Cache inspect.signature in @evaluator wrapper (avoid per-item reflection)

Architecture:
- Extract _evaluate_via_responses as module-level helper; evaluate_traces
  now calls it directly instead of creating a FoundryEvals instance
- Move Foundry-specific typed-content conversion out of core to_eval_data;
  core now returns plain role/content dicts, FoundryEvals applies
  AgentEvalConverter in _evaluate_via_dataset

Tests:
- evaluate_response() deprecation warning emission and delegation
- num_repetitions > 1 with expected_output and expected_tool_calls
- Mock output_items.list in test_evaluate_calls_evals_api
- Update to_eval_data assertions for plain-dict format
- Unknown param error now raised at @evaluator decoration time

Skipped (separate PR): executor reset loop, xfail removal, options alias

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix CI: revert test_full_conversation, fix pyright errors

- Revert test_full_conversation.py to upstream/main (the session
  preservation test was incorrectly changed to assert clearing)
- Fix pyright reportUnnecessaryComparison on get_openai_client() None
  check by adding ignore comment
- Fix pyright reportPrivateUsage: add public EvalItem.split_messages()
  method and use it in FoundryEvals._evaluate_via_dataset instead of
  accessing private _split_conversation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review round 3: reliability, test gaps, cleanup

- Add try/except guard for non-numeric score in _coerce_result
- Add poll_interval minimum bound (0.1s) to prevent tight loops
- Add runtime async client check in _resolve_openai_client
- Remove _ensure_async_result wrapper (10 call sites → direct await)
- Better error message when queries provided without agent
- Import-time asserts for evaluator set consistency
- Remove 28 redundant @pytest.mark.asyncio decorators
- Add doc note about _raw_arguments sensitive data
- Tests: tool_called_check mode=any, _normalize_queries branches,
  _extract_result_counts paths, _extract_per_evaluator, bare check
  via evaluate_agent, output_items assertion, modulo wrapping,
  async client check, queries-without-agent error

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix CI: ruff S101 assert, pyright and mypy arg-type errors

- Replace module-level assert with if/raise for evaluator set
  consistency checks (ruff S101 disallows bare assert)
- Add type: ignore[arg-type] and pyright: ignore[reportArgumentType]
  on OpenAI SDK evals API calls that pass dicts where typed params
  are expected (SDK accepts dicts at runtime)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review round 4: bugs, reliability, test fixes

- Fix all_passed ignoring parent result_counts when sub_results present
- Fix _extract_tool_calls: parse string arguments via json.loads before
  falling back to None (real LLM responses use string arguments)
- Sanitize _raw_arguments to '[unparseable]' to avoid leaking sensitive
  tool-call data to external evaluation services
- Add NOTE comment on to_eval_data message serialization dropping
  non-text content (tool calls, results)
- Eliminate double conversation split in _evaluate_via_dataset: build
  JSONL dicts directly from split_messages + AgentEvalConverter
- Raise poll_interval floor from 0.1s to 1.0s to prevent rate-limit
  exhaustion
- Fix MagicMock(name=...) bug in test: sets display name not .name attr
- Fix mock_output_item.sample: use MagicMock object instead of dict so
  _fetch_output_items exercises error/usage/input/output extraction

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review round 5: reliability, docs, test coverage

Code fixes:
- Move import-time RuntimeError checks to unit tests (avoids breaking
  imports for all users on developer set-drift mistake)
- _filter_tool_evaluators now raises ValueError when all evaluators
  require tools but no items have tools (was silently substituting)
- Add poll_interval upper bound (60s) to prevent single-iteration sleep
- Log exc_info=True in _fetch_output_items for debugging API changes
- Fix evaluate() docstring: remove claim about Responses API optimization
- Validate target dict has 'type' key in evaluate_foundry_target
- Document to_eval_data() limitation: non-text content is omitted

Tests:
- TestEvaluatorSetConsistency: verify _AGENT/_TOOL subsets of _BUILTIN
- TestEvaluateTracesAgentId: agent_id-only path with lookback_hours
- TestFilterToolEvaluatorsRaises: ValueError on all-tool no-items
- TestEvaluateFoundryTargetValidation: target without 'type' key
- Assert items==[] on failed/canceled poll results
- Mock output_items.list in response_ids test for full flow
- TestAllPassedSubResults: result_counts=None + sub_results delegation
  and parent failures override sub_results
- TestBuildOverallItemEmpty: empty workflow outputs returns None

Skipped r5-07 (_raw_arguments length hint): marginal debugging value,
could leak content size information.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix error message: evaluate_responses() → evaluate_traces(response_ids=...)

The referenced function doesn't exist; the correct API is
evaluate_traces(response_ids=...) from the azure-ai package.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove dead to_eval_data() method, fix docstring claims

- Remove to_eval_data() from EvalItem (dead code after r4-05 JSONL refactor)
- Migrate 15 tests from to_eval_data() to split_messages()
- Update sample to use split_messages() + Message properties
- Remove unimplemented Responses API optimization docstring claim
- Update split_messages() docstring to not reference removed method

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Reduce default eval timeout from 600s to 180s (3 minutes)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove dead _evaluate_via_responses method from FoundryEvals

The method was never called — evaluate() uses _evaluate_via_dataset,
and evaluate_traces() calls _evaluate_via_responses_impl directly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert unrelated formatting changes to get-started samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix pyright: remove phantom FoundryMemoryProvider import, apply ruff format

- Remove import of non-existent _foundry_memory_provider module
  (incorrectly kept during rebase conflict resolution)
- Apply ruff formatter to test_local_eval.py and get-started samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix eval samples: use FoundryChatClient for Agent()

The upstream provider-leading client refactor (#4818) made client=
a required parameter on Agent(). Update the three getting-started
eval samples to use FoundryChatClient with FOUNDRY_PROJECT_ENDPOINT,
matching the standard pattern from 01-get-started samples.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify self-reflection sample using FoundryEvals

Replace ~80 lines of manual OpenAI evals API code (create_eval,
run_eval, manual polling, raw JSONL params) with FoundryEvals:

- evaluate_groundedness() uses FoundryEvals.evaluate() with EvalItem
- Remove create_openai_client(), create_eval(), run_eval() functions
- Remove openai SDK type imports (DataSourceConfigCustom, etc.)
- run_self_reflection_batch creates FoundryEvals instance once,
  reuses it for all iterations across all prompts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update eval samples to FoundryChatClient and FOUNDRY_PROJECT_ENDPOINT

- Migrate all foundry_evals samples from AzureOpenAIResponsesClient to FoundryChatClient
- Update env var from AZURE_AI_PROJECT_ENDPOINT to FOUNDRY_PROJECT_ENDPOINT
- Use AzureCliCredential consistently across all samples
- Fix README.md: correct function names (evaluate_dataset -> FoundryEvals.evaluate, evaluate_responses -> evaluate_traces)
- Update self_reflection .env.example and README.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix lint errors in eval samples (E501, ASYNC240, formatting)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove evaluate_all_patterns_sample.py (redundant with focused samples)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix async credential mismatch: use azure.identity.aio for async AIProjectClient

AIProjectClient from azure.ai.projects.aio requires an async credential.
Switch all foundry_evals samples from azure.identity.AzureCliCredential
to azure.identity.aio.AzureCliCredential. Also pass project_client to
FoundryChatClient instead of duplicating endpoint+credential.

Close credential in self_reflection sample to avoid resource leak.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert test_observability.py to upstream/main (not our test)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address moonbox3 review: sphinx docstrings, pagination, isinstance check

- Convert all Example:: / Typical usage:: code blocks to .. code-block:: python
  format matching codebase convention (both _evaluation.py and _foundry_evals.py)
- Add async pagination in _fetch_output_items via async for (handles large result sets)
- Replace hasattr(__aenter__) with isinstance(client, AsyncOpenAI) in _resolve_openai_client
- Move AsyncOpenAI import from TYPE_CHECKING to runtime (needed for isinstance)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix test failures and address remaining moonbox3 review comments

- Fix tests: use MagicMock(spec=AsyncOpenAI) for project_client mocks
  (isinstance check now requires proper type, not duck-typing)
- Fix tests: replace mock_page.__iter__ with _AsyncPage helper for async for
- Fix evaluate_response: auto-extract queries from response messages when
  query is not provided (previously always raised ValueError)
- Add debug logging when skipping internal _-prefixed executor IDs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Tao's PR review comments on Foundry Evals

- T1: Add comment explaining builtin.* pass-through in _resolve_evaluator
- T2: Add comment referencing OpenAI evals API for testing_criteria dict
- T3: Document Mustache-style {{item.*}} template placeholders
- T4: Document poll loop 60s sleep upper bound rationale
- T5: Narrow run type to RunRetrieveResponse, use typed field access
  instead of vars()/getattr dance in _extract_result_counts and
  _extract_per_evaluator; use run.error and run.report_url directly
- T6: Clarify openai_client docstring re: Azure Foundry endpoint
- T8: Remove misleading empty expected_tool_calls from sample
- Update tests to match real SDK PerTestingCriteriaResult shape

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove unnecessary Any union from run type annotations

RunRetrieveResponse is the correct type — no backward compat needed
for a brand new feature.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Accept FoundryChatClient instead of raw AsyncOpenAI

FoundryEvals now takes client: FoundryChatClient as its primary
parameter instead of openai_client: AsyncOpenAI.  The builtin.*
evaluators require a Foundry endpoint, so the type should reflect that.

- FoundryEvals.__init__: client: FoundryChatClient replaces openai_client
- evaluate_traces / evaluate_foundry_target: same change
- _resolve_openai_client: extracts .client from FoundryChatClient
- project_client fallback retained for standalone functions
- All samples updated to construct FoundryChatClient and pass as client=
- Tests updated (openai_client= → client=)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove implicit 60s upper bound on poll interval

If a developer sets a higher poll_interval, respect it. Only clamp
to remaining time and enforce a 1s minimum for rate-limit protection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove 1s floor on poll interval — let the developer control it

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update python/samples/05-end-to-end/evaluation/foundry_evals/.env.example

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Update python/samples/02-agents/evaluation/evaluate_agent.py

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Address eavanvalkenburg review (round 2) on Python eval PR

- Rename model_deployment -> model across FoundryEvals and all samples
- Make model param optional, resolves from client.model
- Convert EvalResults from dataclass to regular class
- Remove deprecated evaluate_response() function
- Refactor splitters: BUILT_IN_SPLITTERS dict + standalone functions
- Change per_turn_items from classmethod to staticmethod
- Simplify EvalCheck type alias to use Awaitable[CheckResult]
- Remove errored property from EvalResults
- Remove default value from Evaluator protocol eval_name
- Rename assert_passed -> raise_for_status, add EvalNotPassedError
- Type agent param as SupportsAgentRun | None
- Fix Arguments docstring
- Update __init__.py exports
- Update all tests and samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Move FoundryEvals to foundry package, split tool eval sample

- Move _foundry_evals.py from azure-ai to foundry package
- Move test_foundry_evals.py to foundry/tests/
- Update lazy re-exports in agent_framework.foundry namespace
- Update .pyi type stubs
- All samples now import from agent_framework.foundry
- Split tool-call evaluation into evaluate_tool_calls_sample.py
- Fix all_passed to check errored count from result_counts
- Fix raise_for_status to include errored item details

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Auto-create FoundryChatClient from env vars when no client provided

FoundryEvals() now works zero-config when FOUNDRY_PROJECT_ENDPOINT and
FOUNDRY_MODEL environment variables are set. Auto-creates a FoundryChatClient
under the hood, matching the established env var pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix pyright errors: remove dead _normalize_queries, suppress EvalAPIError check

- Remove unused _normalize_queries function and its tests
- Add pyright ignore for EvalAPIError None check (defensive guard)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Support multimodal image content in eval pipeline

Add image (data/uri) content handling to AgentEvalConverter.convert_message()
so that Content.from_data() and Content.from_uri() image payloads are
preserved as input_image parts in the Foundry evaluator format.

- Handle Content type='data' and type='uri' → emit input_image parts
- Add 6 unit tests for image content through convert_message/convert_messages
- Add integration test verifying images flow through EvalItem → JSONL path
- Add evaluate_multimodal.py sample demonstrating local image eval

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address remaining review comments

- Fix project_client docstring to say async-only (not sync/async)
- Add builtin evaluator name validation warning in _resolve_evaluator
- Replace getattr with typed attribute access in _poll_eval_run,
  _extract_result_counts, _extract_per_evaluator, _fetch_output_items
- Remove cast import from _foundry_evals (no longer needed)
- Tighten _coerce_result: honour explicit 'passed' when both 'score'
  and 'passed' are present; remove performative cast
- Fix self_reflection sample: add env file existence check
- Fix traces sample: correct Pattern 2 section label
- Update all Foundry eval samples to FoundryChatClient + FOUNDRY_MODEL
  (remove AIProjectClient + AZURE_AI_MODEL_DEPLOYMENT_NAME pattern)
- Add eval_name and OpenAI client docs to FoundryEvals docstring
- Update test mocks to match typed SDK objects (_MockResultCounts)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix ruff lint errors (E501, SIM108, SIM102)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix pyright errors: type-narrow dict to dict[str, Any], add ignore comments

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Replace ConversationSplitter type alias with Protocol

ConversationSplitter is now a runtime-checkable Protocol with a named
'conversation' parameter, making the expected signature self-documenting.

ConversationSplit enum members gain a __call__ method so they satisfy
the protocol directly -- ConversationSplit.LAST_TURN(conversation) works.

This simplifies _split_conversation from an isinstance dispatch to a
single split(conversation) call.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Standardize on AZURE_AI_MODEL_DEPLOYMENT_NAME and fix Unicode in samples

- Replace FOUNDRY_MODEL with AZURE_AI_MODEL_DEPLOYMENT_NAME in all
  eval samples to match repo convention
- Replace Unicode symbols with ASCII equivalents in all eval sample
  print statements to avoid cp1252 encoding errors on Windows

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update python/samples/03-workflows/evaluation/evaluate_workflow.py

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Rename ADR 0020 to 0023 (foundry evals integration)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-03-31 15:53:06 +00:00
3f964c4cdb Removing old code-gen docs from dotnet root (#4997)
Co-authored-by: alliscode <bentho@microsoft.com>
2026-03-31 15:37:59 +00:00
Tao ChenandGitHub 016daf3b98 Python: Fix samples (#4980)
* First samples 1st batch

* Fix sample paths

* Fix workflow samples

* Fix workflow dependency

* Correct env vars

* Increase idle timeout

* Fix workflows HIL sample

* Fix more workflow samples
2026-03-31 15:20:35 +00:00
0f81c277d9 Updated package versions (#4982)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 21:26:05 +00:00
Giles OdigweandGitHub 0e00e5f8dd Python: Update Python Packages for rc6 (#4979)
* python package update

* small fix
2026-03-30 21:12:37 +00:00
westeyandGitHub 31c866172a Fix broken url in samples (#4981) 2026-03-30 19:19:16 +00:00
401e5dc7e8 .NET: Allow Simulating service stored ChatHistory to improve consistency (#4974)
* Allow Simulating service stored ChatHistory to improve consistency

* Fixing bug in ServiceStoredSimulatingChatClient

* Addressing PR comments.

* Address PR comments

* Apply suggestion from @SergeyMenshykh

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix bug

---------

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
2026-03-30 18:52:01 +00:00
18f7ba8632 .NET: Add API breaking change validation for RC packages (#4977)
* Add API breaking change validation for RC packages

Enable .NET Package Validation for release candidate packages to detect
API breaking changes in CI. This follows the same pattern used by
Semantic Kernel, centralized through nuget-package.props.

Changes:
- Enable EnablePackageValidation for IsReleaseCandidate packages
- Update PackageValidationBaselineVersion to 1.0.0-rc4 (latest published)
- Generate CompatibilitySuppressions.xml for existing known API changes
  in 5 packages (AI, AzureAI, OpenAI, Workflows, Workflows.Declarative.AzureAI)
- Opt out Workflows.Declarative.Mcp (not yet published to NuGet)
- Add breaking changes guidance to CONTRIBUTING.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback

- Remove unnecessary empty PackageValidationBaselineVersion override
  in Workflows.Declarative.Mcp.csproj (EnablePackageValidation=false
  is sufficient)
- Tighten CONTRIBUTING.md wording to clarify opt-out possibility

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Enable package validation for GA packages (no VersionSuffix)

Expand the EnablePackageValidation condition to also cover future GA
packages that have no VersionSuffix, not just RC packages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix EnablePackageValidation GA condition to check PackageVersion

The previous condition VersionSuffix=='' matched all packages (preview
included) since VersionSuffix defaults to empty. Now uses two separate
conditions: one for RC, one for true GA (PackageVersion == VersionPrefix).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add IsGeneralAvailable flag for package validation

Replace fragile PackageVersion condition with explicit IsGeneralAvailable
property, following the same per-project self-declaration pattern as
IsReleaseCandidate.

- Directory.Build.props: Add IsGeneralAvailable=false default
- nuget-package.props: EnablePackageValidation on RC OR GA
- CONTRIBUTING.md: Update docs to mention both flags

When packages go GA, they set IsGeneralAvailable=true in their .csproj.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Rename IsGeneralAvailable to IsGenerallyAvailable

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 17:36:43 +00:00
05c53dce2d Suppress CodeQL false positive (#4948)
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2026-03-30 16:34:45 +00:00
ade295b122 .NET: Add inline skills API (#4951)
* add inline skills

* Fix IDE1006 and IDE0004 formatting errors in test files

- Add 'Async' suffix to async test methods in FilteringAgentSkillsSourceTests,
  DeduplicatingAgentSkillsSourceTests, and AgentInMemorySkillsSourceTests
- Use pragma to suppress false-positive IDE0004 on casts needed for overload
  disambiguation in AgentInlineSkillTests and AgentInlineSkillResourceTests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address issues

* address comments

* make inline skills script and resource model classes internal

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 16:23:04 +00:00
CopilotGitHubrogerbarretocopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
4527dee64b Python: fix: update PyRIT repository link from Azure/PyRIT to microsoft/PyRIT (#4960)
* Initial plan

* fix: update PyRIT repository link from Azure/PyRIT to microsoft/PyRIT

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/830b8ccf-a79c-49b6-90c9-3bb3e740bc06

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
2026-03-30 14:48:08 +00:00
f45fc7d402 .NET: [Breaking] Update Foundry Agents for Responses API (#4502)
* Stage

* Add FoundryAgentClient, model param, chatClientFactory, and RAPI samples

- Add model parameter to FoundryAgentClient simple constructor
- Add chatClientFactory parameter to both constructors
- Switch to OpenAI.GetProjectResponsesClientForModel for direct Responses API usage
- Add FoundryAgents-RAPI samples (Step01 Basics, Step02 Multiturn, Step03 FunctionTools)
- Add solution folder entry for FoundryAgents-RAPI samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add auto-discovery constructor and simplify RAPI samples

- Add FoundryAgentClient constructor that reads AZURE_AI_PROJECT_ENDPOINT and
  AZURE_AI_MODEL_DEPLOYMENT_NAME from environment variables with DefaultAzureCredential
- Simplify RAPI samples to use auto-discovery (no env var or credential code)
- Remove Azure.Identity direct references from sample csproj files
- Update READMEs to document environment variable requirements

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add remaining RAPI samples (Step04-Step12)

- Step04: Function tools with human-in-the-loop approvals
- Step05: Structured output with typed responses
- Step06: Persisted conversations with session serialization
- Step07: Observability with OpenTelemetry
- Step08: Dependency injection with hosted service
- Step10: Image multi-modality
- Step11: Agent as function tool (agent composition)
- Step12: Middleware (PII, guardrails, function logging, HITL approval)
- Update solution file and folder README with all new samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add all RAPI samples (Step09-Step23) and switch to AzureCliCredential

- Step09: MCP client as tools (GitHub server via stdio)
- Step13: Plugins with dependency injection
- Step14: Code Interpreter tool
- Step15: Computer Use tool with screenshot simulation
- Step16: File Search with vector stores
- Step17: OpenAPI tools (REST Countries API)
- Step18: Bing Custom Search
- Step19: SharePoint grounding
- Step20: Microsoft Fabric
- Step21: Web Search with citations
- Step22: Memory Search with multi-turn conversations
- Step23: Local MCP via HTTP (Microsoft Learn)
- Switch all samples (Step04-Step12) to use AzureCliCredential with env vars
- Update solution file and README with all 23 samples
- All 23 samples build successfully, tested Step05/06/11/13/21

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Switch Step01-03 samples to AzureCliCredential for consistency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarify connection ID format in SharePoint and Fabric READMEs

Document that SHAREPOINT_PROJECT_CONNECTION_ID and FABRIC_PROJECT_CONNECTION_ID
should use the connection name (e.g., 'SharepointTestTool'), not the full ARM
resource URI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Normalize env vars, fix structured output, update READMEs with connection ID formats

- Normalize AZURE_FOUNDRY_PROJECT_* env vars to AZURE_AI_PROJECT_ENDPOINT / AZURE_AI_MODEL_DEPLOYMENT_NAME across all samples (Steps 18-22 READMEs + Steps 19-20 Program.cs)
- Fix RAPI Step05 StructuredOutput to use full constructor with ResponseFormat for streaming JSON
- Update Deep Research sample to use AzureCliCredential
- Enrich Bing Grounding README with full ARM resource URI format
- Fix Bing Custom Search README env var mismatch (BING_CUSTOM_SEARCH_* -> AZURE_AI_CUSTOM_SEARCH_*)
- Add finding instructions for connection ID and instance name in Bing Custom Search READMEs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Refactor memory samples and switch to DefaultAzureCredential

- Refactor RAPI Step22 MemorySearch: extract store setup to EnsureMemoryStoreAsync local function
- Refactor non-RAPI Step22 MemorySearch: same pattern with explicit memory lifecycle
- Set UpdateDelay=0 on MemoryUpdateOptions and MemorySearchPreviewTool for faster ingestion
- Use WaitForMemoriesUpdateAsync with 500ms polling interval
- Switch Step19 SharePoint, Step20 Fabric, Step22 MemorySearch (both) to DefaultAzureCredential
- Remove SearchOptions from MemorySearchPreviewTool (causes unknown parameter error)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Switch all RAPI samples to DefaultAzureCredential and format

- Replace AzureCliCredential with DefaultAzureCredential across all 20 RAPI samples
- Run dotnet format on all RAPI and non-RAPI Foundry samples
- AzureAI unit tests: 341 passed (net10.0 + net472)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Rename to Microsoft Foundry, add metadata, rename RAPI folder

- Replace 'Azure AI Foundry' / 'Azure Foundry' with 'Microsoft Foundry' in all docs, comments, and XML docs
- Update FoundryAgentClient metadata provider name to 'microsoft.foundry'
- Rename FoundryAgents-RAPI folder to FoundryResponseAgents
- Rewrite FoundryResponseAgents README with comparison table vs Foundry Agents
- Update slnx and parent README with new folder references

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review: simplify sample comments and fix DeepResearch credential

- Remove 'no server-side agent' and 'Responses API directly' phrasing from comments
- Simplify to 'Create a FoundryAgentClient' per review feedback
- Switch Agent_Step15_DeepResearch to DefaultAzureCredential

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Restore full DefaultAzureCredential warning comment in DeepResearch sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add ADR 0020: Foundry agent type naming convention

Proposes naming options for a new MAF type wrapping versioned
Foundry agents (Prompt, ContainerApp, Hosted, Workflow) to
distinguish from the existing FoundryResponsesAgent (RAPI path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify FoundryResponsesAgent samples with env-var constructors and rename folders

- Add env-var constructors to FoundryResponsesAgent (simple + options-based)
- Fix Constructor 1 model optionality (no longer throws on missing AZURE_AI_MODEL_DEPLOYMENT_NAME)
- Add ApplyModelDeploymentFallback helper for options-based constructor
- Update all 23 FoundryResponseAgents samples to remove Environment.GetEnvironmentVariable boilerplate
- Condense 6 simple samples to one-liner constructor calls
- Add XML doc remarks about auto-resolved parameters on all constructors
- Rename FoundryAgents -> FoundryVersionedAgents (server-side, versioned)
- Rename FoundryResponseAgents -> FoundryAgents (now the default path forward)
- Update .slnx and README cross-references for new folder names

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add FoundryAITool factory, rename RAPI folders, and clean up references

- Create FoundryAITool static factory class with 17 methods wrapping AgentTool.Create* and ResponseTool.Create* into AITool returns
- Rename 23 FoundryAgentsRAPI_* subfolders to FoundryAgents_* (drop RAPI prefix)
- Rename .csproj files and update .slnx references accordingly
- Update 12 samples (6 FoundryAgents + 6 FoundryVersionedAgents) to use FoundryAITool
- Replace all FoundryResponsesAgent references with FoundryAgent in comments and READMEs
- Update sample READMEs to reference FoundryAITool methods

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Rename FoundryVersionedAgents subfolders from FoundryAgents_* to FoundryVersionedAgents_*

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add FoundryVersionedAgent class and refactor extension method internals

- Create FoundryVersionedAgent with private ctor and async static factory methods
  (CreateAIAgentAsync/GetAIAgentAsync) with env-var and explicit endpoint tiers
- Extract shared internal helpers from AzureAIProjectChatClientExtensions:
  CreateChatClientAgent, CreateAgentVersionFromOptionsAsync,
  CreateAgentVersionWithProtocolAsync (tools overload),
  CreateChatClientAgentOptions, GetAgentRecordByNameAsync, ThrowIfInvalidAgentName
- Extension methods now delegate to shared internal helpers
- All 49 existing samples continue to build successfully

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add CreateConversationSessionAsync, DeleteAIAgentAsync, auto-resolve model, simplify samples

- Add CreateConversationSessionAsync to FoundryAgent and FoundryVersionedAgent
  (returns ChatClientAgentSession, creates server-side conversation + session in one call)
- Add DeleteAIAgentAsync static method to FoundryVersionedAgent
- Make model parameter optional in env-var factory overloads (auto-resolves from
  AZURE_AI_MODEL_DEPLOYMENT_NAME)
- Update all FoundryVersionedAgents samples to use DeleteAIAgentAsync
- Remove deploymentName env var from samples where only used for model parameter
- Use CreateConversationSessionAsync in Step02_MultiturnConversation
- Use explicit types instead of var for agent/session variables
- All 49 samples build successfully

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove manual AIProjectClient construction from FoundryVersionedAgents samples

- Replace manual AIProjectClient construction with GetService<AIProjectClient>()
  from the FoundryVersionedAgent in all dual-option and tool-specific samples
- Remove AZURE_AI_PROJECT_ENDPOINT env var reads from updated samples
- Remove Azure.Identity usings where no longer needed
- Only Step01.1, Step01.2, Eval_Step01 retain manual construction (pedagogical samples)
- All 49 samples build successfully

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Replace aiProjectClient extension calls with FoundryVersionedAgent factories in all samples

- Replace aiProjectClient.CreateAIAgentAsync with FoundryVersionedAgent.CreateAIAgentAsync
  in Option 2 (Native SDK) paths across Steps 14-21
- Replace aiProjectClient.Agents.DeleteAgentAsync with FoundryVersionedAgent.DeleteAIAgentAsync
- Remove unused AIProjectClient variables and using directives
- Only Step01.1, Step01.2, Eval_Step01 retain direct AIProjectClient usage (pedagogical)
- Step16, Step22 use GetService<AIProjectClient>() for file/memory operations

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove unused using directives from Step01.2, Step09, Eval_Step02

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update ADR 0020 with accepted decision: Option 6

- Add Option 6 detailing FoundryAgent, FoundryVersionedAgent, FoundryAITool,
  env-var auto-discovery, and self-contained factory patterns
- Mark decision as accepted with rationale
- Update current state and metadata sections

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update Step01 basics samples to use FoundryVersionedAgent factories

- Step01.1: Replace manual AIProjectClient/AsAIAgent with FoundryVersionedAgent.CreateAIAgentAsync/GetAIAgentAsync/DeleteAIAgentAsync
- Step01.2: Replace manual AIProjectClient with FoundryVersionedAgent.CreateAIAgentAsync/DeleteAIAgentAsync
- Remove env var boilerplate and Azure.Identity dependency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add DeleteAIAgentVersionAsync to FoundryVersionedAgent

- DeleteAIAgentAsync: deletes the agent and all its versions (existing)
- DeleteAIAgentVersionAsync: deletes only the specific version associated with the agent instance
- Internally delegates to Agents.DeleteAgentAsync vs Agents.DeleteAgentVersionAsync

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix cleanup comments: DeleteAIAgentAsync deletes the agent and all its versions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update all FoundryVersionedAgents READMEs for FoundryVersionedAgent and auto-discovery

- Rewrite main README with FoundryVersionedAgent usage, auto-discovery table, code example
- Fix sample table links from FoundryAgents_Step* to FoundryVersionedAgents_Step*
- Add FoundryAITool references in tool-specific sample descriptions
- Update individual READMEs: fix stale paths, add auto-discovery note after env var blocks
- Update tool references: AgentTool/ResponseTool -> FoundryAITool
- Update parent 02-agents/README.md with FoundryVersionedAgent description

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert unrelated AGUI and Hosting.OpenAI formatting changes to main

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove env-var auto-discovery, add AsAIAgent, mark extensions Obsolete

- Remove 2 env-var constructors from FoundryAgent (keep explicit endpoint ctors)
- Remove 5 env-var factory methods from FoundryVersionedAgent (keep explicit ones)
- Add 3 AsAIAgent static methods to FoundryVersionedAgent (AgentVersion/AgentRecord/AgentReference)
- Mark all 8 AIProjectClient extension methods as [Obsolete] pointing to FoundryVersionedAgent
- Remove ApplyModelDeploymentFallback, env var constants, Azure.Identity usings from source

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update all samples to use explicit endpoint, credential, and model parameters

- Add explicit Environment.GetEnvironmentVariable reads for AZURE_AI_PROJECT_ENDPOINT
  and AZURE_AI_MODEL_DEPLOYMENT_NAME to all 48 sample files
- Pass new Uri(endpoint), new DefaultAzureCredential(), deploymentName to
  FoundryAgent constructors and FoundryVersionedAgent factory methods
- Add using Azure.Identity where missing
- Matches repo-wide pattern used by other non-Foundry samples
- All 49 samples build successfully

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Migrate remaining samples and source from obsoleted extension methods

- Migrate AgentProviders, AgentWithRAG, AgentWithMemory, HostedWorkflow samples to FoundryVersionedAgent
- Migrate AzureAgentProvider.cs to FoundryVersionedAgent.AsAIAgent
- Migrate AzureAIProjectChatClientTests.cs to FoundryVersionedAgent.GetAIAgentAsync
- Remove pragma suppressions from migrated files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add unit tests for FoundryAgent and FoundryVersionedAgent

- FoundryAgentTests.cs: 14 tests covering constructors, validation,
  properties, metadata, GetService, chat client factory, user-agent header
- FoundryVersionedAgentTests.cs: 31 tests covering CreateAIAgentAsync,
  GetAIAgentAsync, AsAIAgent (3 overloads), DeleteAIAgentAsync,
  DeleteAIAgentVersionAsync, validation, invalid names, metadata, GetService

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Finalize Foundry agent migration

Align FoundryAgent and FoundryVersionedAgent samples, docs, and tests with the explicit configuration model, clean up stale README guidance, and fix AzureAI unit test validation/build issues.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply formatter cleanup after validation

Capture the dotnet format follow-up changes produced during branch validation so the committed state matches the successfully built and tested branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add integration tests for FoundryAgent and FoundryVersionedAgent

Mark old AIProjectClient extension-method integration tests as obsolete and add new integration test suites for both FoundryAgent (Responses API) and FoundryVersionedAgent (versioned agents). All 71 non-skipped tests pass against the live Foundry service.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update ADR 0020 with test coverage details

Add integration test coverage note to the Current State section of ADR 0020.

* Simplify Foundry agents and validate moved samples

* Rename FoundryAgent integration tests to ResponsesAgent

The test classes exercise the non-versioned Responses path via
AIProjectClient.AsAIAgent(), not the removed FoundryAgent wrapper type.
Rename files and class names to reflect the actual test surface.

* Update documentation for ChatClientAgent usage

Added example usage of ChatClientAgent with JokerAgent.

* Refactor ChatClientAgent instantiation for clarity

* Revise agent type naming and usage examples

Updated documentation to reflect changes in agent creation methods and added examples for using `ChatClientAgent`.

* Fix Azure SDK namespace migration after rebase

Update Azure.AI.Projects.OpenAI references to Azure.AI.Projects.Agents
and Azure.AI.Extensions.OpenAI to match Azure.AI.Projects 2.0.0-beta.2.

- Replace deprecated namespace across samples, tests, and src
- Fix renamed types: OpenAPIFunctionDefinition -> OpenApiFunctionDefinition,
  BingCustomSearchToolParameters -> BingCustomSearchToolOptions,
  BrowserAutomationToolParameters -> BrowserAutomationToolOptions
- Fix API changes: AgentRecord.Versions -> GetLatestVersion(),
  ResponsesClient constructor, FunctionApprovalRequestContent ->
  ToolApprovalRequestContent
- Apply dotnet format

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address merge markers

* Replace obsolete GetAIAgentAsync with AsAIAgent in samples

Switch Agent_Step07_AsMcpTool and A2AServer to use the non-obsolete
PersistentAgentsClient.AsAIAgent(PersistentAgent) extension instead
of the deprecated GetAIAgentAsync, fixing CS0618 build errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix broken markdown links in Responses sample READMEs

Replace stale ChatClientAgents_Step* folder references with the
correct Agent_Step* names across all Responses sample READMEs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix format errors and address PR review comments

- Fix charset and remove unused using in AzureAIProjectResponsesChatClient
- Fix doc comment tags (code -> c) in FoundryAITool
- Fix stray period in LocalMCP sample comment
- Fix grammar in FoundryMemoryProvider xmldoc
- Fix AIProjectClientAgentRunStreamingConversationTests base class

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply dotnet format fixes to PR-changed files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix build errors from format pass and apply naming conventions

- Fix static call to CreateSessionAsync in Step02 samples and extension tests
- Use expression-bodied lambda in FoundryMemoryProvider (RCS1021)
- Apply PascalCase naming to const fields in ResponsesAgentExtensionCreateTests (IDE1006)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Introduce FoundryAgent sealed type and update AsAIAgent extensions

- Add FoundryAgent sealed class wrapping ChatClientAgent with:
  - Public ctors: (projectEndpoint, credential, model, instructions) and (agentEndpoint, credential)
  - Internal ctor: (AIProjectClient, ChatClientAgent) for extension use
  - CreateConversationSessionAsync() for server-side conversations
  - GetService<ChatClientAgent>() and GetService<AIProjectClient>()
  - MEAI user-agent policy on internally-created AIProjectClient
- Change all AsAIAgent extension return types from ChatClientAgent to FoundryAgent
- Update all samples and tests to use FoundryAgent type
- Add 16 FoundryAgentTests covering ctors, GetService, UserAgent, RunAsync
- Fix pre-existing Agent_Step12_Plugins build error

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Collapse sample folders and add FoundryAgent_Step01 sample

- Move all Responses/* samples up to AgentsWithFoundry/ (flat structure)
- Remove entire Versioned/ folder (26 samples)
- Add FoundryAgent_Step01 sample showing direct FoundryAgent ctor usage
- Update slnx to reflect flat folder structure
- Fix csproj ProjectReference paths for new depth

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update READMEs for flat AgentsWithFoundry structure

- Rewrite AgentsWithFoundry/README.md with FoundryAgent quick start
- Fix cd commands and paths in 11 sample READMEs
- Update 02-agents/README.md to single Foundry link
- Update AGENTS.md tree to flat structure
- Fix AgentWithMemory cross-reference

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix FoundryAgent_Step01 sample with full create/run/delete lifecycle

Show the complete server-side agent lifecycle: create version with
native SDK, wrap as FoundryAgent via AsAIAgent, run, then delete.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert RAPI samples to use AIAgent instead of FoundryAgent

RAPI samples should not reference FoundryAgent directly. Restored
original sample code with only ChatClientAgent -> AIAgent type change
to accommodate the AsAIAgent return type.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Convert versioned-pattern samples to pure RAPI

Step09, Step13, Step17, Step22 were using CreateAgentVersionAsync +
PromptAgentDefinition which is the versioned pattern. Converted to
use AsAIAgent(model, instructions, tools) which is the RAPI path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix format issues from Docker CI check

- FoundryAgent_Step01: CRLF -> LF
- Agent_Step09: missing final newline
- Agent_Step11_Middleware: add internal modifier, final newline
- Agent_Step02: remove redundant cast (IDE0004)
- Agent_Step08: simplify name (IDE0001)
- FoundryAgentTests: s_ prefix, Async suffix naming conventions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Switch Step09 MCP sample to Microsoft Learn HTTP endpoint

Replace npx stdio GitHub MCP server with the public Microsoft Learn
MCP endpoint (https://learn.microsoft.com/api/mcp) using HTTP transport.
No external tooling required to run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix missing final newline in Step09 MCP sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review: use DelegatingAIAgent, clean up Step01 sample

- FoundryAgent now inherits DelegatingAIAgent instead of AIAgent,
  removing manual delegation boilerplate (westey-m feedback)
- Simplified Agent_Step01_Basics to single agent creation path,
  moved composable IChatClient approach to README (westey-m feedback)
- Fixed FoundryAgentTests param name assertion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update sample using Project specialized type instead

* Address PR review feedback: DefaultAzureCredential warnings, sample simplifications, format fixes

- Add DefaultAzureCredential production warning comments to ~25 samples
- Simplify Anthropic and OpenAI Step01 samples to single agent
- Convert Step11 Middleware regex patterns to [GeneratedRegex]
- Remove unnecessary cleanup comment from Step06
- Fix Step09 README MCP transport description
- Enhance FoundryAgent xmldoc with non-persistent agent comparison

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Split Step02, simplify RAG Step04, sharpen Step23 differentiation

- Split Step02 into 02.1 (simple multi-turn via sessions) and 02.2 (server-side conversations via CreateConversationSessionAsync)
- RAG Step04: replace HostedFileSearchTool + MEAI wrapping with native OpenAI FileSearchTool
- Step23: clarify DelegatingAIFunction wrapping pattern vs Step09 basic MCP

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Hosted MCP sample: use ResponseTool.CreateMcpTool and move tool to PromptAgentDefinition

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix broken README link after Step02 split

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Sergey round 3 feedback: branding, README nav, sample rename

- Replace 'Azure AI Foundry' with 'Microsoft Foundry' in ADR 0020
- Fix 3 READMEs: 'ChatClientAgents' → 'AgentsWithFoundry' sample directory
- Rename FoundryAgent_Step01 → Agent_Step00_FoundryAgentLifecycle for naming consistency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 12:09:02 +00:00
ca02146ee4 .NET: BugFix #3433 ChatClientAgent streaming responses missing messageid (#4615)
* Changes

* Fix ChatClientAgent streaming responses missing MessageId

Generate fallback MessageId in ChatClientAgent.RunCoreStreamingAsync when
the underlying LLM provider does not set ChatResponseUpdate.MessageId.
Without a MessageId the AGUI converter's null==null check silently drops
all text content, causing CopilotKit Zod validation errors.

Changes:
- ChatClientAgent: generate msg_{Guid} fallback via ??= in streaming loop
- AgentResponseExtensions: sync wrapper MessageId back to RawRepresentation
  in AsChatResponseUpdate() so downstream consumers see the value
- Add unit tests for both fixes and AGUI streaming MessageId scenarios

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR #4615 review comments

- Fix MessageId seeding: use first-seen provider MessageId (or generate
  fallback) and apply consistently to all chunks in the stream, preventing
  message splitting when providers set MessageId only on the first chunk
- Add test for mixed MessageId scenario (first chunk only)
- Fix skipped TextStreaming test: assert Empty (not NotEmpty) to match
  actual null==null behavior
- Fix skipped ToolCalls test: assert empty ParentMessageId to match
  actual empty-string passthrough behavior

* Handle empty MessageId in AsChatResponseUpdate sync

Treat empty/whitespace MessageId the same as null when syncing from
the AgentResponseUpdate wrapper back to RawRepresentation. Providers
that return empty string MessageId (e.g. tool call responses) now get
the wrapper value recovered correctly.

Add test for empty string MessageId recovery scenario.

* Move MessageId fallback generation to AGUI layer

Move fallback MessageId generation from ChatClientAgent to
AsAGUIEventStreamAsync, addressing the architectural concern that
MessageId is nullable in the AIAgent abstraction and the requirement
for non-null values is specific to the AGUI protocol.

The AGUI layer now generates a fallback MessageId for null or
empty/whitespace values, covering all agent types (not just
ChatClientAgent) including external implementations.

Changes:
- Revert MessageId generation from ChatClientAgent.RunCoreStreamingAsync
- Add fallback MessageId generation in AsAGUIEventStreamAsync for
  null/empty MessageId values (handles both null and whitespace)
- Unskip and update AGUI tests to verify fallback generation
- Update ChatClientAgent tests to reflect passthrough behavior

* Revert AsChatResponseUpdate MessageId sync-back

Remove the MessageId sync-back logic from AsChatResponseUpdate() as it
is no longer needed. With fallback generation moved to the AGUI layer,
the abstraction layer should not mutate the RawRepresentation object.

Revert to the original passthrough behavior for AsChatResponseUpdate()
and update tests accordingly.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 11:55:30 +00:00
3627b9b911 .NET: Skip flaky integration tests blocking merge queue (#4972)
* Skip flaky integration tests blocking merge queue

Skip 7 timing-dependent integration tests that are causing a 73% failure
rate in the merge queue over the past 3 days. All failures are
LLM-latency/timeout related and occur across unrelated PRs.

Skipped tests:
- ConsoleAppSamplesValidation.ReliableStreamingSampleValidationAsync
- ConsoleAppSamplesValidation.SingleAgentOrchestrationHITLSampleValidationAsync
- ConsoleAppSamplesValidation.LongRunningToolsSampleValidationAsync
- SamplesValidation.LongRunningToolsSampleValidationAsync (AzureFunctions)
- SamplesValidation.ReliableStreamingSampleValidationAsync (AzureFunctions)
- ExternalClientTests.CallFunctionToolsAsync
- ExternalClientTests.CallLongRunningFunctionToolsAsync

Tracking issue: https://github.com/microsoft/agent-framework/issues/4971

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Extract skip reason to const per review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 10:51:21 +00:00
b1b528e4a8 Python: [BREAKING] Remove deprecated kwargs compatibility paths (#4858)
* [BREAKING] Remove deprecated kwargs compatibility paths

Remove the deprecated kwargs compatibility shims across core agents, clients, tools, middleware, and telemetry.

Keep workflow kwargs behavior intact in this branch and follow up separately in #4850.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix PR CI fallout for kwargs removal

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updates

* Fix Azure AI CI fallout

Remove the stale _get_current_conversation_id override from the Azure AI client after the OpenAI base helper was deleted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fixed new classes

* Fix Assistants deprecated import gating

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix integration replay regressions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Switch multi-agent hosting samples to Azure chat completions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify Azure multi-agent sample config

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 21:00:12 +00:00
westeyandGitHub ca6cdd142e .NET: Fixes for durable agents integration tests (#4952)
* Fixing for durable agents integration tests

* Add further fixes
2026-03-27 17:38:23 +00:00
6b47cdbf52 Python: Fix broken samples for GitHub Copilot, declarative, and Responses API (#4915)
* Python: Fix broken samples for GitHub Copilot, declarative, and Responses API

- Add missing on_permission_request handler to github_copilot_basic and
  github_copilot_with_session samples (required by copilot SDK)
- Increase timeout for remote MCP query in github_copilot_with_mcp sample
- Soften session isolation claim in github_copilot_with_session sample
- Fix inline_yaml sample: pass project_endpoint via client_kwargs instead
  of relying on YAML connection block (AzureAIClient expects
  project_endpoint, not endpoint)
- Handle raw JSON schemas in Responses client _convert_response_format
  so declarative outputSchema works with the Responses API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Improve raw JSON schema detection heuristic and add tests

- Broaden raw schema detection to handle anyOf, oneOf, allOf, $ref, $defs
  keywords and JSON Schema primitive types, not just 'properties'
- Apply same raw schema handling to azure-ai _shared.py for consistency
- Add unit tests for both openai and azure-ai response_format conversion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 16:27:19 +00:00
cc0cfaaac8 [BREAKING] Python: fix OpenAI Azure routing and provider samples (#4925)
* Python: fix OpenAI Azure routing and provider samples

Prefer OpenAI when OPENAI_API_KEY is present unless Azure is explicitly requested. Clarify constructor docs, keep deprecated Azure wrappers compatible with stricter settings validation, and refresh the provider samples and tests to use the current client patterns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix bandit

* Python: align OpenAI embedding Azure routing

Extend the shared OpenAI-vs-Azure routing and credential behavior to the embedding client, add Azure embedding regression coverage, and refresh the embedding samples to use the generic client path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: fix embedding client pyright check

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: thin OpenAI embedding wrapper

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: document embedding overload routing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: fix callable OpenAI key routing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: fix Azure credential routing tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: address OpenAI review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: narrow Azure routing markers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: refine OpenAI model fallback order

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: narrow Azure deployment docs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: remove embedding routing wording

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: run embedding Azure integration tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* changed variable name

* Python: expand OpenAI package README

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* clarified readme

* Python: fix Azure OpenAI integration setup

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: correct Azure integration env mapping

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated code to fix int tests

* test updates

* test fix

* fix test setup

* updates to tests and setup

* remove openai assistants int tests

* improvements in int tests

* fix env var

* fix env vars

* fix azure responses test

* trigger actions

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 13:33:39 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3611be82cf Bump flatted from 3.3.3 to 3.4.2 in /python/packages/devui/frontend (#4805)
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 03:43:25 +00:00
SergeyMenshykhandGitHub 0fcbe7e105 .NET: [Breaking] Restructure agent skills to use multi-source architecture (#4871)
* initial commit

* address comments

* address comments

* address comments

* address  comments

* rename executor to runner to align naming with python implementation

* rename runner execute method to run method

* remove poc leftovers and fix compilation issues

* make script runner optional

* remove unnecessary pragmas

* make resources and scripts props virtual

* address comments

* update comment for name validation regex

* address comments
2026-03-26 22:27:17 +00:00
Jacob AlberandGitHub 5530bc536b .NET: feat: Implement return-to-previous routing in handoff workflow (#4356)
* feat: Implement return-to-previous routing in handoff workflow

- Also obsoletes HandoffsWorkflowBuilder => HandoffWorkflowBuilder (no "s")

* refactor: Remove instance-shared current agent tracking in handoffs

Because the tracker was instance-shared between the start and end executors, it would be shared between all sessions, resulting in incorrect behaviour.

The corect way to do this is to keep the data in a shared executor scope, which is per-session.

* fix: Fix test logic for Handoff to correctly use checkpointing for multiturn
2026-03-26 20:17:57 +00:00
9bfa593ae7 Python: Move ag_ui_workflow_handoff demo from demos/ to 05-end-to-end/ (#4900)
* Move ag_ui_workflow_handoff demo to 05-end-to-end (#4895)

Move the AG-UI workflow handoff demo from python/samples/demos/ to
python/samples/05-end-to-end/ to follow the current folder structure
convention. Update README paths accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix review feedback: remove build artifacts, fix README paths (#4895)

- Add .gitignore to frontend/ to exclude *.tsbuildinfo, vite.config.js,
  and vite.config.d.ts build artifacts from version control
- Remove the 4 tracked build artifact files from the tree
- Fix step 2 cd path in README to be relative after 'cd python'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarify working directory context in README Step 2 (#4895)

Step 2 uses a python/-relative path (samples/...) which assumes the
user is still in the python/ directory from Step 1. Add a brief note
making this explicit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 17:52:07 +00:00
3585581c7a .NET: Fix bug with per-service-call persistence and approvals (#4933)
* Fix bug with per-service-call persistence and approvals

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-26 17:45:46 +00:00
63dee91a5f .NET: Improve observability sample (#4917)
* Improve .Net observability sample

* Update dotnet/samples/02-agents/AgentOpenTelemetry/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-26 17:33:22 +00:00
westeyandGitHub 3b8b56e6ea Samples fix (#4932) 2026-03-26 16:45:01 +00:00
9691c9c271 .NET: Fix role assignment in ChatMessage construction (#4290)
* Use actual message role when creating ChatMessage

Replace hard-coded ChatRole.User with a ChatRole constructed from the message's Role. The change ensures ToChatMessage and FunctionMessage use the original role (new ChatRole(this.Role)) for both text and contents branches, fixing incorrect role assignment when constructing ChatMessage instances.

* Update changes

* Fix formatting in ToChatMessage tests

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2026-03-26 16:13:00 +00:00
d2977d63da .NET: Add integration test validating OpenAPI tools with AsAIAgent(agentVersion) (#4931)
* .NET: Add integration test for OpenAPI tools with AsAIAgent(agentVersion)

Validates end-to-end flow creating a Foundry agent with an OpenAPI tool
definition via native Azure.AI.Projects SDK types and wrapping it with
AsAIAgent(agentVersion). The test confirms the server-side OpenAPI
function is invoked correctly through RunAsync.

Addresses #4883

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review: RetryFact, PascalCase naming, stronger tool assertion

- Use RetryFact with Skip for manual testing (flaky due to external API)
- Fix agentName -> AgentName to match PascalCase convention in file
- Strengthen tool invocation assertion: require >= 3 Eurozone countries
- Add comment explaining server-side OpenAPI tools don't surface as
  FunctionCallContent in the MEAI abstraction

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 15:35:16 +00:00
84fe6c46ab .NET: Add AsIChatClientWithStoredOutputDisabled for ProjectResponsesClient (#4911)
* Add AsIChatClientWithStoredOutputDisabled for ProjectResponsesClient

Add extension method on ProjectResponsesClient in Microsoft.Agents.AI.AzureAI
package (Azure.AI.Extensions.OpenAI namespace) mirroring the existing extension
on ResponsesClient in the OpenAI package. This enables Azure AI consumers to
disable server-side response storage without depending on the OpenAI package.

- New ProjectResponsesClientExtensions class with AsIChatClientWithStoredOutputDisabled
- Optional deploymentName parameter (model is no longer required)
- Updated OpenAI counterpart doc to remove 'Required' wording for model param
- Added unit tests covering null guard, inner client accessibility,
  StoredOutputEnabled=false, and reasoning encrypted content inclusion/exclusion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Preserve existing RawRepresentationFactory when disabling stored output

Address PR review feedback: wrap/chain the existing factory instead of
replacing it, so upstream configuration (e.g., deploymentName/model defaults
from AsIChatClient) is preserved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 11:19:32 +00:00
SergeyMenshykhandGitHub 6626565f7a ADR to support a multi-source architecture for agent skills (#4787)
* add  adr suggesting a new design to support a multi-source architecture for agent skills

* add deciders

* move the adr to the decisions folder

* remove unnecessary section

* describe adding a custom skill source

* update

* address comments

* add constructor overloads to inline skill resource and script

* consider ai-function as an alternative for skill script and skill resource model classes

* update decision outcome section and sync adr with latest changes in the code
2026-03-26 11:19:09 +00:00
westeyandGitHub bfda595e56 Add ADR to decide consistency of Chat History Persistence (#4816)
* Add ADR to decide consitency of Chat History Persistence

* Add example

* Update ADR with review results

* Remove unecessary clarification

* Rename ADR to no 22
2026-03-26 11:10:33 +00:00
efb14cedb1 Python: Support structuredContent in MCP tool results and fix sampling options type (#4763)
* Support MCP sampling tools capability (#4625)

Forward systemPrompt, tools, and toolChoice from MCP sampling requests
to the chat client's get_response() call. Also advertise the
sampling.tools capability to MCP servers when a client is configured.

- Pass SamplingCapability with tools support to ClientSession
- Convert systemPrompt to instructions in options
- Convert MCP Tool objects to FunctionTool instances for options
- Map MCP ToolChoice.mode to tool_choice in options
- Add tests for all new behaviors and update existing sampling tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix #4625: Support MCP sampling tool with proper typing and structured content

- Fix mypy error by typing sampling callback options as ChatOptions[None]
  instead of dict[str, Any], and importing ChatOptions from _types
- Handle structuredContent from CallToolResult in _parse_tool_result_from_mcp,
  serializing it as JSON text Content when present
- Add tests for structuredContent parsing (with and without regular content)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix lint: add author to TODO comment

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4625: remove default=str, add edge-case tests

- Remove default=str from json.dumps for structuredContent to fail fast
  on non-JSON-serializable values instead of silently converting
- Add test for non-JSON-serializable structuredContent (TypeError)
- Add tests for empty systemPrompt ('') and empty tools list ([]) edge
  cases in sampling callback
- Expand TODO comment noting list[Content] return type constraint for
  future result_type support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Sanitize sampling callback error to avoid leaking internals (#4625)

Log exception details at DEBUG level instead of including them in the
ErrorData message returned to the MCP server, which may be untrusted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4625: move params to options, restore error info

- Remove stale TODO comment about response_format (ChatOptions already has it)
- Restore {ex} in sampling callback error message for useful debugging info
- Set structuredContent as additional_property on Content for structured access
- Move temperature, max_tokens, stop into options dict (not top-level kwargs)
- Only set temperature when provided (not all models support it)
- Add tests for generation params in options and temperature omission

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix MCP sampling callback and structured content error handling (#4625)

- Guard max_tokens like temperature: only set when not None, so options
  can properly evaluate to None when all params are absent
- Wrap json.dumps of structuredContent in try/except to fall back to
  str() for non-serializable values instead of propagating TypeError
- Extract test_connect_sampling_capabilities_with_client into its own
  test function so pytest can discover it independently
- Add test for max_tokens=None omission from options
- Update structured content non-serializable test to expect fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4625: review comment fixes

* Fix MCP and Azure validation regressions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 07:33:19 +00:00
dd3d085539 Python: Include reasoning messages in MESSAGES_SNAPSHOT events (#4844)
* Include reasoning messages in MESSAGES_SNAPSHOT (#4843)

FlowState now tracks reasoning messages emitted during a run.
_emit_text_reasoning() persists reasoning (including encrypted_value)
into flow.reasoning_messages, and _build_messages_snapshot() appends
them to the final MESSAGES_SNAPSHOT event.

Changes:
- Add reasoning_messages field to FlowState
- Update _emit_text_reasoning() to accept optional flow parameter
- Include reasoning_messages in _build_messages_snapshot()
- Add 'reasoning' to ALLOWED_AGUI_ROLES so normalize_agui_role()
  preserves the role through snapshot round-trips
- Skip reasoning messages in agui_messages_to_agent_framework() since
  they are UI-only state and should not be forwarded to LLM providers
- Add regression tests for snapshot emission, encrypted value
  preservation, and multi-turn round-trip with reasoning

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Include reasoning messages in MESSAGES_SNAPSHOT events

Fixes #4843

* Fix PR review feedback for reasoning persistence (#4843)

- Accumulate reasoning text per message_id (append deltas) instead of
  storing only the current chunk, matching flow.accumulated_text pattern
- Use camelCase encryptedValue in snapshot JSON to match AG-UI protocol
  conventions (toolCallId, encryptedValue)
- Normalize snake_case encrypted_value to encryptedValue in
  agui_messages_to_snapshot_format for input compatibility
- Update normalize_agui_role docstring to include reasoning role
- Add tests for incremental reasoning accumulation and key normalization

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4843: Python: agent-framework-ag-ui: include reasoning messages in MESSAGES_SNAPSHOT

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 05:56:10 +00:00
dc27740f1a Python: Fix streaming path to emit mcp_server_tool_result on output_item.done instead of output_item.added (#4821)
* Fix streaming path to deliver mcp_server_tool_result content (#4814)

Remove premature mcp_server_tool_result emission from the
response.output_item.added/mcp_call handler — at that point the MCP
server has not yet responded and output is always None.

Add a handler for response.mcp_call.completed that emits
mcp_server_tool_result with the actual tool output, matching the
non-streaming path behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix streaming path to deliver mcp_server_tool_result content (#4814)

Stop eagerly emitting mcp_server_tool_result on response.output_item.added
(when output is always None). Instead, handle response.output_item.done for
mcp_call items, which carries the full McpCall with populated output.

This matches the non-streaming path which guards with 'if item.output is not
None' before emitting the result.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix test docstring to match actual implementation event name

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review: call_id fallback and raw_representation consistency (#4814)

- Add call_id fallback in response.output_item.done mcp_call handler to
  match the output_item.added handler pattern
- Use done_item instead of event for raw_representation to keep
  consistent with other output_item branches and non-streaming path
- Add test for call_id fallback when id attribute is missing
- Add raw_representation assertions to existing done handler tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review: call_id fallback for non-streaming path and test coverage (#4814)

- Apply defensive call_id fallback (getattr with id/call_id/empty) to
  non-streaming mcp_call path for consistency with streaming path
- Add raw_representation assertion to call_id fallback test
- Add test for empty-string fallback when neither id nor call_id exist

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 02:09:22 +00:00
c1435ac201 Python: Fix A2AAgent to surface message content from in-progress TaskStatusUpdateEvents (#4798)
* Fix A2AAgent dropping message content from in-progress TaskStatusUpdateEvents (#4783)

_updates_from_task() returned [] for working-state tasks when
background=False, silently discarding all intermediate message content
from task.status.message. Now extracts and yields message parts from
in-progress status updates during streaming.

Also fixed MockA2AClient.send_message to yield all queued responses
(enabling multi-event streaming tests) and added text parameter to
add_in_progress_task_response for tests that need status messages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix: gate intermediate status updates behind emit_intermediate flag and add missing test coverage

- Add emit_intermediate parameter to _updates_from_task and _map_a2a_stream
- Thread stream flag from run() so only streaming callers see intermediate updates
- Add IN_PROGRESS_TASK_STATES guard to emit_intermediate condition
- Add role parameter to test helper add_in_progress_task_response
- Add clarifying comment on MockA2AClient.send_message batch semantics
- Add tests for user role mapping, background precedence, non-streaming behavior,
  terminal task with no artifacts, and empty parts edge case

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 02:08:47 +00:00
Jacob AlberandGitHub 0bdcaa5c07 fix: Re-enable the "retrieve object" check in StateManager (#1881) 2026-03-25 22:53:44 +00:00
722 changed files with 32981 additions and 31258 deletions
@@ -34,7 +34,7 @@ runs:
- name: Test Copilot CLI
shell: bash
run: copilot -p "What can you do in one sentence?"
run: copilot --version && copilot -p "What can you do in one sentence?"
- name: Azure CLI Login
uses: azure/login@v2
+26 -20
View File
@@ -60,9 +60,8 @@ jobs:
environment: integration
timeout-minutes: 60
env:
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDINGS_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
@@ -96,10 +95,10 @@ jobs:
environment: integration
timeout-minutes: 60
env:
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
defaults:
run:
@@ -126,7 +125,7 @@ jobs:
uv run pytest --import-mode=importlib
packages/openai/tests/openai/test_openai_chat_completion_client_azure.py
packages/openai/tests/openai/test_openai_chat_client_azure.py
packages/azure-ai/tests/azure_openai
packages/openai/tests/openai/test_openai_embedding_client_azure.py
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
@@ -202,15 +201,16 @@ jobs:
timeout-minutes: 60
env:
UV_PYTHON: "3.11"
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_MODEL: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FUNCTIONS_WORKER_RUNTIME: "python"
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
AzureWebJobsStorage: "UseDevelopmentStorage=true"
@@ -248,17 +248,19 @@ jobs:
--timeout=360 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
# Azure AI integration tests
python-tests-azure-ai:
name: Python Integration Tests - Azure AI
# Foundry integration tests
python-tests-foundry:
name: Python Integration Tests - Foundry
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
env:
AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME }}
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION }}
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
defaults:
run:
@@ -282,9 +284,13 @@ jobs:
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest
timeout-minutes: 15
run: |
uv run --directory packages/azure-ai poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
uv run --directory packages/foundry poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
run: >
uv run pytest --import-mode=importlib
packages/foundry/tests
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
# Azure Cosmos integration tests
python-tests-cosmos:
@@ -341,7 +347,7 @@ jobs:
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-azure-ai,
python-tests-foundry,
python-tests-cosmos
]
steps:
+26 -22
View File
@@ -62,9 +62,7 @@ jobs:
azure:
- 'python/packages/openai/**'
- 'python/packages/core/agent_framework/azure/**'
- 'python/packages/azure-ai/agent_framework_azure_ai/_deprecated_azure_openai.py'
- 'python/packages/azure-ai/tests/azure_openai/**'
- 'python/samples/**/providers/azure/openai_chat_completion_client_azure*.py'
- 'python/samples/**/providers/azure/**'
misc:
- 'python/packages/anthropic/**'
- 'python/packages/ollama/**'
@@ -141,9 +139,8 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDINGS_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
@@ -195,10 +192,10 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
defaults:
run:
@@ -223,7 +220,7 @@ jobs:
uv run pytest --import-mode=importlib
packages/openai/tests/openai/test_openai_chat_completion_client_azure.py
packages/openai/tests/openai/test_openai_chat_client_azure.py
packages/azure-ai/tests/azure_openai
packages/openai/tests/openai/test_openai_embedding_client_azure.py
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
@@ -333,15 +330,16 @@ jobs:
environment: integration
env:
UV_PYTHON: "3.11"
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_MODEL: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FUNCTIONS_WORKER_RUNTIME: "python"
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
AzureWebJobsStorage: "UseDevelopmentStorage=true"
@@ -387,8 +385,8 @@ jobs:
fail-on-empty: false
title: Functions integration test results
python-tests-azure-ai:
name: Python Tests - Azure AI
python-tests-foundry:
name: Python Integration Tests - Foundry
needs: paths-filter
if: >
github.event_name != 'pull_request' &&
@@ -401,8 +399,10 @@ jobs:
env:
AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME }}
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION }}
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
defaults:
run:
@@ -424,9 +424,13 @@ jobs:
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest
timeout-minutes: 15
run: |
uv run --directory packages/azure-ai poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
uv run --directory packages/foundry poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
run: >
uv run pytest --import-mode=importlib
packages/foundry/tests
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
working-directory: ./python
- name: Test Azure AI samples
timeout-minutes: 10
@@ -513,7 +517,7 @@ jobs:
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-azure-ai,
python-tests-foundry,
python-tests-cosmos,
]
steps:
+64 -157
View File
@@ -23,10 +23,8 @@ jobs:
environment: integration
env:
# Required configuration for get-started samples
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
defaults:
run:
working-directory: python
@@ -43,10 +41,8 @@ jobs:
- name: Create .env for samples
run: |
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
echo "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=$AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=$AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" >> .env
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
- name: Run sample validation
run: |
@@ -64,14 +60,13 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
# Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Foundry configuration
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME || vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
@@ -97,11 +92,10 @@ jobs:
- name: Create .env for samples
run: |
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=$AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=$AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_DEPLOYMENT_NAME=$AZURE_OPENAI_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME=$AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME" >> .env
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env
@@ -125,6 +119,7 @@ jobs:
environment: integration
env:
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
defaults:
@@ -144,6 +139,7 @@ jobs:
- name: Create .env for samples
run: |
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_MODEL=$OPENAI_MODEL" >> .env
echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env
echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .env
@@ -158,15 +154,14 @@ jobs:
name: validation-report-02-agents-openai
path: python/samples/sample_validation/reports/
validate-02-agents-azure-openai:
name: Validate 02-agents/providers/azure_openai
validate-02-agents-azure:
name: Validate 02-agents/providers/azure
runs-on: ubuntu-latest
environment: integration
env:
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_API_VERSION: ${{ vars.AZURE_OPENAI_API_VERSION || '' }}
defaults:
run:
working-directory: python
@@ -183,100 +178,19 @@ jobs:
- name: Create .env for samples
run: |
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=$AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=$AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_DEPLOYMENT_NAME=$AZURE_OPENAI_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_API_VERSION=$AZURE_OPENAI_API_VERSION" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure_openai --save-report --report-name 02-agents-azure-openai
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure --save-report --report-name 02-agents-azure
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-azure-openai
path: python/samples/sample_validation/reports/
validate-02-agents-azure-ai:
name: Validate 02-agents/providers/azure_ai
runs-on: ubuntu-latest
environment: integration
env:
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
BING_CONNECTION_ID: ${{ secrets.BING_CONNECTION_ID }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
echo "AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME=$AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME" >> .env
echo "AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME=$AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME" >> .env
echo "BING_CONNECTION_ID=$BING_CONNECTION_ID" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure_ai --save-report --report-name 02-agents-azure-ai
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-azure-ai
path: python/samples/sample_validation/reports/
validate-02-agents-azure-ai-agent:
name: Validate 02-agents/providers/azure_ai_agent
runs-on: ubuntu-latest
environment: integration
env:
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure_ai_agent --save-report --report-name 02-agents-azure-ai-agent
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-azure-ai-agent
name: validation-report-02-agents-azure
path: python/samples/sample_validation/reports/
validate-02-agents-anthropic:
@@ -409,11 +323,16 @@ jobs:
name: validation-report-02-agents-ollama
path: python/samples/sample_validation/reports/
validate-02-agents-foundry-local:
name: Validate 02-agents/providers/foundry_local
if: false # Temporarily disabled - requires local Foundry setup
validate-02-agents-foundry:
name: Validate 02-agents/providers/foundry
if: false # Temporarily disabled - provider folder also contains the local Foundry sample
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME || '' }}
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION || '' }}
defaults:
run:
working-directory: python
@@ -428,15 +347,22 @@ jobs:
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "FOUNDRY_AGENT_NAME=$FOUNDRY_AGENT_NAME" >> .env
echo "FOUNDRY_AGENT_VERSION=$FOUNDRY_AGENT_VERSION" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry_local --save-report --report-name 02-agents-foundry-local
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry --save-report --report-name 02-agents-foundry
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-foundry-local
name: validation-report-02-agents-foundry
path: python/samples/sample_validation/reports/
validate-02-agents-copilotstudio:
@@ -515,13 +441,8 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
# Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
defaults:
run:
working-directory: python
@@ -538,11 +459,8 @@ jobs:
- name: Create .env for samples
run: |
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=$AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=$AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME" >> .env
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
- name: Run sample validation
run: |
@@ -561,12 +479,8 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
# Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# A2A configuration
A2A_AGENT_HOST: http://localhost:5001/
defaults:
@@ -600,19 +514,18 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
# Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure AI Search (for evaluation samples)
AZURE_SEARCH_ENDPOINT: ${{ secrets.AZURE_SEARCH_ENDPOINT }}
AZURE_SEARCH_API_KEY: ${{ secrets.AZURE_SEARCH_API_KEY }}
AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }}
# Evaluation sample
AZURE_AI_MODEL_DEPLOYMENT_NAME_WORKFLOW: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_MODEL_WORKFLOW: ${{ vars.FOUNDRY_MODEL_WORKFLOW || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_MODEL_EVAL: ${{ vars.FOUNDRY_MODEL_EVAL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
defaults:
run:
working-directory: python
@@ -643,12 +556,11 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
# Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
@@ -670,10 +582,10 @@ jobs:
- name: Create .env for samples
run: |
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=$AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_DEPLOYMENT_NAME=$AZURE_OPENAI_DEPLOYMENT_NAME" >> .env
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env
echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .env
@@ -694,13 +606,11 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
# Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
@@ -727,11 +637,10 @@ jobs:
- name: Create .env for samples
run: |
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=$AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=$AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_DEPLOYMENT_NAME=$AZURE_OPENAI_DEPLOYMENT_NAME" >> .env
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env
echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .env
@@ -759,14 +668,12 @@ jobs:
- validate-01-get-started
- validate-02-agents
- validate-02-agents-openai
- validate-02-agents-azure-openai
- validate-02-agents-azure-ai
- validate-02-agents-azure-ai-agent
- validate-02-agents-azure
- validate-02-agents-anthropic
- validate-02-agents-github-copilot
- validate-02-agents-amazon
- validate-02-agents-ollama
- validate-02-agents-foundry-local
- validate-02-agents-foundry
- validate-02-agents-copilotstudio
- validate-02-agents-custom
- validate-03-workflows
+47 -8
View File
@@ -74,6 +74,37 @@ Contributions must maintain API signature and behavioral compatibility. Contribu
that include breaking changes will be rejected. Please file an issue to discuss
your idea or change if you believe that a breaking change is warranted.
#### Automated API Compatibility Validation
The .NET projects use [Package Validation](https://learn.microsoft.com/dotnet/fundamentals/package-validation/overview)
to automatically detect API breaking changes. This validation runs during `dotnet build`
(Release configuration) and `dotnet pack`, comparing the current API surface against the
latest published NuGet baseline version.
**What gets validated:** By default, packable RC packages (`IsReleaseCandidate=true`) and
GA packages (`IsGenerallyAvailable=true`) that have a published NuGet baseline and do not
override validation settings are automatically validated. The shared baseline version and
default validation settings are defined in `dotnet/nuget/nuget-package.props`, but
individual projects may opt out (for example by setting `EnablePackageValidation=false`).
**If the build fails with CP errors (e.g., CP0001, CP0002):**
1. **Unintentional breaking change** — Refactor your code to maintain backward compatibility.
2. **Intentional breaking change** (approved by maintainers) — Generate a suppression file:
```bash
dotnet build <project>.csproj -c Release /p:ApiCompatGenerateSuppressionFile=true
```
This creates or updates a `CompatibilitySuppressions.xml` in the project directory.
Include this file in your PR with justification for the breaking change.
**After each release:**
1. Delete all `CompatibilitySuppressions.xml` files from validated projects.
2. Update `PackageValidationBaselineVersion` in `dotnet/nuget/nuget-package.props` to the
newly published version.
For more details, see the [Package Validation diagnostic IDs](https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids).
### Suggested Workflow
We use and recommend the following workflow:
@@ -92,22 +123,30 @@ We use and recommend the following workflow:
"issue-123" or "githubhandle-issue".
4. Make and commit your changes to your branch.
5. Add new tests corresponding to your change, if applicable.
6. Run the relevant scripts in [the section below](#development-scripts) to ensure that your build is clean and all tests are passing.
6. Run the relevant scripts in [the section below](#development-setup) to ensure that your build is clean and all tests are passing.
7. Create a PR against the repository's **main** branch.
- State in the description what issue or improvement your change is addressing.
- Verify that all the Continuous Integration checks are passing.
8. Wait for feedback or approval of your changes from the code maintainers.
9. When area owners have signed off, and all checks are green, your PR will be merged.
### Development scripts
### Development Setup
The scripts below are used to build, test, and lint within the project.
Each language has its own dev setup guide, coding standards, and build scripts:
- Python: see [python/DEV_SETUP.md](./python/DEV_SETUP.md).
- .NET:
- Build: `dotnet build`
- Test: `dotnet test`
- Linting (auto-fix): `dotnet format`
- **Python**: [Dev Setup](./python/DEV_SETUP.md) · [Coding Standard](./python/CODING_STANDARD.md) · [README](./python/README.md)
- From the `./python` directory:
- Build: `uv run poe build`
- Unit tests: `uv run poe test -A -m "not integration"`
- Integration tests: `uv run poe test -A -m integration` (requires API keys/endpoints)
- Format + lint: `uv run poe syntax`
- All checks: `uv run poe check`
- **.NET**: [README](./dotnet/README.md) · [Agent Instructions](./dotnet/AGENTS.md)
- From the `./dotnet` directory:
- Build: `dotnet build`
- Unit tests: `dotnet test --filter-query "/*UnitTests*/*/*/*"`
- Integration tests: `dotnet test --filter-query "/*IntegrationTests*/*/*/*"` (requires API keys/endpoints)
- Linting (auto-fix): `dotnet format`
### PR - CI Process
+42 -17
View File
@@ -2,7 +2,7 @@
# Welcome to Microsoft Agent Framework!
[![Microsoft Azure AI Foundry Discord](https://dcbadge.limes.pink/api/server/b5zjErwbQM?style=flat)](https://discord.gg/b5zjErwbQM)
[![Microsoft Foundry Discord](https://dcbadge.limes.pink/api/server/b5zjErwbQM?style=flat)](https://discord.gg/b5zjErwbQM)
[![MS Learn Documentation](https://img.shields.io/badge/MS%20Learn-Documentation-blue)](https://learn.microsoft.com/en-us/agent-framework/)
[![PyPI](https://img.shields.io/pypi/v/agent-framework)](https://pypi.org/project/agent-framework/)
[![NuGet](https://img.shields.io/nuget/v/Microsoft.Agents.AI)](https://www.nuget.org/profiles/MicrosoftAgentFramework/)
@@ -137,24 +137,21 @@ var agent = new OpenAIClient("<apikey>")
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
```
Create a simple Agent, using Azure OpenAI Responses with token based auth, that writes a haiku about the Microsoft Agent Framework
Create a simple Agent, using Microsoft Foundry with token-based auth, that writes a haiku about the Microsoft Agent Framework
```c#
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
// dotnet add package Microsoft.Agents.AI.AzureAI --prerelease
// dotnet add package Azure.Identity
// Use `az login` to authenticate with Azure CLI
using System.ClientModel.Primitives;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI;
using OpenAI.Responses;
// Replace <resource> and gpt-4o-mini with your Azure OpenAI resource name and deployment name.
var agent = new OpenAIClient(
new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"),
new OpenAIClientOptions() { Endpoint = new Uri("https://<resource>.openai.azure.com/openai/v1") })
.GetResponsesClient("gpt-4o-mini")
.AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(model: deploymentName, name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
```
@@ -163,15 +160,43 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
### Python
- [Getting Started with Agents](./python/samples/01-get-started): progressive tutorial from hello-world to hosting
- [Getting Started](./python/samples/01-get-started): progressive tutorial from hello-world to hosting
- [Agent Concepts](./python/samples/02-agents): deep-dive samples by topic (tools, middleware, providers, etc.)
- [Getting Started with Workflows](./python/samples/03-workflows): workflow creation and integration with agents
- [Workflows](./python/samples/03-workflows): workflow creation and integration with agents
- [Hosting](./python/samples/04-hosting): A2A, Azure Functions, Durable Task hosting
- [End-to-End](./python/samples/05-end-to-end): full applications, evaluation, and demos
### .NET
- [Getting Started with Agents](./dotnet/samples/02-agents/Agents): basic agent creation and tool usage
- [Agent Provider Samples](./dotnet/samples/02-agents/AgentProviders): samples showing different agent providers
- [Workflow Samples](./dotnet/samples/03-workflows): advanced multi-agent patterns and workflow orchestration
- [Getting Started](./dotnet/samples/01-get-started): progressive tutorial from hello agent to hosting
- [Agent Concepts](./dotnet/samples/02-agents/Agents): basic agent creation and tool usage
- [Agent Providers](./dotnet/samples/02-agents/AgentProviders): samples showing different agent providers
- [Workflows](./dotnet/samples/03-workflows): advanced multi-agent patterns and workflow orchestration
- [Hosting](./dotnet/samples/04-hosting): A2A, Durable Agents, Durable Workflows
- [End-to-End](./dotnet/samples/05-end-to-end): full applications and demos
## Troubleshooting
### Authentication
| Problem | Cause | Fix |
|---------|-------|-----|
| Authentication errors when using Azure credentials | Not signed in to Azure CLI | Run `az login` before starting your app |
| API key errors | Wrong or missing API key | Verify the key and ensure it's for the correct resource/provider |
> **Tip:** `DefaultAzureCredential` is convenient for development but in production, consider using a specific credential (e.g., `ManagedIdentityCredential`) to avoid latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
### Environment Variables
The samples typically read configuration from environment variables. Common required variables:
| Variable | Used by | Purpose |
|----------|---------|---------|
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI samples | Your Azure OpenAI resource URL |
| `AZURE_OPENAI_DEPLOYMENT_NAME` | Azure OpenAI samples | Model deployment name (e.g. `gpt-4o-mini`) |
| `AZURE_AI_PROJECT_ENDPOINT` | Microsoft Foundry samples | Your Microsoft Foundry project endpoint |
| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Microsoft Foundry samples | Model deployment name |
| `OPENAI_API_KEY` | OpenAI (non-Azure) samples | Your OpenAI platform API key |
## Contributor Resources
@@ -0,0 +1,125 @@
---
status: accepted
contact: rogerbarreto
date: 2026-03-06
deciders: rogerbarreto, alliscode
consulted: ""
informed: ""
---
# Foundry agent surface stays centered on `ChatClientAgent`
## Context
The Microsoft Foundry integration exposes two distinct usage patterns:
1. Direct Responses usage, where callers provide model, instructions, and tools at runtime.
2. Server-side versioned agents, where callers create and manage `AgentVersion` resources through `AIProjectClient.Agents`.
We briefly explored adding public wrapper types such as `FoundryAgent`, `FoundryVersionedAgent`, and `FoundryResponsesChatClient` to make those paths feel more specialized. That direction created extra public types, duplicated existing `ChatClientAgent` behavior, and pushed samples toward compatibility helpers instead of the native Azure SDK flow.
## Decision
Keep the public surface centered on `ChatClientAgent`.
- Direct Responses scenarios use `AIProjectClient.AsAIAgent(...)`.
- Server-side versioned scenarios use native `AIProjectClient.Agents` APIs to create or retrieve agent resources, then wrap `AgentRecord` or `AgentVersion` with `AIProjectClient.AsAIAgent(...)`.
- Compatibility helpers such as `AIProjectClient.CreateAIAgentAsync(...)` and `AIProjectClient.GetAIAgentAsync(...)` remain only as obsolete migration shims.
- Public wrapper types `FoundryAgent`, `FoundryVersionedAgent`, `FoundryResponsesChatClient`, and `FoundryResponsesChatClientAgent` are not part of the chosen direction.
## Why
- `ChatClientAgent` is already the framework abstraction used everywhere else.
- `AIProjectClient` is the native Azure SDK entry point for versioned agent lifecycle operations.
- A single agent abstraction avoids parallel type hierarchies for the same backend.
- Samples become clearer when they show either:
- direct Responses construction via `AIProjectClient.AsAIAgent(...)`, or
- native Foundry resource management via `AIProjectClient.Agents`.
## Consequences
### Direct Responses path
Use the convenience overloads on `AIProjectClient`:
```csharp
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
ChatClientAgent agent = aiProjectClient.AsAIAgent(
model: deploymentName,
instructions: "You are good at telling jokes.",
name: "JokerAgent");
```
Or use composed `ChatClientAgent`
```csharp
ProjectResponsesClient projectResponsesClient = new(new Uri(endpoint), new DefaultAzureCredential(), new AgentReference($"model:{deploymentName}"));
ChatClientAgent agent = new(
chatClient: projectResponsesClient.AsIChatClient(),
instructions: "You are good at telling jokes.",
name: "JokerAgent");
```
This path is code-first and does not create a persistent server-side agent.
### Versioned agent path
Use the convenience overloads on `AIProjectClient`:
```csharp
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
AgentVersion version = await aiProjectClient.Agents.CreateAgentVersionAsync(
"JokerAgent",
new AgentVersionCreationOptions(
new PromptAgentDefinition(deploymentName)
{
Instructions = "You are good at telling jokes."
}));
ChatClientAgent agent = aiProjectClient.AsAIAgent(version);
```
Or use composed `ChatClientAgent`
```csharp
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
AgentVersion version = await aiProjectClient.Agents.CreateAgentVersionAsync(
"JokerAgent",
new AgentVersionCreationOptions(
new PromptAgentDefinition(deploymentName)
{
Instructions = "You are good at telling jokes."
}));
ProjectResponsesClient projectResponsesClient = aiProjectClient
.GetProjectOpenAIClient()
.GetProjectResponsesClientForAgent(new AgentReference(version.Name, version.Version));
ChatClientAgent agent = new(
chatClient: projectResponsesClient.AsIChatClient(),
name: "JokerAgent");
```
### Samples
- `FoundryAgents/` samples show the direct Responses path with `AIProjectClient.AsAIAgent(...)`.
- `FoundryVersionedAgents/` samples should show native `AIProjectClient.Agents` create/get/delete flows plus `AsAIAgent(...)`.
### Compatibility APIs
Obsolete helper extensions remain only to ease migration of existing code. New samples and new guidance should not be written against them.
## Rejected direction
Do not introduce or preserve separate public wrapper types whose main purpose is to forward to `ChatClientAgent` while carrying Foundry-specific naming.
That approach:
- duplicates lifecycle concepts already present on `AIProjectClient`,
- fragments the public API,
- complicates samples and docs,
- and makes migration harder by encouraging wrapper-specific affordances.
+960
View File
@@ -0,0 +1,960 @@
status: proposed
date: 2026-03-23
contact: sergeymenshykh
deciders: rbarreto, westey-m, eavanvalkenburg
---
# Agent Skills: Multi-Source Architecture
## Context and Problem Statement
The Agent Framework needs a skills system that lets agents discover and use domain-specific knowledge, reference documents, and executable scripts. Skills can originate from different sources — filesystem directories (SKILL.md files), inline C# code, or reusable class libraries — and the framework must support all three uniformly while allowing extensibility, composition, and filtering.
## Decision Drivers
- Skills must be definable from multiple sources: filesystem, inline code, reusable classes, etc
- Common abstractions are needed so the provider and builder work uniformly regardless of skill origin
- File-based scripts must support user-defined executors, enabling custom runtimes and languages; code/class-based scripts execute in-process as C# delegates
- Skills must be filterable so consumers can include or exclude specific skills based on defined criteria
- Multiple skill sources must be composable into a single provider
- It must be possible to add custom skill sources (e.g., databases, REST APIs, package registries) by implementing a common abstraction
## Architecture
### Model-Facing Tools
Skills are presented to the model as up to three tools that progressively disclose skill content. The system prompt lists available skill names and descriptions; the model then calls these tools on demand:
- **`load_skill(skillName)`** — returns the full skill body (instructions, listed resources, listed scripts)
- **`read_skill_resource(skillName, resourceName)`** — reads a supplementary resource (file-based or code-defined) associated with a skill
- **`run_skill_script(skillName, scriptName, arguments?)`** — executes a script associated with a skill; only registered when at least one skill contains scripts
Each tool delegates to the corresponding method on the resolved `AgentSkill` — calling `Resource.ReadAsync()` or `Script.RunAsync()` respectively.
If skills have no scripts defined, the `run_skill_script` tool is **not advertised** to the model and instructions related to script execution are **not included** in the default skills instructions.
### Abstract Base Types
The architecture defines four abstract base types that all skill variants implement:
```csharp
public abstract class AgentSkill
{
public abstract AgentSkillFrontmatter Frontmatter { get; }
public abstract string Content { get; }
public abstract IReadOnlyList<AgentSkillResource>? Resources { get; }
public abstract IReadOnlyList<AgentSkillScript>? Scripts { get; }
}
public abstract class AgentSkillResource
{
public string Name { get; }
public string? Description { get; }
public abstract Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default);
}
public abstract class AgentSkillScript
{
public string Name { get; }
public string? Description { get; }
public abstract Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
}
public abstract class AgentSkillsSource
{
public abstract Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default);
}
```
Skill metadata is captured via `AgentSkillFrontmatter`:
```csharp
public sealed class AgentSkillFrontmatter
{
public AgentSkillFrontmatter(string name, string description) { ... }
public string Name { get; }
public string Description { get; }
public string? License { get; set; }
public string? Compatibility { get; set; }
public string? AllowedTools { get; set; }
public AdditionalPropertiesDictionary? Metadata { get; set; }
}
```
The type hierarchy at a glance:
```
AgentSkill (abstract) AgentSkillsSource (abstract)
├── AgentFileSkill ├── AgentFileSkillsSource (public)
└── [Programmatic] ├── AgentInMemorySkillsSource (public)
├── AgentInlineSkill ├── AggregatingAgentSkillsSource (public)
└── AgentClassSkill (abstract) └── DelegatingAgentSkillsSource (abstract, public)
├── FilteringAgentSkillsSource (public)
AgentSkillResource (abstract) ├── CachingAgentSkillsSource (public)
├── AgentFileSkillResource └── DeduplicatingAgentSkillsSource (public)
└── AgentInlineSkillResource
AgentSkillScript (abstract)
├── AgentFileSkillScript
└── AgentInlineSkillScript
```
There are two top-level categories of skills:
1. **File-Based Skills** — discovered from `SKILL.md` files on the filesystem. Resources and scripts are files in subdirectories.
2. **Programmatic Skills** — defined in C# code. These are further divided into:
- **Inline Skills** — built at runtime via the `AgentInlineSkill` class and its fluent API. Ideal for quick, agent-specific skill definitions.
- **Class-Based Skills** — defined as reusable C# classes that subclass `AgentClassSkill`. Ideal for packaging skills as shared libraries or NuGet packages.
Both programmatic skill types use `AgentInlineSkillResource` and `AgentInlineSkillScript` for their resources and scripts. They are typically served by `AgentInMemorySkillsSource`, which accepts any `AgentSkill` and is not limited to programmatic skills.
### File-Based Skills
File-based skills are authored as `SKILL.md` files on disk. Resources and scripts are discovered from corresponding subfolders within the skill directory.
**`AgentFileSkill`** — A filesystem-based skill discovered from a directory containing a `SKILL.md` file. Parsed from YAML frontmatter; content is the raw markdown body. Resources and scripts are discovered from files in corresponding subfolders:
```csharp
public sealed class AgentFileSkill : AgentSkill
{
internal AgentFileSkill(
AgentSkillFrontmatter frontmatter, string content, string path,
IReadOnlyList<AgentSkillResource>? resources = null,
IReadOnlyList<AgentSkillScript>? scripts = null) { ... }
}
```
**`AgentFileSkillResource`** — A file-based skill resource. Reads content from a file on disk relative to the skill directory:
```csharp
internal sealed class AgentFileSkillResource : AgentSkillResource
{
public AgentFileSkillResource(string name, string fullPath) { ... }
public string FullPath { get; }
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
{
return File.ReadAllTextAsync(FullPath, Encoding.UTF8, cancellationToken);
}
}
```
**`AgentFileSkillScript`** — A file-based skill script that represents a script file on disk. Delegates execution to an external `AgentFileSkillScriptRunner` callback (e.g., runs Python/shell via `Process.Start`). Throws `NotSupportedException` if no executor is configured:
```csharp
public delegate Task<object?> AgentFileSkillScriptRunner(
AgentFileSkill skill, AgentFileSkillScript script,
AIFunctionArguments arguments, CancellationToken cancellationToken);
public sealed class AgentFileSkillScript : AgentSkillScript
{
private readonly AgentFileSkillScriptRunner _executor;
internal AgentFileSkillScript(string name, string fullPath, AgentFileSkillScriptRunner executor)
: base(name) { ... }
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, ...)
{
return await _executor(fileSkill, this, arguments, cancellationToken);
}
}
```
The executor can be provided at the **provider level** via `AgentSkillsProviderBuilder.UseFileScriptRunner(executor)` and optionally overridden for a **particular file skill** or for a **set of skills** at the file skill source level, giving fine-grained control over how different scripts are executed.
**`AgentFileSkillsSource`** — A skill source that discovers skills from filesystem directories containing `SKILL.md` files. Recursively scans directories (max 2 levels), validates frontmatter, and enforces path traversal and symlink security checks:
```csharp
public sealed partial class AgentFileSkillsSource : AgentSkillsSource
{
public AgentFileSkillsSource(
IEnumerable<string> skillPaths,
AgentFileSkillScriptRunner scriptRunner,
AgentFileSkillsSourceOptions? options = null,
ILoggerFactory? loggerFactory = null) { ... }
}
```
**`AgentFileSkillsSourceOptions`** — Configuration options for `AgentFileSkillsSource`. Allows customizing the allowed file extensions for resources and scripts without adding constructor parameters:
```csharp
public sealed class AgentFileSkillsSourceOptions
{
public IEnumerable<string>? AllowedResourceExtensions { get; set; }
public IEnumerable<string>? AllowedScriptExtensions { get; set; }
}
```
**Example** — A file-based skill on disk and how it is added to a source:
```
skills/
└── unit-converter/
├── SKILL.md # frontmatter + instructions
├── resources/
│ └── conversion-table.csv # discovered as a resource
└── scripts/
└── convert.py # discovered as a script
```
```csharp
var source = new AgentFileSkillsSource(skillPaths: ["./skills"], scriptRunner: SubprocessScriptRunner.RunAsync);
var provider = new AgentSkillsProvider(source);
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
```
### Programmatic Skills
Programmatic skills are defined in C# code rather than discovered from the filesystem. There are two kinds: **inline** and **class-based**. Both use `AgentInlineSkillResource` and `AgentInlineSkillScript` for resources and scripts, and are held by a single `AgentInMemorySkillsSource`.
**`AgentInMemorySkillsSource`** — A general-purpose skill source that holds any `AgentSkill` instances in memory. Although commonly used for programmatic skills (`AgentInlineSkill` and `AgentClassSkill`), it accepts any `AgentSkill` subclass and is not restricted to code-defined skills:
```csharp
public sealed class AgentInMemorySkillsSource : AgentSkillsSource
{
public AgentInMemorySkillsSource(
IEnumerable<AgentSkill> skills,
ILoggerFactory? loggerFactory = null) { ... }
}
```
#### Inline Skills
Inline skills are built at runtime via the `AgentInlineSkill` class and its fluent API. They are ideal for quick, agent-specific skill definitions where a full class hierarchy would be overkill.
**`AgentInlineSkill`** — A skill defined entirely in code. Resources can be static values or functions; scripts are always functions. Constructed with name, description, and instructions, then extended with resources and scripts:
```csharp
public sealed class AgentInlineSkill : AgentSkill
{
public AgentInlineSkill(string name, string description, string instructions, string? license = null, string? compatibility = null, ...) { ... }
public AgentInlineSkill(AgentSkillFrontmatter frontmatter, string instructions) { ... }
public AgentInlineSkill AddResource(object value, string name, string? description = null);
public AgentInlineSkill AddResource(Delegate handler, string name, string? description = null);
public AgentInlineSkill AddScript(Delegate handler, string name, string? description = null);
}
```
**`AgentInlineSkillResource`** — A skill resource that wraps a static value:
```csharp
public sealed class AgentInlineSkillResource : AgentSkillResource
{
public AgentInlineSkillResource(object value, string name, string? description = null)
: base(name, description)
{
_value = value;
}
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
{
return Task.FromResult<object?>(_value);
}
}
```
**`AgentInlineSkillResource`** — A skill resource backed by a delegate. The delegate is invoked via an `AIFunction` each time `ReadAsync` is called, producing a dynamic (computed) value:
```csharp
public sealed class AgentInlineSkillResource : AgentSkillResource
{
public AgentInlineSkillResource(Delegate handler, string name, string? description = null)
: base(name, description)
{
_function = AIFunctionFactory.Create(handler, name: name);
}
public override async Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
{
return await _function.InvokeAsync(new AIFunctionArguments() { Services = serviceProvider }, cancellationToken);
}
}
```
**`AgentInlineSkillScript`** — A skill script backed by a delegate via an `AIFunction`:
```csharp
public sealed class AgentInlineSkillScript : AgentSkillScript
{
private readonly AIFunction _function;
public AgentInlineSkillScript(Delegate handler, string name, string? description = null)
: base(name, description)
{
_function = AIFunctionFactory.Create(handler, name: name);
}
public JsonElement? ParametersSchema => _function.JsonSchema;
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, ...)
{
return await _function.InvokeAsync(arguments, cancellationToken);
}
}
```
**Example** — Creating an inline skill with a resource and script, then adding it to a source:
```csharp
var skill = new AgentInlineSkill(
name: "unit-converter",
description: "Converts between measurement units.",
instructions: """
Use this skill to convert values between metric and imperial units.
Refer to the conversion-table resource for supported unit pairs.
Run the convert script to perform conversions.
"""
)
.AddResource("kg=2.205lb, m=3.281ft, L=0.264gal", "conversion-table", "Supported unit pairs")
.AddScript(Convert, "convert", "Converts a value between units");
var source = new AgentInMemorySkillsSource([skill]);
var provider = new AgentSkillsProvider(source);
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
static string Convert(double value, double factor)
=> JsonSerializer.Serialize(new { result = Math.Round(value * factor, 4) });
```
#### Class-Based Skills
Class-based skills are designed for packaging skills as reusable libraries. Users subclass `AgentClassSkill` and override properties. Unlike inline skills, class-based skills are self-contained, can live in shared libraries or NuGet packages, and are well-suited for dependency injection.
**`AgentClassSkill`** — An abstract base class for defining skills as reusable C# classes that bundle all skill components (frontmatter, instructions, resources, scripts) together. Designed for packaging skills as distributable libraries:
```csharp
public abstract class AgentClassSkill : AgentSkill
{
public abstract string Instructions { get; }
// Content is auto-synthesized from Frontmatter + Instructions + Resources + Scripts
public override string Content =>
SkillContentBuilder.BuildContent(Frontmatter.Name, Frontmatter.Description,
SkillContentBuilder.BuildBody(Instructions, Resources, Scripts));
}
```
**Example** — Defining a class-based skill and adding it to a source:
```csharp
public class UnitConverterSkill : AgentClassSkill
{
public override AgentSkillFrontmatter Frontmatter { get; } =
new("unit-converter", "Converts between measurement units.");
public override string Instructions => """
Use this skill to convert values between metric and imperial units.
Refer to the conversion-table resource for supported unit pairs.
Run the convert script to perform conversions.
""";
public override IReadOnlyList<AgentSkillResource>? Resources { get; } =
[
new AgentInlineSkillResource("kg=2.205lb, m=3.281ft", "conversion-table"),
];
public override IReadOnlyList<AgentSkillScript>? Scripts { get; } =
[
new AgentInlineSkillScript(Convert, "convert"),
];
private static string Convert(double value, double factor)
=> JsonSerializer.Serialize(new { result = Math.Round(value * factor, 4) });
}
var source = new AgentInMemorySkillsSource([new UnitConverterSkill()]);
var provider = new AgentSkillsProvider(source);
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
```
## Filtering, Caching, and Deduplication
The following subsections present alternative approaches for handling filtering, caching, and deduplication of skills across multiple sources.
### Via Composition
In this approach, the `AgentSkillsProvider` accepts a **single** `AgentSkillsSource`. Multiple sources are composed externally via an aggregate source, and cross-cutting concerns like filtering, caching, and deduplication are implemented as **source decorators** — subclasses of `DelegatingAgentSkillsSource` that intercept `GetSkillsAsync()`.
**`FilteringAgentSkillsSource`** — A decorator that applies filter logic before returning results. The decorator pattern keeps filtering orthogonal to source implementations and allows composing multiple filters:
```csharp
public sealed class FilteringAgentSkillsSource : DelegatingAgentSkillsSource
{
private readonly Func<AgentSkill, bool> _predicate;
public FilteringAgentSkillsSource(AgentSkillsSource innerSource, Func<AgentSkill, bool> predicate)
: base(innerSource)
{
_predicate = predicate;
}
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
{
var skills = await this.InnerSource.GetSkillsAsync(cancellationToken);
return skills.Where(_predicate).ToList();
}
}
```
**`CachingAgentSkillsSource`** — A decorator that caches skills after the first load, keeping the provider stateless and giving consumers control over caching granularity per source. For example, file-based skills (expensive to discover) can be cached while code-defined skills remain uncached:
```csharp
public sealed class CachingAgentSkillsSource : DelegatingAgentSkillsSource
{
private IList<AgentSkill>? _cached;
public CachingAgentSkillsSource(AgentSkillsSource innerSource)
: base(innerSource)
{
}
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
{
return _cached ??= await this.InnerSource.GetSkillsAsync(cancellationToken);
}
}
```
**Deduplication** is similarly implemented as a decorator (`DeduplicatingAgentSkillsSource`) that deduplicates by name (case-insensitive, first-one-wins) and logs a warning for skipped duplicates.
**Example** — Combining file-based and code-defined sources with filtering and caching:
```csharp
var fileSource = new CachingAgentSkillsSource(new AgentFileSkillsSource(["./skills"]));
var codeSource = new AgentInMemorySkillsSource([myCodeSkill]);
var compositeSource = new FilteringAgentSkillsSource(
new AggregatingAgentSkillsSource([fileSource, codeSource]),
filter: s => s.Frontmatter.Name != "internal");
var provider = new AgentSkillsProvider(compositeSource);
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
```
**Pros:**
- Clean single-responsibility: the provider serves skills, sources provide them.
- Caching, filtering, and deduplication are composable as source decorators — each concern is a separate, testable wrapper.
**Cons:**
- DI is less flexible: multiple `AgentSkillsSource` implementations registered in the container cannot be auto-injected into the provider. The consumer must manually compose them via an aggregate source.
- Increased public API surface: requires additional public classes (aggregate source, caching decorators, filtering decorators) that consumers need to learn and use.
### Via AgentSkillsProvider
In this approach, the `AgentSkillsProvider` accepts **`IEnumerable<AgentSkillsSource>`** and handles aggregation, filtering, caching, and deduplication internally.
The provider aggregates skills from all registered sources, deduplicates by name (case-insensitive, first-one-wins), caches the result after the first load, and optionally applies filtering via a predicate on `AgentSkillsProviderOptions`. Duplicate skill names are logged as warnings.
**Example** — Registering multiple sources directly with the provider:
```csharp
// Conceptual example — in practice, use AgentSkillsProviderBuilder
var fileSource = new AgentFileSkillsSource(["./skills"]);
var codeSource = new AgentInMemorySkillsSource([myCodeSkill]);
var provider = new AgentSkillsProvider(
sources: [fileSource, codeSource],
options: new AgentSkillsProviderOptions
{
Filter = s => s.Frontmatter.Name != "internal",
});
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
```
**Pros:**
- DI-friendly: register multiple `AgentSkillsSource` implementations in the container, and they are all auto-injected into `AgentSkillsProvider` via `IEnumerable<AgentSkillsSource>`.
- Smaller public API surface: no need for aggregate source, caching decorators, or filtering decorator classes — these concerns are handled internally by the provider.
**Cons:**
- The provider takes on multiple responsibilities — aggregation, caching, deduplication, and filtering.
- Less granular caching control: caching is all-or-nothing across sources rather than per-source as with decorators.
- Less extensible: new behaviors (e.g., ordering, TTL expiration) require modifying the provider rather than adding a decorator.
### Builder Pattern
**`AgentSkillsProviderBuilder`** provides a fluent API for composing skills from multiple sources. The builder centralizes configuration — script executors, approval callbacks, prompt templates, and filtering — so consumers don't need to know the underlying source types.
The builder internally decides how to wire up the object graph: it creates the appropriate source instances, applies caching and filtering, and returns a fully configured `AgentSkillsProvider`. This keeps the setup code concise while still allowing fine-grained control when needed.
**Example** — Using the builder to combine multiple source types with configuration:
```csharp
var provider = new AgentSkillsProviderBuilder()
.UseFileSkill("./skills") // file-based source
.UseInlineSkills(codeSkill) // code-defined source
.UseClassSkills(new ClassSkill()) // class-based source
.UseFileScriptRunner(SubprocessScriptRunner.RunAsync) // script runner
.UseScriptApproval() // optional human-in-the-loop
.UsePromptTemplate(customTemplate) // optional prompt customization
.UseFilter(s => s.Frontmatter.Name != "internal") // optional skill filtering
.Build();
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
```
## Adding a Custom Skill Type
The skills framework is designed for extensibility. While file-based and inline skills cover common
scenarios, you can introduce entirely new skill types by subclassing the four base classes:
| Base class | Purpose |
|-----------------------|-----------------------------------------------------|
| `AgentSkillsSource` | Discovers and loads skills from a particular origin |
| `AgentSkill` | Holds metadata, content, resources, and scripts |
| `AgentSkillResource` | Provides supplementary content to a skill |
| `AgentSkillScript` | Represents an executable action within a skill |
The example below implements a **cloud-based skill type** where skills, resources, and scripts are
all stored in and executed through a remote cloud service (e.g., Azure Blob Storage + Azure Functions).
### Step 1 — Define a custom resource
A `CloudSkillResource` reads resource content from a cloud storage endpoint instead of the local
filesystem:
```csharp
/// <summary>
/// A skill resource backed by a cloud storage endpoint.
/// </summary>
public sealed class CloudSkillResource : AgentSkillResource
{
private readonly HttpClient _httpClient;
public CloudSkillResource(string name, Uri blobUri, HttpClient httpClient, string? description = null)
: base(name, description)
{
BlobUri = blobUri ?? throw new ArgumentNullException(nameof(blobUri));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
/// <summary>
/// Gets the URI of the cloud blob that holds this resource's content.
/// </summary>
public Uri BlobUri { get; }
/// <inheritdoc/>
public override async Task<object?> ReadAsync(
IServiceProvider? serviceProvider = null,
CancellationToken cancellationToken = default)
{
return await _httpClient.GetStringAsync(BlobUri, cancellationToken).ConfigureAwait(false);
}
}
```
### Step 2 — Define a custom script
A `CloudSkillScript` executes a script by calling a cloud function endpoint, passing arguments as
the request body:
```csharp
/// <summary>
/// A skill script executed via a cloud function endpoint.
/// </summary>
public sealed class CloudSkillScript : AgentSkillScript
{
private readonly HttpClient _httpClient;
public CloudSkillScript(string name, Uri functionUri, HttpClient httpClient, string? description = null)
: base(name, description)
{
FunctionUri = functionUri ?? throw new ArgumentNullException(nameof(functionUri));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
/// <summary>
/// Gets the URI of the cloud function that runs this script.
/// </summary>
public Uri FunctionUri { get; }
/// <inheritdoc/>
public override async Task<object?> RunAsync(
AgentSkill skill,
AIFunctionArguments arguments,
CancellationToken cancellationToken = default)
{
var json = JsonSerializer.Serialize(arguments);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(FunctionUri, content, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
}
}
```
### Step 3 — Define a custom skill
A `CloudSkill` bundles cloud-specific metadata (e.g., the base endpoint) with the standard skill
shape:
```csharp
/// <summary>
/// An <see cref="AgentSkill"/> whose content, resources, and scripts are stored in a cloud service.
/// </summary>
public sealed class CloudSkill : AgentSkill
{
public CloudSkill(
AgentSkillFrontmatter frontmatter,
string content,
Uri endpoint,
IReadOnlyList<AgentSkillResource>? resources = null,
IReadOnlyList<AgentSkillScript>? scripts = null)
{
Frontmatter = frontmatter ?? throw new ArgumentNullException(nameof(frontmatter));
Content = content ?? throw new ArgumentNullException(nameof(content));
Endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint));
Resources = resources;
Scripts = scripts;
}
/// <inheritdoc/>
public override AgentSkillFrontmatter Frontmatter { get; }
/// <inheritdoc/>
public override string Content { get; }
/// <summary>
/// Gets the base cloud endpoint for this skill.
/// </summary>
public Uri Endpoint { get; }
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillResource>? Resources { get; }
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillScript>? Scripts { get; }
}
```
### Step 4 — Define a custom source
A `CloudSkillsSource` discovers skills from a cloud catalog API and constructs `CloudSkill`
instances with their associated resources and scripts:
```csharp
/// <summary>
/// A skill source that discovers and loads skills from a cloud catalog API.
/// </summary>
public sealed class CloudSkillsSource : AgentSkillsSource
{
private readonly Uri _catalogUri;
private readonly HttpClient _httpClient;
public CloudSkillsSource(Uri catalogUri, HttpClient httpClient)
{
_catalogUri = catalogUri ?? throw new ArgumentNullException(nameof(catalogUri));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
/// <inheritdoc/>
public override async Task<IList<AgentSkill>> GetSkillsAsync(
CancellationToken cancellationToken = default)
{
// Fetch the skill catalog from the cloud service.
var json = await _httpClient.GetStringAsync(_catalogUri, cancellationToken)
.ConfigureAwait(false);
var catalog = JsonSerializer.Deserialize<CloudSkillCatalog>(json)!;
var skills = new List<AgentSkill>();
foreach (var entry in catalog.Skills)
{
var frontmatter = new AgentSkillFrontmatter(entry.Name, entry.Description);
// Build cloud-backed resources.
var resources = entry.Resources
.Select(r => new CloudSkillResource(r.Name, r.BlobUri, _httpClient, r.Description))
.ToList<AgentSkillResource>();
// Build cloud-backed scripts.
var scripts = entry.Scripts
.Select(s => new CloudSkillScript(s.Name, s.FunctionUri, _httpClient, s.Description))
.ToList<AgentSkillScript>();
skills.Add(new CloudSkill(frontmatter, entry.Content, entry.Endpoint, resources, scripts));
}
return skills;
}
}
```
### Step 5 — Register with the builder
Use `UseSource` to wire the custom source into the provider:
```csharp
var httpClient = new HttpClient();
var provider = new AgentSkillsProviderBuilder()
.UseSource(new CloudSkillsSource(
new Uri("https://my-service.example.com/skills/catalog"),
httpClient))
// Mix with other source types if needed:
.UseFileSkill("/local/skills", scriptRunner)
.UseInlineSkills(someInlineSkill)
.Build();
```
The `AgentSkillsProvider` handles all skill types uniformly — any combination of file-based, inline,
class-based, and custom skills can coexist in the same provider. Custom skills automatically
participate in the model-facing tools (`load_skill`, `read_skill_resource`, `run_skill_script`),
filtering, deduplication, and caching — no additional integration work is required.
## Script Representation: `AgentSkillScript` vs `AIFunction`
Two approaches were considered for representing executable scripts within skills:
### Option A — Custom `AgentSkillScript` abstract base class (original design)
Scripts are modeled as a custom `AgentSkillScript` abstract class with `Name`, `Description`, and
`RunAsync(AgentSkill, AIFunctionArguments, CancellationToken)`. Concrete implementations:
`AgentInlineSkillScript` (wraps a delegate/`AIFunction`) and `AgentFileSkillScript` (wraps a file path + executor delegate).
```csharp
// Base type
public abstract class AgentSkillScript
{
public string Name { get; }
public string? Description { get; }
public abstract Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
}
// AgentSkill exposes scripts as:
public abstract IReadOnlyList<AgentSkillScript>? Scripts { get; }
// Inline script wraps an AIFunction internally
var script = new AgentInlineSkillScript(ConvertUnits, "convert");
// Pre-built AIFunction must be wrapped
var script = new AgentInlineSkillScript(myAIFunction);
// Class-based skill declares scripts as:
public override IReadOnlyList<AgentSkillScript>? Scripts { get; } =
[
new AgentInlineSkillScript(ConvertUnits, "convert"),
];
// Provider executes scripts by passing the owning skill:
await script.RunAsync(skill, arguments, cancellationToken);
```
**Pros:**
- **Explicit skill context at execution time.** `RunAsync` receives the owning `AgentSkill`, so any script can access skill metadata or resources during execution without requiring construction-time wiring.
- **Self-contained abstraction.** A dedicated type communicates clearly that scripts are a skills-framework concept, separate from general-purpose AI functions.
- **Easier extensibility for custom script types.** Third-party implementations can subclass `AgentSkillScript` and access the owning skill in `RunAsync` without special setup.
**Cons:**
- **Wrapper overhead.** `AgentInlineSkillScript` is a thin pass-through around `AIFunction` — it adds a class, a constructor, and an indirection layer for no behavioral difference.
- **Parallel abstraction.** `AgentSkillScript` and `AIFunction` serve overlapping purposes (named callable with arguments), creating two parallel hierarchies for the same concept.
- **Friction for consumers.** Users who already have `AIFunction` instances must wrap them in `AgentInlineSkillScript` to use them as scripts, adding ceremony.
### Option B — Reuse `AIFunction` directly
Scripts are represented as `AIFunction` (from `Microsoft.Extensions.AI`). `AgentSkill.Scripts` returns
`IReadOnlyList<AIFunction>?`. `AgentInlineSkillScript` is eliminated entirely — callers use
`AIFunctionFactory.Create(delegate, name: ...)` or pass `AIFunction` instances directly.
`AgentFileSkillScript` becomes an `AIFunction` subclass that captures its owning `AgentFileSkill` via
an internal back-reference set during construction.
```csharp
// AgentSkill exposes scripts as AIFunction directly:
public abstract IReadOnlyList<AIFunction>? Scripts { get; }
// Inline scripts use AIFunctionFactory — no wrapper class needed
var skill = new AgentInlineSkill("my-skill", "desc", "instructions");
skill.AddScript(ConvertUnits, "convert"); // delegate
skill.AddScript(myAIFunction); // pre-built AIFunction — no wrapping
// Class-based skill declares scripts as:
public override IReadOnlyList<AIFunction>? Scripts { get; } =
[
AIFunctionFactory.Create(ConvertUnits, name: "convert"),
];
// Provider executes scripts via standard AIFunction invocation:
await script.InvokeAsync(arguments, cancellationToken);
// File-based scripts extend AIFunction and capture the owning skill internally:
public sealed class AgentFileSkillScript : AIFunction
{
internal AgentFileSkill? Skill { get; set; } // set by AgentFileSkill constructor
protected override async ValueTask<object?> InvokeCoreAsync(
AIFunctionArguments arguments, CancellationToken cancellationToken)
{
return await _executor(Skill!, this, arguments, cancellationToken);
}
}
```
**Pros:**
- **Fewer types.** Eliminates `AgentSkillScript` and `AgentInlineSkillScript`, reducing the public API surface by two classes.
- **Seamless interop.** Any `AIFunction` — whether from `AIFunctionFactory`, a custom subclass, or an external library — can be used as a skill script with zero wrapping.
- **Consistent with `Microsoft.Extensions.AI` ecosystem.** Scripts share the same type as tool functions used by `IChatClient` and `FunctionInvokingChatClient`, reducing conceptual overhead for developers already familiar with the ecosystem.
**Cons:**
- **No owning-skill context in invocation signature.** `AIFunction.InvokeAsync` does not accept an `AgentSkill` parameter, so `AgentFileSkillScript` must capture its owning skill via an internal setter during construction. This adds a construction-order dependency: the skill must set the back-reference on its scripts.
- **Custom script types lose automatic skill access.** Third-party `AIFunction` subclasses that need the owning skill must implement their own mechanism (e.g., constructor injection, closure capture) instead of receiving it as a method parameter.
- **Semantic overloading.** `AIFunction` now means both "a tool the model can call" and "a script within a skill", which could blur the distinction for framework users.
## Resource Representation: `AgentSkillResource` vs `AIFunction`
Two approaches were considered for representing skill resources (supplementary content such as references, assets, or dynamic data):
### Option A — Custom `AgentSkillResource` abstract base class (original design)
Resources are modeled as a custom `AgentSkillResource` abstract class with `Name`, `Description`, and
`ReadAsync(IServiceProvider?, CancellationToken)`. Concrete implementations:
`AgentInlineSkillResource` (static value, delegate, or `AIFunction` wrapper) and `AgentFileSkillResource` (reads file content from disk).
```csharp
// Base type
public abstract class AgentSkillResource
{
public string Name { get; }
public string? Description { get; }
public abstract Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default);
}
// AgentSkill exposes resources as:
public abstract IReadOnlyList<AgentSkillResource>? Resources { get; }
// Static resource
var resource = new AgentInlineSkillResource("static content", "my-resource");
// Dynamic resource (delegate)
var resource = new AgentInlineSkillResource((IServiceProvider sp) => GetData(sp), "my-resource");
// Pre-built AIFunction must be wrapped
var resource = new AgentInlineSkillResource(myAIFunction);
// Class-based skill declares resources as:
public override IReadOnlyList<AgentSkillResource>? Resources { get; } =
[
new AgentInlineSkillResource("# Conversion Tables\n...", "conversion-table"),
];
// Provider reads resources via:
await resource.ReadAsync(serviceProvider, cancellationToken);
```
**Pros:**
- **Clear semantic distinction.** A dedicated `AgentSkillResource` type distinguishes resources (data providers) from scripts (executable actions), making the API self-documenting.
- **Purpose-built API.** `ReadAsync` communicates intent better than `InvokeAsync` for a data-access operation.
**Cons:**
- **Wrapper overhead.** `AgentInlineSkillResource` wraps `AIFunction` internally for delegate/function cases — adding a class and indirection for no behavioral difference.
- **Parallel abstraction.** `AgentSkillResource` and `AIFunction` serve overlapping purposes (named callable that returns data), creating two parallel hierarchies.
- **Friction for consumers.** Users who already have `AIFunction` instances must wrap them in `AgentInlineSkillResource`, adding ceremony.
### Option B — Reuse `AIFunction` directly
Resources are represented as `AIFunction`. `AgentSkill.Resources` returns `IReadOnlyList<AIFunction>?`.
`AgentInlineSkillResource` becomes an `AIFunction` subclass (retained as a convenience for the static-value
pattern: `new AgentInlineSkillResource("data", "name")`). `AgentFileSkillResource` becomes an `AIFunction`
subclass that reads file content.
```csharp
// AgentSkill exposes resources as AIFunction directly:
public abstract IReadOnlyList<AIFunction>? Resources { get; }
// Static resource — AgentInlineSkillResource is retained as a convenience AIFunction subclass
var resource = new AgentInlineSkillResource("static content", "my-resource");
// Dynamic resource — AgentInlineSkillResource wraps delegate as AIFunction
var resource = new AgentInlineSkillResource((IServiceProvider sp) => GetData(sp), "my-resource");
// Pre-built AIFunction can be used directly — no wrapping needed
skill.AddResource(myAIFunction);
// Class-based skill declares resources as:
public override IReadOnlyList<AIFunction>? Resources { get; } =
[
new AgentInlineSkillResource("# Conversion Tables\n...", "conversion-table"),
];
// Provider reads resources via standard AIFunction invocation:
await resource.InvokeAsync(arguments, cancellationToken);
// File-based resources extend AIFunction directly:
internal sealed class AgentFileSkillResource : AIFunction
{
public string FullPath { get; }
protected override async ValueTask<object?> InvokeCoreAsync(
AIFunctionArguments arguments, CancellationToken cancellationToken)
{
return await File.ReadAllTextAsync(FullPath, Encoding.UTF8, cancellationToken);
}
}
```
**Pros:**
- **Fewer base types.** Eliminates the `AgentSkillResource` abstract class, reducing the public API surface.
- **Seamless interop.** Any `AIFunction` can be used as a skill resource with zero wrapping.
**Cons:**
- **Loss of semantic distinction.** Resources and scripts are now both `AIFunction`, which could make it less obvious which list a function belongs to when reading code.
- **Static values require a wrapper.** Unlike the original `ReadAsync` which could return a stored value directly, `AIFunction.InvokeAsync` implies invocation. `AgentInlineSkillResource` is retained as a convenience subclass to handle the static-value case, so this is not eliminated — just moved to a different class.
## Decision Outcome
### 1. Keep `AgentSkillResource` and `AgentSkillScript` (Option A for both sections)
We are staying with the custom `AgentSkillResource` and `AgentSkillScript` model classes instead of reusing `AIFunction`:
- **Resources have no parameters.** If a consumer provides an `AIFunction` with parameters, those parameters will never be advertised to the LLM, and the resulting call will fail.
- **Approval breaks for `AIFunction`-based representations.** When a resource or script represented by an `AIFunction` is configured with approval, the second approval invocation will not work correctly.
- **Injecting the owning skill into an `AIFunction`-based script is problematic.** Constructor injection would introduce a circular reference between the skill and the script. An internal property setter is possible but adds coupling.
### 2. Make all agent skill classes internal
All agent-skill-related classes are made `internal` to minimize the public API surface while the feature matures. We can reconsider and promote types to `public` later based on community signal.
This leaves two public entry points:
- **`AgentSkillsProvider`** — use directly when all skills come from a single source and filtering is not needed.
- **`AgentSkillsProviderBuilder`** — use when mixing skill types or when filtering support is required.
### 3. Caching at provider level
Caching of tools and instructions is implemented inside `AgentSkillsProvider` rather than as an external decorator. Recreating tools and instructions on every provider call is wasteful, and a caching decorator sitting outside the provider would not have the information needed to cache them effectively.
@@ -0,0 +1,121 @@
---
status: accepted
contact: westey-m
date: 2026-03-23
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub
consulted:
informed:
---
# Chat History Persistence Consistency
## Context and Problem Statement
When using `ChatClientAgent` with tools, the `FunctionInvokingChatClient` (FIC) loops multiple times — service call → tool execution → service call → … — before producing a final response. There are two points of discrepancy between how chat history is stored by the framework's `ChatHistoryProvider` and how the underlying AI service stores chat history (e.g., OpenAI Responses with `store=true`):
1. **Persistence timing**: The AI service persists messages after *each* service call within the FIC loop. The `ChatHistoryProvider` currently persists messages only once, at the *end* of the full agent run (after all FIC loop iterations complete).
2. **Trailing `FunctionResultContent` storage**: When tool calling is terminated mid-loop (e.g., via `FunctionInvokingChatClient` termination filters), the final response from the agent may contain `FunctionResultContent` that was never sent to a subsequent service call. The AI service never stores this trailing `FunctionResultContent`, but the `ChatHistoryProvider` currently stores all response content, including the trailing `FunctionResultContent`.
These discrepancies mean that a `ChatHistoryProvider`-managed conversation and a service-managed conversation can diverge in content and structure, even when processing the same interactions.
### Practical Impact: Resuming After Tool-Call Termination
Today, users of `AIAgent` get different behaviors depending on whether chat history is stored service-side or in a `ChatHistoryProvider`. This creates concrete challenges — for example, when the function call loop is terminated and the user wants to resume the conversation in a subsequent run. With service-stored history, the trailing `FunctionResultContent` is never persisted, so the last stored message is the `FunctionCallContent` from the service. With `ChatHistoryProvider`-stored history, the trailing `FunctionResultContent` *is* persisted. The user cannot know whether the last `FunctionResultContent` is in the chat history or not without inspecting the storage mechanism, making it difficult to write resumption logic that works correctly regardless of the storage backend.
### Relationship Between the Two Discrepancies
The persistence timing and `FunctionResultContent` trimming behaviors are interrelated:
- **Per-service-call persistence**: When messages are persisted after each individual service call, trailing `FunctionResultContent` trimming is unnecessary. If tool calling is terminated, the `FunctionResultContent` from the terminated call was never sent to a subsequent service call, so it is never persisted. The per-service-call approach naturally matches the service's behavior.
- **Per-run persistence**: When messages are batched and persisted at the end of the full run, trailing `FunctionResultContent` trimming becomes necessary to match the service's behavior. Without trimming, the stored history contains `FunctionResultContent` that the service would never have stored.
## Decision Drivers
- **A. Consistency**: The default behavior of `ChatHistoryProvider` should produce stored history that closely matches what the underlying AI service would store, minimizing surprise when switching between framework-managed and service-managed chat history.
- **B. Atomicity**: A run that fails mid-way through a multi-step tool-calling loop should not leave chat history in a partially-updated state, unless the user explicitly opts into that behavior.
- **C. Recoverability**: For long-running tool-calling loops, it should be possible to recover intermediate progress if the process is interrupted, rather than losing all work from the current run.
- **D. Simplicity**: The default behavior should be easy to understand and predict for most users, without requiring knowledge of the FIC loop internals.
- **E. Flexibility**: Regardless of the chosen default, users should be able to opt into the alternative behavior.
## Considered Options
- Option 1: Per-run persistence with opt-in FRC (FunctionResultContent) trimming
- Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)
## Pros and Cons of the Options
### Option 1: Per-run persistence with opt-in FRC trimming
Keep the current default behavior of persisting chat history only at the end of the full agent run. Add `FunctionResultContent` trimming as an opt-in behavior to improve consistency with service storage.
- Good, because runs are atomic — chat history is only updated when the full run succeeds, satisfying driver B.
- Good, because the mental model is simple: one run = one history update, satisfying driver D.
- Good, because trimming trailing `FunctionResultContent` improves consistency with service storage, partially satisfying driver A.
- Bad, because the default persistence timing still differs from the service's behavior (per-run vs. per-service-call), only partially satisfying driver A.
- Bad, because if the process crashes mid-loop, all intermediate progress from the current run is lost, not satisfying driver C.
- Bad, because this option alone does not provide a way for users to opt into per-service-call persistence, not satisfying driver E.
### Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)
Introduce an optional RequirePerServiceCallChatHistoryPersistence setting to persist chat history after each individual service call within the FIC loop, matching the AI service's behavior. Trailing `FunctionResultContent` trimming is unnecessary with this approach (it is naturally handled).
Settings:
- `RequirePerServiceCallChatHistoryPersistence` = `true`
- Good, because the stored history matches the service's behavior when opting in for both timing and content, fully satisfying driver A.
- Good, because intermediate progress is preserved if the process is interrupted, satisfying driver C.
- Good, because no separate `FunctionResultContent` trimming logic is needed, reducing complexity.
- Bad, because chat history may be left in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), not satisfying driver B. A subsequent run cannot proceed without manually providing the missing `FunctionResultContent`.
- Bad, because the mental model is more complex: a single run may produce multiple history updates, partially failing driver D.
- Neutral, because users can opt out to per-run persistence if they prefer atomicity, satisfying driver E.
## Decision Outcome
Chosen option: **Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)**. The existing per-run persistence behavior is retained as-is, requiring no changes from users. Per-service-call persistence is available as an opt-in feature via the `RequirePerServiceCallChatHistoryPersistence` setting. This satisfies drivers B (atomicity) and D (simplicity) for the common case, while fully satisfying driver A (consistency) for users who opt into simulated service-stored behavior. Users who need per-service-call persistence for recoverability (driver C) can enable it explicitly.
### Configuration Matrix
The behavior depends on the combination of `UseProvidedChatClientAsIs` and `RequirePerServiceCallChatHistoryPersistence`:
| `UseProvidedChatClientAsIs` | `RequirePerServiceCallChatHistoryPersistence` | Behavior |
|---|---|---|
| `false` (default) | `false` (default) | **Per-run persistence.** Messages are persisted at the end of the full agent run via the `ChatHistoryProvider`. |
| `false` | `true` | **Per-service-call persistence (simulated).** A `PerServiceCallChatHistoryPersistingChatClient` middleware is automatically injected into the chat client pipeline between `FunctionInvokingChatClient` and the leaf `IChatClient`. Messages are persisted after each service call. A sentinel `ConversationId` causes FIC to treat the conversation as service-managed. |
| `true` | `false` | **Per-run persistence.** No middleware is injected because the user has provided a custom chat client stack. Messages are persisted at the end of the run. |
| `true` | `true` | **User responsibility.** The system checks whether the custom chat client stack includes a `PerServiceCallChatHistoryPersistingChatClient`. If not, a warning is emitted — the user is expected to have added their own per-service-call persistence mechanism. End-of-run persistence is skipped. |
### Consequences
- Good, because per-run persistence is atomic by default — chat history is only updated when the full run succeeds, satisfying driver B.
- Good, because the default mental model is simple: one run = one history update, satisfying driver D.
- Good, because users who opt into `RequirePerServiceCallChatHistoryPersistence` get stored history that matches the service's behavior for both timing and content, fully satisfying driver A.
- Good, because per-service-call persistence preserves intermediate progress if the process is interrupted, satisfying driver C when opted in.
- Good, because no separate `FunctionResultContent` trimming logic is needed when per-service-call persistence is active — it is naturally handled.
- Good, because conflict detection (configurable via `ThrowOnChatHistoryProviderConflict`, `WarnOnChatHistoryProviderConflict`, `ClearOnChatHistoryProviderConflict`) prevents misconfiguration when a service returns a `ConversationId` alongside a configured `ChatHistoryProvider`.
- Bad, because per-service-call persistence (when opted in) may leave chat history in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), requiring manual recovery in rare cases.
- Neutral, because users who want per-service-call consistency can opt in via `RequirePerServiceCallChatHistoryPersistence = true`, satisfying driver E.
- Neutral, because increased write frequency from per-service-call persistence may impact performance for some storage backends; this can be mitigated with a caching decorator.
### Implementation Notes
#### Conversation ID Consistency
When `RequirePerServiceCallChatHistoryPersistence` is enabled, the `PerServiceCallChatHistoryPersistingChatClient`
decorator also updates `session.ConversationId` after each service call. This handles two scenarios:
1. **Framework-managed chat history** — the decorator sets a sentinel `ConversationId` on the response
so that `FunctionInvokingChatClient` treats the conversation as service-managed (clearing accumulated
history between iterations and not injecting duplicate `FunctionCallContent` during approval processing).
2. **Service-stored chat history** — when the service returns a real `ConversationId`, the decorator
updates `session.ConversationId` immediately after each service call, rather than deferring the update
to the end of the run. This ensures intermediate ConversationId changes are captured even if the
process is interrupted mid-loop.
For some service-stored scenarios (e.g., the Conversations API with the Responses API), there is only
one thread with one ID, so every service call returns the same ConversationId and this per-call update
makes no practical difference. Enabling `RequirePerServiceCallChatHistoryPersistence` ensures consistent
per-service-call behavior across all service types regardless of how they manage ConversationIds.
@@ -462,7 +462,7 @@ class FoundryEvals:
### Azure AI: FoundryEvals Constants
```python
from agent_framework_azure_ai import FoundryEvals
from agent_framework.foundry import FoundryEvals
evaluators = [FoundryEvals.RELEVANCE, FoundryEvals.TOOL_CALL_ACCURACY]
```
+1
View File
@@ -17,6 +17,7 @@
<PropertyGroup>
<IsReleaseCandidate>false</IsReleaseCandidate>
<IsGenerallyAvailable>false</IsGenerallyAvailable>
</PropertyGroup>
<PropertyGroup>
+32 -32
View File
@@ -1,4 +1,4 @@
<Solution>
<Solution>
<Configurations>
<BuildType Name="Debug" />
<BuildType Name="Publish" />
@@ -104,7 +104,8 @@
</Folder>
<Folder Name="/Samples/02-agents/AgentSkills/">
<File Path="samples/02-agents/AgentSkills/README.md" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_BasicSkills/Agent_Step01_BasicSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Agent_Step01_FileBasedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step02_CodeDefinedSkills/Agent_Step02_CodeDefinedSkills.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
@@ -121,6 +122,34 @@
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step04_UsingSkills/Agent_Anthropic_Step04_UsingSkills.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AgentsWithFoundry/">
<File Path="samples/02-agents/AgentsWithFoundry/README.md" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Agent_Step00_FoundryAgentLifecycle.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Agent_Step01_Basics.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Agent_Step02.1_MultiturnConversation.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Agent_Step02.2_MultiturnWithServerConversations.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Agent_Step07_Observability.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Agent_Step08_DependencyInjection.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Agent_Step09_UsingMcpClientAsTools.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Agent_Step10_UsingImages.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Agent_Step11_AsFunctionTool.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Agent_Step12_Middleware.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Agent_Step13_Plugins.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Agent_Step14_CodeInterpreter.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Agent_Step16_FileSearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Agent_Step17_OpenAPITools.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Agent_Step18_BingCustomSearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Agent_Step19_SharePoint.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Agent_Step20_MicrosoftFabric.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Agent_Step21_WebSearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Agent_Step22_MemorySearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AgentWithMemory/">
<File Path="samples/02-agents/AgentWithMemory/README.md" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
@@ -143,35 +172,6 @@
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/AgentWithRAG_Step03_CustomRAGDataSource.csproj" />
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/FoundryAgents/">
<File Path="samples/02-agents/FoundryAgents/README.md" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Evaluations_Step01_RedTeaming/FoundryAgents_Evaluations_Step01_RedTeaming.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Evaluations_Step02_SelfReflection/FoundryAgents_Evaluations_Step02_SelfReflection.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step01.1_Basics/FoundryAgents_Step01.1_Basics.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step01.2_Running/FoundryAgents_Step01.2_Running.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/FoundryAgents_Step02_MultiturnConversation.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/FoundryAgents_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/FoundryAgents_Step04_UsingFunctionToolsWithApprovals.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step05_StructuredOutput/FoundryAgents_Step05_StructuredOutput.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step06_PersistedConversations/FoundryAgents_Step06_PersistedConversations.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step07_Observability/FoundryAgents_Step07_Observability.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step08_DependencyInjection/FoundryAgents_Step08_DependencyInjection.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step09_UsingMcpClientAsTools/FoundryAgents_Step09_UsingMcpClientAsTools.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step10_UsingImages/FoundryAgents_Step10_UsingImages.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step11_AsFunctionTool/FoundryAgents_Step11_AsFunctionTool.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step12_Middleware/FoundryAgents_Step12_Middleware.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step13_Plugins/FoundryAgents_Step13_Plugins.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step14_CodeInterpreter/FoundryAgents_Step14_CodeInterpreter.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step15_ComputerUse/FoundryAgents_Step15_ComputerUse.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step16_FileSearch/FoundryAgents_Step16_FileSearch.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step17_OpenAPITools/FoundryAgents_Step17_OpenAPITools.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step18_BingCustomSearch/FoundryAgents_Step18_BingCustomSearch.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step19_SharePoint/FoundryAgents_Step19_SharePoint.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step20_MicrosoftFabric/FoundryAgents_Step20_MicrosoftFabric.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step21_WebSearch/FoundryAgents_Step21_WebSearch.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step22_MemorySearch/FoundryAgents_Step22_MemorySearch.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step23_LocalMCP/FoundryAgents_Step23_LocalMCP.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/ModelContextProtocol/">
<File Path="samples/02-agents/ModelContextProtocol/README.md" />
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj" />
@@ -318,8 +318,8 @@
<Folder Name="/Samples/05-end-to-end/AspNetAgentAuthorization/">
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/docker-compose.yml" />
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/README.md" />
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj" />
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/RazorWebClient.csproj" />
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj" />
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
+7 -5
View File
@@ -2,17 +2,19 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<RCNumber>4</RCNumber>
<RCNumber>5</RCNumber>
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260311.1</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260311.1</PackageVersion>
<GitTag>1.0.0-rc4</GitTag>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260330.1</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260330.1</PackageVersion>
<GitTag>1.0.0-rc5</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
<!-- Package validation. Baseline Version should be the latest version available on NuGet. -->
<PackageValidationBaselineVersion>0.0.1</PackageValidationBaselineVersion>
<PackageValidationBaselineVersion>1.0.0-rc4</PackageValidationBaselineVersion>
<!-- Enable validation for RC packages and GA packages -->
<EnablePackageValidation Condition="'$(IsReleaseCandidate)' == 'true' OR '$(IsGenerallyAvailable)' == 'true'">true</EnablePackageValidation>
<!-- Validate assembly attributes only for Publish builds -->
<NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn>
<!-- Do not validate reference assemblies -->
@@ -70,7 +70,7 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
if (approvalRequest.AdditionalProperties != null)
{
approvalResponse.AdditionalProperties = new AdditionalPropertiesDictionary();
approvalResponse.AdditionalProperties = [];
foreach (var kvp in approvalRequest.AdditionalProperties)
{
approvalResponse.AdditionalProperties[kvp.Key] = kvp.Value;
@@ -131,9 +131,9 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
approvalCalls.Remove(functionResult.CallId);
}
else if (transformedContents != null)
else
{
transformedContents.Add(content);
transformedContents?.Add(content);
}
}
@@ -155,10 +155,10 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
result ??= CopyMessagesUpToIndex(messages, messageIndex);
result.Add(newMessage);
}
else if (result != null)
else
{
// We're already copying messages, so copy this unchanged message too
result.Add(message);
result?.Add(message);
}
// If result is null, we haven't made any changes yet, so keep processing
}
@@ -57,16 +57,10 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
throw new InvalidOperationException("Invalid request_approval tool call");
}
var request = toolCall.Arguments.TryGetValue("request", out var reqObj) &&
var request = (toolCall.Arguments.TryGetValue("request", out var reqObj) &&
reqObj is JsonElement argsElement &&
argsElement.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))) is ApprovalRequest approvalRequest &&
approvalRequest != null ? approvalRequest : null;
if (request == null)
{
throw new InvalidOperationException("Failed to deserialize approval request from tool call");
}
approvalRequest != null ? approvalRequest : null) ?? throw new InvalidOperationException("Failed to deserialize approval request from tool call");
return new ToolApprovalRequestContent(
requestId: request.ApprovalId,
new FunctionCallContent(
@@ -77,17 +71,11 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
private static ToolApprovalResponseContent ConvertToolResultToApprovalResponse(FunctionResultContent result, ToolApprovalRequestContent approval, JsonSerializerOptions jsonSerializerOptions)
{
var approvalResponse = result.Result is JsonElement je ?
var approvalResponse = (result.Result is JsonElement je ?
(ApprovalResponse?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
result.Result is string str ?
(ApprovalResponse?)JsonSerializer.Deserialize(str, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
result.Result as ApprovalResponse;
if (approvalResponse == null)
{
throw new InvalidOperationException("Failed to deserialize approval response from tool result");
}
result.Result as ApprovalResponse) ?? throw new InvalidOperationException("Failed to deserialize approval response from tool result");
return approval.CreateResponse(approvalResponse.Approved);
}
#pragma warning restore MEAI001
@@ -121,7 +109,7 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
// Track approval ID to original call ID mapping
_ = new Dictionary<string, string>();
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
Dictionary<string, ToolApprovalRequestContent> trackedRequestApprovalToolCalls = new(); // Remote approvals
Dictionary<string, ToolApprovalRequestContent> trackedRequestApprovalToolCalls = []; // Remote approvals
for (int messageIndex = 0; messageIndex < messages.Count; messageIndex++)
{
var message = messages[messageIndex];
@@ -146,7 +134,7 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
});
}
else if (content is FunctionResultContent toolResult &&
trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval) == true)
trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval))
{
result ??= CopyMessagesUpToIndex(messages, messageIndex);
transformedContents ??= CopyContentsUpToIndex(message.Contents, j);
@@ -161,9 +149,9 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
AdditionalProperties = message.AdditionalProperties
});
}
else if (result != null)
else
{
result.Add(message);
result?.Add(message);
}
}
}
@@ -72,10 +72,9 @@ internal sealed class StatefulAgent<TState> : DelegatingAIAgent
if (content is DataContent dataContent && dataContent.MediaType == "application/json")
{
// Deserialize the state
TState? newState = JsonSerializer.Deserialize(
if (JsonSerializer.Deserialize(
dataContent.Data.Span,
this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) as TState;
if (newState != null)
this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) is TState newState)
{
this.State = newState;
}
@@ -18,6 +18,7 @@ using OpenTelemetry.Trace;
#region Setup Telemetry
// Source name for this sample's custom ActivitySource and Meter; other instrumentation uses their own sources/categories.
const string SourceName = "OpenTelemetryAspire.ConsoleApp";
const string ServiceName = "AgentOpenTelemetry";
@@ -40,7 +41,6 @@ var resource = ResourceBuilder.CreateDefault()
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
.AddSource(SourceName) // Our custom activity source
.AddSource("*Microsoft.Agents.AI") // Agent Framework telemetry
.AddHttpClientInstrumentation() // Capture HTTP calls to OpenAI
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint));
@@ -54,8 +54,7 @@ using var tracerProvider = tracerProviderBuilder.Build();
// Setup metrics with resource and instrument name filtering
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
.AddMeter(SourceName) // Our custom meter
.AddMeter("*Microsoft.Agents.AI") // Agent Framework metrics
.AddMeter(SourceName) // Our custom meter source
.AddHttpClientInstrumentation() // HTTP client metrics
.AddRuntimeInstrumentation() // .NET runtime metrics
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint))
@@ -128,7 +127,7 @@ var agent = new ChatClientAgent(instrumentedChatClient,
instructions: "You are a helpful assistant that provides concise and informative responses.",
tools: [AIFunctionFactory.Create(GetWeatherAsync)])
.AsBuilder()
.UseOpenTelemetry(SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
.Build();
var session = await agent.CreateSessionAsync();
@@ -6,6 +6,7 @@ using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AzureAI;
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
@@ -30,14 +31,18 @@ var createdAgentVersion = aiProjectClient.Agents.CreateAgentVersion(agentName: J
// agentVersion.Name = <agentName>
// You can use an AIAgent with an already created server side agent version.
AIAgent existingJokerAgent = aiProjectClient.AsAIAgent(createdAgentVersion);
FoundryAgent existingJokerAgent = aiProjectClient.AsAIAgent(createdAgentVersion);
// You can also create another AIAgent version by providing the same name with a different definition.
AIAgent newJokerAgent = await aiProjectClient.CreateAIAgentAsync(name: JokerName, model: deploymentName, instructions: "You are extremely hilarious at telling jokes.");
AgentVersion newJokerAgentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
JokerName,
new AgentVersionCreationOptions(new PromptAgentDefinition(model: deploymentName) { Instructions = "You are extremely hilarious at telling jokes." }));
FoundryAgent newJokerAgent = aiProjectClient.AsAIAgent(newJokerAgentVersion);
// You can also get the AIAgent latest version just providing its name.
AIAgent jokerAgentLatest = await aiProjectClient.GetAIAgentAsync(name: JokerName);
var latestAgentVersion = jokerAgentLatest.GetService<AgentVersion>()!;
AgentRecord jokerAgentRecord = await aiProjectClient.Agents.GetAgentAsync(JokerName);
FoundryAgent jokerAgentLatest = aiProjectClient.AsAIAgent(jokerAgentRecord);
AgentVersion latestAgentVersion = jokerAgentRecord.GetLatestVersion();
// The AIAgent version can be accessed via the GetService method.
Console.WriteLine($"Latest agent version id: {latestAgentVersion.Id}");
@@ -1,50 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use Agent Skills with a ChatClientAgent.
// Agent Skills are modular packages of instructions and resources that extend an agent's capabilities.
// Skills follow the progressive disclosure pattern: advertise -> load -> read resources.
//
// This sample includes the expense-report skill:
// - Policy-based expense filing with references and assets
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// --- Skills Provider ---
// Discovers skills from the 'skills' directory and makes them available to the agent
var skillsProvider = new FileAgentSkillsProvider(skillPath: Path.Combine(AppContext.BaseDirectory, "skills"));
// --- Agent Setup ---
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient()
.AsAIAgent(new ChatClientAgentOptions
{
Name = "SkillsAgent",
ChatOptions = new()
{
Instructions = "You are a helpful assistant.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName);
// --- Example 1: Expense policy question (loads FAQ resource) ---
Console.WriteLine("Example 1: Checking expense policy FAQ");
Console.WriteLine("---------------------------------------");
AgentResponse response1 = await agent.RunAsync("Are tips reimbursable? I left a 25% tip on a taxi ride and want to know if that's covered.");
Console.WriteLine($"Agent: {response1.Text}\n");
// --- Example 2: Filing an expense report (multi-turn with template asset) ---
Console.WriteLine("Example 2: Filing an expense report");
Console.WriteLine("---------------------------------------");
AgentSession session = await agent.CreateSessionAsync();
AgentResponse response2 = await agent.RunAsync("I had 3 client dinners and a $1,200 flight last week. Return a draft expense report and ask about any missing details.",
session);
Console.WriteLine($"Agent: {response2.Text}\n");
@@ -1,63 +0,0 @@
# Agent Skills Sample
This sample demonstrates how to use **Agent Skills** with a `ChatClientAgent` in the Microsoft Agent Framework.
## What are Agent Skills?
Agent Skills are modular packages of instructions and resources that enable AI agents to perform specialized tasks. They follow the [Agent Skills specification](https://agentskills.io/) and implement the progressive disclosure pattern:
1. **Advertise**: Skills are advertised with name + description (~100 tokens per skill)
2. **Load**: Full instructions are loaded on-demand via `load_skill` tool
3. **Resources**: References and other files loaded via `read_skill_resource` tool
## Skills Included
### expense-report
Policy-based expense filing with spending limits, receipt requirements, and approval workflows.
- `references/POLICY_FAQ.md` — Detailed expense policy Q&A
- `assets/expense-report-template.md` — Submission template
## Project Structure
```
Agent_Step01_BasicSkills/
├── Program.cs
├── Agent_Step01_BasicSkills.csproj
└── skills/
└── expense-report/
├── SKILL.md
├── references/
│ └── POLICY_FAQ.md
└── assets/
└── expense-report-template.md
```
## Running the Sample
### Prerequisites
- .NET 10.0 SDK
- Azure OpenAI endpoint with a deployed model
### Setup
1. Set environment variables:
```bash
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
```
2. Run the sample:
```bash
dotnet run
```
### Examples
The sample runs two examples:
1. **Expense policy FAQ** — Asks about tip reimbursement; the agent loads the expense-report skill and reads the FAQ resource
2. **Filing an expense report** — Multi-turn conversation to draft an expense report using the template asset
## Learn More
- [Agent Skills Specification](https://agentskills.io/)
- [Microsoft Agent Framework Documentation](../../../../../docs/)
@@ -1,40 +0,0 @@
---
name: expense-report
description: File and validate employee expense reports according to Contoso company policy. Use when asked about expense submissions, reimbursement rules, receipt requirements, spending limits, or expense categories.
metadata:
author: contoso-finance
version: "2.1"
---
# Expense Report
## Categories and Limits
| Category | Limit | Receipt | Approval |
|---|---|---|---|
| Meals — solo | $50/day | >$25 | No |
| Meals — team/client | $75/person | Always | Manager if >$200 total |
| Lodging | $250/night | Always | Manager if >3 nights |
| Ground transport | $100/day | >$15 | No |
| Airfare | Economy | Always | Manager; VP if >$1,500 |
| Conference/training | $2,000/event | Always | Manager + L&D |
| Office supplies | $100 | Yes | No |
| Software/subscriptions | $50/month | Yes | Manager if >$200/year |
## Filing Process
1. Collect receipts — must show vendor, date, amount, payment method.
2. Categorize per table above.
3. Use template: [assets/expense-report-template.md](assets/expense-report-template.md).
4. For client/team meals: list attendee names and business purpose.
5. Submit — auto-approved if <$500; manager if $500–$2,000; VP if >$2,000.
6. Reimbursement: 10 business days via direct deposit.
## Policy Rules
- Submit within 30 days of transaction.
- Alcohol is never reimbursable.
- Foreign currency: convert to USD at transaction-date rate; note original currency and amount.
- Mixed personal/business travel: only business portion reimbursable; provide comparison quotes.
- Lost receipts (>$25): file Lost Receipt Affidavit from Finance. Max 2 per quarter.
- For policy questions not covered above, consult the FAQ: [references/POLICY_FAQ.md](references/POLICY_FAQ.md). Answers should be based on what this document and the FAQ state.
@@ -1,5 +0,0 @@
# Expense Report Template
| Date | Category | Vendor | Description | Amount (USD) | Original Currency | Original Amount | Attendees | Business Purpose | Receipt Attached |
|------|----------|--------|-------------|--------------|-------------------|-----------------|-----------|------------------|------------------|
| | | | | | | | | | Yes or No |
@@ -1,55 +0,0 @@
# Expense Policy — Frequently Asked Questions
## Meals
**Q: Can I expense coffee or snacks during the workday?**
A: Daily coffee/snacks under $10 are not reimbursable (considered personal). Coffee purchased during a client meeting or team working session is reimbursable as a team meal.
**Q: What if a team dinner exceeds the per-person limit?**
A: The $75/person limit applies as a guideline. Overages up to 20% are accepted with a written justification (e.g., "client dinner at venue chosen by client"). Overages beyond 20% require pre-approval from your VP.
**Q: Do I need to list every attendee?**
A: Yes. For client meals, list the client's name and company. For team meals, list all employee names. For groups over 10, you may attach a separate attendee list.
## Travel
**Q: Can I book a premium economy or business class flight?**
A: Economy class is the standard. Premium economy is allowed for flights over 6 hours. Business class requires VP pre-approval and is generally reserved for flights over 10 hours or medical accommodation.
**Q: What about ride-sharing (Uber/Lyft) vs. rental cars?**
A: Use ride-sharing for trips under 30 miles round-trip. Rent a car for multi-day travel or when ride-sharing would exceed $100/day. Always choose the compact/standard category unless traveling with 3+ people.
**Q: Are tips reimbursable?**
A: Tips up to 20% are reimbursable for meals, taxi/ride-share, and hotel housekeeping. Tips above 20% require justification.
## Lodging
**Q: What if the $250/night limit isn't enough for the city I'm visiting?**
A: For high-cost cities (New York, San Francisco, London, Tokyo, Sydney), the limit is automatically increased to $350/night. No additional approval is needed. For other locations where rates are unusually high (e.g., during a major conference), request a per-trip exception from your manager before booking.
**Q: Can I stay with friends/family instead and get a per-diem?**
A: No. Contoso reimburses actual lodging costs only, not per-diems.
## Subscriptions and Software
**Q: Can I expense a personal productivity tool?**
A: Software must be directly related to your job function. Tools like IDE licenses, design software, or project management apps are reimbursable. General productivity apps (note-taking, personal calendar) are not, unless your manager confirms a business need in writing.
**Q: What about annual subscriptions?**
A: Annual subscriptions over $200 require manager approval before purchase. Submit the approval email with your expense report.
## Receipts and Documentation
**Q: My receipt is faded/damaged. What do I do?**
A: Try to obtain a duplicate from the vendor. If not possible, submit a Lost Receipt Affidavit (available from the Finance SharePoint site). You're limited to 2 affidavits per quarter.
**Q: Do I need a receipt for parking meters or tolls?**
A: For amounts under $15, no receipt is required — just note the date, location, and amount. For $15 and above, a receipt or bank/credit card statement excerpt is required.
## Approval and Reimbursement
**Q: My manager is on leave. Who approves my report?**
A: Expense reports can be approved by your skip-level manager or any manager designated as an alternate approver in the expense system.
**Q: Can I submit expenses from a previous quarter?**
A: The standard 30-day window applies. Expenses older than 30 days require a written explanation and VP approval. Expenses older than 90 days are not reimbursable except in extraordinary circumstances (extended leave, medical emergency) with CFO approval.
@@ -14,6 +14,10 @@
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\SubprocessScriptRunner.cs" Link="SubprocessScriptRunner.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use file-based Agent Skills with a ChatClientAgent.
// Skills are discovered from SKILL.md files on disk and follow the progressive disclosure pattern:
// 1. Advertise — skill names and descriptions in the system prompt
// 2. Load — full instructions loaded on demand via load_skill tool
// 3. Read resources — reference files read via read_skill_resource tool
// 4. Run scripts — scripts executed via run_skill_script tool with a subprocess executor
//
// This sample uses a unit-converter skill that converts between miles, kilometers, pounds, and kilograms.
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// --- Skills Provider ---
// Discovers skills from the 'skills' directory containing SKILL.md files.
// The script runner runs file-based scripts (e.g. Python) as local subprocesses.
var skillsProvider = new AgentSkillsProvider(
Path.Combine(AppContext.BaseDirectory, "skills"),
SubprocessScriptRunner.RunAsync);
// --- Agent Setup ---
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient()
.AsAIAgent(new ChatClientAgentOptions
{
Name = "UnitConverterAgent",
ChatOptions = new()
{
Instructions = "You are a helpful assistant that can convert units.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName);
// --- Example: Unit conversion ---
Console.WriteLine("Converting units with file-based skills");
Console.WriteLine(new string('-', 60));
AgentResponse response = await agent.RunAsync(
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?");
Console.WriteLine($"Agent: {response.Text}");
@@ -0,0 +1,51 @@
# File-Based Agent Skills Sample
This sample demonstrates how to use **file-based Agent Skills** with a `ChatClientAgent`.
## What it demonstrates
- Discovering skills from `SKILL.md` files on disk via `AgentFileSkillsSource`
- The progressive disclosure pattern: advertise → load → read resources → run scripts
- Using the `AgentSkillsProvider` constructor with a skill directory path and script executor
- Running file-based scripts (Python) via a subprocess-based executor
## Skills Included
### unit-converter
Converts between common units (miles↔km, pounds↔kg) using a multiplication factor.
- `references/conversion-table.md` — Conversion factor table
- `scripts/convert.py` — Python script that performs the conversion
## Running the Sample
### Prerequisites
- .NET 10.0 SDK
- Azure OpenAI endpoint with a deployed model
- Python 3 installed and available as `python3` on your PATH
### Setup
```bash
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
```
### Run
```bash
dotnet run
```
### Expected Output
```
Converting units with file-based skills
------------------------------------------------------------
Agent: Here are your conversions:
1. **26.2 miles → 42.16 km** (a marathon distance)
2. **75 kg → 165.35 lbs**
```
@@ -0,0 +1,11 @@
---
name: unit-converter
description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
---
## Usage
When the user requests a unit conversion:
1. First, review `references/conversion-table.md` to find the correct factor
2. Run the `scripts/convert.py` script with `--value <number> --factor <factor>` (e.g. `--value 26.2 --factor 1.60934`)
3. Present the converted value clearly with both units
@@ -0,0 +1,10 @@
# Conversion Tables
Formula: **result = value Ă— factor**
| From | To | Factor |
|-------------|-------------|----------|
| miles | kilometers | 1.60934 |
| kilometers | miles | 0.621371 |
| pounds | kilograms | 0.453592 |
| kilograms | pounds | 2.20462 |
@@ -0,0 +1,29 @@
# Unit conversion script
# Converts a value using a multiplication factor: result = value Ă— factor
#
# Usage:
# python scripts/convert.py --value 26.2 --factor 1.60934
# python scripts/convert.py --value 75 --factor 2.20462
import argparse
import json
def main() -> None:
parser = argparse.ArgumentParser(
description="Convert a value using a multiplication factor.",
epilog="Examples:\n"
" python scripts/convert.py --value 26.2 --factor 1.60934\n"
" python scripts/convert.py --value 75 --factor 2.20462",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--value", type=float, required=True, help="The numeric value to convert.")
parser.add_argument("--factor", type=float, required=True, help="The conversion factor from the table.")
args = parser.parse_args()
result = round(args.value * args.factor, 4)
print(json.dumps({"value": args.value, "factor": args.factor, "result": result}))
if __name__ == "__main__":
main()
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to define Agent Skills entirely in code using AgentInlineSkill.
// No SKILL.md files are needed — skills, resources, and scripts are all defined programmatically.
//
// Three approaches are shown using a unit-converter skill:
// 1. Static resources — inline content provided via AddResource
// 2. Dynamic resources — computed at runtime via a factory delegate
// 3. Code scripts — executable delegates the agent can invoke directly
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// --- Build the code-defined skill ---
var unitConverterSkill = new AgentInlineSkill(
name: "unit-converter",
description: "Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.",
instructions: """
Use this skill when the user asks to convert between units.
1. Review the conversion-table resource to find the factor for the requested conversion.
2. Check the conversion-policy resource for rounding and formatting rules.
3. Use the convert script, passing the value and factor from the table.
""")
// 1. Static Resource: conversion tables
.AddResource(
"conversion-table",
"""
# Conversion Tables
Formula: **result = value Ă— factor**
| From | To | Factor |
|-------------|-------------|----------|
| miles | kilometers | 1.60934 |
| kilometers | miles | 0.621371 |
| pounds | kilograms | 0.453592 |
| kilograms | pounds | 2.20462 |
""")
// 2. Dynamic Resource: conversion policy (computed at runtime)
.AddResource("conversion-policy", () =>
{
const int Precision = 4;
return $"""
# Conversion Policy
**Decimal places:** {Precision}
**Format:** Always show both the original and converted values with units
**Generated at:** {DateTime.UtcNow:O}
""";
})
// 3. Code Script: convert
.AddScript("convert", (double value, double factor) =>
{
double result = Math.Round(value * factor, 4);
return JsonSerializer.Serialize(new { value, factor, result });
});
// --- Skills Provider ---
var skillsProvider = new AgentSkillsProvider(unitConverterSkill);
// --- Agent Setup ---
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient()
.AsAIAgent(new ChatClientAgentOptions
{
Name = "UnitConverterAgent",
ChatOptions = new()
{
Instructions = "You are a helpful assistant that can convert units.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName);
// --- Example: Unit conversion ---
Console.WriteLine("Converting units with code-defined skills");
Console.WriteLine(new string('-', 60));
AgentResponse response = await agent.RunAsync(
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?");
Console.WriteLine($"Agent: {response.Text}");
@@ -0,0 +1,52 @@
# Code-Defined Agent Skills Sample
This sample demonstrates how to define **Agent Skills entirely in code** using `AgentInlineSkill`.
## What it demonstrates
- Creating skills programmatically with `AgentInlineSkill` — no SKILL.md files needed
- **Static resources** via `AddResource` with inline content
- **Dynamic resources** via `AddResource` with a factory delegate (computed at runtime)
- **Code scripts** via `AddScript` with a delegate handler
- Using the `AgentSkillsProvider` constructor with inline skills
## Skills Included
### unit-converter (code-defined)
Converts between common units using multiplication factors. Defined entirely in C# code:
- `conversion-table` — Static resource with factor table
- `conversion-policy` — Dynamic resource with formatting rules (generated at runtime)
- `convert` — Script that performs `value × factor` conversion
## Running the Sample
### Prerequisites
- .NET 10.0 SDK
- Azure OpenAI endpoint with a deployed model
### Setup
```bash
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
```
### Run
```bash
dotnet run
```
### Expected Output
```
Converting units with code-defined skills
------------------------------------------------------------
Agent: Here are your conversions:
1. **26.2 miles → 42.16 km** (a marathon distance)
2. **75 kg → 165.35 lbs**
```
+19 -2
View File
@@ -1,7 +1,24 @@
# AgentSkills Samples
Samples demonstrating Agent Skills capabilities.
Samples demonstrating Agent Skills capabilities. Each sample shows a different way to define and use skills.
| Sample | Description |
|--------|-------------|
| [Agent_Step01_BasicSkills](Agent_Step01_BasicSkills/) | Using Agent Skills with a ChatClientAgent, including progressive disclosure and skill resources |
| [Agent_Step01_FileBasedSkills](Agent_Step01_FileBasedSkills/) | Define skills as `SKILL.md` files on disk with reference documents. Uses a unit-converter skill. |
| [Agent_Step02_CodeDefinedSkills](Agent_Step02_CodeDefinedSkills/) | Define skills entirely in C# code using `AgentInlineSkill`, with static/dynamic resources and scripts. |
## Key Concepts
### File-Based vs Code-Defined Skills
| Aspect | File-Based | Code-Defined |
|--------|-----------|--------------|
| Definition | `SKILL.md` files on disk | `AgentInlineSkill` instances in C# |
| Resources | All files in skill directory (filtered by extension) | `AddResource` (static value or delegate-backed) |
| Scripts | Supported via script executor delegate | `AddScript` delegates |
| Discovery | Automatic from directory path | Explicit via constructor |
| Dynamic content | No (static files only) | Yes (factory delegates) |
| Reusability | Copy skill directory | Inline or shared instances |
For single-source scenarios, use the `AgentSkillsProvider` constructors directly. To combine multiple skill types, use the `AgentSkillsProviderBuilder`.
@@ -0,0 +1,137 @@
// Copyright (c) Microsoft. All rights reserved.
// Sample subprocess-based skill script runner.
// Executes file-based skill scripts as local subprocesses.
// This is provided for demonstration purposes only.
using System.Diagnostics;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
/// <summary>
/// Executes file-based skill scripts as local subprocesses.
/// </summary>
/// <remarks>
/// This runner uses the script's absolute path, converts the arguments
/// to CLI flags, and returns captured output. It is intended for
/// demonstration purposes only.
/// </remarks>
internal static class SubprocessScriptRunner
{
/// <summary>
/// Runs a skill script as a local subprocess.
/// </summary>
public static async Task<object?> RunAsync(
AgentFileSkill skill,
AgentFileSkillScript script,
AIFunctionArguments arguments,
CancellationToken cancellationToken)
{
if (!File.Exists(script.FullPath))
{
return $"Error: Script file not found: {script.FullPath}";
}
string extension = Path.GetExtension(script.FullPath);
string? interpreter = extension switch
{
".py" => "python3",
".js" => "node",
".sh" => "bash",
".ps1" => "pwsh",
_ => null,
};
var startInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = Path.GetDirectoryName(script.FullPath) ?? ".",
};
if (interpreter is not null)
{
startInfo.FileName = interpreter;
startInfo.ArgumentList.Add(script.FullPath);
}
else
{
startInfo.FileName = script.FullPath;
}
if (arguments is not null)
{
foreach (var (key, value) in arguments)
{
if (value is bool boolValue)
{
if (boolValue)
{
startInfo.ArgumentList.Add(NormalizeKey(key));
}
}
else if (value is not null)
{
startInfo.ArgumentList.Add(NormalizeKey(key));
startInfo.ArgumentList.Add(value.ToString()!);
}
}
}
Process? process = null;
try
{
process = Process.Start(startInfo);
if (process is null)
{
return $"Error: Failed to start process for script '{script.Name}'.";
}
Task<string> outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
Task<string> errorTask = process.StandardError.ReadToEndAsync(cancellationToken);
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
string output = await outputTask.ConfigureAwait(false);
string error = await errorTask.ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
output += $"\nStderr:\n{error}";
}
if (process.ExitCode != 0)
{
output += $"\nScript exited with code {process.ExitCode}";
}
return string.IsNullOrEmpty(output) ? "(no output)" : output.Trim();
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Kill the process on cancellation to avoid leaving orphaned subprocesses.
process?.Kill(entireProcessTree: true);
throw;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
return $"Error: Failed to execute script '{script.Name}': {ex.Message}";
}
finally
{
process?.Dispose();
}
}
/// <summary>
/// Normalizes a parameter key to a consistent --flag format.
/// Models may return keys with or without leading dashes (e.g., "value" vs "--value").
/// </summary>
private static string NormalizeKey(string key) => "--" + key.TrimStart('-');
}
@@ -5,20 +5,13 @@
using Anthropic;
using Anthropic.Core;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("ANTHROPIC_CHAT_MODEL_NAME") ?? "claude-haiku-4-5";
AIAgent agent = new AnthropicClient(new ClientOptions { ApiKey = apiKey })
AIAgent agent =
new AnthropicClient(new ClientOptions { ApiKey = apiKey })
.AsAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
var response = await agent.RunAsync("Tell me a joke about a pirate.");
Console.WriteLine(response);
// Invoke the agent with streaming support.
await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate."))
{
Console.WriteLine(update);
}
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -11,6 +11,7 @@ using System.Text.Json;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AzureAI;
using Microsoft.Agents.AI.FoundryMemory;
string foundryEndpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
@@ -19,6 +20,9 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLO
string embeddingModelName = Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-ada-002";
// Create an AIProjectClient for Foundry with Azure Identity authentication.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
DefaultAzureCredential credential = new();
AIProjectClient projectClient = new(new Uri(foundryEndpoint), credential);
@@ -33,11 +37,15 @@ FoundryMemoryProvider memoryProvider = new(
memoryStoreName,
stateInitializer: _ => new(new FoundryMemoryProviderScope("sample-user-123")));
AIAgent agent = await projectClient.CreateAIAgentAsync(deploymentName,
options: new ChatClientAgentOptions()
FoundryAgent agent = projectClient.AsAIAgent(
new ChatClientAgentOptions()
{
Name = "TravelAssistantWithFoundryMemory",
ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
ChatOptions = new()
{
ModelId = deploymentName,
Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details."
},
AIContextProviders = [memoryProvider]
});
@@ -1,4 +1,4 @@
# Agent Framework Retrieval Augmented Generation (RAG)
# Agent Framework Retrieval Augmented Generation (RAG)
These samples show how to create an agent with the Agent Framework that uses Memory to remember previous conversations or facts from previous conversations.
@@ -10,4 +10,4 @@ These samples show how to create an agent with the Agent Framework that uses Mem
|[Memory with Azure AI Foundry](./AgentWithMemory_Step04_MemoryUsingFoundry/)|This sample demonstrates how to create and run an agent that uses Azure AI Foundry's managed memory service to extract and retrieve individual memories.|
|[Bounded Chat History with Overflow](./AgentWithMemory_Step05_BoundedChatHistory/)|This sample demonstrates how to create a bounded chat history provider that overflows older messages to a vector store and recalls them as memories.|
> **See also**: [Memory Search with Foundry Agents](../FoundryAgents/FoundryAgents_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Azure Foundry Agents.
> **See also**: [Memory Search with Foundry Agents](../AgentsWithFoundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Azure Foundry agents.
@@ -4,28 +4,14 @@
using System.ClientModel;
using Microsoft.Agents.AI;
using OpenAI;
using OpenAI.Chat;
using OpenAI.Responses;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("OPENAI_CHAT_MODEL_NAME") ?? "gpt-4o-mini";
AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient(model)
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
AIAgent agent =
new ResponsesClient(new ApiKeyCredential(apiKey))
.AsAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker");
UserChatMessage chatMessage = new("Tell me a joke about a pirate.");
// Invoke the agent and output the text result.
ChatCompletion chatCompletion = await agent.RunAsync([chatMessage]);
Console.WriteLine(chatCompletion.Content.Last().Text);
// Invoke the agent with streaming support.
AsyncCollectionResult<StreamingChatCompletionUpdate> completionUpdates = agent.RunStreamingAsync([chatMessage]);
await foreach (StreamingChatCompletionUpdate completionUpdate in completionUpdates)
{
if (completionUpdate.ContentUpdate.Count > 0)
{
Console.WriteLine(completionUpdate.ContentUpdate[0].Text);
}
}
// Once you have the agent, you can invoke it like any other AIAgent.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -73,16 +73,28 @@ foreach (ClientResult result in getConversationItemsResults.GetRawPages())
using JsonDocument getConversationItemsResultAsJson = JsonDocument.Parse(result.GetRawResponse().Content.ToString());
foreach (JsonElement element in getConversationItemsResultAsJson.RootElement.GetProperty("data").EnumerateArray())
{
// Skip non-message items (e.g. tool calls, reasoning) that lack a "role" property
if (!element.TryGetProperty("role"u8, out var roleElement))
{
continue;
}
string messageId = element.GetProperty("id"u8).ToString();
string messageRole = element.GetProperty("role"u8).ToString();
string messageRole = roleElement.ToString();
Console.WriteLine($" Message ID: {messageId}");
Console.WriteLine($" Message Role: {messageRole}");
foreach (var content in element.GetProperty("content").EnumerateArray())
if (element.TryGetProperty("content"u8, out var contentElement))
{
string messageContentText = content.GetProperty("text"u8).ToString();
Console.WriteLine($" Message Text: {messageContentText}");
foreach (var content in contentElement.EnumerateArray())
{
if (content.TryGetProperty("text"u8, out var textElement))
{
Console.WriteLine($" Message Text: {textElement}");
}
}
}
Console.WriteLine();
}
}
@@ -16,7 +16,7 @@ using Qdrant.Client;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
var afOverviewUrl = "https://github.com/MicrosoftDocs/semantic-kernel-docs/blob/main/agent-framework/overview/agent-framework-overview.md";
var afOverviewUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/overview/index.md";
var afMigrationUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/migration-guide/from-semantic-kernel/index.md";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
@@ -4,11 +4,13 @@
using System.ClientModel;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Agents.AI.AzureAI;
using OpenAI;
using OpenAI.Files;
using OpenAI.Responses;
using OpenAI.VectorStores;
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
@@ -37,14 +39,20 @@ ClientResult<VectorStore> vectorStoreCreate = await vectorStoreClient.CreateVect
FileIds = { uploadResult.Value.Id }
});
var fileSearchTool = new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreCreate.Value.Id)] };
// Use the native OpenAI SDK FileSearchTool directly with the vector store ID.
#pragma warning disable OPENAI001
FileSearchTool fileSearchTool = new([vectorStoreCreate.Value.Id]);
#pragma warning restore OPENAI001
AIAgent agent = await aiProjectClient
.CreateAIAgentAsync(
model: deploymentName,
name: "AskContoso",
instructions: "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
tools: [fileSearchTool]);
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
"AskContoso",
new AgentVersionCreationOptions(
new PromptAgentDefinition(model: deploymentName)
{
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
Tools = { fileSearchTool }
}));
FoundryAgent agent = aiProjectClient.AsAIAgent(agentVersion);
AgentSession session = await agent.CreateSessionAsync();
@@ -3,6 +3,7 @@
// This sample shows how to expose an AI agent as an MCP tool.
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.DependencyInjection;
@@ -18,11 +19,17 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYME
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
// Create a server side agent and expose it as an AIAgent.
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(
model: deploymentName,
instructions: "You are good at telling jokes, and you always start each joke with 'Aye aye, captain!'.",
name: "Joker",
description: "An agent that tells jokes.");
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
"Joker",
new AgentVersionCreationOptions(
new PromptAgentDefinition(model: deploymentName)
{
Instructions = "You are good at telling jokes, and you always start each joke with 'Aye aye, captain!'.",
})
{
Description = "An agent that tells jokes.",
});
AIAgent agent = aiProjectClient.AsAIAgent(agentVersion);
// Convert the agent to an AIFunction and then to an MCP tool.
// The agent name and description will be used as the mcp tool name and description.
@@ -16,5 +16,11 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Assets\walkway.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -22,7 +22,7 @@ var agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential(
ChatMessage message = new(ChatRole.User, [
new TextContent("What do you see in this image?"),
new UriContent("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "image/jpeg")
await DataContent.LoadFromAsync("Assets/walkway.jpg"),
]);
var session = await agent.CreateSessionAsync();
@@ -189,9 +189,9 @@ async Task<AgentResponse> PIIMiddleware(IEnumerable<ChatMessage> messages, Agent
// Regex patterns for PII detection (simplified for demonstration)
Regex[] piiPatterns =
[
new(@"\b\d{3}-\d{3}-\d{4}\b", RegexOptions.Compiled), // Phone number (e.g., 123-456-7890)
new(@"\b[\w\.-]+@[\w\.-]+\.\w+\b", RegexOptions.Compiled), // Email address
new(@"\b[A-Z][a-z]+\s[A-Z][a-z]+\b", RegexOptions.Compiled) // Full name (e.g., John Doe)
MyRegex(), // Phone number (e.g., 123-456-7890)
EmailRegex(), // Email address
FullNameRegex() // Full name (e.g., John Doe)
];
foreach (var pattern in piiPatterns)
@@ -309,3 +309,15 @@ internal sealed class DateTimeContextProvider : MessageAIContextProvider
]);
}
}
internal partial class Program
{
[GeneratedRegex(@"\b\d{3}-\d{3}-\d{4}\b", RegexOptions.Compiled)]
private static partial Regex MyRegex();
[GeneratedRegex(@"\b[\w\.-]+@[\w\.-]+\.\w+\b", RegexOptions.Compiled)]
private static partial Regex EmailRegex();
[GeneratedRegex(@"\b[A-Z][a-z]+\s[A-Z][a-z]+\b", RegexOptions.Compiled)]
private static partial Regex FullNameRegex();
}
@@ -17,10 +17,10 @@ var bingConnectionId = Environment.GetEnvironmentVariable("AZURE_AI_BING_CONNECT
PersistentAgentsAdministrationClientOptions persistentAgentsClientOptions = new();
persistentAgentsClientOptions.Retry.NetworkTimeout = TimeSpan.FromMinutes(20);
// Get a client to create/retrieve server side agents with.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
// Get a client to create/retrieve server side agents with.
PersistentAgentsClient persistentAgentsClient = new(endpoint, new DefaultAzureCredential(), persistentAgentsClientOptions);
// Define and configure the Deep Research tool.
@@ -23,12 +23,14 @@ Before running this sample, ensure you have:
Pay special attention to the purple `Note` boxes in the Azure documentation.
**Note**: The Bing Connection ID must be from the **project**, not the resource. It has the following format:
**Note**: The Bing Grounding Connection ID must be the **full ARM resource URI** from the project, not just the connection name. It has the following format:
```
/subscriptions/<sub_id>/resourceGroups/<rg_name>/providers/<provider_name>/accounts/<account_name>/projects/<project_name>/connections/<connection_name>
/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/projects/<project>/connections/<connection-name>
```
You can find this in the Azure AI Foundry portal under **Management > Connected resources**, or retrieve it programmatically via the connections API (`.id` property).
## Environment Variables
Set the following environment variables:
@@ -37,8 +39,8 @@ Set the following environment variables:
# Replace with your Azure AI Foundry project endpoint
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/"
# Replace with your Bing connection ID from the project
$env:AZURE_AI_BING_CONNECTION_ID="/subscriptions/.../connections/your-bing-connection"
# Replace with your Bing Grounding connection ID (full ARM resource URI)
$env:AZURE_AI_BING_CONNECTION_ID="/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/projects/<project>/connections/<connection-name>"
# Optional, defaults to o3-deep-research
$env:AZURE_AI_REASONING_DEPLOYMENT_NAME="o3-deep-research"
@@ -24,12 +24,12 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
Func<Task<string[]>> loadNextThreeCalendarEvents = async () =>
{
// In a real implementation, this method would connect to a calendar service
return new string[]
{
return
[
"Doctor's appointment today at 15:00",
"Team meeting today at 17:00",
"Birthday party today at 20:00"
};
];
};
// Create an agent with an AI context provider attached that aggregates two other providers:
@@ -87,7 +87,7 @@ namespace SampleApp
internal sealed class TodoListAIContextProvider : AIContextProvider
{
private static List<string> GetTodoItems(AgentSession? session)
=> session?.StateBag.GetValue<List<string>>(nameof(TodoListAIContextProvider)) ?? new List<string>();
=> session?.StateBag.GetValue<List<string>>(nameof(TodoListAIContextProvider)) ?? [];
private static void SetTodoItems(AgentSession? session, List<string> items)
=> session?.StateBag.SetValue(nameof(TodoListAIContextProvider), items);
@@ -1,15 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how the ChatClientAgent persists chat history after each individual
// call to the AI service.
// call to the AI service, using the RequirePerServiceCallChatHistoryPersistence option.
// When an agent uses tools, FunctionInvokingChatClient may loop multiple times
// (service call → tool execution → service call), and intermediate messages (tool calls and
// results) are persisted after each service call. This allows you to inspect or recover them
// even if the process is interrupted mid-loop, but may also result in chat history that is not
// yet finalized (e.g., tool calls without results) being persisted, which may be undesirable in some cases.
//
// To opt into end-of-run persistence instead (atomic run semantics), set
// PersistChatHistoryAtEndOfRun = true on ChatClientAgentOptions.
// To use end-of-run persistence instead (atomic run semantics), remove the
// RequirePerServiceCallChatHistoryPersistence = true setting (or set it to false). End-of-run
// persistence is the default behavior.
//
// The sample runs two multi-turn conversations: one using non-streaming (RunAsync) and one
// using streaming (RunStreamingAsync), to demonstrate correct behavior in both modes.
@@ -53,7 +54,7 @@ static string GetTime([Description("The city name.")] string city) =>
_ => $"{city}: time data not available."
};
// Create the agent — per-service-call persistence is the default behavior.
// Create the agent — per-service-call persistence is enabled via RequirePerServiceCallChatHistoryPersistence.
// The in-memory ChatHistoryProvider is used by default when the service does not require service stored chat
// history, so for those cases, we can inspect the chat history via session.TryGetInMemoryChatHistory().
IChatClient chatClient = string.Equals(store, "TRUE", StringComparison.OrdinalIgnoreCase) ?
@@ -63,6 +64,7 @@ AIAgent agent = chatClient.AsAIAgent(
new ChatClientAgentOptions
{
Name = "WeatherAssistant",
RequirePerServiceCallChatHistoryPersistence = true,
ChatOptions = new()
{
Instructions = "You are a helpful assistant. When asked about multiple cities, call the appropriate tool for each city.",
@@ -1,16 +1,19 @@
# In-Function-Loop Checkpointing
This sample demonstrates how `ChatClientAgent` persists chat history after each individual call to the AI service by default. This per-service-call persistence ensures intermediate progress is saved during the function invocation loop.
This sample demonstrates how `ChatClientAgent` can persist chat history after each individual call to the AI service using the `RequirePerServiceCallChatHistoryPersistence` option. This per-service-call persistence ensures intermediate progress is saved during the function invocation loop.
## What This Sample Shows
When an agent uses tools, the `FunctionInvokingChatClient` loops multiple times (service call → tool execution → service call → …). By default, chat history is persisted after each service call via the `ChatHistoryPersistingChatClient` decorator:
When an agent uses tools, the `FunctionInvokingChatClient` loops multiple times (service call → tool execution → service call → …). By enabling `RequirePerServiceCallChatHistoryPersistence = true`, chat history is persisted after each service call via the `PerServiceCallChatHistoryPersistingChatClient` decorator:
- A `ChatHistoryPersistingChatClient` decorator is automatically inserted into the chat client pipeline
- A `PerServiceCallChatHistoryPersistingChatClient` decorator is inserted into the chat client pipeline
- Before each service call, the decorator loads history from the `ChatHistoryProvider` and prepends it to the request
- After each service call, the decorator notifies the `ChatHistoryProvider` (and any `AIContextProvider` instances) with the new messages
- Only **new** messages are sent to providers on each notification — messages that were already persisted in an earlier call within the same run are deduplicated automatically
To opt into end-of-run persistence instead (atomic run semantics), set `PersistChatHistoryAtEndOfRun = true` on `ChatClientAgentOptions`. In that mode, the decorator marks messages with metadata rather than persisting them immediately, and `ChatClientAgent` persists only the marked messages at the end of the run.
By default (without `RequirePerServiceCallChatHistoryPersistence`), chat history is persisted at the end of the full agent run instead. To use per-service-call persistence, set `RequirePerServiceCallChatHistoryPersistence = true` on `ChatClientAgentOptions`.
With `RequirePerServiceCallChatHistoryPersistence` = true, the behavior matches that of chat history stored in the underlying AI service exactly.
Per-service-call persistence is useful for:
- **Crash recovery** — if the process is interrupted mid-loop, the intermediate tool calls and results are already persisted
@@ -26,7 +29,7 @@ The sample asks the agent about the weather and time in three cities. The model
```
ChatClientAgent
└─ FunctionInvokingChatClient (handles tool call loop)
└─ ChatHistoryPersistingChatClient (persists after each service call)
└─ PerServiceCallChatHistoryPersistingChatClient (persists after each service call)
└─ Leaf IChatClient (Azure OpenAI)
```
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create, use, and clean up a FoundryAgent backed by a server-side
// versioned agent in Azure AI Foundry. It demonstrates the full lifecycle:
// create agent version -> wrap as FoundryAgent -> run -> delete.
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI.AzureAI;
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
const string JokerName = "JokerAgent";
// Create the AIProjectClient to manage server-side agents.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
// Create a server-side agent version using the native SDK.
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
JokerName,
new AgentVersionCreationOptions(
new PromptAgentDefinition(model: deploymentName)
{
Instructions = "You are good at telling jokes.",
}));
// Wrap the agent version as a FoundryAgent using the AsAIAgent extension.
FoundryAgent agent = aiProjectClient.AsAIAgent(agentVersion);
// Once you have the agent, you can invoke it like any other AIAgent.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
// Cleanup: deletes the agent and all its versions.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
@@ -0,0 +1,23 @@
# Agent Step 00 - FoundryAgent Lifecycle
This sample demonstrates the full lifecycle of a `FoundryAgent` backed by a server-side versioned agent in Microsoft Foundry: create → run → delete.
## Prerequisites
- A Microsoft Foundry project endpoint
- A model deployment name (defaults to `gpt-4o-mini`)
- Azure CLI installed and authenticated
## Environment Variables
| Variable | Description | Required |
| --- | --- | --- |
| `AZURE_AI_PROJECT_ENDPOINT` | Microsoft Foundry project endpoint | Yes |
| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Model deployment name | No (defaults to `gpt-4o-mini`) |
## Running the sample
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step00_FoundryAgentLifecycle
```
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and run a basic agent with AIProjectClient.AsAIAgent(...).
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIAgent agent =
new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(model: deploymentName, instructions: "You are good at telling jokes.", name: "JokerAgent");
// Once you have the agent, you can invoke it like any other AIAgent.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -0,0 +1,55 @@
# Creating and Running a Basic Agent with the Responses API
This sample demonstrates how to create and run a basic AI agent using the `ChatClientAgent`, which uses the Microsoft Foundry Responses API directly without creating server-side agent definitions.
## What this sample demonstrates
- Creating a `ChatClientAgent` with instructions and a model
- Running a simple single-turn conversation
- No server-side agent creation or cleanup required
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (for Azure credential authentication)
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the sample
Navigate to the AgentsWithFoundry sample directory and run:
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step01_Basics
```
## Alternative: Composable approach
You can also create the same agent by composing the underlying `IChatClient` directly. This gives you full control over the chat client pipeline:
```csharp
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
AIAgent agent = new ChatClientAgent(
chatClient: aiProjectClient.GetProjectOpenAIClient().GetProjectResponsesClient().AsIChatClient(deploymentName),
instructions: "You are good at telling jokes.",
name: "JokerAgent");
```
This approach is useful when you need to customize the chat client pipeline or swap providers (e.g., Anthropic, OpenAI) while keeping the same agent code.
@@ -9,8 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create a multi-turn conversation agent using sessions.
// Context is preserved across multiple runs via response ID chaining in the session.
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(deploymentName, instructions: "You are good at telling jokes.", name: "JokerAgent");
// Create a session to maintain context across multiple runs.
AgentSession session = await agent.CreateSessionAsync();
// First turn
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
// Second turn — the agent remembers the first turn via the session.
Console.WriteLine(await agent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", session));
@@ -0,0 +1,36 @@
# Multi-turn Conversation
This sample demonstrates how to implement multi-turn conversations where context is preserved across multiple agent runs using sessions and response ID chaining.
## What this sample demonstrates
- Creating an agent with instructions
- Using sessions to maintain conversation context across multiple runs
- Response ID chaining for multi-turn conversations
- No server-side conversation creation required
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (for Azure credential authentication)
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the sample
Navigate to the AgentsWithFoundry sample directory and run:
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step02.1_MultiturnConversation
```
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to use server-side conversations with a FoundryAgent.
// Server-side conversations persist on the Foundry service and are visible in the Foundry Project UI.
// Use this when you need conversation history to be stored and accessible server-side.
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AzureAI;
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
FoundryAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(deploymentName, instructions: "You are good at telling jokes.", name: "JokerAgent");
// CreateConversationSessionAsync creates a server-side ProjectConversation
// that persists on the Foundry service and is visible in the Foundry Project UI.
AgentSession session = await agent.CreateConversationSessionAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
Console.WriteLine(await agent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", session));
// Streaming with server-side conversation context.
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Tell me another joke, but about a ninja this time.", session))
{
Console.Write(update);
}
Console.WriteLine();
@@ -0,0 +1,36 @@
# Multi-turn Conversation with Server-Side Conversations
This sample demonstrates how to use server-side conversations with a `FoundryAgent`. Server-side conversations persist on the Foundry service and are visible in the Foundry Project UI, making them ideal when you need conversation history to be stored and accessible server-side.
## What this sample demonstrates
- Creating a `FoundryAgent` with instructions
- Using `CreateConversationSessionAsync` to create a server-side `ProjectConversation`
- Multi-turn conversations with both text and streaming output
- Server-side conversation persistence visible in the Foundry Project UI
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (for Azure credential authentication)
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the sample
Navigate to the AgentsWithFoundry sample directory and run:
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step02.2_MultiturnWithServerConversations
```
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use function tools.
using System.ComponentModel;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
// Define the function tool.
AITool tool = AIFunctionFactory.Create(GetWeather);
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Create a AIAgent with function tools.
AIAgent agent = aiProjectClient.AsAIAgent(deploymentName,
instructions: "You are a helpful assistant that can get weather information.",
name: "WeatherAssistant",
tools: [tool]);
// Non-streaming agent interaction with function tools.
AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", session));
// Streaming agent interaction with function tools.
session = await agent.CreateSessionAsync();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("What is the weather like in Amsterdam?", session))
{
Console.Write(update);
}
@@ -0,0 +1,37 @@
# Using Function Tools with the Responses API
This sample demonstrates how to use function tools with the `ChatClientAgent`, allowing the agent to call custom functions to retrieve information.
## What this sample demonstrates
- Creating function tools using `AIFunctionFactory`
- Passing function tools to a `ChatClientAgent`
- Running agents with function tools (text output)
- Running agents with function tools (streaming output)
- No server-side agent creation or cleanup required
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (for Azure credential authentication)
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the sample
Navigate to the AgentsWithFoundry sample directory and run:
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step03_UsingFunctionTools
```
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
</Project>
@@ -1,9 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use an agent with function tools that require a human in the loop for approvals.
// It shows both non-streaming and streaming agent interactions using weather-related tools.
// If the agent is hosted in a service, with a remote user, combine this sample with the Persisted Conversations sample to persist the chat history
// while the agent is waiting for user input.
using System.ComponentModel;
using Azure.AI.Projects;
@@ -11,18 +8,13 @@ using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// Create a sample function tool that the agent can use.
[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
const string AssistantInstructions = "You are a helpful assistant that can get weather information.";
const string AssistantName = "WeatherAssistant";
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
@@ -30,16 +22,16 @@ AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredent
ApprovalRequiredAIFunction approvalTool = new(AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather)));
// Create AIAgent directly
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: [approvalTool]);
AIAgent agent = aiProjectClient.AsAIAgent(deploymentName,
instructions: "You are a helpful assistant that can get weather information.",
name: "WeatherAssistant",
tools: [approvalTool]);
// Call the agent with approval-required function tools.
// The agent will request approval before invoking the function.
AgentSession session = await agent.CreateSessionAsync();
AgentResponse response = await agent.RunAsync("What is the weather like in Amsterdam?", session);
// Check if there are any approval requests.
// For simplicity, we are assuming here that only function approvals are pending.
List<ToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
while (approvalRequests.Count > 0)
@@ -53,13 +45,8 @@ while (approvalRequests.Count > 0)
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved)]);
});
// Pass the user input responses back to the agent for further processing.
response = await agent.RunAsync(userInputMessages, session);
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
}
Console.WriteLine($"\nAgent: {response}");
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
@@ -0,0 +1,30 @@
# Using Function Tools with Approvals via the Responses API
This sample demonstrates how to use function tools that require human-in-the-loop approval before execution.
## What this sample demonstrates
- Creating function tools that require approval using `ApprovalRequiredAIFunction`
- Handling approval requests from the agent
- Passing approval responses back to the agent
- No server-side agent creation or cleanup required
## Prerequisites
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (`az login`)
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the sample
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step04_UsingFunctionToolsWithApprovals
```
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
</Project>
@@ -15,29 +15,23 @@ using SampleApp;
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
const string AssistantInstructions = "You are a helpful assistant that extracts structured information about people.";
const string AssistantName = "StructuredOutputAssistant";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Create ChatClientAgent directly
ChatClientAgent agent = await aiProjectClient.CreateAIAgentAsync(
model: deploymentName,
new ChatClientAgentOptions()
AIAgent agent = aiProjectClient.AsAIAgent(new ChatClientAgentOptions
{
Name = "StructuredOutputAssistant",
ChatOptions = new()
{
Name = AssistantName,
ChatOptions = new()
{
Instructions = AssistantInstructions,
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<PersonInfo>()
}
});
ModelId = deploymentName,
Instructions = "You are a helpful assistant that extracts structured information about people.",
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<PersonInfo>()
}
});
// Set PersonInfo as the type parameter of RunAsync method to specify the expected structured output from the agent and invoke the agent with some unstructured input.
// Set PersonInfo as the type parameter of RunAsync method to specify the expected structured output.
AgentResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("Please provide information about John Smith, who is a 35-year-old software engineer.");
// Access the structured output via the Result property of the agent response.
@@ -46,39 +40,21 @@ Console.WriteLine($"Name: {response.Result.Name}");
Console.WriteLine($"Age: {response.Result.Age}");
Console.WriteLine($"Occupation: {response.Result.Occupation}");
// Create the ChatClientAgent with the specified name, instructions, and expected structured output the agent should produce.
ChatClientAgent agentWithPersonInfo = await aiProjectClient.CreateAIAgentAsync(
model: deploymentName,
new ChatClientAgentOptions()
{
Name = AssistantName,
ChatOptions = new()
{
Instructions = AssistantInstructions,
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<PersonInfo>()
}
});
// Invoke the agent with streaming support, then deserialize the assembled response.
IAsyncEnumerable<AgentResponseUpdate> updates = agent.RunStreamingAsync("Please provide information about Jane Doe, who is a 28-year-old data scientist.");
// Invoke the agent with some unstructured input while streaming, to extract the structured information from.
IAsyncEnumerable<AgentResponseUpdate> updates = agentWithPersonInfo.RunStreamingAsync("Please provide information about John Smith, who is a 35-year-old software engineer.");
// Assemble all the parts of the streamed output, since we can only deserialize once we have the full json,
// then deserialize the response into the PersonInfo class.
PersonInfo personInfo = JsonSerializer.Deserialize<PersonInfo>((await updates.ToAgentResponseAsync()).Text, JsonSerializerOptions.Web)
?? throw new InvalidOperationException("Failed to deserialize the streamed response into PersonInfo.");
Console.WriteLine("Assistant Output:");
Console.WriteLine("\nStreaming Assistant Output:");
Console.WriteLine($"Name: {personInfo.Name}");
Console.WriteLine($"Age: {personInfo.Age}");
Console.WriteLine($"Occupation: {personInfo.Occupation}");
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
namespace SampleApp
{
/// <summary>
/// Represents information about a person, including their name, age, and occupation, matched to the JSON schema used in the agent.
/// Represents information about a person.
/// </summary>
[Description("Information about a person including their name, age, and occupation")]
public class PersonInfo
@@ -0,0 +1,29 @@
# Structured Output with the Responses API
This sample demonstrates how to configure an agent to produce structured output using JSON schema.
## What this sample demonstrates
- Using `RunAsync<T>()` to get typed structured output from the agent
- Deserializing streamed responses into structured types
- No server-side agent creation or cleanup required
## Prerequisites
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (`az login`)
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the sample
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step05_StructuredOutput
```
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
</Project>
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use a simple AI agent with a conversation that can be persisted to disk.
// This sample shows how to persist and resume conversations.
using System.Text.Json;
using Azure.AI.Projects;
@@ -10,16 +10,14 @@ using Microsoft.Agents.AI;
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
const string JokerInstructions = "You are good at telling jokes.";
const string JokerName = "JokerAgent";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: JokerName, model: deploymentName, instructions: JokerInstructions);
AIAgent agent = aiProjectClient.AsAIAgent(deploymentName,
instructions: "You are good at telling jokes.",
name: "JokerAgent");
// Start a new session for the agent conversation.
AgentSession session = await agent.CreateSessionAsync();
@@ -42,6 +40,3 @@ AgentSession resumedSession = await agent.DeserializeSessionAsync(reloadedSerial
// Run the agent again with the resumed session.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedSession));
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
@@ -0,0 +1,30 @@
# Persisted Conversations with the Responses API
This sample demonstrates how to persist and resume agent conversations using session serialization.
## What this sample demonstrates
- Serializing agent sessions to JSON for persistence
- Saving and loading sessions from disk
- Resuming conversations with preserved context
- No server-side agent creation or cleanup required
## Prerequisites
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (`az login`)
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the sample
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step06_PersistedConversations
```
@@ -9,8 +9,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.Console" />
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use a simple AI agent with Azure Foundry Agents as the backend that logs telemetry using OpenTelemetry.
// This sample shows how to add OpenTelemetry observability to an agent.
using Azure.AI.Projects;
using Azure.Identity;
@@ -9,15 +9,11 @@ using Microsoft.Agents.AI;
using OpenTelemetry;
using OpenTelemetry.Trace;
string? applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
string? applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
const string JokerInstructions = "You are good at telling jokes.";
const string JokerName = "JokerAgent";
// Create TracerProvider with console exporter
// This will output the telemetry data to the console.
// Create TracerProvider with console exporter.
string sourceName = Guid.NewGuid().ToString("N");
TracerProviderBuilder tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -28,14 +24,16 @@ if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString))
}
using var tracerProvider = tracerProviderBuilder.Build();
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Define the agent you want to create. (Prompt Agent in this case)
AIAgent agent = (await aiProjectClient.CreateAIAgentAsync(name: JokerName, model: deploymentName, instructions: JokerInstructions))
AIAgent agent = aiProjectClient
.AsAIAgent(
deploymentName,
instructions: "You are good at telling jokes.",
name: "JokerAgent")
.AsBuilder()
.UseOpenTelemetry(sourceName: sourceName)
.Build();
@@ -48,8 +46,7 @@ Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session
session = await agent.CreateSessionAsync();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Tell me a joke about a pirate.", session))
{
Console.WriteLine(update);
Console.Write(update);
}
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
Console.WriteLine();
@@ -0,0 +1,31 @@
# Observability with the Responses API
This sample demonstrates how to add OpenTelemetry observability to an agent using console and Azure Monitor exporters.
## What this sample demonstrates
- Configuring OpenTelemetry tracing with console exporter
- Optional Azure Application Insights integration
- Using `.AsBuilder().UseOpenTelemetry()` to add telemetry to the agent
- No server-side agent creation or cleanup required
## Prerequisites
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (`az login`)
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
$env:APPLICATIONINSIGHTS_CONNECTION_STRING="..." # Optional
```
## Run the sample
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step07_Observability
```
@@ -11,8 +11,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to use dependency injection to register a AIAgent and use it from a hosted service.
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SampleApp;
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
AIAgent agent = aiProjectClient.AsAIAgent(deploymentName,
instructions: "You are good at telling jokes.",
name: "JokerAgent");
// Create a host builder that we will register services with and then run.
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Add the AI agent to the service collection.
builder.Services.AddSingleton(agent);
// Add a sample service that will use the agent to respond to user input.
builder.Services.AddHostedService<SampleService>();
// Build and run the host.
using IHost host = builder.Build();
await host.RunAsync().ConfigureAwait(false);
namespace SampleApp
{
/// <summary>
/// A sample service that uses an AI agent to respond to user input.
/// </summary>
internal sealed class SampleService(AIAgent agent, IHostApplicationLifetime appLifetime) : IHostedService
{
private AgentSession? _session;
public async Task StartAsync(CancellationToken cancellationToken)
{
this._session = await agent.CreateSessionAsync(cancellationToken);
_ = this.RunAsync(appLifetime.ApplicationStopping);
}
public async Task RunAsync(CancellationToken cancellationToken)
{
await Task.Delay(100, cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine("\nAgent: Ask me to tell you a joke about a specific topic. To exit just press Ctrl+C or enter without any input.\n");
Console.Write("> ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
appLifetime.StopApplication();
break;
}
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(input, this._session, cancellationToken: cancellationToken))
{
Console.Write(update);
}
Console.WriteLine();
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine("\nShutting down...");
return Task.CompletedTask;
}
}
}
@@ -0,0 +1,30 @@
# Dependency Injection with the Responses API
This sample demonstrates how to register a `ChatClientAgent` in a dependency injection container and use it from a hosted service.
## What this sample demonstrates
- Registering `ChatClientAgent` as an `AIAgent` in the service collection
- Using the agent from a `IHostedService` with an interactive chat loop
- Streaming responses in a hosted service context
- No server-side agent creation or cleanup required
## Prerequisites
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (`az login`)
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the sample
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step08_DependencyInjection
```
@@ -6,16 +6,15 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CA1812</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="ModelContextProtocol" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to use MCP client tools with an agent.
// It connects to the Microsoft Learn MCP server via HTTP and uses its tools.
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// Connect to the Microsoft Learn MCP server via HTTP (Streamable HTTP transport).
Console.WriteLine("Connecting to MCP server at https://learn.microsoft.com/api/mcp ...");
await using McpClient mcpClient = await McpClient.CreateAsync(new HttpClientTransport(new()
{
Endpoint = new Uri("https://learn.microsoft.com/api/mcp"),
Name = "Microsoft Learn MCP",
}));
// Retrieve the list of tools available on the MCP server.
IList<McpClientTool> mcpTools = await mcpClient.ListToolsAsync();
Console.WriteLine($"MCP tools available: {string.Join(", ", mcpTools.Select(t => t.Name))}");
List<AITool> agentTools = [.. mcpTools.Cast<AITool>()];
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
AIAgent agent = aiProjectClient.AsAIAgent(deploymentName,
instructions: "You are a helpful assistant that can help with Microsoft documentation questions. Use the Microsoft Learn MCP tool to search for documentation.",
name: "DocsAgent",
tools: agentTools);
Console.WriteLine($"Agent '{agent.Name}' created. Asking a question...\n");
const string Prompt = "How does one create an Azure storage account using az cli?";
Console.WriteLine($"User: {Prompt}\n");
Console.WriteLine($"Agent: {await agent.RunAsync(Prompt)}");
@@ -0,0 +1,29 @@
# Using MCP Client as Tools with the Responses API
This sample shows how to use MCP (Model Context Protocol) client tools with a `ChatClientAgent` using the Responses API directly.
## What this sample demonstrates
- Connecting to an MCP server via HTTP client transport
- Retrieving MCP tools and passing them to a `ChatClientAgent`
- Using MCP tools for agent interactions without server-side agent creation
## Prerequisites
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (`az login`)
- Node.js installed (for npx/MCP server)
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the sample
```powershell
dotnet run
```
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="assets\walkway.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to use Image Multi-Modality with an AI agent.
// This sample shows how to use image multi-modality with an agent.
using Azure.AI.Projects;
using Azure.Identity;
@@ -10,17 +10,14 @@ using Microsoft.Extensions.AI;
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
const string VisionInstructions = "You are a helpful agent that can analyze images";
const string VisionName = "VisionAgent";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Define the agent you want to create. (Prompt Agent in this case)
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: VisionName, model: deploymentName, instructions: VisionInstructions);
AIAgent agent = aiProjectClient.AsAIAgent(deploymentName,
instructions: "You are a helpful agent that can analyze images.",
name: "VisionAgent");
ChatMessage message = new(ChatRole.User, [
new TextContent("What do you see in this image?"),
@@ -31,8 +28,7 @@ AgentSession session = await agent.CreateSessionAsync();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(message, session))
{
Console.WriteLine(update);
Console.Write(update);
}
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
Console.WriteLine();
@@ -0,0 +1,30 @@
# Using Images with the Responses API
This sample demonstrates how to use image multi-modality with an agent.
## What this sample demonstrates
- Loading images using `DataContent.LoadFromAsync`
- Sending images alongside text to the agent
- Streaming the agent's image analysis response
- No server-side agent creation or cleanup required
## Prerequisites
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and a vision-capable model deployment (e.g., `gpt-4o`)
- Azure CLI installed and authenticated (`az login`)
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o"
```
## Run the sample
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step10_UsingImages
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
</Project>
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use an Azure Foundry Agents AI agent as a function tool.
// This sample shows how to use one agent as a function tool for another agent.
using System.ComponentModel;
using Azure.AI.Projects;
@@ -8,43 +8,29 @@ using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
const string WeatherInstructions = "You answer questions about the weather.";
const string WeatherName = "WeatherAgent";
const string MainInstructions = "You are a helpful assistant who responds in French.";
const string MainName = "MainAgent";
[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Create the weather agent with function tools.
AITool weatherTool = AIFunctionFactory.Create(GetWeather);
AIAgent weatherAgent = await aiProjectClient.CreateAIAgentAsync(
name: WeatherName,
model: deploymentName,
instructions: WeatherInstructions,
AIAgent weatherAgent = aiProjectClient.AsAIAgent(deploymentName,
instructions: "You answer questions about the weather.",
name: "WeatherAgent",
tools: [weatherTool]);
// Create the main agent, and provide the weather agent as a function tool.
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(
name: MainName,
model: deploymentName,
instructions: MainInstructions,
AIAgent agent = aiProjectClient.AsAIAgent(deploymentName,
instructions: "You are a helpful assistant who responds in French.",
name: "MainAgent",
tools: [weatherAgent.AsAIFunction()]);
// Invoke the agent and output the text result.
AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", session));
// Cleanup by agent name removes the agent versions created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
await aiProjectClient.Agents.DeleteAgentAsync(weatherAgent.Name);
@@ -0,0 +1,30 @@
# Agent as a Function Tool with the Responses API
This sample demonstrates how to use one agent as a function tool for another agent.
## What this sample demonstrates
- Creating a specialized agent (weather) with function tools
- Exposing an agent as a function tool using `.AsAIFunction()`
- Composing agents where one agent delegates to another
- No server-side agent creation or cleanup required
## Prerequisites
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (`az login`)
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the sample
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step11_AsFunctionTool
```
@@ -3,15 +3,13 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.AI.Projects" />
</ItemGroup>
<ItemGroup>
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows multiple middleware layers working together with Azure Foundry Agents:
// This sample shows multiple middleware layers working together with a ChatClientAgent:
// agent run (PII filtering and guardrails),
// function invocation (logging and result overrides), and human-in-the-loop
// approval workflows for sensitive function calls.
@@ -12,19 +12,6 @@ using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
// Get Azure AI Foundry configuration from environment variables
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
const string AssistantInstructions = "You are an AI assistant that helps people find information.";
const string AssistantName = "InformationAssistant";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
@@ -33,14 +20,20 @@ static string GetWeather([Description("The location to get the weather for.")] s
static string GetDateTime()
=> DateTimeOffset.Now.ToString();
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
AITool dateTimeTool = AIFunctionFactory.Create(GetDateTime, name: nameof(GetDateTime));
AITool getWeatherTool = AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather));
// Define the agent you want to create. (Prompt Agent in this case)
AIAgent originalAgent = await aiProjectClient.CreateAIAgentAsync(
name: AssistantName,
model: deploymentName,
instructions: AssistantInstructions,
AIAgent originalAgent = aiProjectClient.AsAIAgent(deploymentName,
instructions: "You are an AI assistant that helps people find information.",
name: "InformationAssistant",
tools: [getWeatherTool, dateTimeTool]);
// Adding middleware to the agent level
@@ -63,24 +56,17 @@ AgentResponse piiResponse = await middlewareEnabledAgent.RunAsync("My name is Jo
Console.WriteLine($"Pii filtered response: {piiResponse}");
Console.WriteLine("\n\n=== Example 3: Agent function middleware ===");
// Agent function middleware support is limited to agents that wraps a upstream ChatClientAgent or derived from it.
AgentResponse functionCallResponse = await middlewareEnabledAgent.RunAsync("What's the current time and the weather in Seattle?", session);
Console.WriteLine($"Function calling response: {functionCallResponse}");
// Special per-request middleware agent.
Console.WriteLine("\n\n=== Example 4: Middleware with human in the loop function approval ===");
AIAgent humanInTheLoopAgent = await aiProjectClient.CreateAIAgentAsync(
AIAgent humanInTheLoopAgent = aiProjectClient.AsAIAgent(deploymentName,
instructions: "You are a Human in the loop testing AI assistant that helps people find information.",
name: "HumanInTheLoopAgent",
model: deploymentName,
instructions: "You are an Human in the loop testing AI assistant that helps people find information.",
// Adding a function with approval required
tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather)))]);
// Using the ConsolePromptingApprovalMiddleware for a specific request to handle user approval during function calls.
AgentResponse response = await humanInTheLoopAgent
.AsBuilder()
.Use(ConsolePromptingApprovalMiddleware, null)
@@ -108,7 +94,6 @@ async ValueTask<object?> FunctionCallOverrideWeather(AIAgent agent, FunctionInvo
if (context.Function.Name == nameof(GetWeather))
{
// Override the result of the GetWeather function
result = "The weather is sunny with a high of 25°C.";
}
Console.WriteLine($"Function Name: {context!.Function.Name} - Middleware 2 Post-Invoke");
@@ -118,18 +103,16 @@ async ValueTask<object?> FunctionCallOverrideWeather(AIAgent agent, FunctionInvo
// This middleware redacts PII information from input and output messages.
async Task<AgentResponse> PIIMiddleware(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
{
// Redact PII information from input messages
var filteredMessages = FilterMessages(messages);
Console.WriteLine("Pii Middleware - Filtered Messages Pre-Run");
var response = await innerAgent.RunAsync(filteredMessages, session, options, cancellationToken).ConfigureAwait(false);
var agentResponse = await innerAgent.RunAsync(filteredMessages, session, options, cancellationToken).ConfigureAwait(false);
// Redact PII information from output messages
response.Messages = FilterMessages(response.Messages);
agentResponse.Messages = FilterMessages(agentResponse.Messages);
Console.WriteLine("Pii Middleware - Filtered Messages Post-Run");
return response;
return agentResponse;
static IList<ChatMessage> FilterMessages(IEnumerable<ChatMessage> messages)
{
@@ -138,11 +121,10 @@ async Task<AgentResponse> PIIMiddleware(IEnumerable<ChatMessage> messages, Agent
static string FilterPii(string content)
{
// Regex patterns for PII detection (simplified for demonstration)
Regex[] piiPatterns = [
new(@"\b\d{3}-\d{3}-\d{4}\b", RegexOptions.Compiled), // Phone number (e.g., 123-456-7890)
new(@"\b[\w\.-]+@[\w\.-]+\.\w+\b", RegexOptions.Compiled), // Email address
new(@"\b[A-Z][a-z]+\s[A-Z][a-z]+\b", RegexOptions.Compiled) // Full name (e.g., John Doe)
MyRegex(),
EmailRegex(),
FullNameRegex()
];
foreach (var pattern in piiPatterns)
@@ -157,20 +139,17 @@ async Task<AgentResponse> PIIMiddleware(IEnumerable<ChatMessage> messages, Agent
// This middleware enforces guardrails by redacting certain keywords from input and output messages.
async Task<AgentResponse> GuardrailMiddleware(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
{
// Redact keywords from input messages
var filteredMessages = FilterMessages(messages);
Console.WriteLine("Guardrail Middleware - Filtered messages Pre-Run");
// Proceed with the agent run
var response = await innerAgent.RunAsync(filteredMessages, session, options, cancellationToken);
var agentResponse = await innerAgent.RunAsync(filteredMessages, session, options, cancellationToken);
// Redact keywords from output messages
response.Messages = FilterMessages(response.Messages);
agentResponse.Messages = FilterMessages(agentResponse.Messages);
Console.WriteLine("Guardrail Middleware - Filtered messages Post-Run");
return response;
return agentResponse;
List<ChatMessage> FilterMessages(IEnumerable<ChatMessage> messages)
{
@@ -194,16 +173,13 @@ async Task<AgentResponse> GuardrailMiddleware(IEnumerable<ChatMessage> messages,
// This middleware handles Human in the loop console interaction for any user approval required during function calling.
async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
{
AgentResponse response = await innerAgent.RunAsync(messages, session, options, cancellationToken);
AgentResponse agentResponse = await innerAgent.RunAsync(messages, session, options, cancellationToken);
// For simplicity, we are assuming here that only function approvals are pending.
List<ToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
List<ToolApprovalRequestContent> approvalRequests = agentResponse.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
while (approvalRequests.Count > 0)
{
// Ask the user to approve each function call request.
// Pass the user input responses back to the agent for further processing.
response.Messages = approvalRequests
agentResponse.Messages = approvalRequests
.ConvertAll(functionApprovalRequest =>
{
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {((FunctionCallContent)functionApprovalRequest.ToolCall).Name}");
@@ -211,13 +187,22 @@ async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMes
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved)]);
});
response = await innerAgent.RunAsync(response.Messages, session, options, cancellationToken);
agentResponse = await innerAgent.RunAsync(agentResponse.Messages, session, options, cancellationToken);
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
approvalRequests = agentResponse.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
}
return response;
return agentResponse;
}
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(middlewareEnabledAgent.Name);
internal partial class Program
{
[GeneratedRegex(@"\b\d{3}-\d{3}-\d{4}\b", RegexOptions.Compiled)]
private static partial Regex MyRegex();
[GeneratedRegex(@"\b[\w\.-]+@[\w\.-]+\.\w+\b", RegexOptions.Compiled)]
private static partial Regex EmailRegex();
[GeneratedRegex(@"\b[A-Z][a-z]+\s[A-Z][a-z]+\b", RegexOptions.Compiled)]
private static partial Regex FullNameRegex();
}
@@ -0,0 +1,31 @@
# Middleware with the Responses API
This sample demonstrates multiple middleware layers working together: PII filtering, guardrails, function invocation logging, and human-in-the-loop approval.
## What this sample demonstrates
- Agent-level run middleware (PII filtering, guardrail enforcement)
- Function-level middleware (logging, result overrides)
- Human-in-the-loop approval workflows for sensitive function calls
- Using `.AsBuilder().Use()` to compose middleware
- No server-side agent creation or cleanup required
## Prerequisites
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (`az login`)
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the sample
```powershell
cd dotnet/samples/02-agents/AgentsWithFoundry
dotnet run --project .\Agent_Step12_Middleware
```
@@ -10,13 +10,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,153 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to use plugins with an AI agent. Plugin classes can
// depend on other services that need to be injected. In this sample, the
// AgentPlugin class uses the WeatherProvider and CurrentTimeProvider classes
// to get weather and current time information. Both services are registered
// in the service collection and injected into the plugin.
// Plugin classes may have many methods, but only some are intended to be used
// as AI functions. The AsAITools method of the plugin class shows how to specify
// which methods should be exposed to the AI agent.
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using SampleApp;
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
const string AssistantInstructions = "You are a helpful assistant that helps people find information.";
const string AssistantName = "PluginAssistant";
// Create a service collection to hold the agent plugin and its dependencies.
ServiceCollection services = new();
services.AddSingleton<WeatherProvider>();
services.AddSingleton<CurrentTimeProvider>();
services.AddSingleton<AgentPlugin>(); // The plugin depends on WeatherProvider and CurrentTimeProvider registered above.
IServiceProvider serviceProvider = services.BuildServiceProvider();
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Create a ChatClientAgent with the options-based constructor to pass services.
AIAgent agent = aiProjectClient.AsAIAgent(new ChatClientAgentOptions
{
Name = AssistantName,
ChatOptions = new() { ModelId = deploymentName, Instructions = AssistantInstructions, Tools = serviceProvider.GetRequiredService<AgentPlugin>().AsAITools().ToList() }
},
services: serviceProvider);
// Invoke the agent and output the text result.
AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("Tell me current time and weather in Seattle.", session));
namespace SampleApp
{
/// <summary>
/// The agent plugin that provides weather and current time information.
/// </summary>
internal sealed class AgentPlugin
{
private readonly WeatherProvider _weatherProvider;
/// <summary>
/// Initializes a new instance of the <see cref="AgentPlugin"/> class.
/// </summary>
/// <param name="weatherProvider">The weather provider to get weather information.</param>
public AgentPlugin(WeatherProvider weatherProvider)
{
this._weatherProvider = weatherProvider;
}
/// <summary>
/// Gets the weather information for the specified location.
/// </summary>
/// <remarks>
/// This method demonstrates how to use the dependency that was injected into the plugin class.
/// </remarks>
/// <param name="location">The location to get the weather for.</param>
/// <returns>The weather information for the specified location.</returns>
public string GetWeather(string location)
{
return this._weatherProvider.GetWeather(location);
}
/// <summary>
/// Gets the current date and time for the specified location.
/// </summary>
/// <remarks>
/// This method demonstrates how to resolve a dependency using the service provider passed to the method.
/// </remarks>
/// <param name="sp">The service provider to resolve the <see cref="CurrentTimeProvider"/>.</param>
/// <param name="location">The location to get the current time for.</param>
/// <returns>The current date and time as a <see cref="DateTimeOffset"/>.</returns>
public DateTimeOffset GetCurrentTime(IServiceProvider sp, string location)
{
CurrentTimeProvider currentTimeProvider = sp.GetRequiredService<CurrentTimeProvider>();
return currentTimeProvider.GetCurrentTime(location);
}
/// <summary>
/// Returns the functions provided by this plugin.
/// </summary>
/// <remarks>
/// In real world scenarios, a class may have many methods and only a subset of them may be intended to be exposed as AI functions.
/// This method demonstrates how to explicitly specify which methods should be exposed to the AI agent.
/// </remarks>
/// <returns>The functions provided by this plugin.</returns>
public IEnumerable<AITool> AsAITools()
{
yield return AIFunctionFactory.Create(this.GetWeather);
yield return AIFunctionFactory.Create(this.GetCurrentTime);
}
}
internal sealed class WeatherProvider
{
private readonly string _weatherSummary = "cloudy with a high of 15°C";
/// <summary>
/// The weather provider that returns weather information.
/// </summary>
/// <summary>
/// Gets the weather information for the specified location.
/// </summary>
/// <remarks>
/// The weather information is hardcoded for demonstration purposes.
/// In a real application, this could call a weather API to get actual weather data.
/// </remarks>
/// <param name="location">The location to get the weather for.</param>
/// <returns>The weather information for the specified location.</returns>
public string GetWeather(string location)
{
return $"The weather in {location} is {this._weatherSummary}.";
}
}
internal sealed class CurrentTimeProvider
{
private readonly TimeProvider _timeProvider = TimeProvider.System;
/// <summary>
/// Provides the current date and time.
/// </summary>
/// <remarks>
/// This class returns the current date and time using the system's clock.
/// </remarks>
/// <summary>
/// Gets the current date and time.
/// </summary>
/// <param name="location">The location to get the current time for (not used in this implementation).</param>
/// <returns>The current date and time as a <see cref="DateTimeOffset"/>.</returns>
public DateTimeOffset GetCurrentTime(string location)
{
return this._timeProvider.GetLocalNow();
}
}
}
@@ -0,0 +1,29 @@
# Using Plugins with the Responses API
This sample shows how to use plugins with a `ChatClientAgent` using the Responses API directly, with dependency injection for plugin services.
## What this sample demonstrates
- Creating plugin classes with injected dependencies
- Registering services and building a service provider
- Passing `services` to the `ChatClientAgent` via the options-based constructor
- Using `AIFunctionFactory` to expose plugin methods as AI tools
## Prerequisites
- .NET 10 SDK or later
- Microsoft Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (`az login`)
Set the following environment variables:
```powershell
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the sample
```powershell
dotnet run
```
@@ -9,7 +9,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
@@ -1,61 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to use Code Interpreter Tool with AI Agents.
// This sample shows how to use Code Interpreter Tool with AIProjectClient.AsAIAgent(...).
using System.Text;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Assistants;
using OpenAI.Responses;
const string AgentInstructions = "You are a personal math tutor. When asked a math question, write and run code using the python tool to answer the question.";
const string AgentName = "CoderAgent-RAPI";
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
const string AgentInstructions = "You are a personal math tutor. When asked a math question, write and run code using the python tool to answer the question.";
const string AgentNameMEAI = "CoderAgent-MEAI";
const string AgentNameNative = "CoderAgent-NATIVE";
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Option 1 - Using HostedCodeInterpreterTool + AgentOptions (MEAI + AgentFramework)
// Create the server side agent version
AIAgent agentOption1 = await aiProjectClient.CreateAIAgentAsync(
model: deploymentName,
name: AgentNameMEAI,
AIAgent agent = aiProjectClient.AsAIAgent(
deploymentName,
instructions: AgentInstructions,
name: AgentName,
tools: [new HostedCodeInterpreterTool() { Inputs = [] }]);
// Option 2 - Using PromptAgentDefinition SDK native type
// Create the server side agent version
AIAgent agentOption2 = await aiProjectClient.CreateAIAgentAsync(
name: AgentNameNative,
creationOptions: new AgentVersionCreationOptions(
new PromptAgentDefinition(model: deploymentName)
{
Instructions = AgentInstructions,
Tools = {
ResponseTool.CreateCodeInterpreterTool(
new CodeInterpreterToolContainer(
CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration(fileIds: [])
)
),
}
})
);
// Either invoke option1 or option2 agent, should have same result
// Option 1
AgentResponse response = await agentOption1.RunAsync("I need to solve the equation sin(x) + x^2 = 42");
// Option 2
// AgentResponse response = await agentOption2.RunAsync("I need to solve the equation sin(x) + x^2 = 42");
AgentResponse response = await agent.RunAsync("I need to solve the equation sin(x) + x^2 = 42");
// Get the CodeInterpreterToolCallContent
CodeInterpreterToolCallContent? toolCallContent = response.Messages.SelectMany(m => m.Contents).OfType<CodeInterpreterToolCallContent>().FirstOrDefault();
@@ -87,7 +57,3 @@ foreach (AIAnnotation annotation in response.Messages.SelectMany(m => m.Contents
""");
}
}
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(agentOption1.Name);
await aiProjectClient.Agents.DeleteAgentAsync(agentOption2.Name);

Some files were not shown because too many files have changed in this diff Show More