mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into feature-foundry-agents
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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));
|
||||
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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,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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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?"]
|
||||
|
||||
|
||||
+25
-3
@@ -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
-5
@@ -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.
|
||||
|
||||
Generated
+3422
-3438
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user