Merge branch 'main' into feature-foundry-agents

This commit is contained in:
Chris
2025-11-05 15:49:58 -08:00
committed by GitHub
Unverified
65 changed files with 4215 additions and 4031 deletions
+3 -3
View File
@@ -2,9 +2,9 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251104.2</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251104.2</PackageVersion>
<GitTag>1.0.0-preview.251104.2</GitTag>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251105.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251105.1</PackageVersion>
<GitTag>1.0.0-preview.251105.1</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -110,7 +110,8 @@ foreach (string edge in ByLine(this.Edges))
}
this.Write("\n\n // Build the workflow\n return builder.Build();\n }\n}\n");
this.Write("\n\n // Build the workflow\n return builder.Build(validateOrphans: fal" +
"se);\n }\n}\n");
return this.GenerationEnvironment.ToString();
}
}
@@ -70,6 +70,6 @@ foreach (string edge in ByLine(this.Edges))
#>
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
@@ -52,7 +52,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
this._workflowModel.Build(builder);
// Build final workflow
return builder.WorkflowBuilder.Build();
return builder.WorkflowBuilder.Build(validateOrphans: false);
}
protected override void Visit(ActionScope item)
@@ -11,7 +11,7 @@ namespace Microsoft.Agents.AI.Workflows;
/// <param name="Agent">The AI agent.</param>
/// <param name="EmitEvents">Specifies whether the agent should emit events. If null, the default behavior is applied.</param>
public record AIAgentBinding(AIAgent Agent, bool EmitEvents = false)
: ExecutorBinding(Throw.IfNull(Agent).Name ?? Throw.IfNull(Agent.Id),
: ExecutorBinding(Throw.IfNull(Agent).GetDescriptiveId(),
(_) => new(new AIAgentHostExecutor(Agent, EmitEvents)),
typeof(AIAgentHostExecutor),
Agent)
@@ -14,7 +14,7 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
private readonly AIAgent _agent;
private AgentThread? _thread;
public AIAgentHostExecutor(AIAgent agent, bool emitEvents = false) : base(id: agent.Id)
public AIAgentHostExecutor(AIAgent agent, bool emitEvents = false) : base(id: agent.GetDescriptiveId())
{
this._agent = agent;
this._emitEvents = emitEvents;
@@ -28,7 +28,7 @@ public class WorkflowBuilder
}
private int _edgeCount;
private readonly Dictionary<string, ExecutorBinding> _executors = [];
private readonly Dictionary<string, ExecutorBinding> _executorBindings = [];
private readonly Dictionary<string, HashSet<Edge>> _edges = [];
private readonly HashSet<string> _unboundExecutors = [];
private readonly HashSet<EdgeConnection> _conditionlessConnections = [];
@@ -51,51 +51,51 @@ public class WorkflowBuilder
this._startExecutorId = this.Track(start).Id;
}
private ExecutorBinding Track(ExecutorBinding registration)
private ExecutorBinding Track(ExecutorBinding binding)
{
// If the executor is unbound, create an entry for it, unless it already exists.
// Otherwise, update the entry for it, and remove the unbound tag
if (registration.IsPlaceholder && !this._executors.ContainsKey(registration.Id))
if (binding.IsPlaceholder && !this._executorBindings.ContainsKey(binding.Id))
{
// If this is an unbound executor, we need to track it separately
this._unboundExecutors.Add(registration.Id);
this._unboundExecutors.Add(binding.Id);
}
else if (!registration.IsPlaceholder)
else if (!binding.IsPlaceholder)
{
// If there is already a bound executor with this ID, we need to validate (to best efforts)
// that the two are matching (at least based on type)
if (this._executors.TryGetValue(registration.Id, out ExecutorBinding? existing))
if (this._executorBindings.TryGetValue(binding.Id, out ExecutorBinding? existing))
{
if (existing.ExecutorType != registration.ExecutorType)
if (existing.ExecutorType != binding.ExecutorType)
{
throw new InvalidOperationException(
$"Cannot bind executor with ID '{registration.Id}' because an executor with the same ID but a different type ({existing.ExecutorType.Name} vs {registration.ExecutorType.Name}) is already bound.");
$"Cannot bind executor with ID '{binding.Id}' because an executor with the same ID but a different type ({existing.ExecutorType.Name} vs {binding.ExecutorType.Name}) is already bound.");
}
if (existing.RawValue is not null &&
!ReferenceEquals(existing.RawValue, registration.RawValue))
!ReferenceEquals(existing.RawValue, binding.RawValue))
{
throw new InvalidOperationException(
$"Cannot bind executor with ID '{registration.Id}' because an executor with the same ID but different instance is already bound.");
$"Cannot bind executor with ID '{binding.Id}' because an executor with the same ID but different instance is already bound.");
}
}
else
{
this._executors[registration.Id] = registration;
if (this._unboundExecutors.Contains(registration.Id))
this._executorBindings[binding.Id] = binding;
if (this._unboundExecutors.Contains(binding.Id))
{
this._unboundExecutors.Remove(registration.Id);
this._unboundExecutors.Remove(binding.Id);
}
}
}
if (registration is RequestPortBinding portRegistration)
if (binding is RequestPortBinding portRegistration)
{
RequestPort port = portRegistration.Port;
this._requestPorts[port.Id] = port;
}
return registration;
return binding;
}
/// <summary>
@@ -369,25 +369,86 @@ public class WorkflowBuilder
public WorkflowBuilder AddFanInEdge(ExecutorBinding target, params IEnumerable<ExecutorBinding> sources)
=> this.AddFanInEdge(sources, target);
private void Validate()
private void Validate(bool validateOrphans)
{
// Validate that there are no unbound executors
// Check that there are no "unbound" (defined as placeholders that have not been replaced by real bindings)
// executors.
if (this._unboundExecutors.Count > 0)
{
throw new InvalidOperationException(
$"Workflow cannot be built because there are unbound executors: {string.Join(", ", this._unboundExecutors)}.");
}
// TODO: This is likely a pipe-dream, but can we do any type-checking on the edges? (Not without instantiating the executors...)
// Make sure that all nodes are connected to the start executor (transitively)
HashSet<string> remainingExecutors = new(this._executorBindings.Keys);
Queue<string> toVisit = new([this._startExecutorId]);
if (!validateOrphans)
{
return;
}
while (toVisit.Count > 0)
{
string currentId = toVisit.Dequeue();
bool unvisited = remainingExecutors.Remove(currentId);
if (unvisited &&
this._edges.TryGetValue(currentId, out HashSet<Edge>? outgoingEdges))
{
foreach (Edge edge in outgoingEdges)
{
switch (edge.Data)
{
case DirectEdgeData directEdgeData:
toVisit.Enqueue(directEdgeData.SinkId);
break;
case FanOutEdgeData fanOutEdgeData:
foreach (string targetId in fanOutEdgeData.SinkIds)
{
toVisit.Enqueue(targetId);
}
break;
case FanInEdgeData fanInEdgeData:
toVisit.Enqueue(fanInEdgeData.SinkId);
break;
}
// Ideally we would be able to validate that the types accepted by the target executor(s) are compatible
// with those produced by the source executor. However, this is not possible at this time for a number of
// reasons:
//
// - Right now we do not require users to specify the types produced by Executors exhaustively. This will
// likely change at some point in the future as part of implementing support for polymorphism in message
// handling. Until then it cannot be clear what types are produced by an upstream Executor.
// - Edges with conditionals / target selectors can route messages
// - We intend to expand the API surface of FanIn edges to allow different aggregation and synchronization
// strategies; this could introduce type transformations which we may not be able to validate here.
// - All of the above seem like they can be solved with some effort, but the biggest blocker is that we
// currently support async Executor factories, and Executors register message handlers at runtime, so we
// cannot know which types they accept until they are instantiated, and we cannot instantiate them at
// build time because we are in an obligate (for DI-compatibility) synchronous context.
//
// TODO: Revisit the async Executor factory decision if we have a way to deal with "conditional" and
// "target selector-based" routing.
}
}
}
if (remainingExecutors.Count > 0)
{
throw new InvalidOperationException(
$"Workflow cannot be built because there are unreachable executors: {string.Join(", ", remainingExecutors)}.");
}
}
private Workflow BuildInternal(Activity? activity = null)
private Workflow BuildInternal(bool validateOrphans, Activity? activity = null)
{
activity?.AddEvent(new ActivityEvent(EventNames.BuildStarted));
try
{
this.Validate();
this.Validate(validateOrphans);
}
catch (Exception ex) when (activity is not null)
{
@@ -403,7 +464,7 @@ public class WorkflowBuilder
var workflow = new Workflow(this._startExecutorId, this._name, this._description)
{
ExecutorBindings = this._executors,
ExecutorBindings = this._executorBindings,
Edges = this._edges,
Ports = this._requestPorts,
OutputExecutors = this._outputExecutors
@@ -433,13 +494,15 @@ public class WorkflowBuilder
/// <summary>
/// Builds and returns a workflow instance.
/// </summary>
/// <param name="validateOrphans">Specifies whether workflow validation should check for Executor nodes that are
/// not reachable from the starting executor.</param>
/// <exception cref="InvalidOperationException">Thrown if there are unbound executors in the workflow definition,
/// or if the start executor is not bound.</exception>
public Workflow Build()
public Workflow Build(bool validateOrphans = true)
{
using Activity? activity = s_activitySource.StartActivity(ActivityNames.WorkflowBuild);
var workflow = this.BuildInternal(activity);
var workflow = this.BuildInternal(validateOrphans, activity);
activity?.AddEvent(new ActivityEvent(EventNames.BuildCompleted));
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -114,6 +114,6 @@ public static class WorkflowProvider
builder.AddEdge(workflowTest, addMessage);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -80,6 +80,6 @@ public static class WorkflowProvider
builder.AddEdge(myWorkflow, clearAll);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -175,6 +175,6 @@ public static class WorkflowProvider
builder.AddEdge(conditionItemEvenactionsPost, conditionItemEvenPost);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -167,6 +167,6 @@ public static class WorkflowProvider
builder.AddEdge(conditionGroupTestelseactionsPost, conditionGroupTestPost);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -90,6 +90,6 @@ public static class WorkflowProvider
builder.AddEdge(workflowTest, copyMessages);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -83,6 +83,6 @@ public static class WorkflowProvider
builder.AddEdge(workflowTest, conversationCreate);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -82,6 +82,6 @@ public static class WorkflowProvider
builder.AddEdge(myWorkflow, setVar);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -82,6 +82,6 @@ public static class WorkflowProvider
builder.AddEdge(myWorkflow, setVar);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -89,6 +89,6 @@ public static class WorkflowProvider
builder.AddEdge(endAllRestart, sendActivity1);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -89,6 +89,6 @@ public static class WorkflowProvider
builder.AddEdge(endAllRestart, sendActivity1);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -138,6 +138,6 @@ public static class WorkflowProvider
builder.AddEdge(sendActivity3, endAll);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -109,6 +109,6 @@ public static class WorkflowProvider
builder.AddEdge(myWorkflow, invokeAgent);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -180,6 +180,6 @@ public static class WorkflowProvider
builder.AddEdge(foreachLoopEnd, foreachLoopNext);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -180,6 +180,6 @@ public static class WorkflowProvider
builder.AddEdge(foreachLoopEnd, foreachLoopNext);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -176,6 +176,6 @@ public static class WorkflowProvider
builder.AddEdge(foreachLoopEnd, foreachLoopNext);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -101,6 +101,6 @@ public static class WorkflowProvider
builder.AddEdge(setVar, parseVar);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -98,6 +98,6 @@ public static class WorkflowProvider
builder.AddEdge(setVar, clearVar);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -86,6 +86,6 @@ public static class WorkflowProvider
builder.AddEdge(workflowTest, getMessageSingle);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -99,6 +99,6 @@ public static class WorkflowProvider
builder.AddEdge(workflowTest, getMessagesAll);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -105,6 +105,6 @@ public static class WorkflowProvider
builder.AddEdge(setInput, activityInput);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -85,6 +85,6 @@ public static class WorkflowProvider
builder.AddEdge(myWorkflow, setText);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -1,4 +1,4 @@
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
@@ -82,6 +82,6 @@ public static class WorkflowProvider
builder.AddEdge(myWorkflow, setVar);
// Build the workflow
return builder.Build();
return builder.Build(validateOrphans: false);
}
}
}
@@ -8,6 +8,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Agents.AI.Workflows.Checkpointing;
using Microsoft.Agents.AI.Workflows.Execution;
using Microsoft.Agents.AI.Workflows.Specialized;
using Microsoft.Extensions.AI;
@@ -196,4 +197,50 @@ public class SpecializedExecutorSmokeTests
collected.Text.Should().Be(expectedText);
}
}
[Fact]
public async Task Test_AIAgent_ExecutorId_Use_Agent_NameAsync()
{
const string AgentAName = "TestAgentAName";
const string AgentBName = "TestAgentBName";
TestAIAgent agentA = new(name: AgentAName);
TestAIAgent agentB = new(name: AgentBName);
var workflow = new WorkflowBuilder(agentA).AddEdge(agentA, agentB).Build();
var definition = workflow.ToWorkflowInfo();
// Verify that the agent host executor registration IDs in the workflow definition
// match the agent names when agent names are provided.
// The property DisplayName falls back to using the agent ID when Name is not set.
agentA.GetDescriptiveId().Should().Contain(AgentAName);
agentB.GetDescriptiveId().Should().Contain(AgentBName);
definition.Executors[agentA.GetDescriptiveId()].ExecutorId.Should().Be(agentA.GetDescriptiveId());
definition.Executors[agentB.GetDescriptiveId()].ExecutorId.Should().Be(agentB.GetDescriptiveId());
// This will create an instance of the start agent and verify that the ID
// of the executor instance matches the ID of the registration.
var protocolDescriptor = await workflow.DescribeProtocolAsync();
protocolDescriptor.Accepts.Should().Contain(typeof(ChatMessage));
}
[Fact]
public async Task Test_AIAgent_ExecutorId_Use_Agent_ID_When_Name_Not_ProvidedAsync()
{
TestAIAgent agentA = new();
TestAIAgent agentB = new();
var workflow = new WorkflowBuilder(agentA).AddEdge(agentA, agentB).Build();
var definition = workflow.ToWorkflowInfo();
// Verify that the agent host executor registration IDs in the workflow definition
// match the agent IDs when agent names are not provided.
// The property DisplayName falls back to using the agent ID when Name is not set.
agentA.GetDescriptiveId().Should().Contain(agentA.Id);
agentB.GetDescriptiveId().Should().Contain(agentB.Id);
definition.Executors[agentA.GetDescriptiveId()].ExecutorId.Should().Be(agentA.GetDescriptiveId());
definition.Executors[agentB.GetDescriptiveId()].ExecutorId.Should().Be(agentB.GetDescriptiveId());
// This will create an instance of the start agent and verify that the ID
// of the executor instance matches the ID of the registration.
var protocolDescriptor = await workflow.DescribeProtocolAsync();
protocolDescriptor.Accepts.Should().Contain(typeof(ChatMessage));
}
}
@@ -21,6 +21,51 @@ public partial class WorkflowBuilderSmokeTests
(msg, ctx) => ctx.SendMessageAsync(msg));
}
[Fact]
public void Test_Validation_FailsWhenUnboundExecutors()
{
Func<Workflow> act = () =>
{
return new WorkflowBuilder("start")
.AddEdge(new NoOpExecutor("start"), "unbound")
.Build();
};
act.Should().Throw<InvalidOperationException>();
}
[Fact]
public void Test_Validation_FailsWhenUnreachableExecutors()
{
Func<Workflow> act = () =>
{
return new WorkflowBuilder("start")
.BindExecutor(new NoOpExecutor("start"))
.AddEdge(new NoOpExecutor("unreachable"), new NoOpExecutor("also-unreachable"))
.Build();
};
act.Should().Throw<InvalidOperationException>();
}
[Fact]
public void Test_Validation_AddEdgesOutOfOrderDoesNotImpactReachability()
{
Workflow workflow = new WorkflowBuilder("start")
.BindExecutor(new NoOpExecutor("start"))
.AddEdge(new NoOpExecutor("not-unreachable"), new NoOpExecutor("also-not-unreachable"))
.AddEdge("start", "not-unreachable")
.Build();
workflow.StartExecutorId.Should().Be("start");
workflow.ExecutorBindings.Should().HaveCount(3);
workflow.ExecutorBindings.Should().ContainKey("start");
workflow.ExecutorBindings.Should().ContainKey("not-unreachable");
workflow.ExecutorBindings.Should().ContainKey("also-not-unreachable");
workflow.ExecutorBindings.Values.Should().AllSatisfy(binding => binding.ExecutorType.Should().Be<NoOpExecutor>());
}
[Fact]
public void Test_LateBinding_Executor()
{
+12
View File
@@ -12,6 +12,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **agent-framework-ag-ui**: Initial release of AG-UI protocol integration for Agent Framework ([#1826](https://github.com/microsoft/agent-framework/pull/1826))
- **agent-framework-chatkit**: ChatKit integration with a sample application ([#1273](https://github.com/microsoft/agent-framework/pull/1273))
- Added parameter to disable agent cleanup in AzureAIAgentClient ([#1882](https://github.com/microsoft/agent-framework/pull/1882))
- Add support for Python 3.14 ([#1904](https://github.com/microsoft/agent-framework/pull/1904))
### Changed
- [BREAKING] Replaced AIProjectClient with AgentsClient in Foundry ([#1936](https://github.com/microsoft/agent-framework/pull/1936))
- Updates to Tools ([#1835](https://github.com/microsoft/agent-framework/pull/1835))
### Fixed
- Fix missing packaging dependency ([#1929](https://github.com/microsoft/agent-framework/pull/1929))
## [1.0.0b251104] - 2025-11-04
+16 -2
View File
@@ -4,6 +4,7 @@
import argparse
from enum import Enum
import glob
import logging
import tempfile
import subprocess # nosec
@@ -32,6 +33,16 @@ def with_color(text: str, color: Colors) -> str:
return f"{color.value}{text}{Colors.CEND.value}"
def expand_file_patterns(patterns: list[str]) -> list[str]:
"""Expand glob patterns to actual file paths."""
all_files: list[str] = []
for pattern in patterns:
# Handle both relative and absolute paths
matches = glob.glob(pattern, recursive=True)
all_files.extend(matches)
return sorted(set(all_files)) # Remove duplicates and sort
def extract_python_code_blocks(markdown_file_path: str) -> list[tuple[str, int]]:
"""Extract Python code blocks from a Markdown file."""
with open(markdown_file_path, encoding="utf-8") as file:
@@ -113,7 +124,10 @@ def check_code_blocks(markdown_file_paths: list[str], exclude_patterns: list[str
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Check code blocks in Markdown files for syntax errors.")
# Argument is a list of markdown files containing glob patterns
parser.add_argument("markdown_files", nargs="+", help="Markdown files to check.")
parser.add_argument("markdown_files", nargs="+", help="Markdown files to check (supports glob patterns).")
parser.add_argument("--exclude", action="append", help="Exclude files containing this pattern.")
args = parser.parse_args()
check_code_blocks(args.markdown_files, args.exclude)
# Expand glob patterns to actual file paths
expanded_files = expand_file_patterns(args.markdown_files)
check_code_blocks(expanded_files, args.exclude)
+1 -1
View File
@@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251104"
version = "1.0.0b251105"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -1 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
"""Example agents for AG-UI demonstration."""
from . import agents
__all__ = ["agents"]
@@ -1,3 +1,25 @@
# Copyright (c) Microsoft. All rights reserved.
"""Example agents for AG-UI demonstration."""
from .document_writer_agent import document_writer_agent
from .human_in_the_loop_agent import human_in_the_loop_agent
from .recipe_agent import recipe_agent
from .research_assistant_agent import research_assistant_agent
from .simple_agent import agent as simple_agent
from .task_planner_agent import task_planner_agent
from .task_steps_agent import task_steps_agent_wrapped
from .ui_generator_agent import ui_generator_agent
from .weather_agent import weather_agent
__all__ = [
"document_writer_agent",
"human_in_the_loop_agent",
"recipe_agent",
"research_assistant_agent",
"simple_agent",
"task_planner_agent",
"task_steps_agent_wrapped",
"ui_generator_agent",
"weather_agent",
]
+1
View File
@@ -41,6 +41,7 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["agent_framework_ag_ui"]
force-include = { "examples" = "agent_framework_ag_ui_examples" }
[tool.pytest.ini_options]
asyncio_mode = "auto"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251104"
version = "1.0.0b251105"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -2,7 +2,8 @@
import importlib.metadata
from ._chat_client import AzureAIAgentClient, AzureAISettings
from ._chat_client import AzureAIAgentClient
from ._shared import AzureAISettings
try:
__version__ = importlib.metadata.version(__name__)
@@ -40,9 +40,9 @@ from agent_framework import (
use_chat_middleware,
use_function_invocation,
)
from agent_framework._pydantic import AFBaseSettings
from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException
from agent_framework.observability import use_observability
from azure.ai.agents.aio import AgentsClient
from azure.ai.agents.models import (
Agent,
AgentsNamedToolChoice,
@@ -85,11 +85,11 @@ from azure.ai.agents.models import (
ToolDefinition,
ToolOutput,
)
from azure.ai.projects.aio import AIProjectClient
from azure.core.credentials_async import AsyncTokenCredential
from azure.core.exceptions import HttpResponseError, ResourceNotFoundError
from pydantic import ValidationError
from ._shared import AzureAISettings
if sys.version_info >= (3, 11):
from typing import Self # pragma: no cover
else:
@@ -99,47 +99,6 @@ else:
logger = get_logger("agent_framework.azure")
class AzureAISettings(AFBaseSettings):
"""Azure AI Project settings.
The settings are first loaded from environment variables with the prefix 'AZURE_AI_'.
If the environment variables are not found, the settings can be loaded from a .env file
with the encoding 'utf-8'. If the settings are not found in the .env file, the settings
are ignored; however, validation will fail alerting that the settings are missing.
Keyword Args:
project_endpoint: The Azure AI Project endpoint URL.
Can be set via environment variable AZURE_AI_PROJECT_ENDPOINT.
model_deployment_name: The name of the model deployment to use.
Can be set via environment variable AZURE_AI_MODEL_DEPLOYMENT_NAME.
env_file_path: If provided, the .env settings are read from this file path location.
env_file_encoding: The encoding of the .env file, defaults to 'utf-8'.
Examples:
.. code-block:: python
from agent_framework_azure_ai import AzureAISettings
# Using environment variables
# Set AZURE_AI_PROJECT_ENDPOINT=https://your-project.cognitiveservices.azure.com
# Set AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4
settings = AzureAISettings()
# Or passing parameters directly
settings = AzureAISettings(
project_endpoint="https://your-project.cognitiveservices.azure.com", model_deployment_name="gpt-4"
)
# Or loading from a .env file
settings = AzureAISettings(env_file_path="path/to/.env")
"""
env_prefix: ClassVar[str] = "AZURE_AI_"
project_endpoint: str | None = None
model_deployment_name: str | None = None
TAzureAIAgentClient = TypeVar("TAzureAIAgentClient", bound="AzureAIAgentClient")
@@ -154,7 +113,7 @@ class AzureAIAgentClient(BaseChatClient):
def __init__(
self,
*,
project_client: AIProjectClient | None = None,
agents_client: AgentsClient | None = None,
agent_id: str | None = None,
agent_name: str | None = None,
thread_id: str | None = None,
@@ -169,16 +128,16 @@ class AzureAIAgentClient(BaseChatClient):
"""Initialize an Azure AI Agent client.
Keyword Args:
project_client: An existing AIProjectClient to use. If not provided, one will be created.
agent_id: The ID of an existing agent to use. If not provided and project_client is provided,
a new agent will be created (and deleted after the request). If neither project_client
agents_client: An existing AgentsClient to use. If not provided, one will be created.
agent_id: The ID of an existing agent to use. If not provided and agents_client is provided,
a new agent will be created (and deleted after the request). If neither agents_client
nor agent_id is provided, both will be created and managed automatically.
agent_name: The name to use when creating new agents.
thread_id: Default thread ID to use for conversations. Can be overridden by
conversation_id property when making a request.
project_endpoint: The Azure AI Project endpoint URL.
Can also be set via environment variable AZURE_AI_PROJECT_ENDPOINT.
Ignored when a project_client is passed.
Ignored when a agents_client is passed.
model_deployment_name: The model deployment name to use for agent creation.
Can also be set via environment variable AZURE_AI_MODEL_DEPLOYMENT_NAME.
async_credential: Azure async credential to use for authentication.
@@ -221,9 +180,9 @@ class AzureAIAgentClient(BaseChatClient):
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Azure AI settings.", ex) from ex
# If no project_client is provided, create one
# If no agents_client is provided, create one
should_close_client = False
if project_client is None:
if agents_client is None:
if not azure_ai_settings.project_endpoint:
raise ServiceInitializationError(
"Azure AI project endpoint is required. Set via 'project_endpoint' parameter "
@@ -238,8 +197,8 @@ class AzureAIAgentClient(BaseChatClient):
# Use provided credential
if not async_credential:
raise ServiceInitializationError("Azure credential is required when project_client is not provided.")
project_client = AIProjectClient(
raise ServiceInitializationError("Azure credential is required when agents_client is not provided.")
agents_client = AgentsClient(
endpoint=azure_ai_settings.project_endpoint,
credential=async_credential,
user_agent=AGENT_FRAMEWORK_USER_AGENT,
@@ -250,7 +209,7 @@ class AzureAIAgentClient(BaseChatClient):
super().__init__(**kwargs)
# Initialize instance variables
self.project_client = project_client
self.agents_client = agents_client
self.credential = async_credential
self.agent_id = agent_id
self.agent_name = agent_name
@@ -261,27 +220,6 @@ class AzureAIAgentClient(BaseChatClient):
self._should_close_client = should_close_client # Track whether we should close client connection
self._agent_definition: Agent | None = None # Cached definition for existing agent
async def setup_azure_ai_observability(self, enable_sensitive_data: bool | None = None) -> None:
"""Use this method to setup tracing in your Azure AI Project.
This will take the connection string from the project project_client.
It will override any connection string that is set in the environment variables.
It will disable any OTLP endpoint that might have been set.
"""
try:
conn_string = await self.project_client.telemetry.get_application_insights_connection_string()
except ResourceNotFoundError:
logger.warning(
"No Application Insights connection string found for the Azure AI Project, "
"please call setup_observability() manually."
)
return
from agent_framework.observability import setup_observability
setup_observability(
applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data
)
async def __aenter__(self) -> "Self":
"""Async context manager entry."""
return self
@@ -291,7 +229,7 @@ class AzureAIAgentClient(BaseChatClient):
await self.close()
async def close(self) -> None:
"""Close the project_client and clean up any agents we created."""
"""Close the agents_client and clean up any agents we created."""
await self._cleanup_agent_if_needed()
await self._close_client_if_needed()
@@ -303,7 +241,7 @@ class AzureAIAgentClient(BaseChatClient):
settings: A dictionary of settings for the service.
"""
return cls(
project_client=settings.get("project_client"),
agents_client=settings.get("agents_client"),
agent_id=settings.get("agent_id"),
thread_id=settings.get("thread_id"),
project_endpoint=settings.get("project_endpoint"),
@@ -380,11 +318,14 @@ class AzureAIAgentClient(BaseChatClient):
args["instructions"] = run_options["instructions"]
if "response_format" in run_options:
args["response_format"] = run_options["response_format"]
if "temperature" in run_options:
args["temperature"] = run_options["temperature"]
if "top_p" in run_options:
args["top_p"] = run_options["top_p"]
created_agent = await self.project_client.agents.create_agent(**args)
created_agent = await self.agents_client.create_agent(**args)
self.agent_id = str(created_agent.id)
self._agent_definition = created_agent
self._agent_created = True
@@ -428,7 +369,7 @@ class AzureAIAgentClient(BaseChatClient):
args["tool_outputs"] = tool_outputs
if tool_approvals:
args["tool_approvals"] = tool_approvals
await self.project_client.agents.runs.submit_tool_outputs_stream(**args) # type: ignore[reportUnknownMemberType]
await self.agents_client.runs.submit_tool_outputs_stream(**args) # type: ignore[reportUnknownMemberType]
# Pass the handler to the stream to continue processing
stream = handler # type: ignore
final_thread_id = thread_run.thread_id
@@ -438,7 +379,7 @@ class AzureAIAgentClient(BaseChatClient):
# Now create a new run and stream the results.
run_options.pop("conversation_id", None)
stream = await self.project_client.agents.runs.stream( # type: ignore[reportUnknownMemberType]
stream = await self.agents_client.runs.stream( # type: ignore[reportUnknownMemberType]
final_thread_id, agent_id=agent_id, **run_options
)
@@ -449,9 +390,7 @@ class AzureAIAgentClient(BaseChatClient):
if thread_id is None:
return None
async for run in self.project_client.agents.runs.list(
thread_id=thread_id, limit=1, order=ListSortOrder.DESCENDING
): # type: ignore[reportUnknownMemberType]
async for run in self.agents_client.runs.list(thread_id=thread_id, limit=1, order=ListSortOrder.DESCENDING): # type: ignore[reportUnknownMemberType]
if run.status not in [
RunStatus.COMPLETED,
RunStatus.CANCELLED,
@@ -468,12 +407,12 @@ class AzureAIAgentClient(BaseChatClient):
if thread_id is not None:
if thread_run is not None:
# There was an active run; we need to cancel it before starting a new run.
await self.project_client.agents.runs.cancel(thread_id, thread_run.id)
await self.agents_client.runs.cancel(thread_id, thread_run.id)
return thread_id
# No thread ID was provided, so create a new thread.
thread = await self.project_client.agents.threads.create(
thread = await self.agents_client.threads.create(
tool_resources=run_options.get("tool_resources"), metadata=run_options.get("metadata")
)
thread_id = thread.id
@@ -482,7 +421,7 @@ class AzureAIAgentClient(BaseChatClient):
# once fixed, in the function above, readd:
# `messages=run_options.pop("additional_messages")`
for msg in run_options.pop("additional_messages", []):
await self.project_client.agents.messages.create(
await self.agents_client.messages.create(
thread_id=thread_id, role=msg.role, content=msg.content, metadata=msg.metadata
)
# and remove until here.
@@ -715,21 +654,21 @@ class AzureAIAgentClient(BaseChatClient):
return []
async def _close_client_if_needed(self) -> None:
"""Close project_client session if we created it."""
"""Close agents_client session if we created it."""
if self._should_close_client:
await self.project_client.close()
await self.agents_client.close()
async def _cleanup_agent_if_needed(self) -> None:
"""Clean up the agent if we created it."""
if self._agent_created and self.should_cleanup_agent and self.agent_id is not None:
await self.project_client.agents.delete_agent(self.agent_id)
await self.agents_client.delete_agent(self.agent_id)
self.agent_id = None
self._agent_created = False
async def _load_agent_definition_if_needed(self) -> Agent | None:
"""Load and cache agent details if not already loaded."""
if self._agent_definition is None and self.agent_id is not None:
self._agent_definition = await self.project_client.agents.get_agent(self.agent_id)
self._agent_definition = await self.agents_client.get_agent(self.agent_id)
return self._agent_definition
def _prepare_tool_choice(self, chat_options: ChatOptions) -> None:
@@ -919,59 +858,34 @@ class AzureAIAgentClient(BaseChatClient):
config_args["market"] = market
if set_lang := additional_props.get("set_lang"):
config_args["set_lang"] = set_lang
# Bing Grounding (support both connection_id and connection_name)
# Bing Grounding
connection_id = additional_props.get("connection_id") or os.getenv("BING_CONNECTION_ID")
connection_name = additional_props.get("connection_name") or os.getenv("BING_CONNECTION_NAME")
# Custom Bing Search
custom_connection_name = additional_props.get("custom_connection_name") or os.getenv(
"BING_CUSTOM_CONNECTION_NAME"
custom_connection_id = additional_props.get("custom_connection_id") or os.getenv(
"BING_CUSTOM_CONNECTION_ID"
)
custom_configuration_name = additional_props.get("custom_instance_name") or os.getenv(
custom_instance_name = additional_props.get("custom_instance_name") or os.getenv(
"BING_CUSTOM_INSTANCE_NAME"
)
bing_search: BingGroundingTool | BingCustomSearchTool | None = None
if (
(connection_id or connection_name)
and not custom_connection_name
and not custom_configuration_name
):
if (connection_id) and not custom_connection_id and not custom_instance_name:
if connection_id:
conn_id = connection_id
elif connection_name:
try:
bing_connection = await self.project_client.connections.get(name=connection_name)
except HttpResponseError as err:
raise ServiceInitializationError(
f"Bing connection '{connection_name}' not found in the Azure AI Project.",
err,
) from err
else:
conn_id = bing_connection.id
else:
raise ServiceInitializationError("Neither connection_id nor connection_name provided.")
raise ServiceInitializationError("Parameter connection_id is not provided.")
bing_search = BingGroundingTool(connection_id=conn_id, **config_args)
if custom_connection_name and custom_configuration_name:
try:
bing_custom_connection = await self.project_client.connections.get(
name=custom_connection_name
)
except HttpResponseError as err:
raise ServiceInitializationError(
f"Bing custom connection '{custom_connection_name}' not found in the Azure AI Project.",
err,
) from err
else:
bing_search = BingCustomSearchTool(
connection_id=bing_custom_connection.id,
instance_name=custom_configuration_name,
**config_args,
)
if custom_connection_id and custom_instance_name:
bing_search = BingCustomSearchTool(
connection_id=custom_connection_id,
instance_name=custom_instance_name,
**config_args,
)
if not bing_search:
raise ServiceInitializationError(
"Bing search tool requires either 'connection_id' or 'connection_name' for Bing Grounding "
"or both 'custom_connection_name' and 'custom_instance_name' for Custom Bing Search. "
"Bing search tool requires either 'connection_id' for Bing Grounding "
"or both 'custom_connection_id' and 'custom_instance_name' for Custom Bing Search. "
"These can be provided via additional_properties or environment variables: "
"'BING_CONNECTION_ID', 'BING_CONNECTION_NAME', 'BING_CUSTOM_CONNECTION_NAME', "
"'BING_CONNECTION_ID', 'BING_CUSTOM_CONNECTION_ID', "
"'BING_CUSTOM_INSTANCE_NAME'"
)
tool_definitions.extend(bing_search.definitions)
@@ -1062,4 +976,4 @@ class AzureAIAgentClient(BaseChatClient):
Returns:
The service URL for the chat client, or None if not set.
"""
return self.project_client._config.endpoint
return self.agents_client._config.endpoint # type: ignore
@@ -0,0 +1,46 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from agent_framework._pydantic import AFBaseSettings
class AzureAISettings(AFBaseSettings):
"""Azure AI Project settings.
The settings are first loaded from environment variables with the prefix 'AZURE_AI_'.
If the environment variables are not found, the settings can be loaded from a .env file
with the encoding 'utf-8'. If the settings are not found in the .env file, the settings
are ignored; however, validation will fail alerting that the settings are missing.
Keyword Args:
project_endpoint: The Azure AI Project endpoint URL.
Can be set via environment variable AZURE_AI_PROJECT_ENDPOINT.
model_deployment_name: The name of the model deployment to use.
Can be set via environment variable AZURE_AI_MODEL_DEPLOYMENT_NAME.
env_file_path: If provided, the .env settings are read from this file path location.
env_file_encoding: The encoding of the .env file, defaults to 'utf-8'.
Examples:
.. code-block:: python
from agent_framework.azure import AzureAISettings
# Using environment variables
# Set AZURE_AI_PROJECT_ENDPOINT=https://your-project.cognitiveservices.azure.com
# Set AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4
settings = AzureAISettings()
# Or passing parameters directly
settings = AzureAISettings(
project_endpoint="https://your-project.cognitiveservices.azure.com", model_deployment_name="gpt-4"
)
# Or loading from a .env file
settings = AzureAISettings(env_file_path="path/to/.env")
"""
env_prefix: ClassVar[str] = "AZURE_AI_"
project_endpoint: str | None = None
model_deployment_name: str | None = None
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Azure AI Foundry integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251104"
version = "1.0.0b251105"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+13 -14
View File
@@ -44,31 +44,30 @@ def azure_ai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
@fixture
def mock_ai_project_client() -> MagicMock:
"""Fixture that provides a mock AIProjectClient."""
def mock_agents_client() -> MagicMock:
"""Fixture that provides a mock AgentsClient."""
mock_client = MagicMock()
# Mock agents property
mock_client.agents = MagicMock()
mock_client.agents.create_agent = AsyncMock()
mock_client.agents.delete_agent = AsyncMock()
mock_client.create_agent = AsyncMock()
mock_client.delete_agent = AsyncMock()
# Mock agent creation response
mock_agent = MagicMock()
mock_agent.id = "test-agent-id"
mock_client.agents.create_agent.return_value = mock_agent
mock_client.create_agent.return_value = mock_agent
# Mock threads property
mock_client.agents.threads = MagicMock()
mock_client.agents.threads.create = AsyncMock()
mock_client.agents.messages.create = AsyncMock()
mock_client.threads = MagicMock()
mock_client.threads.create = AsyncMock()
mock_client.messages.create = AsyncMock()
# Mock runs property
mock_client.agents.runs = MagicMock()
mock_client.agents.runs.list = AsyncMock()
mock_client.agents.runs.cancel = AsyncMock()
mock_client.agents.runs.stream = AsyncMock()
mock_client.agents.runs.submit_tool_outputs_stream = AsyncMock()
mock_client.runs = MagicMock()
mock_client.runs.list = AsyncMock()
mock_client.runs.cancel = AsyncMock()
mock_client.runs.stream = AsyncMock()
mock_client.runs.submit_tool_outputs_stream = AsyncMock()
return mock_client
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251104"
version = "1.0.0b251105"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251104"
version = "1.0.0b251105"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251104"
version = "1.0.0b251105"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -10,8 +10,6 @@ Required Environment Variables:
AZURE_AI_MODEL_DEPLOYMENT_NAME: Name of the model deployment to use
Optional Environment Variables:
BING_CONNECTION_NAME: Name of the Bing connection for web search
OR
BING_CONNECTION_ID: ID of the Bing connection for web search
Authentication:
@@ -21,7 +19,7 @@ Authentication:
Example:
export AZURE_AI_PROJECT_ENDPOINT="https://your-project.azure.com"
export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o"
export BING_CONNECTION_NAME="bing-grounding-connection"
export BING_CONNECTION_ID="connection-id"
az login
"""
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Experimental modules for Microsoft Agent Framework"
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251104"
version = "1.0.0b251105"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Mem0 integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251104"
version = "1.0.0b251105"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Microsoft Purview (Graph dataSecurityAndGovernance) integration f
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251104"
version = "1.0.0b251105"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Redis integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251104"
version = "1.0.0b251105"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251104"
version = "1.0.0b251105"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -212,7 +212,7 @@ exclude_dirs = ["tests", "./run_tasks_in_packages_if_exists.py", "./check_md_cod
executor.type = "uv"
[tool.poe.tasks]
markdown-code-lint = """uv run python check_md_code_blocks.py README.md ./packages/**/README.md ./samples/**/*.md --exclude cookiecutter-agent-framework-lab --exclude tau2 --exclude packages/devui/frontend"""
markdown-code-lint = "uv run python check_md_code_blocks.py 'README.md' './packages/**/README.md' './samples/**/*.md' --exclude cookiecutter-agent-framework-lab --exclude tau2 --exclude 'packages/devui/frontend'"
docs-install = "uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit --group=docs"
docs-clean = "rm -rf docs/build"
docs-build = "uv run python ./docs/generate_docs.py"
@@ -38,10 +38,8 @@ Before running the examples, you need to set up your environment variables. You
AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name"
```
3. For samples using Bing Grounding search (like `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`), you'll also need either:
3. For samples using Bing Grounding search (like `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`), you'll also need:
```
BING_CONNECTION_NAME="bing-grounding-connection"
# OR
BING_CONNECTION_ID="your-bing-connection-id"
```
@@ -49,7 +47,7 @@ Before running the examples, you need to set up your environment variables. You
- Go to [Azure AI Foundry portal](https://ai.azure.com)
- Navigate to your project's "Connected resources" section
- Add a new connection for "Grounding with Bing Search"
- Copy either the connection name or ID
- Copy the ID
### Option 2: Using environment variables directly
@@ -58,9 +56,7 @@ Set the environment variables in your shell:
```bash
export AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint"
export AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name"
export BING_CONNECTION_NAME="your-bing-connection-name" # Optional, only needed for web search samples
# OR
export BING_CONNECTION_ID="your-bing-connection-id" # Alternative to BING_CONNECTION_NAME
export BING_CONNECTION_ID="your-bing-connection-id"
```
### Required Variables
@@ -70,4 +66,4 @@ export BING_CONNECTION_ID="your-bing-connection-id" # Alternative to BING_CONNE
### Optional Variables
- `BING_CONNECTION_NAME` or `BING_CONNECTION_ID`: Your Bing connection name or ID (required for `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`)
- `BING_CONNECTION_ID`: Your Bing connection ID (required for `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`)
@@ -5,6 +5,7 @@ import os
from agent_framework import ChatAgent, CitationAnnotation
from agent_framework.azure import AzureAIAgentClient
from azure.ai.agents.aio import AgentsClient
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import ConnectionType
from azure.identity.aio import AzureCliCredential
@@ -38,16 +39,17 @@ async def main() -> None:
# Create the client and manually create an agent with Azure AI Search tool
async with (
AzureCliCredential() as credential,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as client,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
):
ai_search_conn_id = ""
async for connection in client.connections.list():
async for connection in project_client.connections.list():
if connection.type == ConnectionType.AZURE_AI_SEARCH:
ai_search_conn_id = connection.id
break
# 1. Create Azure AI agent with the search tool
azure_ai_agent = await client.agents.create_agent(
azure_ai_agent = await project_client.agents.create_agent(
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
name="HotelSearchAgent",
instructions=(
@@ -69,7 +71,7 @@ async def main() -> None:
)
# 2. Create chat client with the existing agent
chat_client = AzureAIAgentClient(project_client=client, agent_id=azure_ai_agent.id)
chat_client = AzureAIAgentClient(agents_client=agents_client, agent_id=azure_ai_agent.id)
try:
async with ChatAgent(
@@ -112,7 +114,7 @@ async def main() -> None:
finally:
# Clean up the agent manually
await client.agents.delete_agent(azure_ai_agent.id)
await project_client.agents.delete_agent(azure_ai_agent.id)
if __name__ == "__main__":
@@ -12,8 +12,7 @@ uses Bing Grounding search to find real-time information from the web.
Prerequisites:
1. A connected Grounding with Bing Search resource in your Azure AI project
2. Set either BING_CONNECTION_NAME or BING_CONNECTION_ID environment variable
Example: BING_CONNECTION_NAME="bing-grounding-connection"
2. Set BING_CONNECTION_ID environment variable
Example: BING_CONNECTION_ID="your-bing-connection-id"
To set up Bing Grounding:
@@ -27,7 +26,7 @@ To set up Bing Grounding:
async def main() -> None:
"""Main function demonstrating Azure AI agent with Bing Grounding search."""
# 1. Create Bing Grounding search tool using HostedWebSearchTool
# The connection_name or ID will be automatically picked up from environment variable
# The connection ID will be automatically picked up from environment variable
bing_search_tool = HostedWebSearchTool(
name="Bing Grounding Search",
description="Search the web for current information using Bing",
@@ -5,6 +5,7 @@ import os
from agent_framework import ChatAgent
from agent_framework.azure import AzureAIAgentClient
from azure.ai.agents.aio import AgentsClient
from azure.ai.projects.aio import AIProjectClient
from azure.identity.aio import AzureCliCredential
@@ -22,16 +23,17 @@ async def main() -> None:
# Create the client
async with (
AzureCliCredential() as credential,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as client,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
):
azure_ai_agent = await client.agents.create_agent(
azure_ai_agent = await project_client.agents.create_agent(
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
# Create remote agent with default instructions
# These instructions will persist on created agent for every run.
instructions="End each response with [END].",
)
chat_client = AzureAIAgentClient(project_client=client, agent_id=azure_ai_agent.id)
chat_client = AzureAIAgentClient(agents_client=agents_client, agent_id=azure_ai_agent.id)
try:
async with ChatAgent(
@@ -50,7 +52,7 @@ async def main() -> None:
print(f"Agent: {result}\n")
finally:
# Clean up the agent manually
await client.agents.delete_agent(azure_ai_agent.id)
await project_client.agents.delete_agent(azure_ai_agent.id)
if __name__ == "__main__":
@@ -7,7 +7,7 @@ from typing import Annotated
from agent_framework import ChatAgent
from agent_framework.azure import AzureAIAgentClient
from azure.ai.projects.aio import AIProjectClient
from azure.ai.agents.aio import AgentsClient
from azure.identity.aio import AzureCliCredential
from pydantic import Field
@@ -33,16 +33,16 @@ async def main() -> None:
# Create the client
async with (
AzureCliCredential() as credential,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as client,
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
):
# Create an thread that will persist
created_thread = await client.agents.threads.create()
created_thread = await agents_client.threads.create()
try:
async with ChatAgent(
# passing in the client is optional here, so if you take the agent_id from the portal
# you can use it directly without the two lines above.
chat_client=AzureAIAgentClient(project_client=client),
chat_client=AzureAIAgentClient(agents_client=agents_client),
instructions="You are a helpful weather agent.",
tools=get_weather,
) as agent:
@@ -52,7 +52,7 @@ async def main() -> None:
print(f"Result: {result}\n")
finally:
# Clean up the thread manually
await client.agents.threads.delete(created_thread.id)
await agents_client.threads.delete(created_thread.id)
if __name__ == "__main__":
@@ -44,8 +44,6 @@ async def main() -> None:
AzureCliCredential() as credential,
AzureAIAgentClient(async_credential=credential) as chat_client,
):
# enable azure-ai observability
await chat_client.setup_azure_ai_observability()
agent = chat_client.create_agent(
name="DocsAgent",
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
@@ -69,8 +69,6 @@ async def main() -> None:
AzureCliCredential() as credential,
AzureAIAgentClient(async_credential=credential) as chat_client,
):
# enable azure-ai observability
await chat_client.setup_azure_ai_observability()
agent = chat_client.create_agent(
name="DocsAgent",
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
@@ -9,7 +9,9 @@ import dotenv
from agent_framework import ChatAgent
from agent_framework.azure import AzureAIAgentClient
from agent_framework.observability import get_tracer
from azure.ai.agents.aio import AgentsClient
from azure.ai.projects.aio import AIProjectClient
from azure.core.exceptions import ResourceNotFoundError
from azure.identity.aio import AzureCliCredential
from opentelemetry.trace import SpanKind
from opentelemetry.trace.span import format_trace_id
@@ -38,16 +40,36 @@ async def get_weather(
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
async def setup_azure_ai_observability(
project_client: AIProjectClient, enable_sensitive_data: bool | None = None
) -> None:
"""Use this method to setup tracing in your Azure AI Project.
This will take the connection string from the AIProjectClient.
It will override any connection string that is set in the environment variables.
It will disable any OTLP endpoint that might have been set.
"""
try:
conn_string = await project_client.telemetry.get_application_insights_connection_string()
except ResourceNotFoundError:
print("No Application Insights connection string found for the Azure AI Project.")
return
from agent_framework.observability import setup_observability
setup_observability(applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data)
async def main():
async with (
AzureCliCredential() as credential,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project,
AzureAIAgentClient(project_client=project) as client,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
AzureAIAgentClient(agents_client=agents_client) as client,
):
# This will enable tracing and configure the application to send telemetry data to the
# Application Insights instance attached to the Azure AI project.
# This will override any existing configuration.
await client.setup_azure_ai_observability()
await setup_azure_ai_observability(project_client)
questions = ["What's the weather in Amsterdam?", "and in Paris, and which is better?", "Why is the sky blue?"]
@@ -9,7 +9,9 @@ import dotenv
from agent_framework import HostedCodeInterpreterTool
from agent_framework.azure import AzureAIAgentClient
from agent_framework.observability import get_tracer
from azure.ai.agents.aio import AgentsClient
from azure.ai.projects.aio import AIProjectClient
from azure.core.exceptions import ResourceNotFoundError
from azure.identity.aio import AzureCliCredential
from opentelemetry.trace import SpanKind
from opentelemetry.trace.span import format_trace_id
@@ -42,6 +44,25 @@ async def get_weather(
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
async def setup_azure_ai_observability(
project_client: AIProjectClient, enable_sensitive_data: bool | None = None
) -> None:
"""Use this method to setup tracing in your Azure AI Project.
This will take the connection string from the AIProjectClient instance.
It will override any connection string that is set in the environment variables.
It will disable any OTLP endpoint that might have been set.
"""
try:
conn_string = await project_client.telemetry.get_application_insights_connection_string()
except ResourceNotFoundError:
print("No Application Insights connection string found for the Azure AI Project.")
return
from agent_framework.observability import setup_observability
setup_observability(applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data)
async def main() -> None:
"""Run an AI service.
@@ -62,13 +83,14 @@ async def main() -> None:
]
async with (
AzureCliCredential() as credential,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project,
AzureAIAgentClient(project_client=project) as client,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
AzureAIAgentClient(agents_client=agents_client) as client,
):
# This will enable tracing and configure the application to send telemetry data to the
# Application Insights instance attached to the Azure AI project.
# This will override any existing configuration.
await client.setup_azure_ai_observability()
await setup_azure_ai_observability(project_client)
with get_tracer().start_as_current_span(
name="Foundry Telemetry from Agent Framework", kind=SpanKind.CLIENT
@@ -0,0 +1,103 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Never
from agent_framework import (
AgentExecutorResponse,
Executor,
HostedCodeInterpreterTool,
WorkflowBuilder,
WorkflowContext,
handler,
)
from agent_framework.azure import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential
"""
This sample demonstrates how to create a workflow that combines an AI agent executor
with a custom executor.
The workflow consists of two stages:
1. An AI agent with code interpreter capabilities that generates and executes Python code
2. An evaluator executor that reviews the agent's output and provides a final assessment
Key concepts demonstrated:
- Creating an AI agent with tool capabilities (HostedCodeInterpreterTool)
- Building workflows using WorkflowBuilder with an agent and a custom executor
- Using the @handler decorator in the executor to process AgentExecutorResponse from the agent
- Connecting workflow executors with edges to create a processing pipeline
- Yielding final outputs from terminal executors
- Non-streaming workflow execution and result collection
Prerequisites:
- Azure AI services configured with required environment variables
- Azure CLI authentication (run 'az login' before executing)
- Basic understanding of async Python and workflow concepts
"""
class Evaluator(Executor):
"""Custom executor that evaluates the output from an AI agent.
This executor demonstrates how to:
- Create a custom workflow executor that processes agent responses
- Use the @handler decorator to define the processing logic
- Access agent execution details including response text and usage metrics
- Yield final results to complete the workflow execution
The evaluator checks if the agent successfully generated the Fibonacci sequence
and provides feedback on correctness along with resource consumption details.
"""
@handler
async def handle(self, message: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:
"""Evaluate the agent's response and complete the workflow with a final assessment.
This handler:
1. Receives the AgentExecutorResponse containing the agent's complete interaction
2. Checks if the expected Fibonacci sequence appears in the response text
3. Extracts usage details (token consumption, execution time, etc.)
4. Yields a final evaluation string to complete the workflow
Args:
message: The response from the Azure AI agent containing text and metadata
ctx: Workflow context for yielding the final output string
"""
target_text = "1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89"
correctness = target_text in message.agent_run_response.text
consumption = message.agent_run_response.usage_details
await ctx.yield_output(f"Correctness: {correctness}, Consumption: {consumption}")
async def main():
async with (
AzureCliCredential() as credential,
AzureAIAgentClient(async_credential=credential) as chat_client,
):
# Create an agent with code interpretation capabilities
agent = chat_client.create_agent(
name="CodingAgent",
instructions=("You are a helpful assistant that can write and execute Python code to solve problems."),
tools=HostedCodeInterpreterTool(),
)
# Build a workflow: Agent generates code -> Evaluator assesses results
# The agent will be wrapped in a special agent executor which produces AgentExecutorResponse
workflow = WorkflowBuilder().set_start_executor(agent).add_edge(agent, Evaluator(id="evaluator")).build()
# Execute the workflow with a specific coding task
results = await workflow.run(
"Generate the fibonacci numbers to 100 using python code, show the code and execute it."
)
# Extract and display the final evaluation
outputs = results.get_outputs()
if isinstance(outputs, list) and len(outputs) == 1:
print("Workflow results:", outputs[0])
else:
raise ValueError("Unexpected workflow outputs:", outputs)
if __name__ == "__main__":
asyncio.run(main())
@@ -4,7 +4,6 @@ import asyncio
from dataclasses import dataclass
from agent_framework import (
AgentExecutor, # Executor that runs the agent
AgentExecutorRequest, # Message bundle sent to an AgentExecutor
AgentExecutorResponse, # Result returned by an AgentExecutor
ChatMessage, # Chat message structure
@@ -148,6 +147,7 @@ async def main() -> None:
# response_format enforces that the model produces JSON compatible with GuessOutput.
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
agent = chat_client.create_agent(
name="GuessingAgent",
instructions=(
"You guess a number between 1 and 10. "
"If the user says 'higher' or 'lower', adjust your next guess. "
@@ -158,16 +158,15 @@ async def main() -> None:
response_format=GuessOutput,
)
# Build a simple loop: TurnManager <-> AgentExecutor.
# TurnManager coordinates and gathers human replies while AgentExecutor runs the model.
turn_manager = TurnManager(id="turn_manager")
agent_exec = AgentExecutor(agent=agent, id="agent")
# Build a simple loop: TurnManager <-> AgentExecutor.
workflow = (
WorkflowBuilder()
.set_start_executor(turn_manager)
.add_edge(turn_manager, agent_exec) # Ask agent to make/adjust a guess
.add_edge(agent_exec, turn_manager) # Agent's response comes back to coordinator
.add_edge(turn_manager, agent) # Ask agent to make/adjust a guess
.add_edge(agent, turn_manager) # Agent's response comes back to coordinator
).build()
# Human in the loop run: alternate between invoking the workflow and supplying collected responses.
+3422 -3438
View File
File diff suppressed because it is too large Load Diff